step 10.8: wip (not ticked)

This commit is contained in:
bepis
2026-10-23 00:38:55 +11:00
parent add65e9e5b
commit 46663d892e
9 changed files with 400 additions and 28 deletions
+103
View File
@@ -0,0 +1,103 @@
// Direct unit tests for allocatePoints service function.
const { describe, it, before, after } = require('node:test');
const assert = require('node:assert/strict');
const { setup, teardown, cleanup } = require('../helpers');
const NOW = 1_700_000_000_000;
describe('allocatePoints service (direct)', () => {
let db, svc, userIdA;
before(async () => {
await setup();
db = require('../../src/db').getDb();
svc = require('../../src/services/characterService');
// Create a user and character directly
const uid = Number(db.prepare('INSERT INTO users (username, password_hash, created_at) VALUES (?, ?, ?)').run('allocsvc', 'x', NOW).lastInsertRowid);
const { id } = svc.createCharacter(uid, 'SvcHero', 'warrior');
userIdA = uid;
// Give the character 3 unspent points
db.prepare('UPDATE characters SET unspent_points = 3 WHERE id = ?').run(id);
});
after(async () => {
const { resetNow } = require('../../src/utils/time');
resetNow();
await teardown();
cleanup();
});
it('spend 2 of 3 points on STR → str +2, unspent_points 1', () => {
const charBefore = db.prepare('SELECT * FROM characters WHERE user_id = ?').get(userIdA);
assert.equal(charBefore.unspent_points, 3);
assert.equal(charBefore.str, 8); // warrior base
const result = svc.allocatePoints(userIdA, { str: 2 });
assert.equal(result.character.str, 10); // 8 + 2
assert.equal(result.character.unspent_points, 1); // 3 - 2
// Warrior attack = 5 + str*2 + level = 5 + 10*2 + 1 = 26
assert.equal(result.derived.attack, 26);
});
it('spending 4 of 3 → NOT_ENOUGH_POINTS, nothing changed', () => {
const charBefore = db.prepare('SELECT str, unspent_points FROM characters WHERE user_id = ?').get(userIdA);
const oldStr = charBefore.str;
const oldPts = charBefore.unspent_points;
try {
svc.allocatePoints(userIdA, { str: 4 });
assert.fail('Should have thrown');
} catch (err) {
assert.equal(err.code, 'NOT_ENOUGH_POINTS');
}
// Verify nothing changed
const charAfter = db.prepare('SELECT str, unspent_points FROM characters WHERE user_id = ?').get(userIdA);
assert.equal(charAfter.str, oldStr);
assert.equal(charAfter.unspent_points, oldPts);
});
it('spend all 3 evenly (1 each) → unspent_points 0, all stats increased', () => {
// First give points back
db.prepare('UPDATE characters SET unspent_points = 3, str = 8, agi = 5, int = 5, vit = 8 WHERE user_id = ?').run(userIdA);
const charBefore = db.prepare('SELECT * FROM characters WHERE user_id = ?').get(userIdA);
assert.equal(charBefore.unspent_points, 3);
const result = svc.allocatePoints(userIdA, { str: 1, agi: 1, vit: 1 });
assert.equal(result.character.unspent_points, 0);
assert.equal(result.character.str, 9);
assert.equal(result.character.agi, 6);
assert.equal(result.character.vit, 9);
});
it('negative values throw INVALID_FIELD', () => {
try {
svc.allocatePoints(userIdA, { str: -1 });
assert.fail('Should have thrown');
} catch (err) {
assert.equal(err.code, 'INVALID_FIELD');
}
});
it('float values throw INVALID_FIELD', () => {
try {
svc.allocatePoints(userIdA, { str: 1.5 });
assert.fail('Should have thrown');
} catch (err) {
assert.equal(err.code, 'INVALID_FIELD');
}
});
it('unknown keys throw INVALID_FIELD', () => {
try {
svc.allocatePoints(userIdA, { str: 1, luck: 1 });
assert.fail('Should have thrown');
} catch (err) {
assert.equal(err.code, 'INVALID_FIELD');
}
});
});
+148
View File
@@ -0,0 +1,148 @@
// Integration tests for POST /api/character/allocate endpoint.
const { describe, it, before, after } = require('node:test');
const assert = require('node:assert/strict');
const { setup, teardown, cleanup } = require('../helpers');
describe('POST /api/character/allocate (integration)', () => {
let baseUrl;
let loginCookie;
before(async () => {
baseUrl = await setup();
});
after(async () => {
const { resetNow } = require('../../src/utils/time');
resetNow();
await teardown();
cleanup();
});
function allocate(body) {
return fetch(`${baseUrl}/api/character/allocate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Cookie: loginCookie },
body: JSON.stringify(body),
});
}
function registerAndLogin(username) {
return fetch(`${baseUrl}/api/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password: 'password123' }),
}).then(() => fetch(`${baseUrl}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password: 'password123' }),
}));
}
function createHero(cookie, name) {
return fetch(`${baseUrl}/api/character`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Cookie: cookie },
body: JSON.stringify({ name, class: 'warrior' }),
});
}
it('spend 2 of 3 points on STR → str +2, unspent_points 1, derived attack increased', async () => {
const res = await registerAndLogin('alloc1');
loginCookie = res.headers.get('set-cookie');
await createHero(loginCookie, 'Allocher1');
// Use the service directly to set up test state (bypasses HTTP/session layer)
const db = require('../../src/db').getDb();
const svc = require('../../src/services/characterService');
const userId = Number(db.prepare('SELECT id FROM users WHERE username = ?').get('alloc1').id);
const char = db.prepare('SELECT id FROM characters WHERE user_id = ?').get(userId);
db.prepare('UPDATE characters SET unspent_points = 3 WHERE id = ?').run(char.id);
const allocateRes = await allocate({ str: 2 });
assert.equal(allocateRes.status, 200);
const body = await allocateRes.json();
assert.equal(body.ok, true);
assert.equal(body.data.character.str, 10); // warrior base 8 + 2
assert.equal(body.data.character.unspent_points, 1);
// Warrior attack = 5 + str*2 + level = 5 + 10*2 + 1 = 26
assert.equal(body.data.derived.attack, 26);
});
it('spending 4 of 3 → 422 NOT_ENOUGH_POINTS, nothing changed', async () => {
const res = await registerAndLogin('alloc2');
loginCookie = res.headers.get('set-cookie');
await createHero(loginCookie, 'Allocher2');
const db = require('../../src/db').getDb();
const userId = Number(db.prepare('SELECT id FROM users WHERE username = ?').get('alloc2').id);
const char = db.prepare('SELECT id FROM characters WHERE user_id = ?').get(userId);
db.prepare('UPDATE characters SET unspent_points = 3 WHERE id = ?').run(char.id);
const allocateRes = await allocate({ str: 4 });
assert.equal(allocateRes.status, 422);
const body = await allocateRes.json();
assert.equal(body.ok, false);
assert.equal(body.error.code, 'NOT_ENOUGH_POINTS');
// Verify nothing changed in DB
const refreshed = db.prepare('SELECT str, unspent_points FROM characters WHERE id = ?').get(char.id);
assert.equal(refreshed.str, 8); // warrior base unchanged
assert.equal(refreshed.unspent_points, 3); // points unchanged
});
it('negative values → 400', async () => {
const res = await registerAndLogin('alloc3');
loginCookie = res.headers.get('set-cookie');
await createHero(loginCookie, 'Allocher3');
const allocateRes = await allocate({ str: -1 });
assert.equal(allocateRes.status, 400);
const body = await allocateRes.json();
assert.equal(body.ok, false);
});
it('float values → 400', async () => {
const res = await registerAndLogin('alloc4');
loginCookie = res.headers.get('set-cookie');
await createHero(loginCookie, 'Allocher4');
const allocateRes = await allocate({ str: 1.5 });
assert.equal(allocateRes.status, 400);
const body = await allocateRes.json();
assert.equal(body.ok, false);
});
it('unknown keys → 400', async () => {
const res = await registerAndLogin('alloc5');
loginCookie = res.headers.get('set-cookie');
await createHero(loginCookie, 'Allocher5');
const allocateRes = await allocate({ str: 1, luck: 1 });
assert.equal(allocateRes.status, 400);
const body = await allocateRes.json();
assert.equal(body.ok, false);
});
it('another user cannot affect user A\'s character', async () => {
const db = require('../../src/db').getDb();
// Get user A's character (from first test)
const charA = db.prepare('SELECT id, str FROM characters WHERE name = ?').get('Allocher1');
const oldStr = charA.str;
// User B creates hero and tries to allocate
const res = await registerAndLogin('alloc_b');
loginCookie = res.headers.get('set-cookie');
await createHero(loginCookie, 'AllocherB');
// User B allocates on their own character (which has 0 unspent points)
const allocateRes = await allocate({ str: 1 });
assert.equal(allocateRes.status, 422); // NOT_ENOUGH_POINTS for user B
// User A's character is untouched
const refreshed = db.prepare('SELECT str FROM characters WHERE id = ?').get(charA.id);
assert.equal(refreshed.str, oldStr);
});
});
+1 -1
View File
@@ -151,6 +151,6 @@ describe('leveling', () => {
});
it('allocatePoints — non-integer throws', () => {
assert.throws(() => allocatePoints(8, 4, 2, 8, 3, { str: 1.5 }), /positive integer/);
assert.throws(() => allocatePoints(8, 4, 2, 8, 3, { str: 1.5 }), /non-negative integer/);
});
});