e115934061
Publish `@librechat/data-schemas` to NPM / pack (push) Failing after 8s
Publish `@librechat/client` to NPM / pack (push) Failing after 0s
GitNexus Index / index (push) Failing after 1s
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been skipped
Sync Helm Chart Tags / Sync chart tags (push) Failing after 2s
Publish `librechat-data-provider` to NPM / pack (push) Failing after 1s
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Failing after 1s
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Failing after 0s
Sync Helm Chart Tags / Ignore non-main push (push) Has been skipped
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Failing after 1s
Publish `@librechat/client` to NPM / publish-npm (push) Has been cancelled
Publish `librechat-data-provider` to NPM / publish-npm (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / publish-npm (push) Has been cancelled
247 lines
7.0 KiB
JavaScript
247 lines
7.0 KiB
JavaScript
// --- 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' });
|
|
});
|
|
});
|