// 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); }); });