Files
Learn_System/backend/tests/custom-roles.test.js
T
Maxim Dolgolyov 32c2c44b76 feat(permissions): C-3 — пер-ролевые права кастомных ролей (резолвер + конфиг)
Миграция 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>
2026-06-03 15:11:56 +03:00

109 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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');
});
});