mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-10 10:02:36 +01:00
🚀 feat: Refactor customOpenIdData handling to use an object instead of Map and update return types
This commit is contained in:
parent
51cfd9a520
commit
291f76207f
3 changed files with 66 additions and 94 deletions
|
|
@ -3,6 +3,7 @@ const jwtDecode = require('jsonwebtoken/decode');
|
|||
const { Issuer, Strategy: OpenIDStrategy } = require('openid-client');
|
||||
const { findUser, createUser, updateUser } = require('~/models/userMethods');
|
||||
const setupOpenId = require('./openidStrategy');
|
||||
const OpenIdDataMapper = require('./OpenId/openidDataMapper');
|
||||
|
||||
// --- Mocks ---
|
||||
jest.mock('node-fetch');
|
||||
|
|
@ -10,7 +11,6 @@ jest.mock('openid-client');
|
|||
jest.mock('jsonwebtoken/decode');
|
||||
jest.mock('~/server/services/Files/strategies', () => ({
|
||||
getStrategyFunctions: jest.fn(() => ({
|
||||
// You can modify this mock as needed (here returning a dummy function)
|
||||
saveBuffer: jest.fn().mockResolvedValue('/fake/path/to/avatar.png'),
|
||||
})),
|
||||
}));
|
||||
|
|
@ -23,7 +23,7 @@ jest.mock('~/server/utils/crypto', () => ({
|
|||
hashToken: jest.fn().mockResolvedValue('hashed-token'),
|
||||
}));
|
||||
jest.mock('~/server/utils', () => ({
|
||||
isEnabled: jest.fn(() => false), // default to false, override per test if needed
|
||||
isEnabled: jest.fn(() => false),
|
||||
}));
|
||||
jest.mock('~/config', () => ({
|
||||
logger: {
|
||||
|
|
@ -43,7 +43,7 @@ Issuer.discover = jest.fn().mockResolvedValue({
|
|||
}),
|
||||
});
|
||||
|
||||
// To capture the verify callback from the strategy, we grab it from the mock constructor
|
||||
// Capture the verify callback from the strategy via the mock constructor
|
||||
let verifyCallback;
|
||||
OpenIDStrategy.mockImplementation((options, verify) => {
|
||||
verifyCallback = verify;
|
||||
|
|
@ -80,7 +80,6 @@ describe('setupOpenId', () => {
|
|||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clear previous mock calls and reset implementations
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Reset environment variables needed by the strategy
|
||||
|
|
@ -96,6 +95,8 @@ describe('setupOpenId', () => {
|
|||
delete process.env.OPENID_USERNAME_CLAIM;
|
||||
delete process.env.OPENID_NAME_CLAIM;
|
||||
delete process.env.PROXY;
|
||||
delete process.env.OPENID_CUSTOM_DATA;
|
||||
delete process.env.OPENID_PROVIDER;
|
||||
|
||||
// Default jwtDecode mock returns a token that includes the required role.
|
||||
jwtDecode.mockReturnValue({
|
||||
|
|
@ -125,13 +126,8 @@ describe('setupOpenId', () => {
|
|||
});
|
||||
|
||||
it('should create a new user with correct username when username claim exists', async () => {
|
||||
// Arrange – our userinfo already has username 'flast'
|
||||
const userinfo = { ...baseUserinfo };
|
||||
|
||||
// Act
|
||||
const { user } = await validate(tokenset, userinfo);
|
||||
|
||||
// Assert
|
||||
expect(user.username).toBe(userinfo.username);
|
||||
expect(createUser).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
|
@ -147,16 +143,10 @@ describe('setupOpenId', () => {
|
|||
});
|
||||
|
||||
it('should use given_name as username when username claim is missing', async () => {
|
||||
// Arrange – remove username from userinfo
|
||||
const userinfo = { ...baseUserinfo };
|
||||
delete userinfo.username;
|
||||
// Expect the username to be the given name (unchanged case)
|
||||
const expectUsername = userinfo.given_name;
|
||||
|
||||
// Act
|
||||
const { user } = await validate(tokenset, userinfo);
|
||||
|
||||
// Assert
|
||||
expect(user.username).toBe(expectUsername);
|
||||
expect(createUser).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ username: expectUsername }),
|
||||
|
|
@ -166,16 +156,11 @@ describe('setupOpenId', () => {
|
|||
});
|
||||
|
||||
it('should use email as username when username and given_name are missing', async () => {
|
||||
// Arrange – remove username and given_name
|
||||
const userinfo = { ...baseUserinfo };
|
||||
delete userinfo.username;
|
||||
delete userinfo.given_name;
|
||||
const expectUsername = userinfo.email;
|
||||
|
||||
// Act
|
||||
const { user } = await validate(tokenset, userinfo);
|
||||
|
||||
// Assert
|
||||
expect(user.username).toBe(expectUsername);
|
||||
expect(createUser).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ username: expectUsername }),
|
||||
|
|
@ -185,14 +170,9 @@ describe('setupOpenId', () => {
|
|||
});
|
||||
|
||||
it('should override username with OPENID_USERNAME_CLAIM when set', async () => {
|
||||
// Arrange – set OPENID_USERNAME_CLAIM so that the sub claim is used
|
||||
process.env.OPENID_USERNAME_CLAIM = 'sub';
|
||||
const userinfo = { ...baseUserinfo };
|
||||
|
||||
// Act
|
||||
const { user } = await validate(tokenset, userinfo);
|
||||
|
||||
// Assert – username should equal the sub (converted as-is)
|
||||
expect(user.username).toBe(userinfo.sub);
|
||||
expect(createUser).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ username: userinfo.sub }),
|
||||
|
|
@ -202,31 +182,20 @@ describe('setupOpenId', () => {
|
|||
});
|
||||
|
||||
it('should set the full name correctly when given_name and family_name exist', async () => {
|
||||
// Arrange
|
||||
const userinfo = { ...baseUserinfo };
|
||||
const expectedFullName = `${userinfo.given_name} ${userinfo.family_name}`;
|
||||
|
||||
// Act
|
||||
const { user } = await validate(tokenset, userinfo);
|
||||
|
||||
// Assert
|
||||
expect(user.name).toBe(expectedFullName);
|
||||
});
|
||||
|
||||
it('should override full name with OPENID_NAME_CLAIM when set', async () => {
|
||||
// Arrange – use the name claim as the full name
|
||||
process.env.OPENID_NAME_CLAIM = 'name';
|
||||
const userinfo = { ...baseUserinfo, name: 'Custom Name' };
|
||||
|
||||
// Act
|
||||
const { user } = await validate(tokenset, userinfo);
|
||||
|
||||
// Assert
|
||||
expect(user.name).toBe('Custom Name');
|
||||
});
|
||||
|
||||
it('should update an existing user on login', async () => {
|
||||
// Arrange – simulate that a user already exists
|
||||
const existingUser = {
|
||||
_id: 'existingUserId',
|
||||
provider: 'local',
|
||||
|
|
@ -241,13 +210,8 @@ describe('setupOpenId', () => {
|
|||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const userinfo = { ...baseUserinfo };
|
||||
|
||||
// Act
|
||||
await validate(tokenset, userinfo);
|
||||
|
||||
// Assert – updateUser should be called and the user object updated
|
||||
expect(updateUser).toHaveBeenCalledWith(
|
||||
existingUser._id,
|
||||
expect.objectContaining({
|
||||
|
|
@ -260,43 +224,41 @@ describe('setupOpenId', () => {
|
|||
});
|
||||
|
||||
it('should enforce the required role and reject login if missing', async () => {
|
||||
// Arrange – simulate a token without the required role.
|
||||
jwtDecode.mockReturnValue({
|
||||
roles: ['SomeOtherRole'],
|
||||
});
|
||||
const userinfo = { ...baseUserinfo };
|
||||
|
||||
// Act
|
||||
const { user, details } = await validate(tokenset, userinfo);
|
||||
|
||||
// Assert – verify that the strategy rejects login
|
||||
expect(user).toBe(false);
|
||||
expect(details.message).toBe('You must have the "requiredRole" role to log in.');
|
||||
});
|
||||
|
||||
it('should attempt to download and save the avatar if picture is provided', async () => {
|
||||
// Arrange – ensure userinfo contains a picture URL
|
||||
const userinfo = { ...baseUserinfo };
|
||||
|
||||
// Act
|
||||
const { user } = await validate(tokenset, userinfo);
|
||||
|
||||
// Assert – verify that download was attempted and the avatar field was set via updateUser
|
||||
expect(fetch).toHaveBeenCalled();
|
||||
// Our mock getStrategyFunctions.saveBuffer returns '/fake/path/to/avatar.png'
|
||||
expect(user.avatar).toBe('/fake/path/to/avatar.png');
|
||||
});
|
||||
|
||||
it('should not attempt to download avatar if picture is not provided', async () => {
|
||||
// Arrange – remove picture
|
||||
const userinfo = { ...baseUserinfo };
|
||||
delete userinfo.picture;
|
||||
|
||||
// Act
|
||||
await validate(tokenset, userinfo);
|
||||
|
||||
// Assert – fetch should not be called and avatar should remain undefined or empty
|
||||
const { user } = await validate(tokenset, userinfo);
|
||||
expect(fetch).not.toHaveBeenCalled();
|
||||
// Depending on your implementation, user.avatar may be undefined or an empty string.
|
||||
expect(user.avatar).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should map customOpenIdData as an object when OPENID_CUSTOM_DATA is set', async () => {
|
||||
process.env.OPENID_CUSTOM_DATA = 'some,fields';
|
||||
process.env.OPENID_PROVIDER = 'microsoft';
|
||||
const fakeCustomData = { foo: 'bar' };
|
||||
const fakeDataMapper = { mapCustomData: jest.fn().mockResolvedValue(fakeCustomData) };
|
||||
OpenIdDataMapper.getMapper = jest.fn(() => fakeDataMapper);
|
||||
|
||||
const userinfo = { ...baseUserinfo };
|
||||
const { user } = await validate(tokenset, userinfo);
|
||||
expect(OpenIdDataMapper.getMapper).toHaveBeenCalledWith('microsoft');
|
||||
expect(fakeDataMapper.mapCustomData).toHaveBeenCalledWith(tokenset.access_token, 'some,fields');
|
||||
expect(user.customOpenIdData).toEqual({ ...fakeCustomData, roles: ['requiredRole'] });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue