import test from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, rmSync, mkdirSync, statSync, symlinkSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { openStore } from '../server/db.mjs'; import { readConfig } from '../server/config.mjs'; import Database from 'better-sqlite3'; const VISITOR_A = '12345678-1234-4123-8123-123456789abc'; const VISITOR_B = '87654321-4321-4321-a321-cba987654321'; const DOG = 'a'.repeat(64); function fixture(t, close = () => {}) { const root = mkdtempSync(join(tmpdir(), 'arrival-db-')); t.after(() => { close(); rmSync(root, { recursive: true, force: true }); }); return { root, dbPath: join(root, 'private', 'counts.sqlite') }; } function inspect(path, fn) { const db = new Database(path, { readonly: true, fileMustExist: true }); try { return fn(db); } finally { db.close(); } } test('real native SQLite uses exact schema, WAL, bounded busy timeout and private files', t => { let store; const { dbPath } = fixture(t, () => store?.close()); store = openStore({ dbPath }); assert.equal(store.arrivalCount(), 0); inspect(dbPath, db => { assert.match(db.prepare('SELECT sqlite_version() AS version').get().version, /^\d+\.\d+\.\d+$/); assert.equal(db.pragma('journal_mode', { simple: true }), 'wal'); assert.equal(db.pragma('user_version', { simple: true }), 1); assert.deepEqual(db.pragma('table_info(visitors_daily)').map(c => c.name), ['visitor_id', 'visit_date', 'first_seen_at', 'last_seen_at']); assert.deepEqual(db.pragma('table_info(arrivals)').map(c => c.name), ['dog_id', 'first_arrived_at']); assert.ok(db.pragma('index_list(visitors_daily)').some(i => db.pragma(`index_info(${i.name})`).map(c => c.name).join() === 'visit_date')); }); if (process.platform !== 'win32') { assert.equal(statSync(join(dbPath, '..')).mode & 0o777, 0o700); for (const path of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) assert.equal(statSync(path).mode & 0o077, 0); } assert.throws(() => openStore({ dbPath, busyTimeoutMs: -1 })); assert.throws(() => openStore({ dbPath, busyTimeoutMs: 10001 })); }); test('daily upserts preserve first time, update last time and MAU is DISTINCT across days', t => { let store; const { dbPath } = fixture(t, () => store?.close()); let at = '2026-10-01T00:00:00.000Z'; store = openStore({ dbPath, now: () => new Date(at) }); store.recordVisit(VISITOR_A); at = '2026-10-01T01:00:00.000Z'; store.recordVisit(VISITOR_A); let rows = inspect(dbPath, db => db.prepare('SELECT * FROM visitors_daily').all()); assert.deepEqual(rows, [{ visitor_id: VISITOR_A, visit_date: '2026-10-01', first_seen_at: '2026-10-01T00:00:00.000Z', last_seen_at: at }]); at = '2026-10-02T00:00:00.000Z'; store.recordVisit(VISITOR_A); store.recordVisit(VISITOR_B); assert.equal(store.monthlyVisitors('2026-10'), 2); assert.equal(inspect(dbPath, db => db.prepare('SELECT COUNT(*) AS n FROM visitors_daily').get().n), 3); assert.equal(store.monthlyVisitors('2026-09'), 0); }); for (const [before, after, previousMonth, nextMonth] of [ ['2026-10-31T14:59:59.000Z', '2026-10-31T15:00:00.000Z', '2026-10', '2026-11'], ['2026-12-31T14:59:59.000Z', '2026-12-31T15:00:00.000Z', '2026-12', '2027-01'], ]) test(`Seoul month boundary ${previousMonth} is independent of process timezone`, t => { let store; const { dbPath } = fixture(t, () => store?.close()); let at = before; store = openStore({ dbPath, now: () => new Date(at) }); store.recordVisit(VISITOR_A); at = after; store.recordVisit(VISITOR_B); assert.equal(store.monthlyVisitors(previousMonth), 1); assert.equal(store.monthlyVisitors(nextMonth), 1); const dates = inspect(dbPath, db => db.prepare('SELECT visit_date FROM visitors_daily ORDER BY visit_date').all()); assert.equal(dates[1].visit_date, `${nextMonth}-01`); }); test('arrivals are idempotent, first timestamp persists and migrations reopen without data loss', t => { let store; const { dbPath } = fixture(t, () => store?.close()); let at = '2026-10-08T00:00:00.000Z'; store = openStore({ dbPath, now: () => new Date(at) }); store.recordArrival(DOG); at = '2026-10-09T00:00:00.000Z'; store.recordArrival(DOG); assert.equal(store.arrivalCount(), 1); store.close(); store.close(); store = openStore({ dbPath }); assert.equal(store.arrivalCount(), 1); assert.deepEqual(inspect(dbPath, db => db.prepare('SELECT * FROM arrivals').all()), [{ dog_id: DOG, first_arrived_at: '2026-10-08T00:00:00.000Z' }]); }); test('strict month validation handles year bounds and December rollover', t => { let store; const { dbPath } = fixture(t, () => store?.close()); store = openStore({ dbPath }); for (const month of [undefined, null, 202610, '', '2026-1', '2026-00', '2026-13', '0000-01', '10000-01', '2026-10-01', ' 2026-10', '2026-10\n']) assert.throws(() => store.monthlyVisitors(month)); for (const month of ['0001-01', '0099-12', '2026-12', '9999-12']) assert.equal(store.monthlyVisitors(month), 0); }); test('configuration requires explicit absolute private DB path and strict origin/port', t => { const { dbPath } = fixture(t); const config = readConfig({ DB_PATH: dbPath }); assert.deepEqual(config, { dbPath, distRoot: resolve('dist'), host: '127.0.0.1', port: 3000, publicOrigin: 'http://127.0.0.1:3000' }); assert.equal(readConfig({ DB_PATH: dbPath, HOST: '0.0.0.0', PORT: '4010' }).host, '0.0.0.0'); assert.throws(() => readConfig({})); assert.throws(() => readConfig({ DB_PATH: 'relative.sqlite' })); assert.throws(() => readConfig({ DB_PATH: ':memory:' })); for (const path of ['local.sqlite', 'src/db.sqlite', 'public/db.sqlite', 'dist/db.sqlite']) assert.throws(() => readConfig({ DB_PATH: resolve(path) })); for (const value of ['0', '-1', '65536', '3e3', '3000x', '1.2', '']) assert.throws(() => readConfig({ DB_PATH: dbPath, PORT: value })); assert.throws(() => readConfig({ DB_PATH: dbPath, NODE_ENV: 'production' })); for (const origin of ['null', 'ftp://example.com', 'https://user:pass@example.com', 'https://example.com/path', 'https://example.com?x', 'https://example.com#x']) assert.throws(() => readConfig({ DB_PATH: dbPath, NODE_ENV: 'production', PUBLIC_ORIGIN: origin })); assert.equal(readConfig({ DB_PATH: dbPath, NODE_ENV: 'production', PUBLIC_ORIGIN: 'https://example.com' }).publicOrigin, 'https://example.com'); }); test('canonical DB path validation catches existing and future paths through symlinks', t => { const { root } = fixture(t); symlinkSync(resolve('.'), join(root, 'source-link'), 'dir'); for (const path of [join(root, 'source-link', 'db.sqlite'), join(root, 'source-link', 'new', 'db.sqlite')]) { assert.throws(() => readConfig({ DB_PATH: path })); assert.throws(() => openStore({ dbPath: path })); } mkdirSync(join(root, 'private'), { mode: 0o700 }); symlinkSync(join(root, 'private'), join(root, 'safe-link'), 'dir'); const config = readConfig({ DB_PATH: join(root, 'safe-link', 'db.sqlite') }); assert.equal(config.dbPath, join(root, 'private', 'db.sqlite')); }); test('DB paths reject raw and symlink-resolved whitespace before native filename trimming', async t => { const { root, dbPath } = fixture(t); const { writeFileSync } = await import('node:fs'); const { openReadOnlyStore } = await import('../server/db.mjs'); assert.throws(() => readConfig({ DB_PATH: `${dbPath} ` })); assert.throws(() => openStore({ dbPath: `${dbPath}\t` })); assert.throws(() => openReadOnlyStore(`${dbPath}\n`)); const spacedTarget = join(root, 'target.sqlite '); writeFileSync(spacedTarget, '', { mode: 0o600 }); const alias = join(root, 'alias.sqlite'); symlinkSync(spacedTarget, alias); assert.throws(() => readConfig({ DB_PATH: alias })); assert.throws(() => openStore({ dbPath: alias })); });