// --- Mocks --- jest.mock('@librechat/data-schemas', () => ({ logger: { info: jest.fn(), warn: jest.fn(), debug: jest.fn(), error: jest.fn(), }, })); jest.mock('@librechat/api', () => ({ isEnabled: jest.fn(() => false), isEmailDomainAllowed: jest.fn(() => true), getBalanceConfig: jest.fn(() => ({ enabled: false })), resolveAppConfigForUser: jest.fn(async (_getAppConfig, _user) => ({})), })); jest.mock('~/models', () => ({ findUser: jest.fn(), createUser: jest.fn(), updateUser: jest.fn(), countUsers: jest.fn(), })); jest.mock('~/server/services/Config', () => ({ getAppConfig: jest.fn().mockResolvedValue({}), })); // Mock passport-ldapauth to capture verify callback let verifyCallback; jest.mock('passport-ldapauth', () => { return jest.fn().mockImplementation((options, verify) => { verifyCallback = verify; return { name: 'ldap', options, verify }; }); }); const { ErrorTypes } = require('librechat-data-provider'); const { isEmailDomainAllowed, resolveAppConfigForUser } = require('@librechat/api'); const { findUser, createUser, updateUser, countUsers } = require('~/models'); const { getAppConfig } = require('~/server/services/Config'); // Helper to call the verify callback and wrap in a Promise for convenience const callVerify = (userinfo) => new Promise((resolve, reject) => { verifyCallback(userinfo, (err, user, info) => { if (err) return reject(err); resolve({ user, info }); }); }); describe('ldapStrategy', () => { beforeEach(() => { jest.clearAllMocks(); // minimal required env for ldapStrategy module to export process.env.LDAP_URL = 'ldap://example.com'; process.env.LDAP_USER_SEARCH_BASE = 'ou=users,dc=example,dc=com'; // Unset optional envs to exercise defaults delete process.env.LDAP_CA_CERT_PATH; delete process.env.LDAP_FULL_NAME; delete process.env.LDAP_ID; delete process.env.LDAP_USERNAME; delete process.env.LDAP_EMAIL; delete process.env.LDAP_TLS_REJECT_UNAUTHORIZED; delete process.env.LDAP_STARTTLS; // Default model/domain mocks findUser.mockReset().mockResolvedValue(null); createUser.mockReset().mockResolvedValue('newUserId'); updateUser.mockReset().mockImplementation(async (id, user) => ({ _id: id, ...user })); countUsers.mockReset().mockResolvedValue(0); isEmailDomainAllowed.mockReset().mockReturnValue(true); // Ensure requiring the strategy sets up the verify callback jest.isolateModules(() => { require('./ldapStrategy'); }); }); it('uses the first email when LDAP returns multiple emails (array)', async () => { const userinfo = { uid: 'uid123', givenName: 'Alice', cn: 'Alice Doe', mail: ['first@example.com', 'second@example.com'], }; const { user } = await callVerify(userinfo); expect(user.email).toBe('first@example.com'); expect(createUser).toHaveBeenCalledWith( expect.objectContaining({ provider: 'ldap', ldapId: 'uid123', username: 'Alice', email: 'first@example.com', emailVerified: true, name: 'Alice Doe', }), expect.any(Object), ); }); it('blocks login if an existing user has a different provider', async () => { findUser.mockResolvedValue({ _id: 'u1', email: 'first@example.com', provider: 'google' }); const userinfo = { uid: 'uid123', mail: 'first@example.com', givenName: 'Alice', cn: 'Alice Doe', }; const { user, info } = await callVerify(userinfo); expect(user).toBe(false); expect(info).toEqual({ message: ErrorTypes.AUTH_FAILED }); expect(createUser).not.toHaveBeenCalled(); expect(resolveAppConfigForUser).not.toHaveBeenCalled(); }); it('updates an existing ldap user with current LDAP info', async () => { const existing = { _id: 'u2', provider: 'ldap', email: 'old@example.com', ldapId: 'uid123', username: 'olduser', name: 'Old Name', }; findUser.mockResolvedValue(existing); const userinfo = { uid: 'uid123', mail: 'new@example.com', givenName: 'NewFirst', cn: 'NewFirst NewLast', }; const { user } = await callVerify(userinfo); expect(createUser).not.toHaveBeenCalled(); expect(updateUser).toHaveBeenCalledWith( 'u2', expect.objectContaining({ provider: 'ldap', ldapId: 'uid123', email: 'new@example.com', username: 'NewFirst', name: 'NewFirst NewLast', }), ); expect(user.email).toBe('new@example.com'); }); it('falls back to username@ldap.local when no email attributes are present', async () => { const userinfo = { uid: 'uid999', givenName: 'John', cn: 'John Doe', }; const { user } = await callVerify(userinfo); expect(user.email).toBe('John@ldap.local'); }); it('denies login if email domain is not allowed', async () => { isEmailDomainAllowed.mockReturnValue(false); const userinfo = { uid: 'uid123', mail: 'notallowed@blocked.com', givenName: 'Alice', cn: 'Alice Doe', }; const { user, info } = await callVerify(userinfo); expect(user).toBe(false); expect(info).toEqual({ message: 'Email domain not allowed' }); }); it('passes getAppConfig and found user to resolveAppConfigForUser', async () => { const existing = { _id: 'u3', provider: 'ldap', email: 'tenant@example.com', ldapId: 'uid-tenant', username: 'tenantuser', name: 'Tenant User', tenantId: 'tenant-a', role: 'USER', }; findUser.mockResolvedValue(existing); const userinfo = { uid: 'uid-tenant', mail: 'tenant@example.com', givenName: 'Tenant', cn: 'Tenant User', }; await callVerify(userinfo); expect(resolveAppConfigForUser).toHaveBeenCalledWith(getAppConfig, existing); }); it('uses baseConfig for new user without calling resolveAppConfigForUser', async () => { findUser.mockResolvedValue(null); const userinfo = { uid: 'uid-new', mail: 'newuser@example.com', givenName: 'New', cn: 'New User', }; await callVerify(userinfo); expect(resolveAppConfigForUser).not.toHaveBeenCalled(); expect(getAppConfig).toHaveBeenCalledWith({ baseOnly: true }); }); it('should block login when tenant config restricts the domain', async () => { const existing = { _id: 'u-blocked', provider: 'ldap', ldapId: 'uid-tenant', tenantId: 'tenant-strict', role: 'USER', }; findUser.mockResolvedValue(existing); resolveAppConfigForUser.mockResolvedValue({ registration: { allowedDomains: ['other.com'] }, }); isEmailDomainAllowed.mockReturnValueOnce(true).mockReturnValueOnce(false); const userinfo = { uid: 'uid-tenant', mail: 'user@example.com', givenName: 'Test', cn: 'Test' }; const { user, info } = await callVerify(userinfo); expect(user).toBe(false); expect(info).toEqual({ message: 'Email domain not allowed' }); }); });