Changeset 0.9.0 (#50)
This commit is contained in:
247
src/js/__tests__/policy-gating.test.js
Normal file
247
src/js/__tests__/policy-gating.test.js
Normal file
@@ -0,0 +1,247 @@
|
||||
// ==========================================
|
||||
// Policy Gating & UI Logic Tests
|
||||
// ==========================================
|
||||
// Validates that admin policies correctly
|
||||
// gate user-facing features. These catch
|
||||
// the exact class of bugs where a backend
|
||||
// policy exists but the frontend doesn't
|
||||
// check it.
|
||||
//
|
||||
// Run: node --test src/js/__tests__/policy-gating.test.js
|
||||
// ==========================================
|
||||
|
||||
const { describe, it } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const SRC = path.join(__dirname, '..');
|
||||
|
||||
// ── Source code audits ───────────────────────
|
||||
// These tests read the actual source files and verify that required
|
||||
// wiring exists. If someone removes a policy check, CI breaks.
|
||||
|
||||
describe('Policy wiring audit — source code', () => {
|
||||
const uiSrc = fs.readFileSync(path.join(SRC, 'ui.js'), 'utf-8');
|
||||
const appSrc = fs.readFileSync(path.join(SRC, 'app.js'), 'utf-8');
|
||||
const indexSrc = fs.readFileSync(path.join(SRC, '..', 'index.html'), 'utf-8');
|
||||
|
||||
// ── allow_user_byok ──
|
||||
|
||||
it('ui.js has checkUserProvidersAllowed function', () => {
|
||||
assert.ok(uiSrc.includes('checkUserProvidersAllowed'),
|
||||
'MISSING: checkUserProvidersAllowed — provider tab will show when policy is off');
|
||||
});
|
||||
|
||||
it('checkUserProvidersAllowed reads App.policies.allow_user_byok', () => {
|
||||
assert.ok(uiSrc.includes('allow_user_byok'),
|
||||
'MISSING: allow_user_byok check in UI');
|
||||
});
|
||||
|
||||
// ── allow_user_personas ──
|
||||
|
||||
it('ui.js has checkUserPresetsAllowed function', () => {
|
||||
assert.ok(uiSrc.includes('checkUserPresetsAllowed'),
|
||||
'MISSING: checkUserPresetsAllowed — preset button will show when policy is off');
|
||||
});
|
||||
|
||||
it('checkUserPresetsAllowed reads App.policies.allow_user_personas', () => {
|
||||
assert.ok(uiSrc.includes('allow_user_personas'),
|
||||
'MISSING: allow_user_personas check in UI');
|
||||
});
|
||||
|
||||
it('admin settings UI has adminUserPresetsToggle', () => {
|
||||
assert.ok(indexSrc.includes('adminUserPresetsToggle'),
|
||||
'MISSING: preset toggle in admin settings HTML');
|
||||
});
|
||||
|
||||
it('admin settings load reads allow_user_personas', () => {
|
||||
assert.ok(uiSrc.includes("adminUserPresetsToggle"),
|
||||
'MISSING: loadAdminSettings must read allow_user_personas into toggle');
|
||||
});
|
||||
|
||||
it('admin settings save writes allow_user_personas', () => {
|
||||
assert.ok(appSrc.includes("allow_user_personas"),
|
||||
'MISSING: handleSaveAdminSettings must write allow_user_personas');
|
||||
});
|
||||
|
||||
// ── Settings live-apply ──
|
||||
|
||||
it('admin save calls initBanners for policy refresh', () => {
|
||||
assert.ok(appSrc.includes('await initBanners()'),
|
||||
'MISSING: initBanners call after admin save — policies won\'t refresh');
|
||||
});
|
||||
|
||||
it('admin save calls checkUserProvidersAllowed', () => {
|
||||
// Find in handleSaveAdminSettings
|
||||
const saveFunc = appSrc.slice(appSrc.indexOf('handleSaveAdminSettings'));
|
||||
assert.ok(saveFunc.includes('checkUserProvidersAllowed'),
|
||||
'MISSING: checkUserProvidersAllowed call after admin save');
|
||||
});
|
||||
|
||||
it('admin save calls checkUserPresetsAllowed', () => {
|
||||
const saveFunc = appSrc.slice(appSrc.indexOf('handleSaveAdminSettings'));
|
||||
assert.ok(saveFunc.includes('checkUserPresetsAllowed'),
|
||||
'MISSING: checkUserPresetsAllowed call after admin save');
|
||||
});
|
||||
|
||||
it('admin save calls fetchModels to refresh model list', () => {
|
||||
const saveFunc = appSrc.slice(appSrc.indexOf('handleSaveAdminSettings'));
|
||||
assert.ok(saveFunc.includes('fetchModels'),
|
||||
'MISSING: fetchModels call after admin save — model list stale after settings change');
|
||||
});
|
||||
|
||||
// ── Models tab calls preset check ──
|
||||
|
||||
it('models tab switch calls checkUserPresetsAllowed', () => {
|
||||
assert.ok(uiSrc.includes('checkUserPresetsAllowed'),
|
||||
'MISSING: checkUserPresetsAllowed call on models tab switch');
|
||||
});
|
||||
});
|
||||
|
||||
// ── Policy evaluation logic ──────────────────
|
||||
|
||||
describe('Policy evaluation logic', () => {
|
||||
function isPolicyEnabled(policies, key) {
|
||||
return policies?.[key] === 'true';
|
||||
}
|
||||
|
||||
it('policy "true" (string) → enabled', () => {
|
||||
assert.equal(isPolicyEnabled({ allow_user_byok: 'true' }, 'allow_user_byok'), true);
|
||||
});
|
||||
|
||||
it('policy "false" (string) → disabled', () => {
|
||||
assert.equal(isPolicyEnabled({ allow_user_byok: 'false' }, 'allow_user_byok'), false);
|
||||
});
|
||||
|
||||
it('policy boolean true → NOT enabled (must be string)', () => {
|
||||
assert.equal(isPolicyEnabled({ allow_user_byok: true }, 'allow_user_byok'), false,
|
||||
'DANGER: policies are string "true"/"false", not boolean');
|
||||
});
|
||||
|
||||
it('missing policy → disabled', () => {
|
||||
assert.equal(isPolicyEnabled({}, 'allow_user_byok'), false);
|
||||
});
|
||||
|
||||
it('null policies → disabled', () => {
|
||||
assert.equal(isPolicyEnabled(null, 'allow_user_byok'), false);
|
||||
});
|
||||
|
||||
it('undefined policies → disabled', () => {
|
||||
assert.equal(isPolicyEnabled(undefined, 'allow_user_byok'), false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Team member dropdown logic ───────────────
|
||||
// Bug: Empty dropdown because resp.data used instead of resp.users
|
||||
|
||||
describe('Team member dropdown population', () => {
|
||||
function getAvailableUsers(usersResp, membersResp) {
|
||||
const users = usersResp.users || usersResp.data || [];
|
||||
const existingIds = new Set((membersResp.data || []).map(m => m.user_id));
|
||||
return users.filter(u => u.is_active && !existingIds.has(u.id));
|
||||
}
|
||||
|
||||
it('extracts users from {users:[]} response', () => {
|
||||
const usersResp = { users: [
|
||||
{ id: 'u1', username: 'alice', is_active: true },
|
||||
{ id: 'u2', username: 'bob', is_active: true },
|
||||
]};
|
||||
const membersResp = { data: [] };
|
||||
const available = getAvailableUsers(usersResp, membersResp);
|
||||
assert.equal(available.length, 2);
|
||||
});
|
||||
|
||||
it('falls back to {data:[]} response (backward compat)', () => {
|
||||
const usersResp = { data: [
|
||||
{ id: 'u1', username: 'alice', is_active: true },
|
||||
]};
|
||||
const membersResp = { data: [] };
|
||||
const available = getAvailableUsers(usersResp, membersResp);
|
||||
assert.equal(available.length, 1);
|
||||
});
|
||||
|
||||
it('excludes existing team members', () => {
|
||||
const usersResp = { users: [
|
||||
{ id: 'u1', username: 'alice', is_active: true },
|
||||
{ id: 'u2', username: 'bob', is_active: true },
|
||||
]};
|
||||
const membersResp = { data: [{ user_id: 'u1', role: 'admin' }] };
|
||||
const available = getAvailableUsers(usersResp, membersResp);
|
||||
assert.equal(available.length, 1);
|
||||
assert.equal(available[0].id, 'u2');
|
||||
});
|
||||
|
||||
it('excludes inactive users', () => {
|
||||
const usersResp = { users: [
|
||||
{ id: 'u1', username: 'alice', is_active: true },
|
||||
{ id: 'u2', username: 'disabled', is_active: false },
|
||||
]};
|
||||
const membersResp = { data: [] };
|
||||
const available = getAvailableUsers(usersResp, membersResp);
|
||||
assert.equal(available.length, 1);
|
||||
assert.equal(available[0].id, 'u1');
|
||||
});
|
||||
|
||||
it('returns empty for empty users response', () => {
|
||||
const available = getAvailableUsers({ users: [] }, { data: [] });
|
||||
assert.equal(available.length, 0);
|
||||
});
|
||||
|
||||
it('handles completely missing response fields', () => {
|
||||
const available = getAvailableUsers({}, {});
|
||||
assert.equal(available.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Admin settings field mapping ─────────────
|
||||
|
||||
describe('Admin settings field mapping', () => {
|
||||
// Maps what the frontend sends to what the backend expects
|
||||
const settingsFieldMap = {
|
||||
'adminRegToggle': 'allow_registration',
|
||||
'adminRegDefaultState': 'default_user_active',
|
||||
'adminUserProvidersToggle': 'allow_user_byok',
|
||||
'adminUserPresetsToggle': 'allow_user_personas',
|
||||
'adminBannerEnabled': 'banner',
|
||||
};
|
||||
|
||||
const appSrc = fs.readFileSync(path.join(SRC, 'app.js'), 'utf-8');
|
||||
const indexSrc = fs.readFileSync(path.join(SRC, '..', 'index.html'), 'utf-8');
|
||||
|
||||
for (const [elementId, settingKey] of Object.entries(settingsFieldMap)) {
|
||||
it(`HTML has element #${elementId}`, () => {
|
||||
assert.ok(indexSrc.includes(`id="${elementId}"`),
|
||||
`MISSING: #${elementId} in index.html — admin settings incomplete`);
|
||||
});
|
||||
|
||||
it(`app.js writes setting "${settingKey}"`, () => {
|
||||
assert.ok(appSrc.includes(settingKey),
|
||||
`MISSING: "${settingKey}" in handleSaveAdminSettings`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ── HTML element existence checks ────────────
|
||||
|
||||
describe('Critical HTML elements exist', () => {
|
||||
const indexSrc = fs.readFileSync(path.join(SRC, '..', 'index.html'), 'utf-8');
|
||||
|
||||
const requiredElements = [
|
||||
'adminMemberUser', // Team member user dropdown
|
||||
'userPresetList', // User preset list container
|
||||
'userAddPresetBtn', // New preset button (policy-gated)
|
||||
'userAddPresetForm', // Preset form container
|
||||
'userProvidersDisabled', // BYOK disabled notice
|
||||
'providerShowAddBtn', // Add provider button (policy-gated)
|
||||
'adminUserProvidersToggle', // Admin toggle for BYOK
|
||||
'adminUserPresetsToggle', // Admin toggle for presets
|
||||
];
|
||||
|
||||
for (const id of requiredElements) {
|
||||
it(`#${id} exists in index.html`, () => {
|
||||
assert.ok(indexSrc.includes(`id="${id}"`),
|
||||
`MISSING element: #${id} — UI feature will break`);
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user