aa20892a79
- мощный визуал: уравнение на яркой градиентной «сцене» (бело на индиго→фиолет, текстура-клетка), рабочая зона снизу на белом; верный ответ заливает сцену изумрудом, неверный — красным (с pop/shake) - конструктор генераторов — ТОЛЬКО админ: страница /trainer-builder гейт ip.isAdmin; роуты POST/PUT/DELETE /generators → requireRole(admin); сайдбар-пункт hidden:!isAdm - выделен отдельным цветом: янтарная кнопка «Конструктор» в баре режима (только админ) → /trainer-builder - тема пользовательских генераторов: «Мои генераторы» для админа / «Авторские» для остальных (видят published) - тесты custom-generators 13/13 (админ создаёт; учитель/ученик 403); страница-смоук 33/33; эмодзи/eval 0 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
107 lines
4.9 KiB
JavaScript
107 lines
4.9 KiB
JavaScript
'use strict';
|
|
/**
|
|
* Tests: конструктор генераторов тренажёра (P13) — валидация + CRUD + доступ.
|
|
*/
|
|
const { describe, it, before, after } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { app, inject, getToken, cleanup } = require('./setup');
|
|
const cg = require('../src/controllers/customGeneratorController');
|
|
|
|
app.use('/api/practice', require('../src/routes/practice'));
|
|
after(() => cleanup());
|
|
|
|
const SPEC = {
|
|
title: 'Моё уравнение', topic: 'custom', kind: 'solve',
|
|
pick: { a: [2, 9], b: [1, 20], root: [-9, 9] },
|
|
derive: { c: 'a*root + b', cmb: 'a*root' },
|
|
require: 'root != 0',
|
|
lhs: '{a}*x + {b}', rhs: '{c}', answer: 'root', integerAnswer: true,
|
|
solution: [{ note: 'делим на {a}', tex: 'x = {cmb} / {a}' }]
|
|
};
|
|
|
|
describe('validateGenSpec', () => {
|
|
it('принимает корректный спек', () => {
|
|
const v = cg.validateGenSpec(SPEC);
|
|
assert.equal(v.ok, true, v.error);
|
|
assert.equal(v.clean.kind, 'solve');
|
|
assert.deepEqual(v.clean.pick.a, [2, 9]);
|
|
assert.equal(v.clean.integerAnswer, true);
|
|
});
|
|
it('отвергает без заголовка', () => {
|
|
assert.equal(cg.validateGenSpec(Object.assign({}, SPEC, { title: '' })).ok, false);
|
|
});
|
|
it('фильтрует нецелые диапазоны pick', () => {
|
|
const v = cg.validateGenSpec(Object.assign({}, SPEC, { pick: { a: [1.5, 9], b: [1, 20] } }));
|
|
assert.equal(v.ok, true);
|
|
assert.equal(v.clean.pick.a, undefined, 'нецелый диапазон отброшен');
|
|
assert.deepEqual(v.clean.pick.b, [1, 20]);
|
|
});
|
|
it('отвергает слишком большой спек', () => {
|
|
assert.equal(cg.validateGenSpec(Object.assign({}, SPEC, { display: 'x'.repeat(30000) })).ok, false);
|
|
});
|
|
});
|
|
|
|
describe('/api/practice/generators CRUD (конструктор — только админ)', () => {
|
|
let admin, other, teacher, student, gid;
|
|
before(async () => {
|
|
admin = (await getToken('admin')).token;
|
|
other = (await getToken('admin')).token;
|
|
teacher = (await getToken('teacher')).token;
|
|
student = (await getToken('student')).token;
|
|
});
|
|
|
|
it('админ создаёт генератор', async () => {
|
|
const res = await inject('POST', '/api/practice/generators', { spec: SPEC }, admin);
|
|
assert.equal(res.status, 200, `got ${res.status}`);
|
|
assert.equal(res.body.ok, true);
|
|
assert.ok(/^cg\d+$/.test(res.body.generator.id), 'id вида cg<dbid>');
|
|
gid = res.body.generator.dbid;
|
|
});
|
|
|
|
it('учителю создавать запрещено (403)', async () => {
|
|
const res = await inject('POST', '/api/practice/generators', { spec: SPEC }, teacher);
|
|
assert.equal(res.status, 403, `got ${res.status}`);
|
|
});
|
|
|
|
it('ученику создавать запрещено (403)', async () => {
|
|
const res = await inject('POST', '/api/practice/generators', { spec: SPEC }, student);
|
|
assert.equal(res.status, 403, `got ${res.status}`);
|
|
});
|
|
|
|
it('невалидный спек → 400', async () => {
|
|
const res = await inject('POST', '/api/practice/generators', { spec: { title: '' } }, admin);
|
|
assert.equal(res.status, 400, `got ${res.status}`);
|
|
});
|
|
|
|
it('автор видит свой генератор в списке', async () => {
|
|
const res = await inject('GET', '/api/practice/generators', null, admin);
|
|
assert.equal(res.status, 200);
|
|
assert.ok(res.body.generators.some(g => g.dbid === gid), 'свой генератор в списке');
|
|
});
|
|
|
|
it('чужой draft не виден другому админу', async () => {
|
|
const res = await inject('GET', '/api/practice/generators', null, other);
|
|
assert.ok(!res.body.generators.some(g => g.dbid === gid), 'чужой draft скрыт');
|
|
});
|
|
|
|
it('учителю изменять запрещено (403, роль)', async () => {
|
|
const res = await inject('PUT', '/api/practice/generators/' + gid, { spec: SPEC }, teacher);
|
|
assert.equal(res.status, 403, `got ${res.status}`);
|
|
});
|
|
|
|
it('публикация делает генератор видимым другим (и ученику)', async () => {
|
|
const pub = await inject('PUT', '/api/practice/generators/' + gid, { status: 'published' }, admin);
|
|
assert.equal(pub.status, 200);
|
|
assert.equal(pub.body.generator.status, 'published');
|
|
const res = await inject('GET', '/api/practice/generators', null, student);
|
|
assert.ok(res.body.generators.some(g => g.dbid === gid), 'published виден ученику');
|
|
});
|
|
|
|
it('автор удаляет свой генератор', async () => {
|
|
const res = await inject('DELETE', '/api/practice/generators/' + gid, null, admin);
|
|
assert.equal(res.status, 200);
|
|
const after = await inject('GET', '/api/practice/generators/' + gid, null, admin);
|
|
assert.equal(after.status, 404, 'после удаления 404');
|
|
});
|
|
});
|