195 lines
9.7 KiB
JavaScript
195 lines
9.7 KiB
JavaScript
// Generated, do not edit. sha256:17aa65162f57d2f6155970591db45673bb76363e851935e6d5a324bd4bab456f
|
|||
|
|
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 });
|
||
|
|
|
||
|
|
for (const mod of ['providers', 'price-source', 'price-history', 'api-metrics']) {
|
||
|
|
mock.module(`../../client/data/${mod}.js`, { namedExports: await import(`../../server/data/${mod}.js`) });
|
||
|
|
}
|
||
|
|
|
||
|
|
const facade = await import('../../client/price/providers.js');
|
||
|
|
|
||
|
|
const VANDAAG = '2026-09-24';
|
||
|
|
|
||
|
|
let fetchCalls = [];
|
||
|
|
let faalt = false;
|
||
|
|
let reeksTot = null;
|
||
|
|
let fouten = new Map();
|
||
|
|
const geenKoers = { key: 'integrations.price.noValidPrice', params: { provider: 'perdatum' } };
|
||
|
|
const http = status => ({ key: 'integrations.price.httpFailed', params: { provider: 'perdatum', capability: 'history_point', status, detail: '' } });
|
||
|
|
function stub(url, opties) {
|
||
|
|
const body = JSON.parse(opties.body);
|
||
|
|
fetchCalls.push(body);
|
||
|
|
if (faalt) throw new Error('geen netwerk in deze test');
|
||
|
|
if (fouten.has(body.date)) {
|
||
|
|
const f = fouten.get(body.date);
|
||
|
|
return { ok: false, status: 502, json: async () => ({ error: 'geen koers', ...f }) };
|
||
|
|
}
|
||
|
|
if (body.capability === 'daily_history') {
|
||
|
|
const rijen = ['2026-09-18', '2026-09-19', '2026-09-20'].filter(d => d <= reeksTot)
|
||
|
|
.map(d => ({ time: Math.floor(Date.parse(`${d}T00:00:00Z`) / 1000), price: 80 }));
|
||
|
|
return { ok: true, status: 200, json: async () => rijen };
|
||
|
|
}
|
||
|
|
const time = Math.floor(Date.parse(`${body.date}T00:00:00Z`) / 1000);
|
||
|
|
return { ok: true, status: 200, json: async () => ({ time, price: 90 }) };
|
||
|
|
}
|
||
|
|
globalThis.fetch = async (url, opties) => stub(url, opties);
|
||
|
|
|
||
|
|
const { run, all, one, lastId } = harness.exports;
|
||
|
|
let goud, perDatum;
|
||
|
|
|
||
|
|
function provider(key, capabilities, { isSystem = 0 } = {}) {
|
||
|
|
run(`INSERT INTO Provider (key, name, searchName, baseUrl, authenticationKind, authenticationRequired,
|
||
|
|
isActive, isSeed, isSystem, createdAt)
|
||
|
|
VALUES (?,?,?, 'https://voorbeeld.test','none',0,1,1,?,'2026-09-01T10:00:00.000Z')`, [key, key, key, isSystem]);
|
||
|
|
const id = lastId();
|
||
|
|
run(`INSERT INTO ProviderCoverage (providerId, symbol) VALUES (?, 'XAU')`, [id]);
|
||
|
|
for (const c of capabilities) {
|
||
|
|
run(`INSERT INTO ProviderEndpoint (providerId, capability, method, pathTemplate, queryTemplate)
|
||
|
|
VALUES (?, ?, 'GET', '/prijs', NULL)`, [id, c]);
|
||
|
|
}
|
||
|
|
return id;
|
||
|
|
}
|
||
|
|
function bronkeuze(providerId, capability) {
|
||
|
|
const keuze = capability === 'spot' ? 'spot' : 'history';
|
||
|
|
run(`INSERT INTO PriceSource (assetId, capability, providerId, rank, status)
|
||
|
|
VALUES (?, ?, ?, (SELECT COALESCE(MAX(rank), 0) + 1 FROM PriceSource WHERE assetId = ? AND capability = ?), 'confirmed')`,
|
||
|
|
[goud, keuze, providerId, goud, keuze]);
|
||
|
|
}
|
||
|
|
function dagrij(providerId, dag, eur, updatedAt) {
|
||
|
|
run(`INSERT INTO PriceHistory (assetId, observedAt, resolution, fiatPrice, providerId, updatedAt)
|
||
|
|
VALUES (?, ?, 'day', ?, ?, ?)`, [goud, `${dag}T00:00:00Z`, eur, providerId, updatedAt]);
|
||
|
|
}
|
||
|
|
const asset = () => one(`SELECT * FROM Asset WHERE id = ?`, [goud]);
|
||
|
|
const gevraagd = () => fetchCalls.map(c => c.date);
|
||
|
|
const koersOp = dag => one(`SELECT fiatPrice FROM PriceHistory WHERE assetId = ? AND observedAt = ? AND providerId = ?`,
|
||
|
|
[goud, `${dag}T00:00:00Z`, perDatum])?.fiatPrice;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
harness.recreate();
|
||
|
|
facade.forgetSeriesFetches();
|
||
|
|
fetchCalls = [];
|
||
|
|
faalt = false;
|
||
|
|
reeksTot = null;
|
||
|
|
fouten = new Map();
|
||
|
|
run(`INSERT INTO Asset (symbol, name, searchName, kind, unit, subunit, decimals, displayUnit)
|
||
|
|
VALUES ('XAU','Goud','goud','metal','g','mg',3,'unit')`);
|
||
|
|
goud = lastId();
|
||
|
|
perDatum = provider('perdatum', ['spot', 'history_point']);
|
||
|
|
bronkeuze(perDatum, 'history_point');
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('fillClosesFor', () => {
|
||
|
|
it('vervangt een rij die OP zijn eigen dag geschreven is, en laat een slotkoers staan', async () => {
|
||
|
|
dagrij(perDatum, '2026-09-21', 70, '2026-09-21T12:00:00.000Z');
|
||
|
|
dagrij(perDatum, '2026-09-22', 80, '2026-09-23T08:00:00.000Z');
|
||
|
|
const uit = await facade.fillClosesFor(asset(), '2026-09-21', '2026-09-23', { today: VANDAAG });
|
||
|
|
assert.deepEqual(gevraagd(), ['2026-09-21', '2026-09-23'], 'de slotkoers van de 22e wordt niet opnieuw gevraagd');
|
||
|
|
assert.deepEqual(uit, { requested: 2, written: 2, limitReached: false, stopped: false });
|
||
|
|
assert.equal(koersOp('2026-09-21'), 90, 'de middagkoers is overschreven');
|
||
|
|
assert.equal(koersOp('2026-09-22'), 80, 'en de slotkoers staat er nog');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('vraagt vandaag nooit, ook niet als het bereik tot vandaag loopt', async () => {
|
||
|
|
await facade.fillClosesFor(asset(), '2026-09-23', VANDAAG, { today: VANDAAG });
|
||
|
|
assert.deepEqual(gevraagd(), ['2026-09-23']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('laat een koers op manual staan, ook als hij op dezelfde dag is ingevoerd', async () => {
|
||
|
|
const manual = provider('manual', [], { isSystem: 1 });
|
||
|
|
dagrij(manual, '2026-09-22', 315.5, '2026-09-22T09:00:00.000Z');
|
||
|
|
await facade.fillClosesFor(asset(), '2026-09-22', '2026-09-22', { today: VANDAAG });
|
||
|
|
assert.deepEqual(gevraagd(), [], 'een met de hand ingevoerde koers is een bewuste keuze');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('haalt bij een bron met een reeks eerst de reeks, en vraagt alleen de dagen die hij mist', async () => {
|
||
|
|
bronkeuze(provider('reeks', ['daily_history']), 'daily_history');
|
||
|
|
reeksTot = '2026-09-20';
|
||
|
|
const uit = await facade.fillClosesFor(asset(), '2026-09-18', '2026-09-23', { today: VANDAAG });
|
||
|
|
assert.deepEqual(fetchCalls.map(c => c.capability), ['daily_history', 'history_point', 'history_point', 'history_point']);
|
||
|
|
assert.deepEqual(gevraagd().slice(1), ['2026-09-21', '2026-09-22', '2026-09-23']);
|
||
|
|
assert.deepEqual(uit, { requested: 3, written: 3, limitReached: false, stopped: false });
|
||
|
|
});
|
||
|
|
|
||
|
|
it('vraagt niets per datum als de reeks het hele bereik dekt', async () => {
|
||
|
|
bronkeuze(provider('reeks', ['daily_history']), 'daily_history');
|
||
|
|
reeksTot = '2026-09-20';
|
||
|
|
await facade.fillClosesFor(asset(), '2026-09-18', '2026-09-20', { today: VANDAAG });
|
||
|
|
assert.deepEqual(fetchCalls.map(c => c.capability), ['daily_history']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('vraagt bij metaal geen zaterdag en zondag', async () => {
|
||
|
|
const uit = await facade.fillClosesFor(asset(), '2026-09-18', '2026-09-21', { today: VANDAAG });
|
||
|
|
assert.deepEqual(gevraagd(), ['2026-09-18', '2026-09-21'], 'vrijdag en maandag, het weekend ertussen niet');
|
||
|
|
assert.equal(uit.requested, 2);
|
||
|
|
assert.equal(facade.daysToAsk(asset(), '2026-09-18', '2026-09-21'), 2);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('vraagt bij crypto het weekend wél, want die markt is dan open', async () => {
|
||
|
|
run(`UPDATE Asset SET kind = 'crypto' WHERE id = ?`, [goud]);
|
||
|
|
await facade.fillClosesFor(asset(), '2026-09-18', '2026-09-21', { today: VANDAAG });
|
||
|
|
assert.deepEqual(gevraagd(), ['2026-09-18', '2026-09-19', '2026-09-20', '2026-09-21']);
|
||
|
|
assert.equal(facade.daysToAsk(asset(), '2026-09-18', '2026-09-21'), 4);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('slikt een mislukte dag en laat de oude rij staan', async () => {
|
||
|
|
dagrij(perDatum, '2026-09-22', 70, '2026-09-22T12:00:00.000Z');
|
||
|
|
faalt = true;
|
||
|
|
const uit = await facade.fillClosesFor(asset(), '2026-09-22', '2026-09-23', { today: VANDAAG });
|
||
|
|
assert.deepEqual(uit, { requested: 2, written: 0, limitReached: false, stopped: false });
|
||
|
|
assert.equal(koersOp('2026-09-22'), 70, 'een netwerkfout wist niets');
|
||
|
|
assert.deepEqual(onthouden(), [], 'en een netwerkfout is geen dag zonder koers');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
const onthouden = () => all(`SELECT day FROM PriceAbsence ORDER BY day`).map(r => r.day);
|
||
|
|
|
||
|
|
describe('fillClosesFor: een dag zonder koers', () => {
|
||
|
|
it('onthoudt de dag en vraagt hem de volgende ronde niet meer', async () => {
|
||
|
|
fouten.set('2026-09-22', geenKoers);
|
||
|
|
const eerste = await facade.fillClosesFor(asset(), '2026-09-21', '2026-09-23', { today: VANDAAG });
|
||
|
|
assert.equal(eerste.written, 2);
|
||
|
|
assert.deepEqual(onthouden(), ['2026-09-22']);
|
||
|
|
fetchCalls = [];
|
||
|
|
const tweede = await facade.fillClosesFor(asset(), '2026-09-21', '2026-09-23', { today: VANDAAG });
|
||
|
|
assert.deepEqual(gevraagd(), [], 'de 21e en 23e zijn slot, de 22e is onthouden');
|
||
|
|
assert.equal(tweede.requested, 0);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('telt een 404 van de bron mee, en een 429 of 500 niet', async () => {
|
||
|
|
fouten.set('2026-09-21', http(404));
|
||
|
|
fouten.set('2026-09-22', http(429));
|
||
|
|
fouten.set('2026-09-23', http(500));
|
||
|
|
await facade.fillClosesFor(asset(), '2026-09-18', '2026-09-23', { today: VANDAAG });
|
||
|
|
assert.deepEqual(onthouden(), ['2026-09-21']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('onthoudt niets van een bron die in die ronde geen enkele koers gaf', async () => {
|
||
|
|
for (const d of ['2026-09-21', '2026-09-22', '2026-09-23']) fouten.set(d, geenKoers);
|
||
|
|
await facade.fillClosesFor(asset(), '2026-09-21', '2026-09-23', { today: VANDAAG });
|
||
|
|
assert.deepEqual(onthouden(), []);
|
||
|
|
fetchCalls = [];
|
||
|
|
await facade.fillClosesFor(asset(), '2026-09-21', '2026-09-23', { today: VANDAAG });
|
||
|
|
assert.equal(fetchCalls.length, 3, 'de volgende ronde vraagt ze gewoon opnieuw');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('refreshRecentCloses', () => {
|
||
|
|
it(`vraagt de werkdagen van de laatste ${CONFIG.PRICE_HISTORY.RECENT_CLOSE_DAYS} dagen tot en met gisteren, en de tweede ronde niets`, async () => {
|
||
|
|
await facade.refreshRecentCloses([asset()], { today: VANDAAG });
|
||
|
|
const dagen = gevraagd();
|
||
|
|
assert.equal(dagen.length, CONFIG.PRICE_HISTORY.RECENT_CLOSE_DAYS - 4);
|
||
|
|
assert.equal(dagen[0], '2026-09-10');
|
||
|
|
assert.equal(dagen.at(-1), '2026-09-23', 'gisteren is de laatste dag');
|
||
|
|
|
||
|
|
fetchCalls = [];
|
||
|
|
await facade.refreshRecentCloses([asset()], { today: VANDAAG });
|
||
|
|
assert.equal(fetchCalls.length, 0);
|
||
|
|
assert.equal(all(`SELECT id FROM PriceHistory WHERE assetId = ?`, [goud]).length, CONFIG.PRICE_HISTORY.RECENT_CLOSE_DAYS - 4);
|
||
|
|
});
|
||
|
|
});
|