mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-02 20:32:58 +00:00
* fix: gate shared startup config by link access * fix: satisfy shared config CI checks * fix: align shared config client types * fix: reject expired shared link access
622 lines
22 KiB
JavaScript
622 lines
22 KiB
JavaScript
jest.mock('~/cache/getLogStores');
|
|
|
|
const mockGetAppConfig = jest.fn();
|
|
jest.mock('~/server/services/Config/app', () => ({
|
|
getAppConfig: (...args) => mockGetAppConfig(...args),
|
|
}));
|
|
|
|
jest.mock('~/server/services/Config/ldap', () => ({
|
|
getLdapConfig: jest.fn(() => null),
|
|
}));
|
|
|
|
const mockHasCapability = jest.fn();
|
|
jest.mock('~/server/middleware/roles/capabilities', () => ({
|
|
hasCapability: (...args) => mockHasCapability(...args),
|
|
}));
|
|
|
|
const mockGetTenantId = jest.fn(() => undefined);
|
|
jest.mock('@librechat/data-schemas', () => ({
|
|
...jest.requireActual('@librechat/data-schemas'),
|
|
getTenantId: (...args) => mockGetTenantId(...args),
|
|
}));
|
|
|
|
const mockGetCloudFrontConfig = jest.fn(() => null);
|
|
const mockResolveBuildInfo = jest.fn(() => ({
|
|
commit: null,
|
|
commitShort: null,
|
|
branch: null,
|
|
buildDate: null,
|
|
}));
|
|
jest.mock('@librechat/api', () => ({
|
|
...jest.requireActual('@librechat/api'),
|
|
getCloudFrontConfig: (...args) => mockGetCloudFrontConfig(...args),
|
|
resolveBuildInfo: (...args) => mockResolveBuildInfo(...args),
|
|
}));
|
|
|
|
const request = require('supertest');
|
|
const express = require('express');
|
|
const configRoute = require('../config');
|
|
|
|
function createApp(user) {
|
|
const app = express();
|
|
app.disable('x-powered-by');
|
|
if (user) {
|
|
app.use((req, _res, next) => {
|
|
req.user = user;
|
|
next();
|
|
});
|
|
}
|
|
app.use('/api/config', configRoute);
|
|
return app;
|
|
}
|
|
|
|
const baseAppConfig = {
|
|
registration: { socialLogins: ['google', 'github'] },
|
|
interfaceConfig: {
|
|
privacyPolicy: { externalUrl: 'https://example.com/privacy' },
|
|
termsOfService: { externalUrl: 'https://example.com/tos' },
|
|
modelSelect: true,
|
|
},
|
|
turnstileConfig: { siteKey: 'test-key' },
|
|
modelSpecs: { list: [{ name: 'test-spec' }] },
|
|
webSearch: { searchProvider: 'tavily' },
|
|
};
|
|
|
|
const mockUser = {
|
|
id: 'user123',
|
|
role: 'USER',
|
|
tenantId: undefined,
|
|
};
|
|
|
|
afterEach(() => {
|
|
jest.resetAllMocks();
|
|
mockResolveBuildInfo.mockReturnValue({
|
|
commit: null,
|
|
commitShort: null,
|
|
branch: null,
|
|
buildDate: null,
|
|
});
|
|
delete process.env.APP_TITLE;
|
|
delete process.env.CHECK_BALANCE;
|
|
delete process.env.START_BALANCE;
|
|
delete process.env.SANDPACK_BUNDLER_URL;
|
|
delete process.env.SANDPACK_STATIC_BUNDLER_URL;
|
|
delete process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES;
|
|
delete process.env.ALLOW_REGISTRATION;
|
|
delete process.env.ALLOW_SOCIAL_LOGIN;
|
|
delete process.env.ALLOW_PASSWORD_RESET;
|
|
delete process.env.DOMAIN_SERVER;
|
|
delete process.env.GOOGLE_CLIENT_ID;
|
|
delete process.env.GOOGLE_CLIENT_SECRET;
|
|
delete process.env.OPENID_CLIENT_ID;
|
|
delete process.env.OPENID_CLIENT_SECRET;
|
|
delete process.env.OPENID_ISSUER;
|
|
delete process.env.OPENID_SESSION_SECRET;
|
|
delete process.env.GITHUB_CLIENT_ID;
|
|
delete process.env.GITHUB_CLIENT_SECRET;
|
|
delete process.env.DISCORD_CLIENT_ID;
|
|
delete process.env.DISCORD_CLIENT_SECRET;
|
|
delete process.env.SAML_ENTRY_POINT;
|
|
delete process.env.SAML_ISSUER;
|
|
delete process.env.SAML_CERT;
|
|
delete process.env.SAML_SESSION_SECRET;
|
|
delete process.env.ALLOW_ACCOUNT_DELETION;
|
|
delete process.env.ANALYTICS_GTM_ID;
|
|
delete process.env.CUSTOM_FOOTER;
|
|
delete process.env.HELP_AND_FAQ_URL;
|
|
});
|
|
|
|
describe('GET /api/config', () => {
|
|
describe('unauthenticated (no req.user)', () => {
|
|
it('should call getAppConfig with baseOnly when no tenant context', async () => {
|
|
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
|
mockGetTenantId.mockReturnValue(undefined);
|
|
const app = createApp(null);
|
|
|
|
await request(app).get('/api/config');
|
|
|
|
expect(mockGetAppConfig).toHaveBeenCalledWith({ baseOnly: true });
|
|
});
|
|
|
|
it('should call getAppConfig with tenantId when tenant context is present', async () => {
|
|
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
|
mockGetTenantId.mockReturnValue('tenant-abc');
|
|
const app = createApp(null);
|
|
|
|
await request(app).get('/api/config');
|
|
|
|
expect(mockGetAppConfig).toHaveBeenCalledWith({ tenantId: 'tenant-abc' });
|
|
});
|
|
|
|
it('should map tenant-scoped config fields in unauthenticated response', async () => {
|
|
const tenantConfig = {
|
|
...baseAppConfig,
|
|
registration: { socialLogins: ['saml'] },
|
|
turnstileConfig: { siteKey: 'tenant-key' },
|
|
};
|
|
mockGetAppConfig.mockResolvedValue(tenantConfig);
|
|
mockGetTenantId.mockReturnValue('tenant-abc');
|
|
const app = createApp(null);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
expect(response.body.socialLogins).toEqual(['saml']);
|
|
expect(response.body.turnstile).toEqual({ siteKey: 'tenant-key' });
|
|
expect(response.body).not.toHaveProperty('modelSpecs');
|
|
});
|
|
|
|
it('should return minimal payload without authenticated-only fields', async () => {
|
|
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
|
const app = createApp(null);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
expect(response.body).not.toHaveProperty('modelSpecs');
|
|
expect(response.body).not.toHaveProperty('balance');
|
|
expect(response.body).not.toHaveProperty('webSearch');
|
|
expect(response.body).not.toHaveProperty('bundlerURL');
|
|
expect(response.body).not.toHaveProperty('staticBundlerURL');
|
|
expect(response.body).not.toHaveProperty('sharePointFilePickerEnabled');
|
|
expect(response.body).not.toHaveProperty('sharePointBaseUrl');
|
|
expect(response.body).not.toHaveProperty('sharePointPickerGraphScope');
|
|
expect(response.body).not.toHaveProperty('sharePointPickerSharePointScope');
|
|
expect(response.body).not.toHaveProperty('conversationImportMaxFileSize');
|
|
});
|
|
|
|
it('should strip authenticated-only informational fields from unauthenticated response (#12688)', async () => {
|
|
process.env.ANALYTICS_GTM_ID = 'GTM-XYZ';
|
|
process.env.CUSTOM_FOOTER = 'internal footer text';
|
|
process.env.HELP_AND_FAQ_URL = 'https://internal.example.com/faq';
|
|
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
|
const app = createApp(null);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
expect(response.body).not.toHaveProperty('showBirthdayIcon');
|
|
expect(response.body).not.toHaveProperty('helpAndFaqURL');
|
|
expect(response.body).not.toHaveProperty('sharedLinksEnabled');
|
|
expect(response.body).not.toHaveProperty('publicSharedLinksEnabled');
|
|
expect(response.body).not.toHaveProperty('analyticsGtmId');
|
|
expect(response.body).not.toHaveProperty('openidReuseTokens');
|
|
expect(response.body).not.toHaveProperty('allowAccountDeletion');
|
|
expect(response.body).not.toHaveProperty('customFooter');
|
|
});
|
|
|
|
it('should not include share-only fields when share context is requested', async () => {
|
|
process.env.ANALYTICS_GTM_ID = 'GTM-XYZ';
|
|
process.env.CUSTOM_FOOTER = 'public footer text';
|
|
process.env.HELP_AND_FAQ_URL = 'https://internal.example.com/faq';
|
|
process.env.SANDPACK_BUNDLER_URL = 'https://bundler.test';
|
|
process.env.SANDPACK_STATIC_BUNDLER_URL = 'https://static-bundler.test';
|
|
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
|
const app = createApp(null);
|
|
|
|
const response = await request(app).get('/api/config?context=share');
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
expect(response.body).not.toHaveProperty('analyticsGtmId');
|
|
expect(response.body).not.toHaveProperty('customFooter');
|
|
expect(response.body).not.toHaveProperty('bundlerURL');
|
|
expect(response.body).not.toHaveProperty('staticBundlerURL');
|
|
expect(response.body).not.toHaveProperty('helpAndFaqURL');
|
|
expect(response.body).not.toHaveProperty('allowAccountDeletion');
|
|
});
|
|
|
|
it('should include socialLogins and turnstile from base config', async () => {
|
|
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
|
const app = createApp(null);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.body.socialLogins).toEqual(['google', 'github']);
|
|
expect(response.body.turnstile).toEqual({ siteKey: 'test-key' });
|
|
});
|
|
|
|
it('should include only privacyPolicy and termsOfService from interface config', async () => {
|
|
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
|
const app = createApp(null);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.body.interface).toEqual({
|
|
privacyPolicy: { externalUrl: 'https://example.com/privacy' },
|
|
termsOfService: { externalUrl: 'https://example.com/tos' },
|
|
});
|
|
expect(response.body.interface).not.toHaveProperty('modelSelect');
|
|
});
|
|
|
|
it('should not include interface if no privacyPolicy or termsOfService', async () => {
|
|
mockGetAppConfig.mockResolvedValue({
|
|
...baseAppConfig,
|
|
interfaceConfig: { modelSelect: true },
|
|
});
|
|
const app = createApp(null);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.body).not.toHaveProperty('interface');
|
|
});
|
|
|
|
it('should include shared env var fields', async () => {
|
|
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
|
process.env.APP_TITLE = 'Test App';
|
|
const app = createApp(null);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.body.appTitle).toBe('Test App');
|
|
expect(response.body).toHaveProperty('emailLoginEnabled');
|
|
expect(response.body).toHaveProperty('serverDomain');
|
|
});
|
|
|
|
it('should omit CloudFront cookie refresh from unauthenticated response (#12688)', async () => {
|
|
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
|
mockGetCloudFrontConfig.mockReturnValue({
|
|
domain: 'https://cdn.example.com',
|
|
imageSigning: 'cookies',
|
|
cookieDomain: '.example.com',
|
|
privateKey: 'test-private-key',
|
|
keyPairId: 'K123ABC',
|
|
});
|
|
const app = createApp(null);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.body).not.toHaveProperty('cloudFront');
|
|
});
|
|
|
|
it('should return 500 when getAppConfig throws', async () => {
|
|
mockGetAppConfig.mockRejectedValue(new Error('Config service failure'));
|
|
const app = createApp(null);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.statusCode).toBe(500);
|
|
expect(response.body).toHaveProperty('error');
|
|
});
|
|
});
|
|
|
|
describe('authenticated (req.user exists)', () => {
|
|
it('should call getAppConfig with role, userId, and tenantId', async () => {
|
|
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
|
mockGetTenantId.mockReturnValue('fallback-tenant');
|
|
const app = createApp(mockUser);
|
|
|
|
await request(app).get('/api/config');
|
|
|
|
expect(mockGetAppConfig).toHaveBeenCalledWith({
|
|
role: 'USER',
|
|
userId: 'user123',
|
|
tenantId: 'fallback-tenant',
|
|
});
|
|
});
|
|
|
|
it('should prefer user tenantId over getTenantId fallback', async () => {
|
|
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
|
mockGetTenantId.mockReturnValue('fallback-tenant');
|
|
const app = createApp({ ...mockUser, tenantId: 'user-tenant' });
|
|
|
|
await request(app).get('/api/config');
|
|
|
|
expect(mockGetAppConfig).toHaveBeenCalledWith({
|
|
role: 'USER',
|
|
userId: 'user123',
|
|
tenantId: 'user-tenant',
|
|
});
|
|
});
|
|
|
|
it('should include modelSpecs, balance, and webSearch', async () => {
|
|
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
|
process.env.CHECK_BALANCE = 'true';
|
|
process.env.START_BALANCE = '10000';
|
|
const app = createApp(mockUser);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.body.modelSpecs).toEqual({ list: [{ name: 'test-spec' }] });
|
|
expect(response.body.balance).toEqual({ enabled: true, startBalance: 10000 });
|
|
expect(response.body.webSearch).toEqual({ searchProvider: 'tavily' });
|
|
});
|
|
|
|
it('should strip private prompt fields from model spec presets', async () => {
|
|
mockGetAppConfig.mockResolvedValue({
|
|
...baseAppConfig,
|
|
modelSpecs: {
|
|
enforce: false,
|
|
prioritize: true,
|
|
list: [
|
|
{
|
|
name: 'guarded-spec',
|
|
label: 'Guarded Spec',
|
|
skills: ['private-skill'],
|
|
preset: {
|
|
endpoint: 'openAI',
|
|
model: 'gpt-4o',
|
|
promptPrefix: 'private prompt prefix',
|
|
instructions: 'private assistant instructions',
|
|
additional_instructions: 'private additional instructions',
|
|
system: 'private bedrock system',
|
|
context: 'private context',
|
|
examples: [{ input: { content: 'a' }, output: { content: 'b' } }],
|
|
greeting: 'Hello',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
const app = createApp(mockUser);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
expect(response.body.modelSpecs.list[0].preset).toEqual({
|
|
endpoint: 'openAI',
|
|
model: 'gpt-4o',
|
|
greeting: 'Hello',
|
|
});
|
|
expect(response.body.modelSpecs.list[0]).not.toHaveProperty('skills');
|
|
});
|
|
|
|
it('should include full interface config', async () => {
|
|
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
|
const app = createApp(mockUser);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.body.interface).toEqual(baseAppConfig.interfaceConfig);
|
|
});
|
|
|
|
it('should include authenticated-only env var fields', async () => {
|
|
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
|
process.env.SANDPACK_BUNDLER_URL = 'https://bundler.test';
|
|
process.env.SANDPACK_STATIC_BUNDLER_URL = 'https://static-bundler.test';
|
|
process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES = '5000000';
|
|
const app = createApp(mockUser);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.body.bundlerURL).toBe('https://bundler.test');
|
|
expect(response.body.staticBundlerURL).toBe('https://static-bundler.test');
|
|
expect(response.body.conversationImportMaxFileSize).toBe(5000000);
|
|
});
|
|
|
|
it('should include post-login informational fields', async () => {
|
|
process.env.ANALYTICS_GTM_ID = 'GTM-XYZ';
|
|
process.env.CUSTOM_FOOTER = 'authenticated footer text';
|
|
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
|
const app = createApp(mockUser);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.body).toHaveProperty('helpAndFaqURL');
|
|
expect(response.body).toHaveProperty('sharedLinksEnabled');
|
|
expect(response.body).toHaveProperty('publicSharedLinksEnabled');
|
|
expect(response.body).toHaveProperty('showBirthdayIcon');
|
|
expect(response.body).toHaveProperty('openidReuseTokens');
|
|
expect(response.body.analyticsGtmId).toBe('GTM-XYZ');
|
|
expect(response.body.customFooter).toBe('authenticated footer text');
|
|
});
|
|
|
|
it('should advertise CloudFront cookie refresh when signed-cookie mode is active', async () => {
|
|
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
|
mockGetCloudFrontConfig.mockReturnValue({
|
|
domain: 'https://cdn.example.com',
|
|
imageSigning: 'cookies',
|
|
cookieDomain: '.example.com',
|
|
privateKey: 'test-private-key',
|
|
keyPairId: 'K123ABC',
|
|
});
|
|
const app = createApp(mockUser);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.body.cloudFront).toEqual({
|
|
cookieRefresh: {
|
|
endpoint: '/api/auth/cloudfront/refresh',
|
|
domain: 'https://cdn.example.com',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('should omit CloudFront cookie refresh when signed-cookie mode is inactive', async () => {
|
|
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
|
mockGetCloudFrontConfig.mockReturnValue({
|
|
domain: 'https://cdn.example.com',
|
|
imageSigning: 'url',
|
|
});
|
|
const app = createApp(mockUser);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.body).not.toHaveProperty('cloudFront');
|
|
});
|
|
|
|
it('should omit CloudFront cookie refresh when cookie mode cannot mint cookies', async () => {
|
|
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
|
mockGetCloudFrontConfig.mockReturnValue({
|
|
domain: 'https://cdn.example.com',
|
|
imageSigning: 'cookies',
|
|
});
|
|
const app = createApp(mockUser);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.body).not.toHaveProperty('cloudFront');
|
|
});
|
|
|
|
it('should merge per-user balance override into config', async () => {
|
|
mockGetAppConfig.mockResolvedValue({
|
|
...baseAppConfig,
|
|
balance: {
|
|
enabled: true,
|
|
startBalance: 50000,
|
|
},
|
|
});
|
|
const app = createApp(mockUser);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.body.balance).toEqual(
|
|
expect.objectContaining({
|
|
enabled: true,
|
|
startBalance: 50000,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should set allowAccountDeletion to false for authenticated users without ACCESS_ADMIN', async () => {
|
|
process.env.ALLOW_ACCOUNT_DELETION = 'false';
|
|
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
|
mockHasCapability.mockResolvedValue(false);
|
|
const app = createApp(mockUser);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.body.allowAccountDeletion).toBe(false);
|
|
expect(mockHasCapability).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should override allowAccountDeletion to true for users with ACCESS_ADMIN capability', async () => {
|
|
process.env.ALLOW_ACCOUNT_DELETION = 'false';
|
|
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
|
mockHasCapability.mockResolvedValue(true);
|
|
const app = createApp(mockUser);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.body.allowAccountDeletion).toBe(true);
|
|
expect(mockHasCapability).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not call hasCapability when allowAccountDeletion is already true', async () => {
|
|
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
|
const app = createApp(mockUser);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.body.allowAccountDeletion).toBe(true);
|
|
expect(mockHasCapability).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should return 500 when getAppConfig throws', async () => {
|
|
mockGetAppConfig.mockRejectedValue(new Error('Config service failure'));
|
|
const app = createApp(mockUser);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.statusCode).toBe(500);
|
|
expect(response.body).toHaveProperty('error');
|
|
});
|
|
});
|
|
|
|
describe('buildInfo payload', () => {
|
|
const populatedBuildInfo = {
|
|
commit: 'abcdef1234567890abcdef1234567890abcdef12',
|
|
commitShort: 'abcdef1',
|
|
branch: 'dev',
|
|
buildDate: '2026-04-20T12:00:00Z',
|
|
};
|
|
|
|
it('includes buildInfo in authenticated response when interface flag is not explicitly disabled', async () => {
|
|
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
|
mockResolveBuildInfo.mockReturnValue(populatedBuildInfo);
|
|
const app = createApp(mockUser);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.body.buildInfo).toEqual(populatedBuildInfo);
|
|
});
|
|
|
|
it('omits buildInfo when interface.buildInfo is false', async () => {
|
|
mockGetAppConfig.mockResolvedValue({
|
|
...baseAppConfig,
|
|
interfaceConfig: { ...baseAppConfig.interfaceConfig, buildInfo: false },
|
|
});
|
|
mockResolveBuildInfo.mockReturnValue(populatedBuildInfo);
|
|
const app = createApp(mockUser);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.body).not.toHaveProperty('buildInfo');
|
|
});
|
|
|
|
it('omits buildInfo when all resolver fields are null', async () => {
|
|
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
|
mockResolveBuildInfo.mockReturnValue({
|
|
commit: null,
|
|
commitShort: null,
|
|
branch: null,
|
|
buildDate: null,
|
|
});
|
|
const app = createApp(mockUser);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.body).not.toHaveProperty('buildInfo');
|
|
});
|
|
|
|
it('includes buildInfo in unauthenticated response when flag is not disabled', async () => {
|
|
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
|
mockResolveBuildInfo.mockReturnValue(populatedBuildInfo);
|
|
const app = createApp(null);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.body.buildInfo).toEqual(populatedBuildInfo);
|
|
});
|
|
|
|
it('omits buildInfo in unauthenticated response when interface.buildInfo is false', async () => {
|
|
mockGetAppConfig.mockResolvedValue({
|
|
...baseAppConfig,
|
|
interfaceConfig: { ...baseAppConfig.interfaceConfig, buildInfo: false },
|
|
});
|
|
mockResolveBuildInfo.mockReturnValue(populatedBuildInfo);
|
|
const app = createApp(null);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.body).not.toHaveProperty('buildInfo');
|
|
});
|
|
|
|
it('propagates interface.buildInfo=false in unauthenticated response so clients can hide About tab', async () => {
|
|
mockGetAppConfig.mockResolvedValue({
|
|
...baseAppConfig,
|
|
interfaceConfig: { ...baseAppConfig.interfaceConfig, buildInfo: false },
|
|
});
|
|
const app = createApp(null);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.body.interface).toBeDefined();
|
|
expect(response.body.interface.buildInfo).toBe(false);
|
|
});
|
|
|
|
it('does not add interface.buildInfo=true to unauthenticated response (default stays implicit)', async () => {
|
|
mockGetAppConfig.mockResolvedValue({
|
|
...baseAppConfig,
|
|
interfaceConfig: { privacyPolicy: { externalUrl: 'https://x' }, buildInfo: true },
|
|
});
|
|
const app = createApp(null);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.body.interface).toBeDefined();
|
|
expect(response.body.interface).not.toHaveProperty('buildInfo');
|
|
});
|
|
|
|
it('includes interface block with only buildInfo=false when nothing else is set', async () => {
|
|
mockGetAppConfig.mockResolvedValue({
|
|
...baseAppConfig,
|
|
interfaceConfig: { buildInfo: false },
|
|
});
|
|
const app = createApp(null);
|
|
|
|
const response = await request(app).get('/api/config');
|
|
|
|
expect(response.body.interface).toEqual({ buildInfo: false });
|
|
});
|
|
});
|
|
});
|