import { test } from 'node:test'; import assert from 'node:assert/strict'; import crypto from 'node:crypto'; import { mkdtempSync, readFileSync, readdirSync, statSync } from 'node:fs'; import { spawnSync } from 'node:child_process'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { DatabaseSync } from 'node:sqlite'; import { openDb } from '../src/db.js'; import { exportSite } from '../scripts/export-site.mjs'; const KEY = 'abcdef0123456789abcd'; const OTHER = 'zzzzzzzz0123456789zz'; const SCRIPT = new URL('../scripts/export-site.mjs', import.meta.url).pathname; function setup() { const dir = mkdtempSync(join(tmpdir(), 'exp-')); const dbPath = join(dir, 'a.db'); const db = openDb(dbPath); for (const k of [KEY, OTHER]) db.prepare('INSERT INTO sites (siteKey, projectId, domains, active) VALUES (?,?,?,1)').run(k, 'p', '[]'); const ts = Date.UTC(2026, 8, 1, 10); db.prepare("INSERT INTO daily VALUES (?, '2026-08-31','pv_total','',5)").run(KEY); db.prepare("INSERT INTO daily VALUES (?, '2026-08-31','pv_path','/a',2)").run(KEY); db.prepare("INSERT INTO daily VALUES (?, '2026-08-31','pv_total','',9)").run(OTHER); db.prepare("INSERT INTO events_raw (site, ts, type, path, ref) VALUES (?,?, 'pv', '/x', ?)").run(KEY, ts, 'a,"b"\nc'); db.prepare("INSERT INTO events_raw (site, ts, type, path) VALUES (?,?, 'pv', '/o')").run(OTHER, ts); db.prepare('INSERT INTO clicks (site, ts, path, vp, x, y, sel) VALUES (?,?,?,?,?,?,?)').run(KEY, ts, '/', 'd', 1.5, 2, '=1+1'); db.prepare('INSERT INTO clicks (site, ts, path, vp, x, y, sel) VALUES (?,?,?,?,?,?,?)').run(OTHER, ts, '/', 'd', 1, 2, 'x'); db.exec('PRAGMA wal_checkpoint(TRUNCATE)'); db.close(); return { dir, dbPath }; } const hash = (p) => crypto.createHash('sha256').update(readFileSync(p)).digest('hex'); test('Export: drei CSV-Dateien mit Kopfzeilen, nur diese Website, Quotes und Formelschutz', () => { const { dir, dbPath } = setup(); const before = hash(dbPath); const out = join(dir, 'out'); exportSite({ dbPath, siteKey: KEY, outDir: out }); assert.deepEqual(readdirSync(out).sort(), [`${KEY}-ereignisse.csv`, `${KEY}-klicks.csv`, `${KEY}-tageswerte.csv`]); const t = readFileSync(join(out, `${KEY}-tageswerte.csv`), 'utf8'); assert.equal(t.split('\r\n')[0], 'day,dim,key,count'); assert.equal(t.split('\r\n').filter(Boolean).length, 3); const e = readFileSync(join(out, `${KEY}-ereignisse.csv`), 'utf8'); assert.equal(e.split('\r\n')[0], 'time,type,path,vp,ref,country,device,visitor,sec,scroll,kind,host'); assert.ok(e.includes('2026-09-01T10:00:00.000Z,pv,/x,,"a,""b""\nc"')); assert.ok(!e.includes('/o')); const c = readFileSync(join(out, `${KEY}-klicks.csv`), 'utf8'); assert.equal(c.split('\r\n')[0], 'time,path,vp,x,y,sel'); assert.equal(c.split('\r\n').filter(Boolean).length, 2); assert.ok(c.includes(",'=1+1")); assert.equal(hash(dbPath), before); }); test('Export: Datenbank ist nur lesend geöffnet', () => { const { dbPath } = setup(); const ro = new DatabaseSync(dbPath, { readOnly: true }); assert.throws(() => ro.exec("INSERT INTO sites (siteKey, projectId, domains) VALUES ('q','p','[]')")); ro.close(); }); test('CLI: unbekannter oder ungültiger Schlüssel => Exit 1 ohne Dateien', () => { const { dir, dbPath } = setup(); for (const k of ['unbekannt0123456789ab', 'ABC']) { const out = join(dir, 'o-' + k); const r = spawnSync(process.execPath, [SCRIPT, k, '--db', dbPath, '--out', out]); assert.equal(r.status, 1); assert.throws(() => readdirSync(out)); } const ok = spawnSync(process.execPath, [SCRIPT, KEY, '--db', dbPath, '--out', join(dir, 'ok')]); assert.equal(ok.status, 0); assert.equal(readdirSync(join(dir, 'ok')).length, 3); }); test('Export: Formelschutz nur fuer Text (Tab, CR, Minus), Zahlen bleiben unveraendert, Dateien 0600', () => { const { dir, dbPath } = setup(); const db = new DatabaseSync(dbPath); const ts = Date.UTC(2026, 8, 1, 10); const ins = db.prepare('INSERT INTO clicks (site, ts, path, vp, x, y, sel) VALUES (?,?,?,?,?,?,?)'); ins.run(KEY, ts, '/', 'd', -3, -4.5, '\tcmd'); ins.run(KEY, ts, '/', 'd', 1, 2, '\rcmd'); ins.run(KEY, ts, '/', 'd', 1, 2, '-cmd'); db.close(); const out = join(dir, 'out'); const files = exportSite({ dbPath, siteKey: KEY, outDir: out }); const c = readFileSync(join(out, `${KEY}-klicks.csv`), 'utf8'); assert.ok(c.includes(',-3,-4.5,'), 'negative Zahlen ohne Praefix'); assert.ok(!c.includes("'-3")); assert.ok(c.includes(",\"'\tcmd\"") || c.includes(",'\tcmd"), 'Tab gesichert'); assert.ok(c.includes(`"'\rcmd"`), 'CR gesichert'); assert.ok(c.includes(",'-cmd")); for (const f of files) assert.equal(statSync(f).mode & 0o777, 0o600); });