Analyse: Messdienst: Ping ohne Einwilligung, Zeitstempel auf Stunde, Pfade ohne Kennungen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-01 18:50:26 +00:00
1 parent 8d64a18d35
commit 96409c6306
7 files changed
+149 -10

No files matched your search

+14
View File
@@ -136,3 +136,17 @@ test('OPTIONS-Preflight: CORS-Köpfe nur für erlaubte Origins bekannter Website
assert.equal((await pre('abc', 'https://kunde.de')).headers.get('access-control-allow-origin'), null);
s.close();
});
test('Heatmap: gleiche Stunde (ts-Gleichstand), Limit schneidet über die niedrigste id ab', async () => {
const s = await setup();
const ins = s.db.prepare('INSERT INTO clicks (site, ts, path, vp, x, y) VALUES (?,?,?,?,?,?)');
const ts = NOW - 3600000;
ins.run(KEY, ts, '/g', 'd', 0.99, 0.99); // niedrigste id
s.db.exec('BEGIN');
for (let i = 0; i < 5000; i++) ins.run(KEY, ts, '/g', 'd', 0.5, 0.5);
s.db.exec('COMMIT');
const j = await (await s.get(tok({ path: '/g' }))).json();
assert.equal(j.points.length, 5000);
assert.ok(!j.points.some((p) => p.x === 0.99));
s.close();
});
+75
View File
@@ -350,3 +350,78 @@ test('X-Forwarded-For: rechtester Eintrag zählt für das Mengenlimit', async ()
assert.equal(t.rows().length, 121);
t.close();
});
test('T2: pv ohne ref/vp wird gespeichert (vp NULL, ref NULL), Gerät und Land bleiben', async () => {
const t = await setup();
await t.post({ t: 'pv', site: KEY, path: '/' });
const r = t.rows();
assert.equal(r.length, 1);
assert.equal(r[0].type, 'pv');
assert.equal(r[0].vp, null);
assert.equal(r[0].ref, null);
assert.equal(r[0].device, 'm');
assert.equal(r[0].country, 'DE');
await t.post({ t: 'pv', site: KEY, path: '/', vp: 'x' });
assert.equal(t.rows().length, 1);
t.close();
});
test('T2: cx braucht gültiges vp, speichert ref, eigene Domain ergibt NULL', async () => {
const t = await setup();
await t.post({ t: 'cx', site: KEY, path: '/', ref: 'https://www.google.com/x' });
await t.post({ t: 'cx', site: KEY, path: '/', ref: 'google.com', vp: 'x' });
assert.equal(t.rows().length, 0);
await t.post({ t: 'cx', site: KEY, path: '/', ref: 'https://www.google.com/x', vp: 'd' });
await t.post({ t: 'cx', site: KEY, path: '/', ref: 'https://www.kunde.de/x', vp: 'm' });
const r = t.rows();
assert.equal(r.length, 2);
assert.equal(r[0].type, 'cx');
assert.equal(r[0].ref, 'www.google.com');
assert.equal(r[0].vp, 'd');
assert.equal(r[0].country, null);
assert.equal(r[0].device, null);
assert.equal(r[1].ref, null);
t.close();
});
test('T3: alle Zeitstempel auf die volle Stunde', async () => {
const t = await setup();
const hour = Date.UTC(2026, 9, 1, 10, 0, 0);
await t.post(pv());
await t.post({ t: 'cx', site: KEY, path: '/', vp: 'd' });
await t.post({ t: 'en', site: KEY, path: '/', vp: 'd', sec: 5, scroll: 10 });
await t.post({ t: 'ct', site: KEY, path: '/', kind: 'tel' });
await t.post({ t: 'ck', site: KEY, path: '/', vp: 'd', x: 0.5, y: 0.5 });
const r = t.rows();
assert.deepEqual(r.map((x) => x.type), ['pv', 'cx', 'en', 'ct']);
for (const x of r) assert.equal(x.ts, hour);
const c = t.db.prepare('SELECT ts FROM clicks').all();
assert.equal(c.length, 1);
assert.equal(c[0].ts, hour);
assert.equal(t.db.prepare('SELECT COUNT(*) n FROM events_raw WHERE ts % 3600000 != 0').get().n, 0);
t.close();
});
test('T14: kennungsartige Pfadsegmente werden zu :id, alle Typen', async () => {
const t = await setup();
const cases = [
['/kunde/123456789/rechnung', '/kunde/:id/rechnung'],
['/u/3f2b8c1e-5d4a-4e0b-9a7c-1b2c3d4e5f60', '/u/:id'],
['/mail/max%40example.com', '/mail/:id'],
['/mail/max@example.com', '/mail/:id'],
['/preise/2026', '/preise/2026'],
['/blog/12345', '/blog/12345'],
['/', '/'],
['/a/123456?x=1#h', '/a/:id'],
];
for (const [p] of cases) await t.post(pv({ path: p }));
assert.deepEqual(t.rows().map((r) => r.path), cases.map((c) => c[1]));
await t.post({ t: 'cx', site: KEY, path: '/kunde/987654321', vp: 'd' });
await t.post({ t: 'en', site: KEY, path: '/kunde/987654321', vp: 'd', sec: 1, scroll: 1 });
await t.post({ t: 'ct', site: KEY, path: '/kunde/987654321', kind: 'tel' });
await t.post({ t: 'ck', site: KEY, path: '/kunde/987654321', vp: 'd', x: 0.1, y: 0.1 });
const paths = t.rows().slice(cases.length).map((r) => r.path);
assert.deepEqual(paths, ['/kunde/:id', '/kunde/:id', '/kunde/:id']);
assert.equal(t.db.prepare('SELECT path FROM clicks').get().path, '/kunde/:id');
t.close();
});
+31
View File
@@ -204,3 +204,34 @@ test('scheduleNightly: früh feuernder Timer läuft nicht vorzeitig und nie dopp
s.stop();
assert.equal(pending.length, 0);
});
test('T2: pv ohne Einwilligung + cx: pv_total 1, pv_ref nur {google.com:1}; cx zählt nie pv_total', () => {
const { db, ev } = mk();
const t = NOW - D;
ev({ ts: t, type: 'pv', path: '/', vp: null, ref: null, country: 'DE', device: 'd' });
ev({ ts: t, type: 'cx', vp: 'd', ref: 'google.com' });
ev({ ts: t, type: 'cx', vp: 'd', ref: null });
ev({ ts: t, type: 'pv', path: '/', vp: 'd', ref: null, country: 'DE', device: 'd' });
runNightly(db, { now: NOW });
const d = '2026-10-09';
assert.equal(get(db, d, 'pv_total'), 2);
assert.equal(get(db, d, 'pv_ref', 'google.com'), 1);
assert.equal(get(db, d, 'pv_ref', ''), 2); // pv mit vp und cx ohne ref
const refs = db.prepare("SELECT key FROM daily WHERE site=? AND dim='pv_ref' ORDER BY key").all(KEY).map((r) => r.key);
assert.deepEqual(refs, ['', 'google.com']);
});
test('T2: Stats-Endpunkt refs live und aus daily, nie doppelt', async () => {
const { db, ev } = mk();
ev({ ts: NOW - D, type: 'pv', path: '/', vp: null, country: 'DE', device: 'd' });
ev({ ts: NOW - D, type: 'cx', vp: 'd', ref: 'google.com' });
const a = await admin(db);
const live = await (await a.get(`/a/admin/stats/${KEY}?range=7`)).json();
assert.equal(live.totals.pv, 1);
assert.deepEqual(live.refs, [{ key: 'google.com', count: 1 }]);
runNightly(db, { now: NOW });
const agg = await (await a.get(`/a/admin/stats/${KEY}?range=7`)).json();
assert.equal(agg.totals.pv, 1);
assert.deepEqual(agg.refs, [{ key: 'google.com', count: 1 }]);
a.close();
});