const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); const crypto = require('node:crypto'); const http = require('node:http'); const { createRequire } = require('node:module'); const express = require('express'); const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); const root = path.join(__dirname, '..'); const secret = 'synthetic-policy-test-secret'; const password = bcrypt.hashSync('synthetic-password', 4); // Execute actual local modules, including server.js and its real routers. Only // infrastructure boundaries (DB, IdP, providers, logs, timers, listener) are fake. function fixture(envOverrides = {}) { const state = { settings: { 'models.custom': JSON.stringify([{ id: 'allowed', name: 'Allowed' }, { id: 'other', name: 'Other' }]), 'models.disabled': '[]', 'models.default': 'allowed', 'oidc.enabled': 'true', 'oidc.disable_local_auth': 'false', 'oidc.issuer': 'https://idp.example', 'oidc.client_id': 'synthetic-client', 'feature.read_aloud': 'true', 'feature.nextcloud': 'true', 'feature.memories': 'true', 'tts.model': 'local-kitten-tts', 'tts.voice': 'Luna', 'stt.model': 'synthetic-stt' }, user: { id: 7, email: 'synthetic@example.test', name: 'Synthetic', role: 'admin', password, email_verified: true, disabled: false }, writes: [], queries: [], requests: [], grants: [], logs: [], authorizations: new Map(), usedCodes: new Set(), claims: { sub: 'subject-7', email: 'synthetic@example.test', email_verified: true, name: 'Synthetic' } }; const env = { JWT_SECRET: secret, DATA_ENCRYPTION_KEY: 'a'.repeat(64), APP_URL: 'https://app.example', AI_PROVIDER: 'litellm', LITELLM_API_BASE: 'https://gateway.example/v1', LITELLM_API_KEY: 'synthetic', ...envOverrides }; const db = { async getSetting(key) { if (state.settingsError) throw new Error('synthetic settings failure'); return state.settings[key] ?? null; }, async setSetting(key, value) { state.settings[key] = value; }, async get(sql, params) { state.queries.push(sql); if (sql.includes('COUNT(*)') && sql.includes('FROM users')) return { count: 1 }; if (sql.includes('FROM users')) return state.user ? { ...state.user } : null; if (sql.includes('FROM user_sessions')) return { id: 'session-7', last_activity: new Date() }; if (sql.includes('COUNT(*)') && sql.includes('user_memories')) return { cnt: 1 }; throw new Error('Unexpected DB get: ' + sql); }, async all(sql) { state.queries.push(sql); if (sql.includes('user_memories')) return [{ id: 1, category: 'physical_exam', name: 'Synthetic template', content: 'MEMORY_SENTINEL' }]; return []; }, async run(sql, params) { state.writes.push({ sql, params }); if (sql.includes('INSERT INTO user_sessions') && state.sessionError) throw new Error('synthetic session failure'); if (sql.includes('INSERT INTO users')) state.user = { id: 8, email: params[0], password: params[1], name: params[2], role: params[3], email_verified: true }; return { lastInsertRowid: 8, changes: 1 }; } }; const logger = new Proxy({}, { get: () => (...args) => state.logs.push(args) }); const fakeOIDC = { discovery: async () => ({}), randomPKCECodeVerifier: () => crypto.randomBytes(32).toString('base64url'), calculatePKCECodeChallenge: async verifier => crypto.createHash('sha256').update(verifier).digest('base64url'), buildAuthorizationUrl(config, params) { state.authorizations.set(params.state, params); return new URL('https://idp.example/authorize?' + new URLSearchParams(params)); }, async authorizationCodeGrant(config, url, options) { state.grants.push(options); const auth = state.authorizations.get(options.expectedState); assert.ok(auth, 'grant belongs to an initiated transaction'); assert.equal(options.expectedNonce, auth.nonce); assert.equal(await fakeOIDC.calculatePKCECodeChallenge(options.pkceCodeVerifier), auth.code_challenge); const code = url.searchParams.get('code'); if (!code || state.usedCodes.has(code)) throw new Error('code already consumed'); state.usedCodes.add(code); if (state.idpError) throw new Error('sensitive-verifier-' + options.pkceCodeVerifier); return { claims: () => state.claims }; } }; class FakeOpenAI { constructor() { this.chat = { completions: { create: async payload => { state.requests.push(payload); if (state.providerError) throw new Error('synthetic provider failure'); if (payload.stream) return (async function* () { yield { choices: [{ delta: { content: 'generated' }, finish_reason: 'stop' }] }; })(); return { choices: [{ message: { content: state.aiContent || 'generated' }, finish_reason: 'stop' }] }; } } }; } } async function providerFetch(url, options) { state.requests.push({ url: String(url), options }); assert.ok(String(url).startsWith('https://gateway.example/') || String(url).startsWith('https://api.pwnedpasswords.com/'), 'Unexpected network URL'); return { ok: !state.httpError, status: state.httpError || 200, json: async () => state.httpData || { text: 'synthetic transcript' }, text: async () => '', arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer, headers: { get: () => 'audio/wav' } }; } const cache = new Map(); function load(file) { const filename = path.resolve(root, file); const rel = path.relative(root, filename).replaceAll(path.sep, '/'); if (rel === 'src/db/database.js') return db; if (rel === 'src/utils/logger.js') return logger; if (rel === 'src/utils/notify.js') return new Proxy({}, { get: () => async () => {} }); if (rel === 'src/middleware/logging.js') return (req, res, next) => next(); if (rel === 'src/utils/metrics.js') return { metricsMiddleware: (req, res, next) => next(), metricsHandler: (req, res) => res.end() }; if (cache.has(filename)) return cache.get(filename).exports; const module = { exports: {} }; cache.set(filename, module); const nativeRequire = createRequire(filename); function localRequire(name) { if (name.startsWith('.')) { const resolved = nativeRequire.resolve(name); if (resolved.startsWith(root + path.sep) && resolved.endsWith('.js')) return load(resolved); return nativeRequire(name); } if (name === 'openid-client') return fakeOIDC; if (name === 'openai') return { OpenAI: FakeOpenAI }; if (name === 'dns') return { promises: { lookup: async () => [{ address: '8.8.8.8' }] } }; if (name === 'axios') { const dav = async options => { if (!state.allowDav || !options.url.startsWith('https://cloud.example/')) throw new Error('Unexpected axios request'); state.requests.push({ dav: options }); return { data: '', headers: { 'content-type': 'text/plain' }, status: 200 }; }; return Object.assign(dav, { get: (url, options) => dav({ url, ...options }) }); } if (name === 'dotenv') return { config() {} }; if (name === 'http' && rel === 'server.js') return { createServer(app) { state.app = app; return { listen() {}, close() {} }; } }; return nativeRequire(name); } vm.runInNewContext(fs.readFileSync(filename, 'utf8'), { module, exports: module.exports, require: localRequire, __dirname: path.dirname(filename), __filename: filename, process: { env, on() {}, exit(code) { throw new Error('Unexpected process exit ' + code); } }, console: logger, Buffer, URL, URLSearchParams, TextEncoder, TextDecoder, AbortController, File, Blob, FormData, fetch: providerFetch, setTimeout: () => ({ unref() {} }), clearTimeout() {}, setInterval: () => ({ unref() {} }), clearInterval() {} }, { filename }); return module.exports; } async function serve(t, serverModule = false) { if (serverModule) load('server.js'); else { state.app = express(); state.app.use(express.json()); state.app.use(require('cookie-parser')()); state.app.use('/api/auth', load('src/routes/oidc.js')); state.app.use('/api/auth', load('src/routes/auth.js')); } const server = http.createServer(state.app); await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); t.after(() => new Promise(resolve => { server.close(resolve); server.closeAllConnections(); })); const base = 'http://127.0.0.1:' + server.address().port; return async function request(route, { method = 'GET', body, cookie, role, authenticated = false } = {}) { if (role) state.user.role = role; const headers = {}; if (cookie) headers.cookie = cookie; if (authenticated || role) headers.authorization = 'Bearer ' + jwt.sign({ userId: 7 }, secret); if (body !== undefined && !(body instanceof FormData)) headers['content-type'] = 'application/json'; const response = await fetch(base + route, { method, headers, body: body instanceof FormData ? body : body === undefined ? undefined : JSON.stringify(body), redirect: 'manual' }); const text = await response.text(); let data; try { data = JSON.parse(text); } catch (_) { data = text; } return { status: response.status, headers: response.headers, data }; }; } return { state, db, load, serve }; } async function initiate(request) { const response = await request('/api/auth/oidc'); assert.equal(response.status, 302); const url = new URL(response.headers.get('location')); const cookie = response.headers.getSetCookie().find(c => c.startsWith('ped_oidc=')); assert.match(cookie, /HttpOnly/); assert.match(cookie, /Secure/); assert.match(cookie, /SameSite=Lax/); assert.match(cookie, /Path=\/api\/auth\/oidc/); assert.match(url.searchParams.get('state'), /^[a-f0-9]{48}$/); assert.equal(url.searchParams.has('code_verifier'), false); return { state: url.searchParams.get('state'), cookie: cookie.split(';')[0] }; } function authCookies(response) { return response.headers.getSetCookie().filter(c => c.startsWith('ped_auth=')); } function assertCleared(response) { assert.match(response.headers.getSetCookie().find(c => c.startsWith('ped_oidc=')), /Expires=Thu, 01 Jan 1970/); } function signTransaction(payload) { const body = Buffer.from(JSON.stringify(payload)).toString('base64url'); return 'ped_oidc=' + body + '.' + crypto.createHmac('sha256', secret).update(body).digest('base64url'); } test('OIDC is browser-bound with opaque state; successful PKCE/nonce exchange creates session before cookie', async t => { const f = fixture(); const request = await f.serve(t); const a = await initiate(request); const b = await initiate(request); const wrong = await request('/api/auth/oidc/callback?state=' + a.state + '&code=one', { cookie: b.cookie }); assert.match(wrong.headers.get('location'), /invalid_state/); assertCleared(wrong); assert.equal(f.state.grants.length, 0); const good = await request('/api/auth/oidc/callback?state=' + a.state + '&code=one', { cookie: a.cookie }); assert.match(good.headers.get('location'), /sso=ok/); assertCleared(good); assert.equal(authCookies(good).length, 1); assert.ok(f.state.writes.some(w => w.sql.includes('INSERT INTO user_sessions'))); assert.ok(f.state.writes.some(w => w.sql.includes('UPDATE users SET oidc_sub'))); const replay = await request('/api/auth/oidc/callback?state=' + a.state + '&code=one'); assert.match(replay.headers.get('location'), /invalid_state/); assertCleared(replay); const copiedReplay = await request('/api/auth/oidc/callback?state=' + a.state + '&code=one', { cookie: a.cookie }); assert.match(copiedReplay.headers.get('location'), /sso_failed/); assert.equal(authCookies(copiedReplay).length, 0); for (const grant of f.state.grants) assert.ok(!JSON.stringify(f.state.logs).includes(grant.pkceCodeVerifier)); }); test('OIDC rejects expired, malformed, forged and missing transactions without reaching IdP', async t => { const f = fixture(); const request = await f.serve(t); const a = await initiate(request); const payload = { s: a.state, n: 'b'.repeat(48), v: 'v'.repeat(43), expires: Date.now() + 60000 }; for (const cookie of [undefined, 'ped_oidc=bad.x', 'ped_oidc=' + 'a'.repeat(2100), a.cookie + 'x', signTransaction({ ...payload, expires: Date.now() - 1 }), signTransaction({ ...payload, expires: Date.now() + 600000 }), signTransaction({ ...payload, v: 'short' }), signTransaction({ ...payload, n: [] })]) { const response = await request('/api/auth/oidc/callback?state=' + a.state + '&code=x', { cookie }); assert.match(response.headers.get('location'), /invalid_state/); assertCleared(response); assert.equal(authCookies(response).length, 0); } for (const query of ['state=bad', 'state=' + a.state + '&state=' + a.state, 'state[x]=bad']) { const response = await request('/api/auth/oidc/callback?' + query, { cookie: a.cookie }); assert.match(response.headers.get('location'), /invalid_state/); } assert.equal(f.state.grants.length, 0); }); test('OIDC refuses unsafe linking, disabled/mismatched identities, and session failure; keeps linked/new flows', async t => { const f = fixture(); const request = await f.serve(t); const baseline = { ...f.state.user }; for (const scenario of [ { user: { email_verified: false }, error: 'account_link_required' }, { user: { disabled: true }, error: 'disabled' }, { user: { oidc_sub: 'other-sub' }, error: 'sub_mismatch' }, { claims: { email_verified: false }, error: 'email_unverified' }, { sessionError: true, error: 'sso_failed' }, { idpError: true, error: 'sso_failed' }, { user: { oidc_sub: 'subject-7', email_verified: false }, success: true }, { newUser: true, success: true } ]) { f.state.user = scenario.newUser ? null : { ...baseline, ...scenario.user }; f.state.claims.email_verified = scenario.claims ? false : true; f.state.sessionError = !!scenario.sessionError; f.state.idpError = !!scenario.idpError; f.state.writes.length = 0; const a = await initiate(request); const response = await request('/api/auth/oidc/callback?state=' + a.state + '&code=' + a.state, { cookie: a.cookie }); assertCleared(response); assert.match(response.headers.get('location'), new RegExp(scenario.success ? 'sso=ok' : scenario.error)); assert.equal(authCookies(response).length, scenario.success ? 1 : 0); if (!scenario.success && !scenario.sessionError) assert.equal(f.state.writes.length, 0, 'no account mutation before identity/disabled checks'); for (const grant of f.state.grants) assert.ok(!JSON.stringify(f.state.logs).includes(grant.pkceCodeVerifier)); } }); test('active SSO-only policy denies local login/registration/credential creation, but disabled OIDC cannot lock out local auth', async t => { const f = fixture(); const request = await f.serve(t, true); f.state.settings['oidc.disable_local_auth'] = 'true'; for (const route of ['/api/auth/login', '/api/auth/register', '/api/auth/forgot-password', '/api/auth/reset-password', '/api/auth/change-password', '/api/auth/setup-2fa', '/api/auth/verify-2fa', '/api/auth/2fa/backup-codes', '/api/admin/users/7/reset-password']) { const response = await request(route, { method: 'POST', body: {}, authenticated: true }); assert.equal(response.status, 403, route); assert.equal(response.data.code, 'sso_only'); assert.equal(authCookies(response).length, 0); } assert.equal(f.state.writes.length, 0); assert.equal((await request('/api/auth/oidc-status')).data.disableLocalAuth, true); assert.equal((await request('/api/auth/registration-status')).data.registrationEnabled, false); assert.equal((await request('/api/auth/me', { authenticated: true })).data.user.canLocalAuth, false); f.state.settings['oidc.enabled'] = 'false'; assert.equal((await request('/api/auth/oidc-status')).data.disableLocalAuth, false); const local = await request('/api/auth/login', { method: 'POST', body: { email: f.state.user.email, password: 'synthetic-password' } }); assert.equal(local.status, 200); assert.equal(authCookies(local).length, 1); f.state.settingsError = true; const unavailable = await request('/api/auth/login', { method: 'POST', body: {} }); assert.equal(unavailable.status, 503); assert.equal(authCookies(unavailable).length, 0); }); test('local login and auto-verified registration never issue success/cookie on session insert failure', async t => { const f = fixture(); const request = await f.serve(t); f.state.sessionError = true; const login = await request('/api/auth/login', { method: 'POST', body: { email: f.state.user.email, password: 'synthetic-password' } }); assert.equal(login.status, 500); assert.equal(authCookies(login).length, 0); assert.equal(login.data.success, undefined); f.state.user = null; const registration = await request('/api/auth/register', { method: 'POST', body: { email: 'new@example.test', password: 'synthetic-password', name: 'Synthetic' } }); assert.equal(registration.status, 500); assert.equal(authCookies(registration).length, 0); assert.equal(registration.data.success, undefined); assert.equal(f.state.writes.filter(w => w.sql.includes('INSERT INTO user_sessions')).length, 2); }); test('final model allowlist is enforced for streaming/nonstream, invalid settings/outage, defaults and fallback', async () => { const f = fixture({ LITELLM_DEFAULT_MODEL: 'not-enabled', LITELLM_FALLBACK_MODEL: 'not-enabled' }); const ai = f.load('src/utils/ai.js'); const models = f.load('src/utils/models.js'); for (const call of [options => ai.callAI([], options), options => ai.callAIStream([], options, () => {})]) { await call({ model: 'allowed' }); f.state.settings['models.disabled'] = '["other"]'; await assert.rejects(call({ model: 'other' }), /Model not permitted/); f.state.settings['models.custom'] = '[]'; await assert.rejects(call({ model: 'allowed' }), /Model not permitted/); await assert.rejects(call({}), /Model not permitted/); for (const invalid of ['', '{', '{}', 'null', '[null]', '[{"id":""}]']) { f.state.settings['models.custom'] = invalid; await assert.rejects(call({ model: 'allowed' })); } f.state.settings['models.custom'] = '[{"id":"allowed"}]'; for (const invalid of ['', '{}', 'null', '[null]']) { f.state.settings['models.disabled'] = invalid; await assert.rejects(call({ model: 'allowed' })); } f.state.settings['models.disabled'] = '[]'; await call({ model: 'allowed' }); // warm path must not retain grants during outage f.state.settingsError = true; await assert.rejects(call({ model: 'allowed' })); await assert.rejects(call({})); f.state.settingsError = false; } delete f.state.settings['models.custom']; delete f.state.settings['models.disabled']; assert.equal((await models.getAllowedModelIds(f.db)).size, 0); await assert.rejects(ai.callAI([], {}), /Model not permitted/); f.state.settings['models.custom'] = '[{"id":"allowed"}]'; f.state.settings['models.default'] = 'removed'; assert.equal(await models.getEffectiveDefaultModel(f.db), 'allowed'); await ai.callAI([], {}); assert.equal(f.state.requests.at(-1).model, 'allowed'); f.state.settings['ai.allow_model_fallback'] = 'true'; f.state.providerError = true; const start = f.state.requests.length; await assert.rejects(ai.callAI([], { model: 'allowed' })); assert.equal(f.state.requests.length - start, 1, 'disabled fallback never reaches provider'); f.state.providerError = false; await ai.callAI([], { model: 'admin-probe', skipAllowlistCheck: true }); assert.equal(f.state.requests.at(-1).model, 'admin-probe'); }); test('merged static/custom roster filters disabled custom models too', async () => { const f = fixture({ AI_PROVIDER: 'openrouter' }); const models = f.load('src/utils/models.js'); f.state.settings['models.disabled'] = '["allowed", "google/gemini-2.5-flash"]'; const roster = await models.getAvailableModelsWithOverrides(f.db); assert.ok(roster.length); assert.ok(!roster.some(m => ['allowed', 'google/gemini-2.5-flash'].includes(m.id))); f.state.settings['models.disabled'] = JSON.stringify(models.getAvailableModels().map(m => m.id).concat(['allowed', 'other'])); assert.equal((await models.getAllowedModelIds(f.db)).size, 0); }); test('real server model API/default setters/toggles/removal agree, and admin probe bypass is protected', async t => { const f = fixture({ LITELLM_DEFAULT_MODEL: 'absent' }); const request = await f.serve(t, true); const put = (route, body, role = 'admin') => request(route, { method: 'PUT', body, role }); assert.equal((await put('/api/admin/config/models/default', { modelId: 'absent' })).status, 400); assert.equal((await put('/api/admin/config/models/toggle', { modelId: 'allowed', enabled: 'false' })).status, 400); assert.equal((await put('/api/admin/config/models/toggle', { modelId: 'absent', enabled: false })).status, 400); assert.equal((await put('/api/admin/config/models/toggle', { modelId: 'allowed', enabled: false })).status, 200); assert.equal((await put('/api/admin/config/models/default', { modelId: 'allowed' })).status, 400); let advertised = (await request('/api/models')).data; assert.deepEqual(advertised.models.map(m => m.id), ['other']); assert.equal(advertised.defaultModel, 'other'); assert.equal(f.state.settings['models.default'], ''); assert.equal((await put('/api/admin/config/models/toggle', { modelId: 'allowed', enabled: true })).status, 200); assert.equal((await put('/api/admin/config/models/default', { modelId: 'allowed' })).status, 200); await request('/api/admin/config/models/custom/allowed', { method: 'DELETE', role: 'admin' }); advertised = (await request('/api/models')).data; assert.deepEqual(advertised.models.map(m => m.id), ['other']); assert.equal(advertised.defaultModel, 'other'); const note = await request('/api/notes/from-voice', { method: 'POST', role: 'user', body: { transcript: 'Synthetic personal note' } }); assert.equal(note.status, 200); assert.equal(note.data.model, 'other', 'omitted note model uses final default, not disabled env fallback'); assert.equal((await put('/api/admin/config/models.default', { value: 'absent' })).status, 400); assert.equal((await request('/api/admin/config/models/test', { method: 'POST', role: 'user', body: { modelId: 'absent' } })).status, 403); const probe = await request('/api/admin/config/models/test', { method: 'POST', role: 'admin', body: { modelId: 'absent' } }); assert.equal(probe.data.success, true); assert.equal(f.state.requests.at(-1).model, 'absent'); f.state.settings['models.custom'] = '[]'; assert.deepEqual((await request('/api/models')).data.models, []); assert.equal((await request('/api/models')).data.defaultModel, ''); f.state.settingsError = true; const error = await request('/api/models'); assert.equal(error.status, 503); assert.deepEqual(error.data.models, []); }); test('feature routes deny before data/provider access; user status is nonsensitive and announcements/moderator mounts stay protected', async t => { const f = fixture(); const request = await f.serve(t, true); for (const name of ['read_aloud', 'nextcloud', 'memories']) f.state.settings['feature.' + name] = 'false'; const endpoints = [ ['POST', '/api/text-to-speech'], ['POST', '/api/nextcloud/connect'], ['POST', '/api/nextcloud/export'], ['POST', '/api/nextcloud/disconnect'], ['GET', '/api/memories'], ['GET', '/api/memories/context'], ['POST', '/api/memories'], ['PUT', '/api/memories/1'], ['DELETE', '/api/memories/1'], ['GET', '/api/admin/learning/webdav-browse'], ['POST', '/api/admin/learning/ai-generate'], ['POST', '/api/user/webdav-path'], ['POST', '/api/admin/learning/webdav-path'] ]; for (const [method, route] of endpoints) { const response = await request(route, { method, authenticated: true, body: method === 'GET' ? undefined : { webdavPath: '/synthetic.txt', text: 'Synthetic' } }); assert.equal(response.status, 403, route); } assert.equal(f.state.writes.length, 0); assert.equal(f.state.requests.length, 0); assert.ok(!f.state.queries.some(q => q.includes('user_memories') || q.includes('nextcloud_token'))); const clinical = await request('/api/clinical-assistant/status', { role: 'user' }); assert.equal(clinical.status, 200); assert.equal(clinical.data.success, true, 'personal Nextcloud flag does not block clinical MCP status'); const features = await request('/api/user/features', { role: 'user' }); assert.deepEqual(features.data, { features: { read_aloud: false, nextcloud: false, memories: false } }); assert.equal((await request('/api/user/features')).status, 401); f.state.settings['announcement.enabled'] = 'true'; f.state.settings['announcement.text'] = 'Synthetic announcement'; const announcement = await request('/api/admin/config/announcement', { role: 'user' }); assert.equal(announcement.status, 200); assert.equal(announcement.data.text, 'Synthetic announcement'); assert.equal((await request('/api/admin/config/announcement')).status, 401); assert.equal((await request('/api/admin/config', { role: 'user' })).status, 403); assert.equal((await request('/api/admin/config', { role: 'moderator' })).status, 403); f.state.aiContent = '{"title":"Synthetic","body":"

Synthetic

","category_name":"General"}'; const generated = await request('/api/admin/learning/ai-generate', { method: 'POST', role: 'moderator', body: { topic: 'Synthetic educational topic' } }); assert.equal(generated.status, 200); assert.equal(generated.data.success, true); assert.equal((await request('/api/admin/learning/ai-generate', { method: 'POST', role: 'user', body: { topic: 'Synthetic' } })).status, 403); f.state.settings['feature.memories'] = 'true'; assert.equal((await request('/api/memories', { role: 'user' })).data.memories.length, 1); assert.match((await request('/api/memories/context', { role: 'user' })).data.context, /MEMORY_SENTINEL/); f.state.settingsError = true; assert.equal((await request('/api/text-to-speech', { method: 'POST', authenticated: true, body: { text: 'Synthetic' } })).status, 503); }); test('every actual generation memory consumer omits disabled templates and includes enabled templates', async t => { const f = fixture(); const request = await f.serve(t, true); const endpoints = ['/api/generate-hpi-encounter', '/api/generate-hpi-dictation', '/api/generate-soap', '/api/generate-hospital-course', '/api/well-visit/note', '/api/sick-visit/note', '/api/ed-encounters/generate']; for (const enabled of [false, true]) { f.state.settings['feature.memories'] = String(enabled); for (const route of endpoints) { const start = f.state.requests.length; const response = await request(route, { method: 'POST', role: 'user', body: { transcript: 'Synthetic clinical transcript', chiefComplaint: 'Synthetic concern', patientAge: '5 years', notes: [{ date: '2026-01-01', content: 'Synthetic note' }], physicianMemories: 'MEMORY_SENTINEL' } }); assert.equal(response.status, 200, route + ': ' + JSON.stringify(response.data)); assert.ok(f.state.requests.length > start, route); assert.equal(JSON.stringify(f.state.requests.at(-1).messages).includes('MEMORY_SENTINEL'), enabled, route); } } }); test('TTS blank/incompatible user voice falls through to admin/env/family, and STT tests do not claim HTTP errors as success', async t => { const f = fixture({ LITELLM_TTS_VOICE: 'Rosie' }); const request = await f.serve(t, true); for (const [voice, admin, expected] of [[undefined, 'Luna', 'Luna'], ['', '', 'Rosie'], [' ', 'Luna', 'Luna'], ['F1', 'Luna', 'Luna'], ['Bella', 'Luna', 'Bella']]) { f.state.user.tts_voice = voice; f.state.settings['tts.voice'] = admin; const result = await request('/api/text-to-speech', { method: 'POST', role: 'user', body: { text: 'Synthetic' } }); assert.equal(result.status, 200); assert.equal(JSON.parse(f.state.requests.at(-1).options.body).voice, expected); } f.state.settings['tts.model'] = 'local-supertonic-tts'; f.state.settings['tts.voice'] = ''; f.state.user.tts_voice = ''; await request('/api/text-to-speech', { method: 'POST', role: 'user', body: { text: 'Synthetic' } }); assert.equal(JSON.parse(f.state.requests.at(-1).options.body).voice, 'F1'); const sttBody = { audioBase64: Buffer.from('synthetic').toString('base64') }; f.state.httpError = 401; const bad = await request('/api/admin/config/stt/test', { method: 'POST', role: 'admin', body: sttBody }); assert.equal(bad.data.success, false); assert.match(bad.data.error, /401/); f.state.httpError = null; const good = await request('/api/admin/config/stt/test', { method: 'POST', role: 'admin', body: sttBody }); assert.equal(good.data.success, true); assert.equal(good.data.text, 'synthetic transcript'); }); test('both actual directory file routes enforce parser bound and retain 1 MiB upload limit', async t => { const f = fixture(); const request = await f.serve(t, true); const transfer = require('../src/utils/extensionTransfer'); const payload = transfer.exportPayload([{ location: 'Synthetic', name: 'Desk', number: '123' }]); const compressed = require('node:zlib').deflateRawSync(Buffer.from(JSON.stringify('x'.repeat(4 * 1024 * 1024 + 1)))); const header = Buffer.alloc(31); header.writeUInt32LE(0x04034b50, 0); header.writeUInt16LE(20, 4); header.writeUInt16LE(8, 8); header.writeUInt32LE(compressed.length, 18); header.writeUInt32LE(1, 22); header.writeUInt16LE(1, 26); header[30] = 97; const bomb = Buffer.concat([header, compressed]); for (const route of ['/api/extensions/import-file/preview', '/api/extensions/import-file']) { for (const file of [Buffer.from(JSON.stringify(payload)), transfer.createJsonZip('directory.json', payload), bomb, Buffer.alloc(1024 * 1024 + 1, 32)]) { const form = new FormData(); form.append('file', new Blob([file]), 'directory.zip'); const before = f.state.writes.length; const response = await request(route, { method: 'POST', authenticated: true, body: form }); if (file === bomb || file.length > 1024 * 1024) { assert.ok(response.status >= 400); assert.equal(f.state.writes.length, before); } else { assert.equal(response.status, 200); assert.equal(response.data.success, true); } } } }); test('Azure configured deployment cannot bypass final roster through provider URL or streaming model override', async () => { const f = fixture({ AI_PROVIDER: 'azure', AZURE_OPENAI_ENDPOINT: 'https://azure.example', AZURE_OPENAI_API_KEY: 'synthetic', AZURE_DEPLOYMENT_NAME: 'blocked-deployment' }); const ai = f.load('src/utils/ai.js'); for (const call of [o => ai.callAI([], o), o => ai.callAIStream([], o, () => {})]) { await assert.rejects(call({ model: 'allowed' }), /Model not permitted/); assert.equal(f.state.requests.length, 0); } }); test('enabled personal Nextcloud and memory CRUD remain usable through actual handlers with fake storage/WebDAV', async t => { const f = fixture(); const request = await f.serve(t, true); f.state.allowDav = true; f.state.user.nextcloud_url = 'https://cloud.example'; f.state.user.nextcloud_user = 'synthetic'; f.state.user.nextcloud_token = 'synthetic-token'; for (const [route, body] of [ ['/api/nextcloud/connect', { nextcloudUrl: 'https://cloud.example', username: 'synthetic', appPassword: 'synthetic-token' }], ['/api/nextcloud/export', { content: 'Synthetic document', type: 'note' }], ['/api/user/webdav-path', { path: '/Synthetic' }], ['/api/nextcloud/disconnect', {}] ]) { const response = await request(route, { method: 'POST', role: 'user', body }); assert.equal(response.status, 200, route); assert.equal(response.data.success, true); } const browse = await request('/api/admin/learning/webdav-browse', { role: 'moderator' }); assert.equal(browse.status, 200); assert.equal(browse.data.success, true); assert.ok(f.state.requests.some(r => r.dav && r.dav.method === 'PUT')); for (const [method, route] of [['POST', '/api/memories'], ['PUT', '/api/memories/1'], ['DELETE', '/api/memories/1']]) { const response = await request(route, { method, role: 'user', body: { name: 'Synthetic template', category: 'physical_exam', content: 'Synthetic content' } }); assert.equal(response.status, 200); assert.equal(response.data.success, true); } }); test('LearningAI saved WebDAV path requires enabled Nextcloud for admins and moderators, without writes on denial/outage', async t => { const f = fixture(); const request = await f.serve(t, true); for (const role of ['admin', 'moderator']) { for (const unavailable of [false, true]) { f.state.settings['feature.nextcloud'] = 'false'; f.state.settingsError = unavailable; const before = f.state.writes.length; const response = await request('/api/admin/learning/webdav-path', { method: 'POST', role, body: { path: '/Synthetic' } }); assert.equal(response.status, unavailable ? 503 : 403); assert.equal(f.state.writes.length, before); assert.equal(f.state.requests.length, 0); } f.state.settingsError = false; f.state.settings['feature.nextcloud'] = 'true'; const before = f.state.writes.length; const response = await request('/api/admin/learning/webdav-path', { method: 'POST', role, body: { path: '/Synthetic' } }); assert.equal(response.status, 200); assert.equal(response.data.success, true); assert.equal(f.state.writes.length, before + 1); assert.match(f.state.writes.at(-1).sql, /UPDATE users SET webdav_learning_path/); assert.deepEqual(Array.from(f.state.writes.at(-1).params), ['/Synthetic', 7]); } const before = f.state.writes.length; assert.equal((await request('/api/admin/learning/webdav-path', { method: 'POST', role: 'user', body: { path: '/Synthetic' } })).status, 403); assert.equal(f.state.writes.length, before); }); test('actual native admin script disables selected default, displays backend replacement, and saves it; discovery excludes disabled options', async t => { const { JSDOM } = require('jsdom'); const { pathToFileURL } = require('node:url'); const f = fixture({ AI_PROVIDER: 'openrouter' }); const request = await f.serve(t, true); const models = f.load('src/utils/models.js').getAvailableModels(); const selected = models[0].id; f.state.settings['models.default'] = selected; f.state.settings['models.disabled'] = JSON.stringify([models[1].id, 'allowed']); const dom = new JSDOM(fs.readFileSync(path.join(root, 'public/components/admin.html'), 'utf8'), { url: 'https://app.example' }); const originals = new Map(['window', 'document', 'fetch', 'getAuthHeaders', 'showToast'].map(key => [key, Object.getOwnPropertyDescriptor(global, key)])); t.after(() => { for (const [key, value] of originals) { if (value) Object.defineProperty(global, key, value); else delete global[key]; } dom.window.close(); }); const toasts = []; const calls = []; Object.assign(global, { window: dom.window, document: dom.window.document, getAuthHeaders: () => ({}), showToast: (...args) => toasts.push(args) }); global.fetch = async (url, options = {}) => { calls.push(url); if (url === '/api/admin/config/models' || url.startsWith('/api/admin/config/models/')) { if (url.includes('/discover?')) return { json: async () => ({ success: true, count: 1, models: [{ id: 'discovered', name: 'Discovered' }] }) }; const response = await request(url, { method: options.method || 'GET', role: 'admin', body: options.body ? JSON.parse(options.body) : undefined }); return { json: async () => response.data }; } return { json: async () => ({}) }; // unrelated admin panels are outside this regression }; await import(pathToFileURL(path.join(root, 'public/js/admin.js')).href); // Native Node imports share global.fetch; preserve loopback transport for the real handlers. const browserFetch = global.fetch; global.fetch = async (url, options) => String(url).startsWith('http://127.0.0.1:') ? originals.get('fetch').value(url, options) : browserFetch(url, options); const waitFor = async predicate => { for (let i = 0; i < 100; i++) { if (predicate()) return; await new Promise(resolve => setTimeout(resolve, 5)); } assert.fail('Admin UI did not settle'); }; document.dispatchEvent(new dom.window.CustomEvent('tabChanged', { detail: { tab: 'admin' } })); const select = document.getElementById('admin-default-model'); await waitFor(() => select.value === selected); const assertDisabledAbsent = () => { for (const id of JSON.parse(f.state.settings['models.disabled'])) assert.ok(!Array.from(select.options).some(o => o.value === id), id); }; assertDisabledAbsent(); const cb = document.querySelector('.admin-model-toggle[data-model-id="' + selected + '"]'); cb.checked = false; cb.dispatchEvent(new dom.window.Event('change')); await waitFor(() => select.value && select.value !== selected); const advertised = await request('/api/models'); assert.equal(select.value, advertised.data.defaultModel); assertDisabledAbsent(); document.getElementById('btn-save-default-model').click(); await waitFor(() => toasts.some(([message]) => message.startsWith('Default model set:'))); assert.equal(f.state.settings['models.default'], select.value); assert.equal(calls.filter(url => url === '/api/admin/config/models').length, 2, 'successful toggle refreshes models'); document.getElementById('btn-discover-models').click(); await waitFor(() => document.querySelector('.admin-add-discovered')); document.querySelector('.admin-add-discovered').click(); await waitFor(() => select.value === 'discovered'); assertDisabledAbsent(); assert.ok(Array.from(select.options).some(o => o.value === 'other'), 'enabled custom model retained'); });