| 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'); |
| }); |
| }); |
| }); |
|
|