import { strict as assert } from 'node:assert'; import { readFile, rm, writeFile } from 'node:fs/promises'; import path from 'node:path'; import { afterEach, beforeEach, describe, it } from 'node:test'; import { SectionError, SectionStore, slugify } from '../src/storage/sections.ts'; import { tempDir } from './support/harness.ts'; const SEED = [ { slug: 'food', label: 'Food', shortLabel: 'Food', note: null, order: 1 }, { slug: 'bar', label: 'Bar', shortLabel: 'Bar', note: null, order: 2 }, ]; let dataDir: string; beforeEach(async () => { dataDir = await tempDir('sections'); }); afterEach(async () => { await rm(dataDir, { recursive: true, force: true }); }); async function loaded(seed = SEED) { const store = new SectionStore({ dataDir, seed }); await store.load(); return store; } describe('slugify', () => { it('turns the labels people type into stable web addresses', () => { assert.equal(slugify('Straight Drinks'), 'straight-drinks'); assert.equal(slugify("What's On"), 'whats-on'); assert.equal(slugify('Desserts & Coffee'), 'desserts-coffee'); assert.equal(slugify(' Bar '), 'bar'); assert.equal(slugify('Wine / Beer'), 'wine-beer'); assert.equal(slugify('----'), ''); }); }); describe('SectionStore', () => { it('seeds from the config the first time and writes the file', async () => { const store = await loaded(); assert.deepEqual( store.list().map((section) => section.slug), ['food', 'bar'], ); const file = JSON.parse(await readFile(path.join(dataDir, 'sections.json'), 'utf8')); assert.equal(file.sections.length, 2); }); it('keeps the file as the source of truth afterwards, ignoring the seed', async () => { await loaded(); const second = new SectionStore({ dataDir, seed: [] }); await second.load(); assert.deepEqual( second.list().map((section) => section.slug), ['food', 'bar'], ); }); it('refuses to serve an unreadable file rather than silently reseeding', async () => { await writeFile(path.join(dataDir, 'sections.json'), '{ not json'); const store = new SectionStore({ dataDir, seed: SEED }); await assert.rejects(() => store.load(), SectionError); }); it('complains if it is read before loading', () => { const store = new SectionStore({ dataDir, seed: SEED }); assert.throws(() => store.list(), /load\(\)/); }); it('adds a category at the end, with a slug from the name', async () => { const store = await loaded(); const created = await store.create({ label: 'Straight Drinks' }); assert.equal(created.slug, 'straight-drinks'); assert.equal(created.order, 3); assert.deepEqual( store.list().map((section) => section.label), ['Food', 'Bar', 'Straight Drinks'], ); }); it('makes a slug unique, and steers around the names the app reserves', async () => { const store = await loaded(); await store.create({ label: 'Desserts' }); assert.equal((await store.create({ label: 'Desserts' })).slug, 'desserts-2'); assert.equal((await store.create({ label: 'Admin' })).slug, 'admin-2'); assert.equal((await store.create({ label: 'Media' })).slug, 'media-2'); }); it('rejects an empty name and a name that makes no web address', async () => { const store = await loaded(); await assert.rejects(() => store.create({ label: ' ' }), SectionError); await assert.rejects(() => store.create({ label: '!!!' }), SectionError); }); it('renames without moving the web address customers already have', async () => { const store = await loaded(); const renamed = await store.rename('bar', { label: 'Bar & Cocktails' }); assert.equal(renamed.label, 'Bar & Cocktails'); assert.equal(renamed.slug, 'bar'); assert.equal(store.bySlug('bar')?.label, 'Bar & Cocktails'); }); it('moves a category and renumbers the rest', async () => { const store = await loaded(); await store.move('bar', 'up'); assert.deepEqual( store.list().map((section) => section.slug), ['bar', 'food'], ); assert.deepEqual( store.list().map((section) => section.order), [1, 2], ); // Past either end is a no-op, not an error. await store.move('bar', 'up'); assert.deepEqual( store.list().map((section) => section.slug), ['bar', 'food'], ); }); it('removes a category and closes the gap in the order', async () => { const store = await loaded(); await store.create({ label: 'Wine' }); await store.remove('bar'); assert.deepEqual( store.list().map((section) => [section.slug, section.order]), [ ['food', 1], ['wine', 2], ], ); }); it('reports an unknown slug rather than inventing one', async () => { const store = await loaded(); await assert.rejects(() => store.rename('pizza', { label: 'Pizza' }), SectionError); await assert.rejects(() => store.remove('pizza'), SectionError); }); it('survives add, rename, move and remove being read by a second instance', async () => { const store = await loaded(); const created = await store.create({ label: 'Desserts' }); await store.rename(created.slug, { label: 'Puddings' }); await store.move(created.slug, 'up'); const reopened = new SectionStore({ dataDir, seed: [] }); await reopened.load(); assert.deepEqual( reopened.list().map((section) => section.label), ['Food', 'Puddings', 'Bar'], ); }); });