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; let adminId: string; let originalSso: Awaited>; let originalDirectory: Awaited>; let provider: Awaited>; 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'); }); });