89 lines
7.4 KiB
TypeScript
89 lines
7.4 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
|
import { Client } from 'ldapts';
|
|
import { createPool } from '../../src/db';
|
|
import { buildApp } from '../../src/app';
|
|
import { defaultDirectorySettings, readDirectorySettings, saveDirectorySettings } from '../../src/directory';
|
|
import { readSettings, saveSettings } from '../../src/oidc';
|
|
import { startMockOidc } from '../helpers/mock-oidc.mjs';
|
|
import { testAccount } from '../helpers/account';
|
|
|
|
const pool = createPool();
|
|
process.env.OIDC_ALLOW_HTTP = 'true';
|
|
process.env.APP_ORIGINS = 'http://localhost:4174';
|
|
const app = buildApp(pool, false);
|
|
const headers = { 'x-model-request': '1' };
|
|
let admin: Record<string, string>;
|
|
let adminId: string;
|
|
let originalSso: Awaited<ReturnType<typeof readSettings>>;
|
|
let originalDirectory: Awaited<ReturnType<typeof readDirectorySettings>>;
|
|
let provider: Awaited<ReturnType<typeof startMockOidc>>;
|
|
const directory = { ...defaultDirectorySettings(), enabled: true, url: 'ldaps://directory.example.invalid:636', baseDn: 'DC=example,DC=invalid', bindDn: 'reader@example.invalid', bindPassword: 'directory-test-secret' };
|
|
const cookies = (response: { cookies: Array<{ name: string; value: string }> }) => response.cookies.map(c => `${c.name}=${c.value}`).join('; ');
|
|
async function login() {
|
|
const start = await app.inject({ method: 'POST', url: '/auth/login', headers });
|
|
expect(start.statusCode).toBe(200);
|
|
const response = await fetch(start.json().url, { redirect: 'manual' });
|
|
const callback = new URL(response.headers.get('location')!);
|
|
const complete = await app.inject({ url: `/auth/callback${callback.search}`, headers: { cookie: cookies(start) } });
|
|
expect(complete.headers.location).toBe('/');
|
|
const sessionHeaders = { ...headers, cookie: cookies(complete) };
|
|
const session = (await app.inject({ url: '/auth/session', headers: sessionHeaders })).json();
|
|
return { user: session.user, headers: { ...sessionHeaders, 'x-model-user': session.user.id } };
|
|
}
|
|
beforeAll(async () => {
|
|
expect(process.env.DB_NAME).toBe('model_delivery_test');
|
|
originalSso = await readSettings(pool); originalDirectory = await readDirectorySettings(pool);
|
|
const status = (await app.inject('/local-admin/status')).json();
|
|
const result = await app.inject({ method: 'POST', url: `/local-admin/${status.setupRequired ? 'setup' : 'login'}`, headers, payload: status.setupRequired ? { ...testAccount, setupToken: process.env.LOCAL_ADMIN_SETUP_TOKEN } : { username: testAccount.username, password: testAccount.password } });
|
|
expect(result.statusCode).toBe(200);
|
|
admin = { ...headers, cookie: cookies(result) }; adminId = result.json().user.id;
|
|
provider = await startMockOidc(); provider.controls.subject = 'REZNIK\\tom.ruff'; provider.controls.omitName = true;
|
|
await saveSettings(pool, { enabled: true, issuer: provider.issuer, discoveryUrl: `${provider.issuer}/.well-known/openid-configuration`, clientId: 'model-test', clientSecret: 'mock-test-secret', redirectUri: 'http://localhost:4174/api/auth/callback', scopes: 'openid email', authMethod: 'client_secret_post', offlineAccess: false, promptConsent: false, displayNameClaim: 'name' }, adminId);
|
|
});
|
|
afterAll(async () => {
|
|
vi.restoreAllMocks();
|
|
if (originalDirectory) await saveDirectorySettings(pool, originalDirectory.settings, originalDirectory.issuer, adminId);
|
|
if (originalSso) await saveSettings(pool, originalSso, adminId);
|
|
await app.close(); await pool.end(); await provider?.close();
|
|
});
|
|
describe('directory settings and greeting', () => {
|
|
it('guards settings and diagnostic routes, encrypts passwords and preserves them only for the same server/account', async () => {
|
|
for (const url of ['/local-admin/directory', '/local-admin/directory/test']) {
|
|
expect((await app.inject({ method: 'POST', url, headers, payload: {} })).statusCode).toBe(401);
|
|
expect((await app.inject({ method: 'POST', url, headers: admin, remoteAddress: '192.0.2.1', payload: {} })).statusCode).toBe(403);
|
|
expect((await app.inject({ method: 'POST', url, headers: { ...admin, origin: 'https://other.invalid' }, payload: {} })).statusCode).toBe(403);
|
|
}
|
|
const saved = await app.inject({ method: 'POST', url: '/local-admin/directory', headers: admin, payload: directory });
|
|
expect(saved.statusCode).toBe(200);
|
|
expect(saved.json()).toMatchObject({ bindPassword: '', passwordConfigured: true, issuer: provider.issuer });
|
|
expect((await app.inject({ url: '/local-admin/directory', headers: admin })).body).not.toContain(directory.bindPassword);
|
|
expect((await pool.query('SELECT encrypted_config FROM directory_settings WHERE id = 1'))[0].encrypted_config).not.toContain(directory.bindPassword);
|
|
expect((await app.inject({ method: 'POST', url: '/local-admin/directory', headers: admin, payload: { ...directory, bindPassword: '' } })).statusCode).toBe(200);
|
|
expect((await app.inject({ method: 'POST', url: '/local-admin/directory', headers: admin, payload: { ...directory, url: 'ldaps://other.invalid', bindPassword: '' } })).statusCode).toBe(400);
|
|
});
|
|
it('reads a name after OIDC, keeps it during outages, supports personal overrides and invalidates stale cache when disabled', async () => {
|
|
const bind = vi.spyOn(Client.prototype, 'bind').mockResolvedValue();
|
|
vi.spyOn(Client.prototype, 'search').mockResolvedValue({ searchEntries: [{ dn: 'CN=test', sAMAccountName: 'tom.ruff', displayName: 'תום רוף' }], searchReferences: [] });
|
|
vi.spyOn(Client.prototype, 'unbind').mockResolvedValue();
|
|
const test = await app.inject({ method: 'POST', url: '/local-admin/directory/test', headers: admin, payload: { subject: 'REZNIK\\tom.ruff' } });
|
|
expect(test.json()).toEqual({ code: 'FOUND', displayName: 'תום רוף' });
|
|
const first = await login();
|
|
expect(first.user).toMatchObject({ displayName: 'תום רוף', role: 'staff', username: 'test.sso' });
|
|
expect((await app.inject({ url: '/local-admin/directory', headers: first.headers })).statusCode).toBe(403);
|
|
expect((await app.inject({ method: 'POST', url: '/local-admin/directory/test', headers: first.headers, payload: { subject: 'REZNIK\\other' } })).statusCode).toBe(403);
|
|
const stored = (await pool.query('SELECT display_name, directory_display_name FROM app_users WHERE id = ?', [first.user.id]))[0];
|
|
expect(stored).toMatchObject({ display_name: 'test.sso', directory_display_name: 'תום רוף' });
|
|
bind.mockRejectedValue(new Error('directory-test-secret: temporary outage'));
|
|
expect((await login()).user).toMatchObject({ id: first.user.id, displayName: 'תום רוף' });
|
|
const custom = await app.inject({ method: 'POST', url: '/auth/profile', headers: first.headers, payload: { displayName: 'שם אישי' } });
|
|
expect(custom.json().user.displayName).toBe('שם אישי');
|
|
expect((await login()).user.displayName).toBe('שם אישי');
|
|
const reset = await app.inject({ method: 'POST', url: '/auth/profile', headers: first.headers, payload: { displayName: null } });
|
|
expect(reset.json().user.displayName).toBe('תום רוף');
|
|
const failedTest = await app.inject({ method: 'POST', url: '/local-admin/directory/test', headers: admin, payload: { subject: 'REZNIK\\tom.ruff' } });
|
|
expect(failedTest.statusCode).toBe(422); expect(failedTest.json()).toEqual({ code: 'UNAVAILABLE' });
|
|
await saveDirectorySettings(pool, { ...directory, enabled: false }, provider.issuer, adminId);
|
|
expect((await app.inject({ url: '/auth/session', headers: first.headers })).json().user.displayName).toBe('test.sso');
|
|
});
|
|
});
|