94 lines
4.6 KiB
JavaScript
94 lines
4.6 KiB
JavaScript
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);
|
|
});
|