be4d43105e
Node.js/Express backend + vanilla JS frontend. Features: real-time collaborative whiteboard (SSE), multi-page support, LaTeX formulas, shapes/connectors, coordinate systems, number lines, compass, zoom/pan, Catmull-Rom pencil smoothing, ruler/protractor with rotation & resize controls, minimap navigation overlay, auto-measurements, multi-page thumbnails sidebar, PNG export, page templates. Student/teacher workflows: classes, assignments, library, dashboard. Mobile responsive. SQLite (better-sqlite3). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
87 lines
3.1 KiB
JavaScript
87 lines
3.1 KiB
JavaScript
const { describe, it, before, after } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { db, inject, getToken, cleanup } = require('./setup');
|
|
|
|
after(() => cleanup());
|
|
|
|
describe('Shop', () => {
|
|
let studentToken, studentId, adminToken, itemId;
|
|
|
|
before(async () => {
|
|
const s = await getToken('student');
|
|
studentToken = s.token; studentId = s.userId;
|
|
const a = await getToken('admin');
|
|
adminToken = a.token;
|
|
|
|
// Give student some coins
|
|
db.prepare('UPDATE users SET coins = 500 WHERE id = ?').run(studentId);
|
|
|
|
// Create a shop item via admin
|
|
const res = await inject('POST', '/api/shop/admin/items', {
|
|
name: 'Test Frame', type: 'frame', price: 100, category: 'cosmetic',
|
|
data: '{"css":"box-shadow:0 0 5px red"}', icon: 'star'
|
|
}, adminToken);
|
|
assert.equal(res.status, 200);
|
|
itemId = res.body.id;
|
|
});
|
|
|
|
it('lists items for student', async () => {
|
|
const res = await inject('GET', '/api/shop/items', null, studentToken);
|
|
assert.equal(res.status, 200);
|
|
assert.ok(Array.isArray(res.body.items));
|
|
assert.equal(res.body.coins, 500);
|
|
});
|
|
|
|
it('purchases item atomically', async () => {
|
|
const res = await inject('POST', `/api/shop/items/${itemId}/purchase`, {}, studentToken);
|
|
assert.equal(res.status, 200);
|
|
assert.equal(res.body.ok, true);
|
|
assert.equal(res.body.coins, 400); // 500 - 100
|
|
});
|
|
|
|
it('rejects duplicate purchase', async () => {
|
|
const res = await inject('POST', `/api/shop/items/${itemId}/purchase`, {}, studentToken);
|
|
assert.equal(res.status, 400);
|
|
assert.ok(res.body.error.includes('уже купили'));
|
|
});
|
|
|
|
it('rejects purchase with insufficient coins', async () => {
|
|
// Create expensive item
|
|
const cr = await inject('POST', '/api/shop/admin/items', {
|
|
name: 'Expensive', type: 'title', price: 99999
|
|
}, adminToken);
|
|
const expId = cr.body.id;
|
|
|
|
const res = await inject('POST', `/api/shop/items/${expId}/purchase`, {}, studentToken);
|
|
assert.equal(res.status, 400);
|
|
assert.ok(res.body.error.includes('монет'));
|
|
});
|
|
|
|
it('activates purchased item', async () => {
|
|
const res = await inject('POST', '/api/shop/activate', { itemId }, studentToken);
|
|
assert.equal(res.status, 200);
|
|
assert.equal(res.body.type, 'frame');
|
|
});
|
|
|
|
it('validates activate type field', async () => {
|
|
const res = await inject('POST', '/api/shop/activate', { type: 'invalid' }, studentToken);
|
|
assert.equal(res.status, 400);
|
|
});
|
|
|
|
it('validates admin create item fields', async () => {
|
|
const res = await inject('POST', '/api/shop/admin/items', { name: 'X' }, adminToken);
|
|
assert.equal(res.status, 400); // missing type, price
|
|
});
|
|
|
|
it('validates admin award-coins fields', async () => {
|
|
const res = await inject('POST', '/api/shop/admin/award-coins', { userId: 'abc' }, adminToken);
|
|
assert.equal(res.status, 400);
|
|
});
|
|
|
|
it('returns coin balance', async () => {
|
|
const res = await inject('GET', '/api/shop/coins', null, studentToken);
|
|
assert.equal(res.status, 200);
|
|
assert.equal(typeof res.body.coins, 'number');
|
|
});
|
|
});
|