feat(wishes): трекер пожеланий по улучшению системы
Любой авторизованный пользователь подаёт пожелание (заголовок, категория, описание); видит только свои. Админ видит все, фильтрует по статусу, ведёт по статусам (новое → запланировано → в работе → готово / отклонено) и пишет ответ автору. Автор получает уведомление при смене статуса (pushNotif). Бэкенд: миграция 080 (таблица wishes), wishController (list/create/update/remove с валидацией и whitelist категорий/статусов), routes/wishes (PATCH — только админ, DELETE — автор«новое»/админ, проверка в хендлере), смонтировано в server.js. Тесты 15/15. Фронт: страница /wishes (форма + список со статус-бейджами; у админа — фильтры, смена статуса, ответ, удаление), пункт «Пожелания» в сайдбаре (все роли), фиче-флаг feature_wishes_enabled (тумблер в админ-модулях + whitelist + FEATURE_HREFS; админ видит всегда). Клиентские врапперы LS.wish*. ⚠️ Живой БД нужен npm run migrate (080). lint:routes 0; node --check всех файлов + инлайна. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
'use strict';
|
||||
/**
|
||||
* Integration tests: /api/wishes — трекер пожеланий по улучшению.
|
||||
* Covers: auth-only; создание (валидация); приватность (автор видит только свои,
|
||||
* админ — все + counts); триаж только админом (403 ученику); смена статуса; удаление
|
||||
* (автор «новое» / админ; чужое нельзя).
|
||||
*/
|
||||
const { describe, it, before, after } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { app, inject, getToken, cleanup } = require('./setup');
|
||||
|
||||
app.use('/api/wishes', require('../src/routes/wishes'));
|
||||
after(() => cleanup());
|
||||
|
||||
describe('/api/wishes', () => {
|
||||
let s1, s2, admin;
|
||||
|
||||
before(async () => {
|
||||
s1 = await getToken('student');
|
||||
s2 = await getToken('student');
|
||||
admin = await getToken('admin');
|
||||
});
|
||||
|
||||
it('POST /wishes requires auth (401)', async () => {
|
||||
const res = await inject('POST', '/api/wishes', { title: 'x' }, null);
|
||||
assert.equal(res.status, 401);
|
||||
});
|
||||
|
||||
it('создание: пустой заголовок → 400', async () => {
|
||||
const res = await inject('POST', '/api/wishes', { title: ' ' }, s1.token);
|
||||
assert.equal(res.status, 400);
|
||||
});
|
||||
|
||||
let wishId;
|
||||
it('создание пожелания учеником → 201, статус new', async () => {
|
||||
const res = await inject('POST', '/api/wishes',
|
||||
{ title: 'Тёмная тема', body: 'Хочу ночной режим', category: 'ui' }, s1.token);
|
||||
assert.equal(res.status, 201, JSON.stringify(res.body));
|
||||
assert.equal(res.body.status, 'new');
|
||||
assert.equal(res.body.category, 'ui');
|
||||
assert.equal(res.body.user_id, s1.userId);
|
||||
wishId = res.body.id;
|
||||
});
|
||||
|
||||
it('неизвестная категория → other', async () => {
|
||||
const res = await inject('POST', '/api/wishes', { title: 'Что-то', category: 'hack' }, s1.token);
|
||||
assert.equal(res.body.category, 'other');
|
||||
});
|
||||
|
||||
it('приватность: автор видит свои', async () => {
|
||||
const res = await inject('GET', '/api/wishes', null, s1.token);
|
||||
assert.equal(res.status, 200);
|
||||
assert.ok(res.body.wishes.some(w => w.id === wishId));
|
||||
assert.equal(res.body.isAdmin, false);
|
||||
});
|
||||
|
||||
it('приватность: другой ученик НЕ видит чужое', async () => {
|
||||
const res = await inject('GET', '/api/wishes', null, s2.token);
|
||||
assert.ok(!res.body.wishes.some(w => w.id === wishId));
|
||||
});
|
||||
|
||||
it('админ видит все + counts', async () => {
|
||||
const res = await inject('GET', '/api/wishes', null, admin.token);
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(res.body.isAdmin, true);
|
||||
assert.ok(res.body.wishes.some(w => w.id === wishId));
|
||||
assert.ok(res.body.counts && typeof res.body.counts.new === 'number');
|
||||
// у админа в списке есть имя автора
|
||||
const w = res.body.wishes.find(x => x.id === wishId);
|
||||
assert.ok(w.author_name);
|
||||
});
|
||||
|
||||
it('триаж учеником запрещён (403)', async () => {
|
||||
const res = await inject('PATCH', `/api/wishes/${wishId}`, { status: 'done' }, s1.token);
|
||||
assert.equal(res.status, 403);
|
||||
});
|
||||
|
||||
it('админ меняет статус + ответ → 200', async () => {
|
||||
const res = await inject('PATCH', `/api/wishes/${wishId}`,
|
||||
{ status: 'planned', admin_note: 'Запланировано на лето' }, admin.token);
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body));
|
||||
assert.equal(res.body.status, 'planned');
|
||||
assert.equal(res.body.admin_note, 'Запланировано на лето');
|
||||
});
|
||||
|
||||
it('неверный статус → 400', async () => {
|
||||
const res = await inject('PATCH', `/api/wishes/${wishId}`, { status: 'bogus' }, admin.token);
|
||||
assert.equal(res.status, 400);
|
||||
});
|
||||
|
||||
it('фильтр по статусу у админа', async () => {
|
||||
const res = await inject('GET', '/api/wishes?status=planned', null, admin.token);
|
||||
assert.ok(res.body.wishes.every(w => w.status === 'planned'));
|
||||
assert.ok(res.body.wishes.some(w => w.id === wishId));
|
||||
});
|
||||
|
||||
it('автор НЕ может удалить уже обработанное (не new) → 403', async () => {
|
||||
const res = await inject('DELETE', `/api/wishes/${wishId}`, null, s1.token);
|
||||
assert.equal(res.status, 403);
|
||||
});
|
||||
|
||||
it('чужой ученик не может удалить → 403', async () => {
|
||||
const res = await inject('DELETE', `/api/wishes/${wishId}`, null, s2.token);
|
||||
assert.equal(res.status, 403);
|
||||
});
|
||||
|
||||
it('автор удаляет своё «новое» → 200', async () => {
|
||||
const c = await inject('POST', '/api/wishes', { title: 'Черновик' }, s1.token);
|
||||
const res = await inject('DELETE', `/api/wishes/${c.body.id}`, null, s1.token);
|
||||
assert.equal(res.status, 200);
|
||||
});
|
||||
|
||||
it('админ удаляет любое → 200', async () => {
|
||||
const res = await inject('DELETE', `/api/wishes/${wishId}`, null, admin.token);
|
||||
assert.equal(res.status, 200);
|
||||
const gone = await inject('GET', '/api/wishes', null, admin.token);
|
||||
assert.ok(!gone.body.wishes.some(w => w.id === wishId));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user