From ab4c53d6dfb4f5031e7665b796b370e384b03f0f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 13:32:21 +0000 Subject: [PATCH] Analyse: Statistik-Abruf auf richtigen Pfad + Vertragstest Co-Authored-By: Claude Sonnet 5.5 --- server/services/analyticsClient.js | 2 +- test/analyticsClient.test.js | 8 ++-- test/analyticsContract.test.js | 62 ++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 5 deletions(-) create mode 100644 test/analyticsContract.test.js diff --git a/server/services/analyticsClient.js b/server/services/analyticsClient.js index ccbefe6..9b97552 100644 --- a/server/services/analyticsClient.js +++ b/server/services/analyticsClient.js @@ -51,7 +51,7 @@ export function createAnalyticsClient(opts = {}) { return request('DELETE', sitePath(siteKey)) }, getStats(siteKey, range) { - return request('GET', `${sitePath(siteKey)}/stats?range=${encodeURIComponent(range)}`) + return request('GET', `/a/admin/stats/${encodeURIComponent(siteKey)}?range=${encodeURIComponent(range)}`) }, // Format (R3): ., byte-gleich zu analytics/src/heat.js heatToken({ siteKey, path, vp, now = Date.now() }) { diff --git a/test/analyticsClient.test.js b/test/analyticsClient.test.js index 8ce56d4..20042d6 100644 --- a/test/analyticsClient.test.js +++ b/test/analyticsClient.test.js @@ -42,14 +42,14 @@ test('deleteSite und getStats', async () => { const c = mk() await c.deleteSite('abcdefghij123456') assert.equal(calls[0].method, 'DELETE') - const s = await c.getStats('abcdefghij123456', '7d') + const s = await c.getStats('abcdefghij123456', '7') assert.deepEqual(s, { pv: 3 }) - assert.equal(calls[1].url, '/a/admin/sites/abcdefghij123456/stats?range=7d') + assert.equal(calls[1].url, '/a/admin/stats/abcdefghij123456?range=7') }) test('Fehler tragen status, ohne Token in der Meldung', async () => { reset(() => ({ status: 500, body: { error: 'kaputt' } })) - await assert.rejects(mk().getStats('abcdefghij123456', '7d'), (e) => { + await assert.rejects(mk().getStats('abcdefghij123456', '7'), (e) => { assert.equal(e.status, 500) assert.ok(!e.message.includes('tok-secret')) return true @@ -71,7 +71,7 @@ test('Timeout wird als Fehler gemeldet', async () => { const slow = http.createServer(() => {}) await new Promise((r) => slow.listen(0, '127.0.0.1', r)) const c = mk({ url: `http://127.0.0.1:${slow.address().port}`, timeoutMs: 50 }) - await assert.rejects(c.getStats('abcdefghij123456', '7d'), (e) => { + await assert.rejects(c.getStats('abcdefghij123456', '7'), (e) => { assert.ok(e.status >= 500) assert.ok(!e.message.includes('tok-secret')) return true diff --git a/test/analyticsContract.test.js b/test/analyticsContract.test.js new file mode 100644 index 0000000..ed8d052 --- /dev/null +++ b/test/analyticsContract.test.js @@ -0,0 +1,62 @@ +// Vertragstest: echtes Portal-Client gegen die echten Collector-Server (im Prozess, In-Memory-DB). +import { test, before, after } from 'node:test' +import assert from 'node:assert/strict' +import { createAnalyticsClient } from '../server/services/analyticsClient.js' +import { createAdminServer, createPublicServer } from '../analytics/src/server.js' +import { openDb } from '../analytics/src/db.js' + +const SITE = 'abcdef0123456789abcd' +const DOMAIN = 'kunde.example.com' +const config = { adminToken: 'contract-admin-token', heatSecret: 'contract-heat-secret', distDir: new URL('../analytics/client/dist', import.meta.url).pathname, baseHost: 'project.webklar.com' } + +let db, admin, pub, client, pubUrl +const listen = (s) => new Promise((r) => s.listen(0, '127.0.0.1', () => r(`http://127.0.0.1:${s.address().port}`))) + +before(async () => { + db = openDb(':memory:') + const adminServer = createAdminServer({ config, db }) + admin = adminServer + pub = createPublicServer({ config, db }) + const adminUrl = await listen(admin) + pubUrl = await listen(pub) + client = createAnalyticsClient({ url: adminUrl, adminToken: config.adminToken, heatSecret: config.heatSecret }) +}) +after(() => { admin.close(); pub.close() }) + +const post = (body) => fetch(`${pubUrl}/a/e`, { + method: 'POST', body: JSON.stringify({ site: SITE, ...body }), + headers: { 'Content-Type': 'text/plain', Origin: `https://${DOMAIN}`, 'X-Forwarded-For': '203.0.113.7', 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) Mobile Safari' }, +}) + +test('Vertrag: putSite -> Ereignisse -> getStats -> heatToken -> deleteSite', async () => { + await client.putSite(SITE, { projectId: 'p1', domains: [DOMAIN], active: true }) + + await post({ t: 'pv', path: '/', ref: 'google.com', vp: 'd' }) + await post({ t: 'pv', path: '/', ref: '', vp: 'd' }) + await post({ t: 'pv', path: '/kontakt', ref: '', vp: 'd' }) + await post({ t: 'ck', path: '/', vp: 'd', x: 0.25, y: 0.5, sel: 'body:nth-of-type(1) > a:nth-of-type(1)' }) + await post({ t: 'ck', path: '/', vp: 'd', x: 0.75, y: 0.1, sel: 'body:nth-of-type(1) > a:nth-of-type(2)' }) + await post({ t: 'ct', path: '/', kind: 'tel' }) + await post({ t: 'bn', a: 'accept' }) + + const stats = await client.getStats(SITE, '7') + assert.equal(stats.totals.pv, 3) + assert.equal(stats.totals.contacts, 1) + assert.equal(stats.series.length, 7) + assert.equal(stats.series.reduce((n, d) => n + d.pv, 0), 3) + assert.deepEqual(stats.pages, [{ key: '/', count: 2 }, { key: '/kontakt', count: 1 }]) + assert.deepEqual(stats.refs, [{ key: '', count: 2 }, { key: 'google.com', count: 1 }]) + assert.equal(stats.banner.accept, 1) + assert.ok(Array.isArray(stats.devices) && Array.isArray(stats.countries) && Array.isArray(stats.contacts)) + + const token = client.heatToken({ siteKey: SITE, path: '/', vp: 'd' }) + const heat = await fetch(`${pubUrl}/a/heat?token=${encodeURIComponent(token)}`) + assert.equal(heat.status, 200) + const { points } = await heat.json() + assert.equal(points.length, 2) + assert.deepEqual(points.map((p) => p.x).sort(), [0.25, 0.75]) + + await client.deleteSite(SITE) + await assert.rejects(client.getStats(SITE, '7'), (e) => e.status === 404) + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM events_raw').get().n, 0) +})