import { test } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, readFileSync, statSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { openDb } from '../src/db.js'; import { runNightly, dailyCutoffDay, DAILY_MONTHS } from '../src/jobs.js'; const KEY = 'abcdef0123456789abcd'; const NOW = Date.UTC(2026, 9, 1, 10); const D = 86400 * 1000; test('dailyCutoffDay: 25 Kalendermonate zurück, Monatsende begrenzt', () => { assert.equal(DAILY_MONTHS, 25); assert.equal(dailyCutoffDay(Date.UTC(2026, 9, 1, 10)), '2024-09-01'); assert.equal(dailyCutoffDay(Date.UTC(2026, 2, 31, 10)), '2024-02-29'); }); function mk() { const db = openDb(':memory:'); db.prepare('INSERT INTO sites (siteKey, projectId, domains, active) VALUES (?,?,?,1)').run(KEY, 'p', '["a.de"]'); return db; } test('Tageswerte älter als 25 Monate werden gelöscht, Grenztag bleibt', () => { const db = mk(); for (const day of ['2024-08-31', '2024-09-01']) { db.prepare("INSERT INTO daily (site, day, dim, key, count) VALUES (?,?, 'pv_total','',3)").run(KEY, day); db.prepare('INSERT INTO agg_state (site, day) VALUES (?,?)').run(KEY, day); } const r = runNightly(db, { now: NOW }); assert.deepEqual(db.prepare('SELECT day FROM daily').all().map((x) => x.day), ['2024-09-01']); assert.deepEqual(db.prepare('SELECT day FROM agg_state').all().map((x) => x.day), ['2024-09-01']); assert.equal(r.dailyDeleted, 1); }); test('ausgeschaltete Website ohne offSince bekommt offSince, zweiter Lauf ändert nichts', () => { const db = mk(); db.prepare('UPDATE sites SET active = 0').run(); const r = runNightly(db, { now: NOW }); assert.equal(db.prepare('SELECT offSince FROM sites').get().offSince, new Date(NOW).toISOString()); assert.equal(r.orphansMarked, 1); runNightly(db, { now: NOW + D }); assert.equal(db.prepare('SELECT offSince FROM sites').get().offSince, new Date(NOW).toISOString()); }); test('Website mit offSince > 60 Tage wird weiter komplett gelöscht', () => { const db = mk(); db.prepare('UPDATE sites SET active = 0, offSince = ?').run(new Date(NOW - 61 * D).toISOString()); const r = runNightly(db, { now: NOW }); assert.equal(r.sitesDeleted, 1); assert.equal(db.prepare('SELECT COUNT(*) n FROM sites').get().n, 0); }); test('Zeilen ohne sites-Eintrag werden gelöscht, andere bleiben', () => { const db = mk(); const GONE = 'gonegonegonegonegone'; for (const s of [KEY, GONE]) { db.prepare("INSERT INTO events_raw (site, ts, type) VALUES (?,?, 'pv')").run(s, NOW - 1000); db.prepare('INSERT INTO clicks (site, ts, path, vp, x, y) VALUES (?,?,?,?,?,?)').run(s, NOW - 1000, '/', 'd', 1, 1); db.prepare("INSERT INTO daily (site, day, dim, key, count) VALUES (?, '2026-09-01','pv_total','',1)").run(s); db.prepare("INSERT INTO agg_state (site, day) VALUES (?, '2026-09-01')").run(s); } const r = runNightly(db, { now: NOW }); for (const t of ['events_raw', 'clicks', 'daily', 'agg_state']) { assert.deepEqual(db.prepare(`SELECT DISTINCT site FROM ${t}`).all().map((x) => x.site), [KEY], t); } assert.ok(r.orphanRowsDeleted >= 4); }); test('secure_delete: gelöschter Pfad steht nicht mehr in der Datei', () => { const dir = mkdtempSync(join(tmpdir(), 'sd-')); const p = join(dir, 'a.db'); const db = openDb(p); assert.equal(db.prepare('PRAGMA secure_delete').get().secure_delete, 1); db.prepare("INSERT INTO events_raw (site, ts, type, path) VALUES ('x', 1, 'pv', 'GEHEIMER-PFAD-4711')").run(); db.exec('PRAGMA wal_checkpoint(TRUNCATE)'); db.exec("DELETE FROM events_raw WHERE path = 'GEHEIMER-PFAD-4711'"); db.exec('PRAGMA wal_checkpoint(TRUNCATE)'); assert.ok(!readFileSync(p).includes('GEHEIMER-PFAD-4711')); db.close(); }); test('runNightly leert das WAL', () => { const dir = mkdtempSync(join(tmpdir(), 'wal-')); const p = join(dir, 'a.db'); const db = openDb(p); db.prepare('INSERT INTO sites (siteKey, projectId, domains, active) VALUES (?,?,?,1)').run(KEY, 'p', '[]'); runNightly(db, { now: NOW }); assert.equal(statSync(p + '-wal').size, 0); db.close(); });