mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
- Updated the `findUser` method to normalize email fields to lowercase and trimmed whitespace for case-insensitive matching. - Enhanced the `normalizeEmailInCriteria` function to handle email normalization in search criteria, including `` conditions. - Added tests to ensure email normalization works correctly across various scenarios, including case differences and whitespace handling.
277 lines
8.6 KiB
JavaScript
277 lines
8.6 KiB
JavaScript
const { logger } = require('@librechat/data-schemas');
|
|
const { ErrorTypes } = require('librechat-data-provider');
|
|
const { createSocialUser, handleExistingUser } = require('./process');
|
|
const socialLogin = require('./socialLogin');
|
|
const { findUser } = require('~/models');
|
|
|
|
jest.mock('@librechat/data-schemas', () => {
|
|
const actualModule = jest.requireActual('@librechat/data-schemas');
|
|
return {
|
|
...actualModule,
|
|
logger: {
|
|
error: jest.fn(),
|
|
info: jest.fn(),
|
|
warn: jest.fn(),
|
|
},
|
|
};
|
|
});
|
|
|
|
jest.mock('./process', () => ({
|
|
createSocialUser: jest.fn(),
|
|
handleExistingUser: jest.fn(),
|
|
}));
|
|
|
|
jest.mock('@librechat/api', () => ({
|
|
...jest.requireActual('@librechat/api'),
|
|
isEnabled: jest.fn().mockReturnValue(true),
|
|
isEmailDomainAllowed: jest.fn().mockReturnValue(true),
|
|
}));
|
|
|
|
jest.mock('~/models', () => ({
|
|
findUser: jest.fn(),
|
|
}));
|
|
|
|
jest.mock('~/server/services/Config', () => ({
|
|
getAppConfig: jest.fn().mockResolvedValue({
|
|
fileStrategy: 'local',
|
|
balance: { enabled: false },
|
|
}),
|
|
}));
|
|
|
|
describe('socialLogin', () => {
|
|
const mockGetProfileDetails = ({ profile }) => ({
|
|
email: profile.emails[0].value,
|
|
id: profile.id,
|
|
avatarUrl: profile.photos?.[0]?.value || null,
|
|
username: profile.name?.givenName || 'user',
|
|
name: `${profile.name?.givenName || ''} ${profile.name?.familyName || ''}`.trim(),
|
|
emailVerified: profile.emails[0].verified || false,
|
|
});
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('Finding users by provider ID', () => {
|
|
it('should find user by provider ID (googleId) when email has changed', async () => {
|
|
const provider = 'google';
|
|
const googleId = 'google-user-123';
|
|
const oldEmail = 'old@example.com';
|
|
const newEmail = 'new@example.com';
|
|
|
|
const existingUser = {
|
|
_id: 'user123',
|
|
email: oldEmail,
|
|
provider: 'google',
|
|
googleId: googleId,
|
|
};
|
|
|
|
/** Mock findUser to return user on first call (by googleId), null on second call */
|
|
findUser
|
|
.mockResolvedValueOnce(existingUser) // First call: finds by googleId
|
|
.mockResolvedValueOnce(null); // Second call would be by email, but won't be reached
|
|
|
|
const mockProfile = {
|
|
id: googleId,
|
|
emails: [{ value: newEmail, verified: true }],
|
|
photos: [{ value: 'https://example.com/avatar.png' }],
|
|
name: { givenName: 'John', familyName: 'Doe' },
|
|
};
|
|
|
|
const loginFn = socialLogin(provider, mockGetProfileDetails);
|
|
const callback = jest.fn();
|
|
|
|
await loginFn(null, null, null, mockProfile, callback);
|
|
|
|
/** Verify it searched by googleId first */
|
|
expect(findUser).toHaveBeenNthCalledWith(1, { googleId: googleId });
|
|
|
|
/** Verify it did NOT search by email (because it found user by googleId) */
|
|
expect(findUser).toHaveBeenCalledTimes(1);
|
|
|
|
/** Verify handleExistingUser was called with the new email */
|
|
expect(handleExistingUser).toHaveBeenCalledWith(
|
|
existingUser,
|
|
'https://example.com/avatar.png',
|
|
expect.any(Object),
|
|
newEmail,
|
|
);
|
|
|
|
/** Verify callback was called with success */
|
|
expect(callback).toHaveBeenCalledWith(null, existingUser);
|
|
});
|
|
|
|
it('should find user by provider ID (facebookId) when using Facebook', async () => {
|
|
const provider = 'facebook';
|
|
const facebookId = 'fb-user-456';
|
|
const email = 'user@example.com';
|
|
|
|
const existingUser = {
|
|
_id: 'user456',
|
|
email: email,
|
|
provider: 'facebook',
|
|
facebookId: facebookId,
|
|
};
|
|
|
|
findUser.mockResolvedValue(existingUser); // Always returns user
|
|
|
|
const mockProfile = {
|
|
id: facebookId,
|
|
emails: [{ value: email, verified: true }],
|
|
photos: [{ value: 'https://example.com/fb-avatar.png' }],
|
|
name: { givenName: 'Jane', familyName: 'Smith' },
|
|
};
|
|
|
|
const loginFn = socialLogin(provider, mockGetProfileDetails);
|
|
const callback = jest.fn();
|
|
|
|
await loginFn(null, null, null, mockProfile, callback);
|
|
|
|
/** Verify it searched by facebookId first */
|
|
expect(findUser).toHaveBeenCalledWith({ facebookId: facebookId });
|
|
expect(findUser.mock.calls[0]).toEqual([{ facebookId: facebookId }]);
|
|
|
|
expect(handleExistingUser).toHaveBeenCalledWith(
|
|
existingUser,
|
|
'https://example.com/fb-avatar.png',
|
|
expect.any(Object),
|
|
email,
|
|
);
|
|
|
|
expect(callback).toHaveBeenCalledWith(null, existingUser);
|
|
});
|
|
|
|
it('should fallback to finding user by email if not found by provider ID', async () => {
|
|
const provider = 'google';
|
|
const googleId = 'google-user-789';
|
|
const email = 'user@example.com';
|
|
|
|
const existingUser = {
|
|
_id: 'user789',
|
|
email: email,
|
|
provider: 'google',
|
|
googleId: 'old-google-id', // Different googleId (edge case)
|
|
};
|
|
|
|
/** First call (by googleId) returns null, second call (by email) returns user */
|
|
findUser
|
|
.mockResolvedValueOnce(null) // By googleId
|
|
.mockResolvedValueOnce(existingUser); // By email
|
|
|
|
const mockProfile = {
|
|
id: googleId,
|
|
emails: [{ value: email, verified: true }],
|
|
photos: [{ value: 'https://example.com/avatar.png' }],
|
|
name: { givenName: 'Bob', familyName: 'Johnson' },
|
|
};
|
|
|
|
const loginFn = socialLogin(provider, mockGetProfileDetails);
|
|
const callback = jest.fn();
|
|
|
|
await loginFn(null, null, null, mockProfile, callback);
|
|
|
|
/** Verify both searches happened */
|
|
expect(findUser).toHaveBeenNthCalledWith(1, { googleId: googleId });
|
|
/** Email passed as-is; findUser implementation handles case normalization */
|
|
expect(findUser).toHaveBeenNthCalledWith(2, { email: email });
|
|
expect(findUser).toHaveBeenCalledTimes(2);
|
|
|
|
/** Verify warning log */
|
|
expect(logger.warn).toHaveBeenCalledWith(
|
|
`[${provider}Login] User found by email: ${email} but not by ${provider}Id`,
|
|
);
|
|
|
|
expect(handleExistingUser).toHaveBeenCalled();
|
|
expect(callback).toHaveBeenCalledWith(null, existingUser);
|
|
});
|
|
|
|
it('should create new user if not found by provider ID or email', async () => {
|
|
const provider = 'google';
|
|
const googleId = 'google-new-user';
|
|
const email = 'newuser@example.com';
|
|
|
|
const newUser = {
|
|
_id: 'newuser123',
|
|
email: email,
|
|
provider: 'google',
|
|
googleId: googleId,
|
|
};
|
|
|
|
/** Both searches return null */
|
|
findUser.mockResolvedValue(null);
|
|
createSocialUser.mockResolvedValue(newUser);
|
|
|
|
const mockProfile = {
|
|
id: googleId,
|
|
emails: [{ value: email, verified: true }],
|
|
photos: [{ value: 'https://example.com/avatar.png' }],
|
|
name: { givenName: 'New', familyName: 'User' },
|
|
};
|
|
|
|
const loginFn = socialLogin(provider, mockGetProfileDetails);
|
|
const callback = jest.fn();
|
|
|
|
await loginFn(null, null, null, mockProfile, callback);
|
|
|
|
/** Verify both searches happened */
|
|
expect(findUser).toHaveBeenCalledTimes(2);
|
|
|
|
/** Verify createSocialUser was called */
|
|
expect(createSocialUser).toHaveBeenCalledWith({
|
|
email: email,
|
|
avatarUrl: 'https://example.com/avatar.png',
|
|
provider: provider,
|
|
providerKey: 'googleId',
|
|
providerId: googleId,
|
|
username: 'New',
|
|
name: 'New User',
|
|
emailVerified: true,
|
|
appConfig: expect.any(Object),
|
|
});
|
|
|
|
expect(callback).toHaveBeenCalledWith(null, newUser);
|
|
});
|
|
});
|
|
|
|
describe('Error handling', () => {
|
|
it('should return error if user exists with different provider', async () => {
|
|
const provider = 'google';
|
|
const googleId = 'google-user-123';
|
|
const email = 'user@example.com';
|
|
|
|
const existingUser = {
|
|
_id: 'user123',
|
|
email: email,
|
|
provider: 'local', // Different provider
|
|
};
|
|
|
|
findUser
|
|
.mockResolvedValueOnce(null) // By googleId
|
|
.mockResolvedValueOnce(existingUser); // By email
|
|
|
|
const mockProfile = {
|
|
id: googleId,
|
|
emails: [{ value: email, verified: true }],
|
|
photos: [{ value: 'https://example.com/avatar.png' }],
|
|
name: { givenName: 'John', familyName: 'Doe' },
|
|
};
|
|
|
|
const loginFn = socialLogin(provider, mockGetProfileDetails);
|
|
const callback = jest.fn();
|
|
|
|
await loginFn(null, null, null, mockProfile, callback);
|
|
|
|
/** Verify error callback */
|
|
expect(callback).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
code: ErrorTypes.AUTH_FAILED,
|
|
provider: 'local',
|
|
}),
|
|
);
|
|
|
|
expect(logger.info).toHaveBeenCalledWith(
|
|
`[${provider}Login] User ${email} already exists with provider local`,
|
|
);
|
|
});
|
|
});
|
|
});
|