/** * `celilo person list --json` — the shape the web console maps an identity * against. * * Worth its own test for a reason that is not obvious from the three lines it * covers. The console server decides whether an authenticated browser subject * corresponds to a real person, and it decides it by reading THIS. Its own * tests answer that question against a fake celilo, so they prove the console * parses what it was told to expect and say nothing about what celilo emits. * This is the end that closes. * * If the payload ever stops being `[{ name, timezone }]`, the console does not * error: it maps nobody, draws no acknowledge control, and reports every * operator as unknown to the fleet. That reads as a configuration problem for * as long as anyone is willing to believe it. */ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { closeDb, getDb } from '../../db/client'; import { createPerson } from '../../services/alerting/people'; import { setupTestDatabaseAt } from '../../test-utils/database'; import { resetTestDbPath } from '../../test-utils/db-path'; import { handlePerson } from './notify-config'; let dir: string; beforeEach(async () => { dir = mkdtempSync(join(tmpdir(), 'celilo-person-')); const dbPath = join(dir, 'test.db'); const db = await setupTestDatabaseAt(dbPath); createPerson(db, { name: 'peter', timezone: 'America/Los_Angeles' }); createPerson(db, { name: 'ada', timezone: 'Europe/London' }); db.$client.close(); process.env.CELILO_DB_PATH = dbPath; }); afterEach(() => { closeDb(); // Reset to the scratch path, never restore: the saved value may be another // suite's temp database or unset — which sends the next var-less reader to // the operator's real celilo.db (celilo#1315). resetTestDbPath(); rmSync(dir, { recursive: true, force: true }); }); /** Run `person list`, insisting it succeeded before reading what it said. */ async function personList(flags: Record): Promise { const result = await handlePerson('list', [], flags); if (!result.success) throw new Error(`person list failed: ${result.error}`); return result.message; } async function listJson(): Promise<{ name: string; timezone: string }[]> { return JSON.parse(await personList({ json: true })); } describe('celilo person list --json', () => { test('names every person, sorted the way listPeople sorts them', async () => { expect((await listJson()).map((p) => p.name)).toEqual(['ada', 'peter']); }); test('carries the timezone, which is the only other thing a person IS', async () => { const ada = (await listJson()).find((p) => p.name === 'ada'); expect(ada?.timezone).toBe('Europe/London'); }); test('an empty fleet is an empty array, not a sentence about it', async () => { // The human listing prints "Nobody configured." and a hint. A caller that // parsed that would throw, which is a better failure than the one this // avoids — but the console polls this, so it gets JSON either way. getDb().$client.run('DELETE FROM people'); expect(await listJson()).toEqual([]); }); test('without --json it stays the human table, and says how many', async () => { expect(await personList({})).toContain('2 person'); }); });