mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
185 lines
7.2 KiB
TypeScript
185 lines
7.2 KiB
TypeScript
import {
|
|
ADMIN_FORM_SETTING_KEYS,
|
|
APP_SETTINGS,
|
|
ENCRYPTED_APP_SETTING_KEYS,
|
|
MANAGED_LOCKED_APP_SETTING_KEYS,
|
|
MASKED_ADMIN_SETTING_KEYS,
|
|
PUBLIC_CONFIG_SETTING_KEYS,
|
|
isOidcConfigured,
|
|
isOidcConfiguredIn,
|
|
readAppSetting,
|
|
resolveAppSetting,
|
|
type AppSettingDef,
|
|
} from '../../../src/nest/common/app-settings.registry';
|
|
import { MANAGED_LOCKED_SETTING_KEYS } from '../../../src/nest/common/managed';
|
|
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
/** A stand-in for AppSettingsRepository.getValue over a fixed set of rows. */
|
|
function repoOf(rows: Record<string, string>) {
|
|
const getValue = vi.fn(async (key: string) => (key in rows ? rows[key] : null));
|
|
return { getValue };
|
|
}
|
|
|
|
afterEach(() => vi.unstubAllEnvs());
|
|
|
|
describe('the derived key lists', () => {
|
|
it('APPSET-001: the admin form answers the keys it always has, in the same order', () => {
|
|
expect([...ADMIN_FORM_SETTING_KEYS]).toEqual([
|
|
'allow_registration',
|
|
'allowed_file_types',
|
|
'require_mfa',
|
|
'smtp_host',
|
|
'smtp_port',
|
|
'smtp_user',
|
|
'smtp_pass',
|
|
'smtp_from',
|
|
'smtp_skip_tls_verify',
|
|
'notification_channels',
|
|
'admin_webhook_url',
|
|
'admin_ntfy_server',
|
|
'admin_ntfy_topic',
|
|
'admin_ntfy_token',
|
|
'notify_trip_reminder',
|
|
'password_login',
|
|
'password_registration',
|
|
'oidc_login',
|
|
'oidc_registration',
|
|
'passkey_login',
|
|
'webauthn_rp_id',
|
|
'webauthn_origins',
|
|
'places_provider',
|
|
]);
|
|
});
|
|
|
|
it('APPSET-002: the masked and the encrypted keys', () => {
|
|
expect([...MASKED_ADMIN_SETTING_KEYS].sort()).toEqual(['admin_ntfy_token', 'admin_webhook_url', 'smtp_pass']);
|
|
expect([...ENCRYPTED_APP_SETTING_KEYS].sort()).toEqual([
|
|
'admin_ntfy_token',
|
|
'admin_webhook_url',
|
|
'oidc_client_secret',
|
|
'smtp_pass',
|
|
]);
|
|
expect((APP_SETTINGS.smtp_pass as AppSettingDef).encrypted).toBe('always');
|
|
expect((APP_SETTINGS.admin_webhook_url as AppSettingDef).encrypted).toBe('if-plain');
|
|
});
|
|
|
|
it('APPSET-003: the public config reads the keys it always read', () => {
|
|
expect([...PUBLIC_CONFIG_SETTING_KEYS].sort()).toEqual(
|
|
[
|
|
'places_provider',
|
|
'oidc_display_name',
|
|
'oidc_issuer',
|
|
'oidc_client_id',
|
|
'require_mfa',
|
|
'notification_channel',
|
|
'notify_trip_reminder',
|
|
'smtp_host',
|
|
'notification_channels',
|
|
'places_photos_enabled',
|
|
'places_autocomplete_enabled',
|
|
'places_details_enabled',
|
|
'places_enrich_enabled',
|
|
'place_shadow_enabled',
|
|
].sort(),
|
|
);
|
|
});
|
|
|
|
it('APPSET-004: a registered key is managed-locked exactly when the managed list names it', () => {
|
|
const locked = new Set<string>(MANAGED_LOCKED_SETTING_KEYS);
|
|
const registered = Object.keys(APP_SETTINGS);
|
|
expect([...MANAGED_LOCKED_APP_SETTING_KEYS].sort()).toEqual(registered.filter((k) => locked.has(k)).sort());
|
|
});
|
|
|
|
it('APPSET-005: every documented default is a value its schema accepts', () => {
|
|
for (const [key, def] of Object.entries(APP_SETTINGS) as Array<[string, AppSettingDef]>) {
|
|
if (def.default !== null) expect(def.schema.safeParse(def.default).success, key).toBe(true);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('reading', () => {
|
|
it('APPSET-010: readAppSetting answers what getValue answers', async () => {
|
|
const repo = repoOf({ require_mfa: 'true' });
|
|
expect(await readAppSetting(repo, 'require_mfa')).toBe('true');
|
|
expect(await readAppSetting(repo, 'smtp_host')).toBeNull();
|
|
expect(repo.getValue).toHaveBeenCalledWith('smtp_host');
|
|
});
|
|
|
|
it('APPSET-011: resolveAppSetting takes a non-empty environment value over the stored one', async () => {
|
|
const repo = repoOf({ smtp_host: 'stored.example' });
|
|
vi.stubEnv('SMTP_HOST', 'env.example');
|
|
expect(await resolveAppSetting(repo, 'smtp_host')).toBe('env.example');
|
|
vi.stubEnv('SMTP_HOST', '');
|
|
expect(await resolveAppSetting(repo, 'smtp_host')).toBe('stored.example');
|
|
// A key without an environment variable reads the row.
|
|
expect(await resolveAppSetting(repoOf({ require_mfa: 'true' }), 'require_mfa')).toBe('true');
|
|
});
|
|
|
|
it('APPSET-012: every env-backed key reads its own variable, the secrets included', async () => {
|
|
// The two secrets are read past the register by their callers (they are stored
|
|
// encrypted), so this is the one place their mapping is pinned.
|
|
const variables: Record<string, [string, string]> = {
|
|
smtp_host: ['SMTP_HOST', 'smtp.env.example'],
|
|
smtp_port: ['SMTP_PORT', '2525'],
|
|
smtp_user: ['SMTP_USER', 'mailer'],
|
|
smtp_pass: ['SMTP_PASS', 'env-secret'],
|
|
smtp_from: ['SMTP_FROM', 'trek@env.example'],
|
|
oidc_issuer: ['OIDC_ISSUER', 'https://idp.env.example'],
|
|
oidc_client_id: ['OIDC_CLIENT_ID', 'trek-env'],
|
|
oidc_client_secret: ['OIDC_CLIENT_SECRET', 'oidc-env-secret'],
|
|
oidc_display_name: ['OIDC_DISPLAY_NAME', 'Company SSO'],
|
|
oidc_discovery_url: ['OIDC_DISCOVERY_URL', 'https://idp.env.example/.well-known/openid-configuration'],
|
|
};
|
|
const withEnv = (Object.entries(APP_SETTINGS) as Array<[string, AppSettingDef]>)
|
|
.filter(([, def]) => def.env)
|
|
.map(([key]) => key);
|
|
expect(withEnv.sort()).toEqual(Object.keys(variables).sort());
|
|
|
|
const keys = Object.keys(variables) as Array<keyof typeof APP_SETTINGS>;
|
|
const stored = Object.fromEntries(keys.map((key) => [key, `stored-${key}`]));
|
|
const resolveAll = () => Promise.all(keys.map((key) => resolveAppSetting(repoOf(stored), key)));
|
|
|
|
for (const [name] of Object.values(variables)) vi.stubEnv(name, '');
|
|
expect(await resolveAll()).toEqual(keys.map((key) => `stored-${key}`));
|
|
for (const [name, value] of Object.values(variables)) vi.stubEnv(name, value);
|
|
expect(await resolveAll()).toEqual(keys.map((key) => variables[key][1]));
|
|
});
|
|
});
|
|
|
|
describe('isOidcConfigured', () => {
|
|
it('APPSET-020: wants an issuer and a client id, from either side, and nothing else', async () => {
|
|
vi.stubEnv('OIDC_ISSUER', '');
|
|
vi.stubEnv('OIDC_CLIENT_ID', '');
|
|
expect(await isOidcConfigured(repoOf({}))).toBe(false);
|
|
expect(await isOidcConfigured(repoOf({ oidc_issuer: 'https://idp' }))).toBe(false);
|
|
expect(await isOidcConfigured(repoOf({ oidc_issuer: 'https://idp', oidc_client_id: 'trek' }))).toBe(true);
|
|
expect(await isOidcConfigured(repoOf({ oidc_issuer: '', oidc_client_id: 'trek' }))).toBe(false);
|
|
vi.stubEnv('OIDC_ISSUER', 'https://idp');
|
|
expect(await isOidcConfigured(repoOf({ oidc_client_id: 'trek' }))).toBe(true);
|
|
});
|
|
|
|
it('APPSET-021: does not read the client id when there is no issuer', async () => {
|
|
vi.stubEnv('OIDC_ISSUER', '');
|
|
const repo = repoOf({});
|
|
await isOidcConfigured(repo);
|
|
expect(repo.getValue).not.toHaveBeenCalledWith('oidc_client_id');
|
|
});
|
|
|
|
it('APPSET-022: the batch form answers the same over values already read', () => {
|
|
vi.stubEnv('OIDC_ISSUER', '');
|
|
vi.stubEnv('OIDC_CLIENT_ID', '');
|
|
expect(isOidcConfiguredIn(new Map())).toBe(false);
|
|
expect(
|
|
isOidcConfiguredIn(
|
|
new Map([
|
|
['oidc_issuer', 'https://idp'],
|
|
['oidc_client_id', 'trek'],
|
|
]),
|
|
),
|
|
).toBe(true);
|
|
vi.stubEnv('OIDC_CLIENT_ID', 'trek');
|
|
expect(isOidcConfiguredIn(new Map([['oidc_issuer', 'https://idp']]))).toBe(true);
|
|
});
|
|
});
|