Files
Haven/test/adminRole.integration.test.js
ancsemi 97a23e0598 Admin role fixes: reset to default makes it again, and a deleted one stays deleted
- Reset roles to default now makes the Admin role again and gives it to the
  admin, as on a new server, instead of leaving admin_role_id pointing at
  the role that was just deleted.
- A server converted by the first version of the migration, whose admin had
  since deleted the Admin role, no longer gets a new one on upgrade.
2026-10-02 07:57:19 -04:00

158 lines
7.6 KiB
JavaScript

/**
* The Admin role (#5707) follows admin: a new server's first admin gets it,
* a transfer hands it to the new admin, and a server converted by the first
* version of the migration (which did not note the role's id) is picked up.
*
* node --test test/adminRole.integration.test.js
*/
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const http = require('node:http');
const path = require('node:path');
const fs = require('node:fs');
const os = require('node:os');
const { spawn, execFileSync } = require('node:child_process');
const { io } = require('socket.io-client');
const Database = require('better-sqlite3');
const PORT = 3398;
const BASE = `http://127.0.0.1:${PORT}`;
const ROOT = path.join(__dirname, '..');
const DATA = path.join(os.tmpdir(), `haven-admin-role-${Date.now()}`);
let server;
const post = (p, body) => fetch(BASE + p, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
}).then(async (r) => ({ status: r.status, body: await r.json().catch(() => ({})) }));
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
function connect(token) {
const s = io(BASE, { auth: { token }, transports: ['websocket'], forceNew: true });
return new Promise((res, rej) => { s.on('connect', () => res(s)); s.on('connect_error', rej); });
}
function serverRoles(userId) {
const db = new Database(path.join(DATA, 'haven.db'), { readonly: true });
try {
return db.prepare(`SELECT r.name FROM user_roles ur JOIN roles r ON r.id = ur.role_id
WHERE ur.user_id = ? AND ur.channel_id IS NULL ORDER BY r.name`).all(userId).map((r) => r.name);
} finally { db.close(); }
}
test.before(async () => {
fs.mkdirSync(DATA, { recursive: true });
server = spawn(process.execPath, ['server.js'], {
cwd: ROOT,
env: { ...process.env, PORT: String(PORT), HOST: '127.0.0.1', FORCE_HTTP: 'true', HAVEN_DATA_DIR: DATA, ADMIN_USERNAME: 'admin' },
stdio: 'ignore',
});
for (let i = 0; i < 60; i++) {
try {
await new Promise((res, rej) => http.get(`${BASE}/api/health`, (r) => (r.statusCode === 200 ? res() : rej())).on('error', rej));
return;
} catch { await wait(500); }
}
throw new Error('server did not start');
});
test.after(() => {
server?.kill();
try { fs.rmSync(DATA, { recursive: true, force: true }); } catch { /* ignore */ }
});
test('a new server\'s admin wears the Admin role, and a transfer passes it on', async () => {
const admin = await post('/api/auth/register', { username: 'admin', password: 'rolecheckpw1', eulaVersion: '2.0', ageVerified: true });
assert.ok(admin.body.token, 'admin registered');
const bob = await post('/api/auth/register', { username: 'bob', password: 'rolecheckpw2', eulaVersion: '2.0', ageVerified: true });
assert.ok(bob.body.token, 'bob registered');
const adminId = admin.body.user.id;
const bobId = bob.body.user.id;
assert.ok(serverRoles(adminId).includes('Admin'), 'the first admin has the Admin role');
assert.ok(!serverRoles(bobId).includes('Admin'));
const s = await connect(admin.body.token);
const res = await new Promise((r) => s.emit('transfer-admin', { userId: bobId, password: 'rolecheckpw1' }, r));
assert.ok(res && res.success, `transfer failed: ${JSON.stringify(res)}`);
s.close();
assert.ok(serverRoles(bobId).includes('Admin'), 'the new admin has the Admin role');
assert.deepEqual(serverRoles(adminId), ['Former Admin'], 'the old admin keeps Former Admin only');
// Reset roles to default: the Admin role comes back, on the current admin,
// and admin_role_id points at it rather than at a deleted role.
const sb = await connect(bob.body.token);
const reset = await new Promise((r) => sb.emit('reset-roles-to-default', {}, r));
assert.ok(reset && !reset.error, `reset failed: ${JSON.stringify(reset)}`);
sb.close();
assert.ok(serverRoles(bobId).includes('Admin'), 'the admin wears the Admin role after a reset');
const db = new Database(path.join(DATA, 'haven.db'), { readonly: true });
try {
const id = db.prepare("SELECT value FROM server_settings WHERE key = 'admin_role_id'").get().value;
assert.equal(db.prepare('SELECT name FROM roles WHERE id = ?').get(id)?.name, 'Admin', 'admin_role_id points at the new Admin role');
} finally { db.close(); }
});
test('a converted server whose admin deleted the Admin role does not get it back', () => {
const dir = path.join(os.tmpdir(), `haven-admin-role-gone-${Date.now()}`);
fs.mkdirSync(dir, { recursive: true });
const run = (code) => execFileSync(process.execPath, ['-e', code], { cwd: ROOT, env: { ...process.env, HAVEN_DATA_DIR: dir }, encoding: 'utf8' });
try {
run(`
const db = require('./src/database').initDatabase();
db.prepare("INSERT INTO users (username, password_hash, is_admin) VALUES ('boss', 'x', 1)").run();
const role = db.prepare("SELECT value FROM server_settings WHERE key = 'admin_role_id'").get().value;
db.prepare('DELETE FROM roles WHERE id = ?').run(role);
db.prepare("DELETE FROM server_settings WHERE key = 'admin_role_id'").run();
db.close();
`);
const out = run(`
const db = require('./src/database').initDatabase();
const id = db.prepare("SELECT value FROM server_settings WHERE key = 'admin_role_id'").get().value;
const count = db.prepare("SELECT COUNT(*) c FROM roles WHERE level = 99").get().c;
process.stdout.write(JSON.stringify({ id, count }));
`);
const { id, count } = JSON.parse(out.slice(out.lastIndexOf('{"id"')));
assert.equal(id, 'none', 'noted as having no Admin role');
assert.equal(count, 0, 'no Admin role is made');
} finally {
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
}
});
test('a server converted by the first version of the migration gets its role id noted', () => {
const dir = path.join(os.tmpdir(), `haven-admin-role-old-${Date.now()}`);
fs.mkdirSync(dir, { recursive: true });
const run = (code) => execFileSync(process.execPath, ['-e', code], { cwd: ROOT, env: { ...process.env, HAVEN_DATA_DIR: dir }, encoding: 'utf8' });
try {
// Build the state the first version left: an admin holding a level 99
// "Admin" role, the converted flag set, and no admin_role_id.
run(`
const db = require('./src/database').initDatabase();
db.prepare("INSERT INTO users (username, password_hash, is_admin) VALUES ('boss', 'x', 1)").run();
const role = db.prepare("SELECT value FROM server_settings WHERE key = 'admin_role_id'").get().value;
db.prepare('DELETE FROM roles WHERE id = ?').run(role);
const id = db.prepare("INSERT INTO roles (name, level, scope, color) VALUES ('Admin', 99, 'server', '#e74c3c')").run().lastInsertRowid;
db.prepare("INSERT INTO user_roles (user_id, role_id, channel_id, granted_by) VALUES (1, ?, NULL, 1)").run(id);
db.prepare("DELETE FROM server_settings WHERE key = 'admin_role_id'").run();
db.close();
`);
const out = run(`
const db = require('./src/database').initDatabase();
const id = db.prepare("SELECT value FROM server_settings WHERE key = 'admin_role_id'").get().value;
const role = db.prepare('SELECT name, level FROM roles WHERE id = ?').get(id);
const count = db.prepare("SELECT COUNT(*) c FROM roles WHERE level = 99").get().c;
process.stdout.write(JSON.stringify({ role, count }));
`);
const { role, count } = JSON.parse(out.slice(out.lastIndexOf('{"role"')));
assert.deepEqual(role, { name: 'Admin', level: 99 }, 'the role the admin already wears is the one noted');
assert.equal(count, 1, 'no second Admin role is made');
} finally {
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
}
});