| | import { ErrorTypes } from 'librechat-data-provider'; |
| | import { logger } from '@librechat/data-schemas'; |
| | import type { IUser, UserMethods } from '@librechat/data-schemas'; |
| | import { findOpenIDUser } from './openid'; |
| |
|
| | jest.mock('@librechat/data-schemas', () => ({ |
| | ...jest.requireActual('@librechat/data-schemas'), |
| | logger: { |
| | warn: jest.fn(), |
| | info: jest.fn(), |
| | }, |
| | })); |
| |
|
| | describe('findOpenIDUser', () => { |
| | let mockFindUser: jest.MockedFunction<UserMethods['findUser']>; |
| |
|
| | beforeEach(() => { |
| | mockFindUser = jest.fn(); |
| | jest.clearAllMocks(); |
| | (logger.warn as jest.Mock).mockClear(); |
| | (logger.info as jest.Mock).mockClear(); |
| | }); |
| |
|
| | describe('Primary condition searches', () => { |
| | it('should find user by openidId', async () => { |
| | const mockUser: IUser = { |
| | _id: 'user123', |
| | provider: 'openid', |
| | openidId: 'openid_123', |
| | email: 'user@example.com', |
| | username: 'testuser', |
| | } as IUser; |
| |
|
| | mockFindUser.mockResolvedValueOnce(mockUser); |
| |
|
| | const result = await findOpenIDUser({ |
| | openidId: 'openid_123', |
| | findUser: mockFindUser, |
| | email: 'user@example.com', |
| | }); |
| |
|
| | expect(mockFindUser).toHaveBeenCalledWith({ |
| | $or: [{ openidId: 'openid_123' }], |
| | }); |
| | expect(result).toEqual({ |
| | user: mockUser, |
| | error: null, |
| | migration: false, |
| | }); |
| | }); |
| |
|
| | it('should find user by idOnTheSource', async () => { |
| | const mockUser: IUser = { |
| | _id: 'user123', |
| | provider: 'openid', |
| | idOnTheSource: 'source_123', |
| | email: 'user@example.com', |
| | username: 'testuser', |
| | } as IUser; |
| |
|
| | mockFindUser.mockResolvedValueOnce(mockUser); |
| |
|
| | const result = await findOpenIDUser({ |
| | openidId: 'openid_123', |
| | findUser: mockFindUser, |
| | idOnTheSource: 'source_123', |
| | }); |
| |
|
| | expect(mockFindUser).toHaveBeenCalledWith({ |
| | $or: [{ openidId: 'openid_123' }, { idOnTheSource: 'source_123' }], |
| | }); |
| | expect(result).toEqual({ |
| | user: mockUser, |
| | error: null, |
| | migration: false, |
| | }); |
| | }); |
| |
|
| | it('should find user by both openidId and idOnTheSource', async () => { |
| | const mockUser: IUser = { |
| | _id: 'user123', |
| | provider: 'openid', |
| | openidId: 'openid_123', |
| | idOnTheSource: 'source_123', |
| | email: 'user@example.com', |
| | username: 'testuser', |
| | } as IUser; |
| |
|
| | mockFindUser.mockResolvedValueOnce(mockUser); |
| |
|
| | const result = await findOpenIDUser({ |
| | openidId: 'openid_123', |
| | findUser: mockFindUser, |
| | idOnTheSource: 'source_123', |
| | email: 'user@example.com', |
| | }); |
| |
|
| | expect(mockFindUser).toHaveBeenCalledWith({ |
| | $or: [{ openidId: 'openid_123' }, { idOnTheSource: 'source_123' }], |
| | }); |
| | expect(result).toEqual({ |
| | user: mockUser, |
| | error: null, |
| | migration: false, |
| | }); |
| | }); |
| | }); |
| |
|
| | describe('Email-based searches', () => { |
| | it('should find user by email when primary conditions fail', async () => { |
| | const mockUser: IUser = { |
| | _id: 'user123', |
| | provider: 'openid', |
| | openidId: 'openid_456', |
| | email: 'user@example.com', |
| | username: 'testuser', |
| | } as IUser; |
| |
|
| | mockFindUser |
| | .mockResolvedValueOnce(null) |
| | .mockResolvedValueOnce(mockUser); |
| |
|
| | const result = await findOpenIDUser({ |
| | openidId: 'openid_123', |
| | findUser: mockFindUser, |
| | email: 'user@example.com', |
| | }); |
| |
|
| | expect(mockFindUser).toHaveBeenNthCalledWith(1, { |
| | $or: [{ openidId: 'openid_123' }], |
| | }); |
| | expect(mockFindUser).toHaveBeenNthCalledWith(2, { email: 'user@example.com' }); |
| | expect(result).toEqual({ |
| | user: mockUser, |
| | error: null, |
| | migration: false, |
| | }); |
| | }); |
| |
|
| | it('should return null user when email is not found', async () => { |
| | mockFindUser |
| | .mockResolvedValueOnce(null) |
| | .mockResolvedValueOnce(null); |
| |
|
| | const result = await findOpenIDUser({ |
| | openidId: 'openid_123', |
| | findUser: mockFindUser, |
| | email: 'user@example.com', |
| | }); |
| |
|
| | expect(mockFindUser).toHaveBeenCalledTimes(2); |
| | expect(result).toEqual({ |
| | user: null, |
| | error: null, |
| | migration: false, |
| | }); |
| | }); |
| |
|
| | it('should not search by email if not provided', async () => { |
| | mockFindUser.mockResolvedValueOnce(null); |
| |
|
| | const result = await findOpenIDUser({ |
| | openidId: 'openid_123', |
| | findUser: mockFindUser, |
| | }); |
| |
|
| | expect(mockFindUser).toHaveBeenCalledTimes(1); |
| | expect(mockFindUser).toHaveBeenCalledWith({ |
| | $or: [{ openidId: 'openid_123' }], |
| | }); |
| | expect(result).toEqual({ |
| | user: null, |
| | error: null, |
| | migration: false, |
| | }); |
| | }); |
| | }); |
| |
|
| | describe('Provider conflict handling', () => { |
| | it('should return error when user has different provider', async () => { |
| | const mockUser: IUser = { |
| | _id: 'user123', |
| | provider: 'google', |
| | email: 'user@example.com', |
| | username: 'testuser', |
| | } as IUser; |
| |
|
| | mockFindUser |
| | .mockResolvedValueOnce(null) |
| | .mockResolvedValueOnce(mockUser); |
| |
|
| | const result = await findOpenIDUser({ |
| | openidId: 'openid_123', |
| | findUser: mockFindUser, |
| | email: 'user@example.com', |
| | }); |
| |
|
| | expect(result).toEqual({ |
| | user: null, |
| | error: ErrorTypes.AUTH_FAILED, |
| | migration: false, |
| | }); |
| | }); |
| |
|
| | it('should allow login when user has openid provider', async () => { |
| | const mockUser: IUser = { |
| | _id: 'user123', |
| | provider: 'openid', |
| | openidId: 'openid_456', |
| | email: 'user@example.com', |
| | username: 'testuser', |
| | } as IUser; |
| |
|
| | mockFindUser |
| | .mockResolvedValueOnce(null) |
| | .mockResolvedValueOnce(mockUser); |
| |
|
| | const result = await findOpenIDUser({ |
| | openidId: 'openid_123', |
| | findUser: mockFindUser, |
| | email: 'user@example.com', |
| | }); |
| |
|
| | expect(result).toEqual({ |
| | user: mockUser, |
| | error: null, |
| | migration: false, |
| | }); |
| | }); |
| | }); |
| |
|
| | describe('User migration scenarios', () => { |
| | it('should prepare user for migration when email exists without openidId', async () => { |
| | const mockUser: IUser = { |
| | _id: 'user123', |
| | email: 'user@example.com', |
| | username: 'testuser', |
| | |
| | } as IUser; |
| |
|
| | mockFindUser |
| | .mockResolvedValueOnce(null) |
| | .mockResolvedValueOnce(mockUser); |
| |
|
| | const result = await findOpenIDUser({ |
| | openidId: 'openid_123', |
| | findUser: mockFindUser, |
| | email: 'user@example.com', |
| | }); |
| |
|
| | expect(result).toEqual({ |
| | user: { |
| | ...mockUser, |
| | provider: 'openid', |
| | openidId: 'openid_123', |
| | }, |
| | error: null, |
| | migration: true, |
| | }); |
| | }); |
| |
|
| | it('should not migrate user who already has openidId', async () => { |
| | const mockUser: IUser = { |
| | _id: 'user123', |
| | provider: 'openid', |
| | openidId: 'existing_openid', |
| | email: 'user@example.com', |
| | username: 'testuser', |
| | } as IUser; |
| |
|
| | mockFindUser |
| | .mockResolvedValueOnce(null) |
| | .mockResolvedValueOnce(mockUser); |
| |
|
| | const result = await findOpenIDUser({ |
| | openidId: 'openid_123', |
| | findUser: mockFindUser, |
| | email: 'user@example.com', |
| | }); |
| |
|
| | expect(result).toEqual({ |
| | user: mockUser, |
| | error: null, |
| | migration: false, |
| | }); |
| | }); |
| |
|
| | it('should handle user with no provider but existing openidId', async () => { |
| | const mockUser: IUser = { |
| | _id: 'user123', |
| | openidId: 'existing_openid', |
| | email: 'user@example.com', |
| | username: 'testuser', |
| | |
| | } as IUser; |
| |
|
| | mockFindUser |
| | .mockResolvedValueOnce(null) |
| | .mockResolvedValueOnce(mockUser); |
| |
|
| | const result = await findOpenIDUser({ |
| | openidId: 'openid_123', |
| | findUser: mockFindUser, |
| | email: 'user@example.com', |
| | }); |
| |
|
| | expect(result).toEqual({ |
| | user: mockUser, |
| | error: null, |
| | migration: false, |
| | }); |
| | }); |
| | }); |
| |
|
| | describe('Custom strategy names', () => { |
| | it('should use custom strategy name in logs', async () => { |
| | const loggerWarn = logger.warn as jest.Mock; |
| | loggerWarn.mockClear(); |
| |
|
| | mockFindUser.mockResolvedValueOnce(null).mockResolvedValueOnce(null); |
| |
|
| | await findOpenIDUser({ |
| | openidId: 'openid_123', |
| | findUser: mockFindUser, |
| | email: 'user@example.com', |
| | strategyName: 'customStrategy', |
| | }); |
| |
|
| | expect(loggerWarn).toHaveBeenCalledWith(expect.stringContaining('[customStrategy]')); |
| | }); |
| |
|
| | it('should default to openid strategy name', async () => { |
| | const loggerWarn = logger.warn as jest.Mock; |
| | loggerWarn.mockClear(); |
| |
|
| | mockFindUser.mockResolvedValueOnce(null).mockResolvedValueOnce(null); |
| |
|
| | await findOpenIDUser({ |
| | openidId: 'openid_123', |
| | findUser: mockFindUser, |
| | email: 'user@example.com', |
| | }); |
| |
|
| | expect(loggerWarn).toHaveBeenCalledWith(expect.stringContaining('[openid]')); |
| | }); |
| | }); |
| |
|
| | describe('Edge cases', () => { |
| | it('should handle empty string openidId', async () => { |
| | mockFindUser.mockResolvedValueOnce(null); |
| |
|
| | const result = await findOpenIDUser({ |
| | openidId: '', |
| | findUser: mockFindUser, |
| | }); |
| |
|
| | expect(mockFindUser).not.toHaveBeenCalled(); |
| | expect(result).toEqual({ |
| | user: null, |
| | error: null, |
| | migration: false, |
| | }); |
| | }); |
| |
|
| | it('should handle empty string idOnTheSource', async () => { |
| | mockFindUser.mockResolvedValueOnce(null); |
| |
|
| | const result = await findOpenIDUser({ |
| | openidId: 'openid_123', |
| | findUser: mockFindUser, |
| | idOnTheSource: '', |
| | }); |
| |
|
| | expect(mockFindUser).toHaveBeenCalledWith({ |
| | $or: [{ openidId: 'openid_123' }], |
| | }); |
| | expect(result).toEqual({ |
| | user: null, |
| | error: null, |
| | migration: false, |
| | }); |
| | }); |
| |
|
| | it('should handle both openidId and idOnTheSource as empty strings', async () => { |
| | await findOpenIDUser({ |
| | openidId: '', |
| | findUser: mockFindUser, |
| | idOnTheSource: '', |
| | email: 'user@example.com', |
| | }); |
| |
|
| | |
| | expect(mockFindUser).toHaveBeenCalledTimes(1); |
| | expect(mockFindUser).toHaveBeenCalledWith({ email: 'user@example.com' }); |
| | }); |
| |
|
| | it('should pass email to findUser for case-insensitive lookup (findUser handles normalization)', async () => { |
| | const mockUser: IUser = { |
| | _id: 'user123', |
| | provider: 'openid', |
| | openidId: 'openid_456', |
| | email: 'user@example.com', |
| | username: 'testuser', |
| | } as IUser; |
| |
|
| | mockFindUser |
| | .mockResolvedValueOnce(null) |
| | .mockResolvedValueOnce(mockUser); |
| |
|
| | const result = await findOpenIDUser({ |
| | openidId: 'openid_123', |
| | findUser: mockFindUser, |
| | email: 'User@Example.COM', |
| | }); |
| |
|
| | |
| | expect(mockFindUser).toHaveBeenNthCalledWith(2, { email: 'User@Example.COM' }); |
| | expect(result).toEqual({ |
| | user: mockUser, |
| | error: null, |
| | migration: false, |
| | }); |
| | }); |
| |
|
| | it('should handle findUser throwing an error', async () => { |
| | mockFindUser.mockRejectedValueOnce(new Error('Database error')); |
| |
|
| | await expect( |
| | findOpenIDUser({ |
| | openidId: 'openid_123', |
| | findUser: mockFindUser, |
| | }), |
| | ).rejects.toThrow('Database error'); |
| | }); |
| | }); |
| | }); |
| |
|