From b1693060960e399f5af81b07792bfc23c3ecc4c0 Mon Sep 17 00:00:00 2001 From: Dustin Healy <54083382+dustinhealy@users.noreply.github.com> Date: Tue, 24 Jun 2025 18:11:06 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=AA=20ci:=20Add=20Tests=20for=20Custom?= =?UTF-8?q?=20Endpoint=20Header=20Resolution=20(#8045)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Enhanced existing tests for the `resolveHeaders` function to cover all user field placeholders and messy scenarios. * Added basic integration tests for custom endpoints initialization file --- .../Endpoints/custom/initialize.spec.js | 93 +++++++++++++++ packages/api/src/utils/env.spec.ts | 112 ++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 api/server/services/Endpoints/custom/initialize.spec.js diff --git a/api/server/services/Endpoints/custom/initialize.spec.js b/api/server/services/Endpoints/custom/initialize.spec.js new file mode 100644 index 0000000000..7e28995127 --- /dev/null +++ b/api/server/services/Endpoints/custom/initialize.spec.js @@ -0,0 +1,93 @@ +const initializeClient = require('./initialize'); + +jest.mock('@librechat/api', () => ({ + resolveHeaders: jest.fn(), + getOpenAIConfig: jest.fn(), + createHandleLLMNewToken: jest.fn(), +})); + +jest.mock('librechat-data-provider', () => ({ + CacheKeys: { TOKEN_CONFIG: 'token_config' }, + ErrorTypes: { NO_USER_KEY: 'NO_USER_KEY', NO_BASE_URL: 'NO_BASE_URL' }, + envVarRegex: /\$\{([^}]+)\}/, + FetchTokenConfig: {}, + extractEnvVariable: jest.fn((value) => value), +})); + +jest.mock('@librechat/agents', () => ({ + Providers: { OLLAMA: 'ollama' }, +})); + +jest.mock('~/server/services/UserService', () => ({ + getUserKeyValues: jest.fn(), + checkUserKeyExpiry: jest.fn(), +})); + +jest.mock('~/server/services/Config', () => ({ + getCustomEndpointConfig: jest.fn().mockResolvedValue({ + apiKey: 'test-key', + baseURL: 'https://test.com', + headers: { 'x-user': '{{LIBRECHAT_USER_ID}}', 'x-email': '{{LIBRECHAT_USER_EMAIL}}' }, + models: { default: ['test-model'] }, + }), +})); + +jest.mock('~/server/services/ModelService', () => ({ + fetchModels: jest.fn(), +})); + +jest.mock('~/app/clients/OpenAIClient', () => { + return jest.fn().mockImplementation(() => ({ + options: {}, + })); +}); + +jest.mock('~/server/utils', () => ({ + isUserProvided: jest.fn().mockReturnValue(false), +})); + +jest.mock('~/cache/getLogStores', () => + jest.fn().mockReturnValue({ + get: jest.fn(), + }), +); + +describe('custom/initializeClient', () => { + const mockRequest = { + body: { endpoint: 'test-endpoint' }, + user: { id: 'user-123', email: 'test@example.com' }, + app: { locals: {} }, + }; + const mockResponse = {}; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('calls resolveHeaders with headers and user', async () => { + const { resolveHeaders } = require('@librechat/api'); + await initializeClient({ req: mockRequest, res: mockResponse, optionsOnly: true }); + expect(resolveHeaders).toHaveBeenCalledWith( + { 'x-user': '{{LIBRECHAT_USER_ID}}', 'x-email': '{{LIBRECHAT_USER_EMAIL}}' }, + { id: 'user-123', email: 'test@example.com' }, + ); + }); + + it('throws if endpoint config is missing', async () => { + const { getCustomEndpointConfig } = require('~/server/services/Config'); + getCustomEndpointConfig.mockResolvedValueOnce(null); + await expect( + initializeClient({ req: mockRequest, res: mockResponse, optionsOnly: true }), + ).rejects.toThrow('Config not found for the test-endpoint custom endpoint.'); + }); + + it('throws if user is missing', async () => { + await expect( + initializeClient({ + req: { ...mockRequest, user: undefined }, + res: mockResponse, + optionsOnly: true, + }), + ).rejects.toThrow("Cannot read properties of undefined (reading 'id')"); + }); +}); diff --git a/packages/api/src/utils/env.spec.ts b/packages/api/src/utils/env.spec.ts index 35f3f13272..4cb8da0d6b 100644 --- a/packages/api/src/utils/env.spec.ts +++ b/packages/api/src/utils/env.spec.ts @@ -314,4 +314,116 @@ describe('resolveHeaders', () => { 'Dot-Header': 'dot-value', }); }); + + // Additional comprehensive tests for all user field placeholders + it('should replace all allowed user field placeholders', () => { + const user = { + id: 'abc', + name: 'Test User', + username: 'testuser', + email: 'me@example.com', + provider: 'google', + role: 'admin', + googleId: 'gid', + facebookId: 'fbid', + openidId: 'oid', + samlId: 'sid', + ldapId: 'lid', + githubId: 'ghid', + discordId: 'dcid', + appleId: 'aid', + emailVerified: true, + twoFactorEnabled: false, + termsAccepted: true, + }; + + const headers = { + 'X-User-ID': '{{LIBRECHAT_USER_ID}}', + 'X-User-Name': '{{LIBRECHAT_USER_NAME}}', + 'X-User-Username': '{{LIBRECHAT_USER_USERNAME}}', + 'X-User-Email': '{{LIBRECHAT_USER_EMAIL}}', + 'X-User-Provider': '{{LIBRECHAT_USER_PROVIDER}}', + 'X-User-Role': '{{LIBRECHAT_USER_ROLE}}', + 'X-User-GoogleId': '{{LIBRECHAT_USER_GOOGLEID}}', + 'X-User-FacebookId': '{{LIBRECHAT_USER_FACEBOOKID}}', + 'X-User-OpenIdId': '{{LIBRECHAT_USER_OPENIDID}}', + 'X-User-SamlId': '{{LIBRECHAT_USER_SAMLID}}', + 'X-User-LdapId': '{{LIBRECHAT_USER_LDAPID}}', + 'X-User-GithubId': '{{LIBRECHAT_USER_GITHUBID}}', + 'X-User-DiscordId': '{{LIBRECHAT_USER_DISCORDID}}', + 'X-User-AppleId': '{{LIBRECHAT_USER_APPLEID}}', + 'X-User-EmailVerified': '{{LIBRECHAT_USER_EMAILVERIFIED}}', + 'X-User-TwoFactorEnabled': '{{LIBRECHAT_USER_TWOFACTORENABLED}}', + 'X-User-TermsAccepted': '{{LIBRECHAT_USER_TERMSACCEPTED}}', + }; + + const result = resolveHeaders(headers, user); + + expect(result['X-User-ID']).toBe('abc'); + expect(result['X-User-Name']).toBe('Test User'); + expect(result['X-User-Username']).toBe('testuser'); + expect(result['X-User-Email']).toBe('me@example.com'); + expect(result['X-User-Provider']).toBe('google'); + expect(result['X-User-Role']).toBe('admin'); + expect(result['X-User-GoogleId']).toBe('gid'); + expect(result['X-User-FacebookId']).toBe('fbid'); + expect(result['X-User-OpenIdId']).toBe('oid'); + expect(result['X-User-SamlId']).toBe('sid'); + expect(result['X-User-LdapId']).toBe('lid'); + expect(result['X-User-GithubId']).toBe('ghid'); + expect(result['X-User-DiscordId']).toBe('dcid'); + expect(result['X-User-AppleId']).toBe('aid'); + expect(result['X-User-EmailVerified']).toBe('true'); + expect(result['X-User-TwoFactorEnabled']).toBe('false'); + expect(result['X-User-TermsAccepted']).toBe('true'); + }); + + it('should handle multiple placeholders in one value', () => { + const user = { id: 'abc', email: 'me@example.com' }; + const headers = { + 'X-Multi': 'User: {{LIBRECHAT_USER_ID}}, Env: ${TEST_API_KEY}, Custom: {{MY_CUSTOM}}', + }; + const customVars = { MY_CUSTOM: 'custom-value' }; + const result = resolveHeaders(headers, user, customVars); + expect(result['X-Multi']).toBe('User: abc, Env: test-api-key-value, Custom: custom-value'); + }); + + it('should leave unknown placeholders unchanged', () => { + const user = { id: 'abc' }; + const headers = { + 'X-Unknown': '{{SOMETHING_NOT_RECOGNIZED}}', + 'X-Known': '{{LIBRECHAT_USER_ID}}', + }; + const result = resolveHeaders(headers, user); + expect(result['X-Unknown']).toBe('{{SOMETHING_NOT_RECOGNIZED}}'); + expect(result['X-Known']).toBe('abc'); + }); + + it('should handle a mix of all types', () => { + const user = { + id: 'abc', + email: 'me@example.com', + emailVerified: true, + twoFactorEnabled: false, + }; + const headers = { + 'X-User': '{{LIBRECHAT_USER_ID}}', + 'X-Env': '${TEST_API_KEY}', + 'X-Custom': '{{MY_CUSTOM}}', + 'X-Multi': 'ID: {{LIBRECHAT_USER_ID}}, ENV: ${TEST_API_KEY}, CUSTOM: {{MY_CUSTOM}}', + 'X-Unknown': '{{NOT_A_REAL_PLACEHOLDER}}', + 'X-Empty': '', + 'X-Boolean': '{{LIBRECHAT_USER_EMAILVERIFIED}}', + }; + const customVars = { MY_CUSTOM: 'custom-value' }; + const result = resolveHeaders(headers, user, customVars); + + expect(result['X-User']).toBe('abc'); + expect(result['X-Env']).toBe('test-api-key-value'); + expect(result['X-Custom']).toBe('custom-value'); + expect(result['X-Multi']).toBe('ID: abc, ENV: test-api-key-value, CUSTOM: custom-value'); + expect(result['X-Unknown']).toBe('{{NOT_A_REAL_PLACEHOLDER}}'); + expect(result['X-Empty']).toBe(''); + expect(result['X-Boolean']).toBe('true'); + }); });