32c2c44b76
Миграция 056: снят CHECK с role_permissions.role (пересборка) → можно хранить набор прав произвольной кастомной роли. isEnabled(uid,permRole,baseRole,key): user override → role_permissions[customRole] → фолбэк role_permissions[base] → дефолт реестра(base). requirePermission передаёт permRole=customRole||role. getMyPermissions/getUserPermissions: roleMap = база + наложение кастомной роли. Тест C-3: права кастомной роли перекрывают базу, фолбэк на базу. custom-roles 8/8, permissions 17/17, backend без регрессий. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
109 lines
5.6 KiB
JavaScript
109 lines
5.6 KiB
JavaScript
'use strict';
|
||
/**
|
||
* Phase C, Stage C-1 (фундамент): таблица roles + effectiveRoles() — кастомная роль
|
||
* наследует «базовые роли» (какие встроенные гейты requireRole проходит).
|
||
* Здесь тестируем ядро (effectiveRoles) и поведение requireRole-мидлвари на нём.
|
||
* Присвоение кастомной роли пользователю (users.custom_role) — Stage C-2
|
||
* (в канонической схеме у users.role есть CHECK, прямое присвоение невозможно).
|
||
*/
|
||
const { describe, it, before, after } = require('node:test');
|
||
const assert = require('node:assert/strict');
|
||
const { db, getToken, inject, cleanup } = require('./setup');
|
||
const auth = require('../src/middleware/auth');
|
||
|
||
after(() => cleanup());
|
||
|
||
describe('custom roles — effectiveRoles (C-1)', () => {
|
||
before(() => {
|
||
const ins = db.prepare("INSERT OR IGNORE INTO roles (name,label,base_roles,is_builtin) VALUES (?,?,?,0)");
|
||
ins.run('methodist', 'Методист', JSON.stringify(['teacher']));
|
||
ins.run('guestx', 'Гость', JSON.stringify([]));
|
||
});
|
||
|
||
it('встроенная роль раскрывается в саму себя (без БД)', () => {
|
||
assert.deepEqual(auth.effectiveRoles('teacher'), ['teacher']);
|
||
assert.deepEqual(auth.effectiveRoles('admin'), ['admin']);
|
||
});
|
||
|
||
it('кастомная роль наследует base_roles + себя', () => {
|
||
assert.deepEqual(auth.effectiveRoles('methodist').sort(), ['methodist', 'teacher']);
|
||
});
|
||
|
||
it('кастомная роль без base = только она сама', () => {
|
||
assert.deepEqual(auth.effectiveRoles('guestx'), ['guestx']);
|
||
});
|
||
|
||
it('неизвестная роль = только она сама', () => {
|
||
assert.deepEqual(auth.effectiveRoles('nope'), ['nope']);
|
||
});
|
||
|
||
// requireRole-мидлварь на основе effectiveRoles: allow-путь
|
||
function passes(role, allowed) {
|
||
let ok = false;
|
||
const res = { status: () => ({ json: () => {} }) };
|
||
auth.requireRole(...allowed)({ user: { role, id: 1 }, ip: '127.0.0.1', method: 'GET', originalUrl: '/t', headers: {} }, res, () => { ok = true; });
|
||
return ok;
|
||
}
|
||
|
||
it('requireRole пропускает кастомную роль по наследованию', () => {
|
||
assert.equal(passes('methodist', ['teacher', 'admin']), true);
|
||
assert.equal(passes('guestx', ['teacher', 'admin']), false);
|
||
assert.equal(passes('teacher', ['teacher', 'admin']), true);
|
||
assert.equal(passes('student', ['teacher', 'admin']), false);
|
||
});
|
||
});
|
||
|
||
describe('custom roles — назначение пользователю (C-2)', () => {
|
||
let adminToken;
|
||
before(async () => {
|
||
adminToken = (await getToken('admin')).token;
|
||
db.prepare("INSERT OR IGNORE INTO roles (name,label,base_roles,is_builtin) VALUES ('methodist','Методист',?,0)")
|
||
.run(JSON.stringify(['teacher']));
|
||
});
|
||
|
||
it('PATCH role=кастомная → users.role=база, custom_role=имя', async () => {
|
||
const u = await getToken('student');
|
||
const r = await inject('PATCH', `/api/admin/users/${u.userId}/role`, { role: 'methodist' }, adminToken);
|
||
assert.equal(r.status, 200, JSON.stringify(r.body));
|
||
assert.equal(r.body.base, 'teacher');
|
||
const row = db.prepare('SELECT role, custom_role FROM users WHERE id=?').get(u.userId);
|
||
assert.equal(row.role, 'teacher', 'функциональная база = teacher');
|
||
assert.equal(row.custom_role, 'methodist');
|
||
|
||
// обратно на встроенную → custom_role очищается
|
||
const r2 = await inject('PATCH', `/api/admin/users/${u.userId}/role`, { role: 'student' }, adminToken);
|
||
assert.equal(r2.status, 200);
|
||
const row2 = db.prepare('SELECT role, custom_role FROM users WHERE id=?').get(u.userId);
|
||
assert.equal(row2.role, 'student');
|
||
assert.equal(row2.custom_role, null);
|
||
});
|
||
|
||
it('PATCH с несуществующей ролью → 400', async () => {
|
||
const u = await getToken('student');
|
||
const bad = await inject('PATCH', `/api/admin/users/${u.userId}/role`, { role: 'ghostrole' }, adminToken);
|
||
assert.equal(bad.status, 400);
|
||
});
|
||
});
|
||
|
||
describe('custom roles — пер-ролевые права (C-3)', () => {
|
||
let adminToken, mUser;
|
||
before(async () => {
|
||
adminToken = (await getToken('admin')).token;
|
||
db.prepare("INSERT OR IGNORE INTO roles (name,label,base_roles,is_builtin) VALUES ('methodist2','Методист 2',?,0)")
|
||
.run(JSON.stringify(['teacher']));
|
||
mUser = await getToken('student');
|
||
db.prepare("UPDATE users SET role='teacher', custom_role='methodist2' WHERE id=?").run(mUser.userId);
|
||
// кастомная роль ВКЛЮЧАЕТ questions.manage (у teacher по умолчанию 0)
|
||
db.prepare("INSERT OR REPLACE INTO role_permissions (role,permission,enabled) VALUES ('methodist2','questions.manage',1)").run();
|
||
});
|
||
|
||
it('права кастомной роли перекрывают базу; фолбэк на базу для прочих ключей', async () => {
|
||
const view = await inject('GET', `/api/permissions/users/${mUser.userId}`, null, adminToken);
|
||
assert.equal(view.status, 200, JSON.stringify(view.body));
|
||
const qm = view.body.permissions.find(p => p.key === 'questions.manage');
|
||
const cm = view.body.permissions.find(p => p.key === 'classes.manage');
|
||
assert.equal(qm.effective, true, 'questions.manage включён кастомной ролью (база teacher=0)');
|
||
assert.equal(cm.effective, true, 'classes.manage — по фолбэку базы teacher=1');
|
||
});
|
||
});
|