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> @
109 lines
4.9 KiB
JavaScript
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}`);
|
|
});
|
|
});
|