Files
Learn_System/backend/tests/game.test.js
Maxim Dolgolyov 351251d652 @
feat(quantik-game): фаза 1 — оболочка игры + физ-уровень + прогресс (MVP)

Страница /quantik монтирует уровень-спеку в SimEngine (игровой режим: HUD из
Ф0 + слайдеры закона + play/reset), на победу шлёт результат и показывает
экран успеха (звёзды/время/попытки, inline SVG). Уровень phys-artillery-1
как данные (levels.js): гравитация + запуск тела из угла/скорости, портал,
бонус-звезда. Бэкенд: миграция 076 game_progress (UNIQUE user+level),
/api/game/progress (GET свой / POST upsert best time/stars, attempts++,
auth-only, валидация входа), клиент LS.gameProgress*, пункт сайдбара.
game.test.js 13/13; npm test 251 pass/8 baseline; lint:routes 0.
Уровень проверен на реальном интеграторе (311 выигрышных комбо, 31 на 3★).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-13 15:31:25 +03:00

109 lines
4.9 KiB
JavaScript

'use strict';
/**
* Integration tests: /api/game — прогресс игрока «Квантик» (Фаза 1).
* Covers: submit создаёт строку; лучший результат перезаписывает, худший — нет;
* attempts++; auth-only (401 без токена); валидация входа (400).
*/
const { describe, it, before, after } = require('node:test');
const assert = require('node:assert/strict');
const { app, inject, getToken, cleanup } = require('./setup');
// Mount /api/game on the shared test app (setup.js не монтирует новые роуты).
app.use('/api/game', require('../src/routes/game'));
after(() => cleanup());
const LVL = 'phys-artillery-1';
describe('/api/game progress', () => {
let token;
before(async () => {
token = (await getToken('student')).token;
});
it('GET /progress requires auth (401)', async () => {
const res = await inject('GET', '/api/game/progress', null, null);
assert.equal(res.status, 401, `got ${res.status}`);
});
it('POST /progress requires auth (401)', async () => {
const res = await inject('POST', '/api/game/progress', { level_id: LVL, time_ms: 1000, stars: 1 }, null);
assert.equal(res.status, 401, `got ${res.status}`);
});
it('submit creates a progress row', async () => {
const res = await inject('POST', '/api/game/progress', { level_id: LVL, time_ms: 5000, stars: 1 }, token);
assert.equal(res.status, 200, `got ${res.status}`);
assert.equal(res.body.ok, true);
assert.equal(res.body.progress.level_id, LVL);
assert.equal(res.body.progress.best_time_ms, 5000);
assert.equal(res.body.progress.best_stars, 1);
assert.equal(res.body.progress.attempts, 1);
});
it('GET /progress lists the row', async () => {
const res = await inject('GET', '/api/game/progress', null, token);
assert.equal(res.status, 200, `got ${res.status}`);
assert.ok(Array.isArray(res.body.progress), 'progress is array');
const row = res.body.progress.find(r => r.level_id === LVL);
assert.ok(row, 'level row present');
assert.equal(row.best_time_ms, 5000);
});
it('better result (less time, more stars) overwrites best; attempts++', async () => {
const res = await inject('POST', '/api/game/progress', { level_id: LVL, time_ms: 3200, stars: 2 }, token);
assert.equal(res.status, 200, `got ${res.status}`);
assert.equal(res.body.progress.best_time_ms, 3200, 'time improved');
assert.equal(res.body.progress.best_stars, 2, 'stars improved');
assert.equal(res.body.progress.attempts, 2, 'attempts incremented');
});
it('worse result does NOT overwrite best, but still counts an attempt', async () => {
const res = await inject('POST', '/api/game/progress', { level_id: LVL, time_ms: 9999, stars: 0 }, token);
assert.equal(res.status, 200, `got ${res.status}`);
assert.equal(res.body.progress.best_time_ms, 3200, 'best time kept');
assert.equal(res.body.progress.best_stars, 2, 'best stars kept');
assert.equal(res.body.progress.attempts, 3, 'attempts still incremented');
});
it('progress is per-user (другой игрок начинает с нуля)', async () => {
const other = (await getToken('student')).token;
const res = await inject('POST', '/api/game/progress', { level_id: LVL, time_ms: 7000, stars: 1 }, other);
assert.equal(res.status, 200, `got ${res.status}`);
assert.equal(res.body.progress.attempts, 1, 'fresh user has attempts=1');
assert.equal(res.body.progress.best_time_ms, 7000);
});
it('validation: missing level_id → 400', async () => {
const res = await inject('POST', '/api/game/progress', { time_ms: 1000, stars: 1 }, token);
assert.equal(res.status, 400, `got ${res.status}`);
});
it('validation: negative time_ms → 400', async () => {
const res = await inject('POST', '/api/game/progress', { level_id: LVL, time_ms: -5, stars: 1 }, token);
assert.equal(res.status, 400, `got ${res.status}`);
});
it('validation: non-integer time_ms → 400', async () => {
const res = await inject('POST', '/api/game/progress', { level_id: LVL, time_ms: 12.5, stars: 1 }, token);
assert.equal(res.status, 400, `got ${res.status}`);
});
it('validation: stars out of range (>3) → 400', async () => {
const res = await inject('POST', '/api/game/progress', { level_id: LVL, time_ms: 1000, stars: 4 }, token);
assert.equal(res.status, 400, `got ${res.status}`);
});
it('validation: negative stars → 400', async () => {
const res = await inject('POST', '/api/game/progress', { level_id: LVL, time_ms: 1000, stars: -1 }, token);
assert.equal(res.status, 400, `got ${res.status}`);
});
it('validation: level_id too long → 400', async () => {
const res = await inject('POST', '/api/game/progress',
{ level_id: 'x'.repeat(200), time_ms: 1000, stars: 1 }, token);
assert.equal(res.status, 400, `got ${res.status}`);
});
});