Files
AtreidesAssetsFree/tests/price/slotkoersen.test.mjs
T

195 lines
9.7 KiB
JavaScript
Raw Normal View History

2026-10-29 11:57:29 +01:00
// 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);
});
});