Files
Haven/test/extensionUpdates.integration.test.js
T
ancsemi 695604bbb1 Move extension updates into their own files
A pure move: the two admin routes leave server.js for
src/routes/extensionUpdates.js (io is read through late, as backup.js does,
since it does not exist yet at registration), and the Settings UI leaves
app-admin-controls.js for a new app-extension-updates.js mixin. Proved with
an identical client method set and identical route handlers.
2026-10-03 15:46:55 -04:00

98 lines
3.8 KiB
JavaScript

'use strict';
// Use the actual route handlers on loopback to check the HTTP authorization
// boundary. The updater is a stub here; file/download behavior lives in the
// unit tests. Flip admin status during apply to model an in-flight demotion.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const express = require('express');
test('admin API rejects anonymous and non-admin callers and rechecks authorization at apply', async t => {
const app = express();
app.use(express.json());
let admin = true;
let invoked = 0;
// Register the production routes with isolated dependencies, without
// starting voice services or opening a user DB.
require('../src/routes/extensionUpdates')({
app,
verifyToken: token => token === 'scoped' ? { id: 'admin', purpose: 'connect' } : { id: token },
verifyAdminFromDb: user => user.id === 'admin' && admin,
extensionUpdater: {
check: async () => { invoked++; return {}; },
apply: async (token, user, authorized) => {
admin = false;
assert.equal(authorized(), false);
throw new Error('Administrator permission is required.');
},
},
late: { io: { emit() { assert.fail('failed update must not notify clients'); } } },
});
// server.js registers them with the same dependencies.
const serverSource = fs.readFileSync(path.join(__dirname, '../server.js'), 'utf8');
assert.match(serverSource, /require\('\.\/src\/routes\/extensionUpdates'\)\(\{ app, verifyToken, verifyAdminFromDb, extensionUpdater, late \}\)/);
const server = app.listen(0, '127.0.0.1');
await new Promise((resolve, reject) => {
server.once('listening', resolve);
server.once('error', reject);
});
t.after(() => new Promise(resolve => server.close(resolve)));
const url = `http://127.0.0.1:${server.address().port}/api/admin/extensions`;
assert.equal((await fetch(url + '/check', { method: 'POST' })).status, 401);
assert.equal(
(await fetch(url + '/check', { method: 'POST', headers: { Authorization: 'Bearer member' } })).status,
403,
);
assert.equal((await fetch(url + '/check', { method: 'POST', headers: { Authorization: 'Bearer scoped' } })).status, 401);
assert.equal(invoked, 0);
assert.equal(
(await fetch(url + '/check', { method: 'POST', headers: { Authorization: 'Bearer admin' } })).status,
200,
);
assert.equal(
(await fetch(url + '/apply', { method: 'POST', headers: { Authorization: 'Bearer admin' } })).status,
400,
);
});
test('a successful update broadcasts a reload notification to connected clients', async t => {
const app = express();
app.use(express.json());
const emitted = [];
require('../src/routes/extensionUpdates')({
app,
verifyToken: () => ({ id: 'admin' }),
verifyAdminFromDb: () => true,
extensionUpdater: {
check: async () => ({}),
apply: async (token, user, authorized) => {
assert.equal(token, 'approved-offer');
assert.equal(user, 'admin');
assert.equal(authorized(), true);
return { version: '1.1.0', reloadRequired: true };
},
},
late: { io: { emit(event) { emitted.push(event); } } },
});
const server = app.listen(0, '127.0.0.1');
await new Promise((resolve, reject) => {
server.once('listening', resolve);
server.once('error', reject);
});
t.after(() => new Promise(resolve => server.close(resolve)));
const response = await fetch(
`http://127.0.0.1:${server.address().port}/api/admin/extensions/apply`,
{
method: 'POST',
headers: { Authorization: 'Bearer admin', 'Content-Type': 'application/json' },
body: JSON.stringify({ token: 'approved-offer' }),
},
);
assert.equal(response.status, 200);
assert.deepEqual(emitted, ['extensions-updated']);
});