// Generated, do not edit. sha256:fc183f4c5a327b7b47dcb1a430409a48861cde95adfddadca5231c79a33dc477 import { describe, it, beforeEach, mock } from 'node:test'; import assert from 'node:assert/strict'; import { createDbHarness } from '../helpers/db-node.mjs'; import { CONFIG } from '../../shared/config.js'; const harness = createDbHarness(); mock.module('../../server/db.js', { namedExports: harness.exports }); const { recordIntegrationCall, listIntegrationCalls, lastIntegrationFailures, countServiceCall, serviceState } = await import('../../server/data/integration-log.js'); beforeEach(() => harness.recreate()); const rijen = () => harness.exports.all(`SELECT * FROM IntegrationCall ORDER BY id`); const dagenGeleden = (d) => new Date(Date.now() - d * 86_400_000).toISOString(); const fout = (extra = {}) => ({ target: 'provider', providerKey: 'goldapi', operation: 'spot', subject: 'XAU', outcome: 'fail', httpStatus: 403, errorKey: 'integrations.price.httpFailed', detail: 'Forbidden', latencyMs: 120, ...extra, }); function oudeRij(occurredAt, providerKey = 'goldapi') { harness.exports.run( `INSERT INTO IntegrationCall (target, providerKey, operation, outcome, occurredAt) VALUES ('provider', ?, 'spot', 'fail', ?)`, [providerKey, occurredAt]); } describe('recordIntegrationCall: wat er nooit in mag', () => { it('vervangt een sleutel die de bron in zijn melding herhaalt, ook URL-gecodeerd', async () => { const sleutel = 'goldapi-abc+123/xyz'; await recordIntegrationCall(fout({ detail: `Invalid key ${sleutel} (${encodeURIComponent(sleutel)})` }), { secrets: [sleutel] }); const [r] = rijen(); assert.ok(!r.detail.includes(sleutel), r.detail); assert.ok(!r.detail.includes(encodeURIComponent(sleutel)), r.detail); assert.match(r.detail, /\*\*\*/); }); it('vervangt het token van de dienst in een Bearer-waarde, ook zonder dat het token is meegegeven', async () => { await recordIntegrationCall({ target: 'service', operation: '/v1/spot', outcome: 'fail', detail: 'echo Authorization: Bearer s3cr3t-token-waarde' }); assert.equal(rijen()[0].detail, 'echo Authorization: Bearer ***'); }); it('laat geen URL staan, want bij query_key staat de sleutel in de query', async () => { await recordIntegrationCall(fout({ detail: 'GET https://api.example.test/v1/price?apikey=K123456 failed' })); const [r] = rijen(); assert.ok(!/https?:|K123456|example\.test/.test(r.detail), r.detail); assert.match(r.detail, //); }); it('vervangt een sleutelparameter in losse tekst', async () => { await recordIntegrationCall(fout({ detail: 'rejected token=abcdef1234 and api_key=zzz999' })); assert.equal(rijen()[0].detail, 'rejected token=*** and api_key=***'); }); it('weigert een URL als operatie in plaats van hem op te schonen', async () => { await assert.rejects(recordIntegrationCall(fout({ operation: 'https://api.example.test/spot?key=x' })), /operation is a path or a capability/); await assert.rejects(recordIntegrationCall(fout({ operation: '/v1/spot?instrument=x' })), /operation is a path or a capability/); assert.equal(rijen().length, 0); }); it('kort een lange melding in tot de grens, zichtbaar', async () => { await recordIntegrationCall(fout({ detail: 'x'.repeat(1000) })); const { detail } = rijen()[0]; assert.equal(detail.length, CONFIG.INTEGRATION_LOG.DETAIL_MAX_CHARS); assert.ok(detail.endsWith('…')); }); it('bewaart in errorKey alleen sleutels en geen vrije tekst uit een antwoord', async () => { await recordIntegrationCall({ target: 'service', operation: '/v1/series', outcome: 'fail', errorKey: 'marketService.rejected api.noSeries ' }); assert.equal(rijen()[0].errorKey, 'marketService.rejected api.noSeries'); }); it('bewaart een request-id alleen bij de dienst en alleen in de afgesproken vorm', async () => { await recordIntegrationCall({ target: 'service', operation: '/v1/spot', outcome: 'fail', requestId: 'a1b2c3d4-0000-4000-8000-000000000000' }); await recordIntegrationCall({ target: 'service', operation: '/v1/spot', outcome: 'fail', requestId: 'kort' }); await recordIntegrationCall(fout({ requestId: 'a1b2c3d4-0000-4000-8000-000000000000' })); assert.deepEqual(rijen().map(r => r.requestId), ['a1b2c3d4-0000-4000-8000-000000000000', null, null]); }); }); describe('recordIntegrationCall: contractguards', () => { it('weigert een onbekende soort en een onbekende uitkomst, ook "ok"', async () => { await assert.rejects(recordIntegrationCall(fout({ target: 'lnd' })), /Unknown integration target/); await assert.rejects(recordIntegrationCall(fout({ outcome: 'ok' })), /Unknown integration outcome/); assert.equal(rijen().length, 0); }); it('eist een providerKey bij een provider en negeert hem bij de dienst', async () => { await assert.rejects(recordIntegrationCall(fout({ providerKey: '' })), /needs a providerKey/); await recordIntegrationCall({ target: 'service', providerKey: 'goldapi', operation: '/v1/spot', outcome: 'refused' }); assert.equal(rijen()[0].providerKey, null); }); it('legt het tijdstip van versturen vast tot op de milliseconde', async () => { await recordIntegrationCall(fout({ occurredAt: Date.UTC(2026, 9, 19, 8, 30, 15, 123) })); assert.equal(rijen()[0].occurredAt, '2026-10-19T08:30:15.123Z'); }); }); describe('opruimen bij elke schrijfactie', () => { it('haalt rijen ouder dan de termijn weg en laat rijen binnen de termijn staan', async () => { const dagen = CONFIG.INTEGRATION_LOG.RETENTION_DAYS; oudeRij(dagenGeleden(dagen + 1), 'oud'); oudeRij(dagenGeleden(dagen - 1), 'binnen'); await recordIntegrationCall(fout()); assert.deepEqual(rijen().map(r => r.providerKey), ['binnen', 'goldapi']); }); it('houdt het plafond aan door de oudste rijen weg te halen', async () => { const max = CONFIG.INTEGRATION_LOG.MAX_ROWS; const nu = new Date().toISOString(); harness.exports.run(`WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < ?) INSERT INTO IntegrationCall (target, providerKey, operation, outcome, occurredAt) SELECT 'provider', 'p' || i, 'spot', 'fail', ? FROM n`, [max, nu]); await recordIntegrationCall(fout()); const { n } = harness.exports.one(`SELECT COUNT(*) AS n FROM IntegrationCall`); assert.equal(n, max); assert.ok(!harness.exports.one(`SELECT 1 AS x FROM IntegrationCall WHERE providerKey = 'p1'`)); assert.ok(harness.exports.one(`SELECT 1 AS x FROM IntegrationCall WHERE providerKey = 'p2'`)); assert.ok(harness.exports.one(`SELECT 1 AS x FROM IntegrationCall WHERE providerKey = 'goldapi'`)); }); }); describe('countServiceCall: de dienst in ApiCallMetric', () => { const tellingen = () => harness.exports.all(`SELECT capability, okCount, failCount FROM ApiCallMetric`); it('telt onder de systeemprovider van de dienst', async () => { harness.exports.run(`INSERT INTO Provider (key, name, searchName, baseUrl, authenticationKind, authenticationRequired, isActive, isSeed, isSystem, createdAt) VALUES ('atreides_services', 'Atreides Services', 'atreides services', '', 'none', 0, 1, 1, 1, '2026-10-19')`); await countServiceCall({ capability: 'spot', ok: true, latencyMs: 30 }); await countServiceCall({ capability: 'spot', ok: false, latencyMs: 10 }); assert.deepEqual(tellingen(), [{ capability: 'spot', okCount: 1, failCount: 1 }]); }); it('telt niets zonder die rij, in plaats van te gooien', async () => { await countServiceCall({ capability: 'spot', ok: true, latencyMs: 30 }); assert.deepEqual(tellingen(), []); }); }); describe('serviceState: het oordeel van het lampje', () => { const dienst = () => harness.exports.run(`INSERT INTO Provider (key, name, searchName, baseUrl, authenticationKind, authenticationRequired, isActive, isSeed, isSystem, createdAt) VALUES ('atreides_services', 'Atreides Services', 'atreides services', '', 'none', 0, 1, 1, 1, '2026-10-19')`); const gelukt = (minuut) => harness.exports.run(`INSERT INTO ApiCallMetric (providerId, capability, bucketMinute, okCount, failCount, sumLatencyMs, maxLatencyMs) SELECT id, 'spot', ?, 1, 0, 10, 10 FROM Provider WHERE key = 'atreides_services'`, [minuut]); const mislukt = (at, outcome = 'fail') => recordIntegrationCall({ target: 'service', operation: '/v1/spot', outcome, occurredAt: at }); it('zonder log en zonder teller: geen oordeel', () => { dienst(); assert.deepEqual({ ...serviceState(), lastFail: null }, { ok: null, at: null, lastFail: null, lastOkAt: null }); }); it('een fout na het laatste succes is rood, ook in dezelfde minuut', async () => { dienst(); gelukt('2026-10-19T08:00'); await mislukt('2026-10-19T08:00:30.000Z'); assert.deepEqual([serviceState().ok, serviceState().at], [false, '2026-10-19T08:00:30.000Z']); }); it('een succes na de laatste fout is groen', async () => { dienst(); await mislukt('2026-10-19T08:00:30.000Z'); gelukt('2026-10-19T08:01'); assert.deepEqual([serviceState().ok, serviceState().at], [true, '2026-10-19T08:01:00.000Z']); }); it('een leeg antwoord maakt het lampje niet rood', async () => { dienst(); gelukt('2026-10-19T08:00'); await mislukt('2026-10-19T08:05:00.000Z', 'empty'); assert.equal(serviceState().ok, true); }); }); describe('lezen', () => { it('listIntegrationCalls: nieuwste eerst, binnen het venster, te filteren', async () => { oudeRij(dagenGeleden(10), 'buiten'); await recordIntegrationCall(fout({ providerKey: 'a' })); await recordIntegrationCall({ target: 'service', operation: '/v1/series', outcome: 'empty' }); await recordIntegrationCall(fout({ providerKey: 'b' })); assert.deepEqual(listIntegrationCalls().map(r => r.providerKey), ['b', null, 'a']); assert.deepEqual(listIntegrationCalls({ target: 'service' }).map(r => r.operation), ['/v1/series']); assert.deepEqual(listIntegrationCalls({ providerKey: 'a' }).length, 1); assert.deepEqual(listIntegrationCalls({ days: 30 }).map(r => r.providerKey), ['b', null, 'a', 'buiten']); assert.deepEqual(listIntegrationCalls({ limit: 1 }).map(r => r.providerKey), ['b']); }); it('lastIntegrationFailures: per bron de laatste, de dienst als eigen bron', async () => { await recordIntegrationCall(fout({ providerKey: 'a', detail: 'eerste' })); await recordIntegrationCall({ target: 'service', operation: '/v1/spot', outcome: 'fail', detail: 'dienst' }); await recordIntegrationCall(fout({ providerKey: 'a', detail: 'tweede' })); assert.deepEqual(lastIntegrationFailures().map(r => [r.providerKey, r.detail]), [['a', 'tweede'], [null, 'dienst']]); }); });