// ── Event series, the arc (EVENTS.md §D/§I, Phase 4) ─────────────────────── // // One small table, and the reason it is worth testing at all is the two rules // that are not obvious from its four columns: // // • the slug is derived once and FROZEN. The public arc page lives at it, so // a rename that moved it would break every link — including the ones inside // the Discord posts this feature will eventually write. // • the delete is a real delete, and it is the only one in this feature. A // definition is archived instead, because a run pins its version and history // that cannot be explained defeats the audit. A series pins nothing: it is a // label, `series_id` is ON DELETE SET NULL, and the count of what it detached // is what an operator needs to be told. process.env.DB_HOST = '127.0.0.1' process.env.DB_PORT = '59999' const { test, beforeEach, afterEach, after } = require('node:test') const assert = require('node:assert/strict') const seriesDb = require('../src/model/events/eventSeries.db') const series = require('../src/model/events/eventSeries.model') const db = require('../src/utils/db') after(() => db.close()) let store const original = { ...seriesDb } beforeEach(() => { store = { rows: new Map(), nextId: 1 } Object.assign(seriesDb, { list: async () => [...store.rows.values()].sort((a, b) => a.ordering - b.ordering || a.id - b.id), getById: async (id) => store.rows.get(id) || null, exists: async (id) => store.rows.has(id), insert: async (s) => { const id = store.nextId++ store.rows.set(id, { ...s, id, definition_count: 0 }) return id }, update: async (id, s) => { const existing = store.rows.get(id) store.rows.set(id, { ...existing, ...s }) return 1 }, remove: async (id) => { store.rows.delete(id) return 1 }, }) }) afterEach(() => Object.assign(seriesDb, original)) test('a series is created with a slug derived from its name', async () => { const result = await series.create({ name: 'Royal Spy Mission', ordering: 2 }, 7) assert.equal(result.ok, true) assert.equal(result.status, 201) assert.equal(result.series.slug, 'royal-spy-mission') assert.equal(result.series.ordering, 2) assert.equal(result.series.created_by, 7) }) test('two series with the same name get distinct slugs', async () => { // `slug` is UNIQUE in the schema, so without this the second create is a 1452 // reaching a controller as a 500. const first = await series.create({ name: 'Winter Arc' }) const second = await series.create({ name: 'Winter Arc' }) assert.equal(first.series.slug, 'winter-arc') assert.equal(second.series.slug, 'winter-arc-2') }) test('renaming a series does NOT move its slug', async () => { // The rule with teeth. The arc page lives at the slug, and a rename is the // ordinary act of an editor tidying up wording months later. const created = await series.create({ name: 'Royal Spy Mission' }) const updated = await series.update(created.series.id, { name: 'The Royal Spy Missions' }) assert.equal(updated.ok, true) assert.equal(updated.series.name, 'The Royal Spy Missions') assert.equal(updated.series.slug, 'royal-spy-mission') }) test('a nameless series is refused, and so is a nonsense ordering', async () => { assert.deepEqual((await series.create({ name: ' ' })).errors, ['name is required']) const bad = await series.create({ name: 'Fine', ordering: -3 }) assert.equal(bad.ok, false) assert.match(bad.errors.join(' '), /ordering must be an integer/) }) test('deleting a series answers with how many definitions it detached', async () => { // The whole consequence of this delete is about the rows it does NOT delete, // so the count is the answer rather than a detail. const created = await series.create({ name: 'Winter Arc' }) store.rows.get(created.series.id).definition_count = 3 const removed = await series.remove(created.series.id) assert.equal(removed.ok, true) assert.equal(removed.detached, 3) assert.equal(await seriesDb.getById(created.series.id), null) }) test('acting on a series that is not there is a 404, never a 500', async () => { assert.equal((await series.update(999, { name: 'x' })).status, 404) assert.equal((await series.remove(999)).status, 404) }) test('ordering places a series among the others, and defaults to zero', async () => { await series.create({ name: 'Third', ordering: 30 }) await series.create({ name: 'First', ordering: 10 }) await series.create({ name: 'Unordered' }) const listed = await series.list() assert.deepEqual(listed.map((s) => s.name), ['Unordered', 'First', 'Third']) })