// Generated, do not edit. sha256:3e601bf7d829d14e2bd02a09636bf79c397a27d8499910e51c05e4aa817aa93b import { describe, it, beforeEach, mock } from 'node:test'; import { toSearchName } from '../../shared/search-name.js'; import assert from 'node:assert/strict'; import { createDbHarness } from '../helpers/db-node.mjs'; const harness = createDbHarness(); mock.module('../../server/db.js', { namedExports: harness.exports }); const { apiCallsByProvider, apiMetricsByCapability, apiMetricsGlobal, apiMetricsSeries, apiMetricsTotals, recordApiCall, } = await import('../../server/data/api-metrics.js'); const VANAF = '2020-01-01T00:00'; let ctx; beforeEach(() => { harness.recreate(); ctx = { mempool: provider('mempool'), coingecko: provider('coingecko') }; }); function provider(key) { harness.exports.run( `INSERT INTO Provider (key, name, searchName, baseUrl, authenticationKind, authenticationRequired, isActive, isSeed, isSystem, createdAt) VALUES (?,?,?,?,'none',0,1,0,0,'2024-01-01T00:00:00')`, [key, key, toSearchName(key), `https://${key}.test`]); return harness.exports.lastId(); } function bucket({ providerId = ctx.mempool, capability = 'spot', minute, ok = 1, fail = 0, sum = 100, max = 100 }) { harness.exports.run( `INSERT INTO ApiCallMetric (providerId, capability, bucketMinute, okCount, failCount, sumLatencyMs, maxLatencyMs) VALUES (?,?,?,?,?,?,?)`, [providerId, capability, minute, ok, fail, sum, max]); } const rijen = () => harness.exports.all( `SELECT * FROM ApiCallMetric ORDER BY providerId, capability, bucketMinute`); describe('recordApiCall', () => { it('registreert een gelukte call in de bucket van deze minuut', async () => { await recordApiCall({ providerId: ctx.mempool, capability: 'spot', ok: true, latency_ms: 120 }); const r = rijen(); assert.equal(r.length, 1); assert.equal(r[0].okCount, 1); assert.equal(r[0].failCount, 0); assert.equal(r[0].sumLatencyMs, 120); assert.equal(r[0].maxLatencyMs, 120); assert.match(r[0].bucketMinute, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/, 'de bucket is een minuut-prefix, want daarop wordt later per uur en dag gegroepeerd'); }); it('registreert een mislukte call apart', async () => { await recordApiCall({ providerId: ctx.mempool, capability: 'spot', ok: false, latency_ms: 80 }); assert.equal(rijen()[0].okCount, 0); assert.equal(rijen()[0].failCount, 1); assert.equal(rijen()[0].sumLatencyMs, 80, 'ook een mislukte call heeft een latency'); }); it('telt op binnen dezelfde minuut in plaats van te overschrijven', async () => { await recordApiCall({ providerId: ctx.mempool, capability: 'spot', ok: true, latency_ms: 100 }); await recordApiCall({ providerId: ctx.mempool, capability: 'spot', ok: true, latency_ms: 200 }); await recordApiCall({ providerId: ctx.mempool, capability: 'spot', ok: false, latency_ms: 50 }); const r = rijen(); assert.equal(r.length, 1, 'één rij per (provider, capability, minuut)'); assert.equal(r[0].okCount, 2); assert.equal(r[0].failCount, 1); assert.equal(r[0].sumLatencyMs, 350, 'de som telt op'); assert.equal(r[0].maxLatencyMs, 200, 'het maximum telt NIET op maar neemt het hoogste'); }); it('houdt capabilities en providers gescheiden', async () => { await recordApiCall({ providerId: ctx.mempool, capability: 'spot', ok: true, latency_ms: 10 }); await recordApiCall({ providerId: ctx.mempool, capability: 'daily_history', ok: true, latency_ms: 10 }); await recordApiCall({ providerId: ctx.coingecko, capability: 'spot', ok: true, latency_ms: 10 }); assert.equal(rijen().length, 3, 'de sleutel is (provider, capability, minuut)'); }); it('negeert een call zonder geldige provider', async () => { await recordApiCall({ providerId: null, capability: 'spot', ok: true, latency_ms: 10 }); await recordApiCall({ providerId: undefined, capability: 'spot', ok: true, latency_ms: 10 }); await recordApiCall({ providerId: 'mempool', capability: 'spot', ok: true, latency_ms: 10 }); await recordApiCall({ providerId: 1.5, capability: 'spot', ok: true, latency_ms: 10 }); assert.deepEqual(rijen(), [], 'geen enkele rij, en ook geen fout'); }); it('maakt van een ontbrekende of onzinnige latency een 0', async () => { await recordApiCall({ providerId: ctx.mempool, capability: 'spot', ok: true }); assert.equal(rijen()[0].sumLatencyMs, 0, 'de kolom is NOT NULL, dus dit moet een getal zijn'); harness.recreate(); ctx.mempool = provider('mempool'); await recordApiCall({ providerId: ctx.mempool, capability: 'spot', ok: true, latency_ms: NaN }); assert.equal(rijen()[0].sumLatencyMs, 0); }); it('rondt af en klemt een negatieve latency op 0', async () => { await recordApiCall({ providerId: ctx.mempool, capability: 'spot', ok: true, latency_ms: 12.6 }); assert.equal(rijen()[0].sumLatencyMs, 13); harness.recreate(); ctx.mempool = provider('mempool'); await recordApiCall({ providerId: ctx.mempool, capability: 'spot', ok: true, latency_ms: -5 }); assert.equal(rijen()[0].sumLatencyMs, 0, 'een negatieve duur is nooit een meting'); }); }); describe('apiMetricsTotals', () => { it('telt over de buckets en leidt gemiddelde en maximum af', () => { bucket({ minute: '2024-05-01T10:00', ok: 2, fail: 0, sum: 300, max: 200 }); bucket({ minute: '2024-05-01T10:01', ok: 1, fail: 1, sum: 100, max: 60 }); assert.deepEqual(apiMetricsTotals(ctx.mempool, VANAF), { ok: 3, fail: 1, calls: 4, avg_latency_ms: 100, maxLatencyMs: 200, }); }); it('geeft nullen als er niets is, zonder deling door nul', () => { assert.deepEqual(apiMetricsTotals(ctx.mempool, VANAF), { ok: 0, fail: 0, calls: 0, avg_latency_ms: 0, maxLatencyMs: 0, }); }); it('negeert buckets vóór de grens', () => { bucket({ minute: '2024-04-30T23:59', ok: 5, sum: 500, max: 500 }); bucket({ minute: '2024-05-01T00:00', ok: 1, sum: 100, max: 100 }); const t = apiMetricsTotals(ctx.mempool, '2024-05-01T00:00'); assert.equal(t.ok, 1, 'de grens is inclusief: >= sinceBucket'); assert.equal(t.maxLatencyMs, 100, 'ook het maximum kijkt alleen binnen het venster'); }); it('kijkt alleen naar de gevraagde provider', () => { bucket({ providerId: ctx.mempool, minute: '2024-05-01T10:00', ok: 1 }); bucket({ providerId: ctx.coingecko, minute: '2024-05-01T10:00', ok: 7 }); assert.equal(apiMetricsTotals(ctx.mempool, VANAF).ok, 1); assert.equal(apiMetricsTotals(ctx.coingecko, VANAF).ok, 7); }); it('rondt het gemiddelde af op een heel getal', () => { bucket({ minute: '2024-05-01T10:00', ok: 3, fail: 0, sum: 100, max: 50 }); assert.equal(apiMetricsTotals(ctx.mempool, VANAF).avg_latency_ms, 33); }); }); describe('apiMetricsGlobal', () => { it('telt over alle providers heen', () => { bucket({ providerId: ctx.mempool, minute: '2024-05-01T10:00', ok: 2, fail: 1, sum: 300, max: 150 }); bucket({ providerId: ctx.coingecko, minute: '2024-05-01T10:00', ok: 1, fail: 0, sum: 100, max: 400 }); assert.deepEqual(apiMetricsGlobal(VANAF), { ok: 3, fail: 1, calls: 4, avg_latency_ms: 100, maxLatencyMs: 400, }); }); it('spiegelt de vorm van apiMetricsTotals', () => { bucket({ minute: '2024-05-01T10:00', ok: 1, sum: 10, max: 10 }); assert.deepEqual(Object.keys(apiMetricsGlobal(VANAF)).sort(), Object.keys(apiMetricsTotals(ctx.mempool, VANAF)).sort()); }); it('geeft nullen op een lege tabel', () => { assert.deepEqual(apiMetricsGlobal(VANAF), { ok: 0, fail: 0, calls: 0, avg_latency_ms: 0, maxLatencyMs: 0, }); }); }); describe('apiCallsByProvider', () => { it('telt gelukte en mislukte aanroepen samen, per provider, over alle capabilities', () => { bucket({ providerId: ctx.mempool, capability: 'spot', minute: '2024-05-01T10:00', ok: 2, fail: 1 }); bucket({ providerId: ctx.mempool, capability: 'daily_history', minute: '2024-05-02T10:00', ok: 1, fail: 0 }); bucket({ providerId: ctx.coingecko, minute: '2024-05-01T10:00', ok: 0, fail: 4 }); const uit = new Map(apiCallsByProvider(VANAF).map(r => [r.providerId, r.calls])); assert.equal(uit.get(ctx.mempool), 4); assert.equal(uit.get(ctx.coingecko), 4, 'vier mislukte aanroepen kosten ook vier van het tegoed'); }); it('telt vorige maand niet mee', () => { bucket({ minute: '2024-04-30T23:59', ok: 50 }); bucket({ minute: '2024-05-01T00:00', ok: 1 }); assert.deepEqual(apiCallsByProvider('2024-05-01T00:00').map(r => r.calls), [1]); }); }); describe('apiMetricsByCapability', () => { it('groepeert per capability, alfabetisch', () => { bucket({ capability: 'spot', minute: '2024-05-01T10:00', ok: 1, sum: 100, max: 100 }); bucket({ capability: 'spot', minute: '2024-05-01T10:01', ok: 2, sum: 200, max: 150 }); bucket({ capability: 'daily_history', minute: '2024-05-01T10:00', ok: 1, fail: 2, sum: 90, max: 90 }); const r = apiMetricsByCapability(ctx.mempool, VANAF); assert.deepEqual(r.map(x => x.capability), ['daily_history', 'spot'], 'vaste volgorde, zodat het uitklap-paneel niet danst tussen twee aanroepen'); assert.equal(r[0].fail, 2); assert.equal(r[1].ok, 3); assert.equal(r[1].max_lat, 150); }); it('geeft een lege lijst zonder metingen', () => { assert.deepEqual(apiMetricsByCapability(ctx.mempool, VANAF), []); }); }); describe('apiMetricsSeries', () => { beforeEach(() => { bucket({ minute: '2024-05-01T10:00', ok: 1, sum: 10, max: 10 }); bucket({ minute: '2024-05-01T10:30', ok: 2, sum: 20, max: 40 }); bucket({ minute: '2024-05-01T11:00', ok: 1, fail: 1, sum: 30, max: 90 }); bucket({ minute: '2024-05-02T09:00', ok: 5, sum: 50, max: 20 }); }); it('groepeert per minuut bij prefixlengte 16', () => { assert.equal(apiMetricsSeries(ctx.mempool, VANAF, 16).length, 4, 'elke bucket apart'); }); it('groepeert per uur bij prefixlengte 13', () => { const r = apiMetricsSeries(ctx.mempool, VANAF, 13); assert.deepEqual(r.map(x => x.b), ['2024-05-01T10', '2024-05-01T11', '2024-05-02T09']); assert.equal(r[0].ok, 3, 'de twee buckets van 10:xx samen'); assert.equal(r[0].max_lat, 40, 'het maximum van dat uur, niet de som'); }); it('groepeert per dag bij prefixlengte 10', () => { const r = apiMetricsSeries(ctx.mempool, VANAF, 10); assert.deepEqual(r.map(x => x.b), ['2024-05-01', '2024-05-02']); assert.equal(r[0].ok, 4); assert.equal(r[0].fail, 1); assert.equal(r[1].ok, 5); }); it('loopt in chronologische volgorde', () => { const b = apiMetricsSeries(ctx.mempool, VANAF, 13).map(x => x.b); assert.deepEqual(b, [...b].sort(), 'een sparkline moet van links naar rechts vooruit lopen'); }); it('is sparse: buckets zonder calls komen er niet in', () => { const uren = apiMetricsSeries(ctx.mempool, VANAF, 13).map(x => x.b); assert.equal(uren.includes('2024-05-01T12'), false); }); it('respecteert de ondergrens', () => { assert.deepEqual(apiMetricsSeries(ctx.mempool, '2024-05-02T00:00', 10).map(x => x.b), ['2024-05-02']); }); }); describe('randwaardes: apiMetricsSeries op de randen', () => { beforeEach(() => { bucket({ minute: '2024-05-01T10:00', ok: 1, sum: 10, max: 10 }); bucket({ minute: '2024-05-05T10:00', ok: 1, sum: 10, max: 10 }); }); it('geeft een lege lijst als er ná de grens niets meer is', () => { assert.deepEqual(apiMetricsSeries(ctx.mempool, '2030-01-01T00:00', 10), []); }); it('geeft exact één punt terug als er één bucket binnen de grens valt', () => { const reeks = apiMetricsSeries(ctx.mempool, '2024-05-05T10:00', 10); assert.equal(reeks.length, 1, 'één bucket blijft één punt; er wordt niets aangevuld'); assert.equal(reeks[0].b, '2024-05-05'); }); it('is sparse: de dagen tússen twee metingen komen niet in de reeks', () => { const reeks = apiMetricsSeries(ctx.mempool, VANAF, 10); assert.deepEqual(reeks.map(r => r.b), ['2024-05-01', '2024-05-05']); }); });