// Generated, do not edit. sha256:f793f1998c617a8dfc8fce95de7c88d4b4d4d1a08ecaa3c19643f70bcb8ebab4 import { describe, it, beforeEach, mock } from 'node:test'; import assert from 'node:assert/strict'; import { toSearchName } from '../../shared/search-name.js'; import { createDbHarness } from '../helpers/db-node.mjs'; import { CONFIG, ENUMS } from '../../shared/config.js'; const harness = createDbHarness(); mock.module('../../server/db.js', { namedExports: harness.exports }); const { addManualPricePoint, getPriceAt, getPricePoint, latestPricePoint, latestSpotPoint, listAllPriceHistory, listPriceGaps, listPriceHistory, priceHistoryCount, recordCurrentSpot, upsertPriceHistory, ignorePriceGap, listIgnoredPriceGaps, unignorePriceGap, recordNoPriceDays, listNoPriceDays, listPriceAbsences, deletePriceAbsences, } = await import('../../server/data/price-history.js'); const DREMPEL = CONFIG.PRICE_HISTORY.NEAREST_MAX_SECONDS; let btc, eth; beforeEach(() => { harness.recreate(); providerIds.clear(); providerId('mempool'); providerId('bitvavo'); btc = asset('BTC'); eth = asset('ETH'); dekking('mempool', 'BTC', 'ETH'); dekking('bitvavo', 'BTC', 'ETH'); }); function asset(symbol) { harness.exports.run( `INSERT INTO Asset (symbol, name, searchName, kind, unit, subunit, decimals, displayUnit) VALUES (?,?,?,'crypto',?,NULL,8,'unit')`, [symbol, symbol, toSearchName(symbol), symbol]); return harness.exports.lastId(); } function provider(key) { harness.exports.run( `INSERT INTO Provider (key, name, searchName, baseUrl, authenticationKind, authenticationRequired, isActive, isSeed, isSystem, createdAt) VALUES (?,?,?,'','none',0,1,1,0,'2024-01-01T00:00:00Z')`, [key, key, toSearchName(key)]); return harness.exports.lastId(); } const providerIds = new Map(); const providerId = key => { if (!providerIds.has(key)) providerIds.set(key, provider(key)); return providerIds.get(key); }; function dekking(key, ...symbolen) { for (const s of symbolen) harness.exports.run(`INSERT INTO ProviderCoverage (providerId, symbol) VALUES (?,?)`, [providerId(key), s]); } function systeemProvider(key) { harness.exports.run( `INSERT INTO Provider (key, name, searchName, baseUrl, authenticationKind, authenticationRequired, isActive, isSeed, isSystem, createdAt) VALUES (?,?,?,'','none',0,1,1,1,'2024-01-01T00:00:00Z')`, [key, key, toSearchName(key)]); providerIds.set(key, harness.exports.lastId()); return providerIds.get(key); } function punt({ assetId = null, observedAt, resolution = 'day', fiatPrice = 40000, source = 'mempool' }) { harness.exports.run( `INSERT INTO PriceHistory (assetId, observedAt, resolution, fiatPrice, providerId, updatedAt) VALUES (?,?,?,?,?,?)`, [assetId ?? btc, observedAt, resolution, fiatPrice, providerId(source), observedAt]); return harness.exports.lastId(); } const unix = iso => Math.floor(Date.parse(iso) / 1000); const vanaf = (momentIso, sec) => new Date((unix(momentIso) + sec) * 1000).toISOString(); describe('upsertPriceHistory', () => { it('valideert de resolutie tegen de enum', async () => { assert.deepEqual([...ENUMS.PRICE_RESOLUTION], ['day', 'timestamp']); await assert.rejects( () => upsertPriceHistory(btc, [{ time: unix('2024-01-01T12:00:00Z'), price: 1 }], 'mempool', 'uurtje'), /resolution/); }); it('normaliseert een dag-punt naar middernacht UTC', async () => { await upsertPriceHistory(btc, [{ time: unix('2024-03-05T14:37:11Z'), price: 1000 }], 'mempool', 'day'); assert.equal(harness.exports.one(`SELECT observedAt FROM PriceHistory`).observedAt, '2024-03-05T00:00:00Z'); }); it('bewaart bij een timestamp-punt het exacte moment', async () => { await upsertPriceHistory(btc, [{ time: unix('2024-03-05T14:37:11Z'), price: 1000 }], 'mempool', 'timestamp'); assert.equal(harness.exports.one(`SELECT observedAt FROM PriceHistory`).observedAt, '2024-03-05T14:37:11.000Z'); }); it('werkt een bestaand punt bij in plaats van te dupliceren', async () => { const t = unix('2024-03-05T00:00:00Z'); await upsertPriceHistory(btc, [{ time: t, price: 1000 }], 'mempool', 'day'); await upsertPriceHistory(btc, [{ time: t, price: 2000 }], 'mempool', 'day'); const rijen = harness.exports.all(`SELECT fiatPrice FROM PriceHistory`); assert.equal(rijen.length, 1, 'ON CONFLICT werkt bij, dus geen tweede rij'); assert.equal(rijen[0].fiatPrice, 2000, 'de nieuwste waarde wint'); }); it('bewaart de bron achter een koers van de dienst, en werkt hem bij met de koers', async () => { const t = unix('2024-03-05T00:00:00Z'); const bron = () => harness.exports.one(`SELECT sourceName FROM PriceHistory`).sourceName; await upsertPriceHistory(btc, [{ time: t, price: 1000 }], 'mempool', 'day'); assert.equal(bron(), null, 'zonder bron blijft hij leeg'); await upsertPriceHistory(btc, [{ time: t, price: 1001, sourceName: 'Bitvavo' }], 'mempool', 'day'); assert.equal(bron(), 'Bitvavo'); await upsertPriceHistory(btc, [{ time: t, price: 1002, sourceName: 'ECB' }], 'mempool', 'day'); assert.equal(bron(), 'ECB', 'een nieuwere koers brengt zijn eigen bron mee'); }); it('zet createdAt bij het aanmaken en laat hem staan bij bijwerken', async () => { const t = unix('2024-03-05T00:00:00Z'); await upsertPriceHistory(btc, [{ time: t, price: 1000 }], 'mempool', 'day'); const eerst = harness.exports.one(`SELECT createdAt FROM PriceHistory`).createdAt; assert.ok(eerst, 'een nieuwe rij heeft een createdAt'); harness.exports.run(`UPDATE PriceHistory SET createdAt = '2024-03-06T08:00:00.000Z'`); await upsertPriceHistory(btc, [{ time: t, price: 2000 }], 'mempool', 'day'); const r = harness.exports.one(`SELECT createdAt, fiatPrice FROM PriceHistory`); assert.equal(r.fiatPrice, 2000); assert.equal(r.createdAt, '2024-03-06T08:00:00.000Z', 'bijwerken raakt createdAt niet'); }); it('houdt reeksen van verschillende bronnen naast elkaar', async () => { const t = unix('2024-03-05T00:00:00Z'); await upsertPriceHistory(btc, [{ time: t, price: 1000 }], 'mempool', 'day'); await upsertPriceHistory(btc, [{ time: t, price: 1010 }], 'bitvavo', 'day'); assert.equal(priceHistoryCount(btc), 2, 'de bron zit in de unieke sleutel'); }); it('slaat een punt zonder koers over in plaats van te weigeren', async () => { await upsertPriceHistory(btc, [ { time: unix('2024-03-05T00:00:00Z') }, { time: unix('2024-03-06T00:00:00Z'), price: 1100 }, ], 'mempool', 'day'); const rijen = harness.exports.all(`SELECT observedAt, fiatPrice FROM PriceHistory`); assert.deepEqual(rijen.map(r => [r.observedAt, r.fiatPrice]), [['2024-03-06T00:00:00Z', 1100]]); }); it('slaat een koers van nul of lager over', async () => { await upsertPriceHistory(btc, [{ time: unix('2012-03-05T00:00:00Z'), price: 0 }], 'mempool', 'day'); await upsertPriceHistory(btc, [{ time: unix('2012-03-06T00:00:00Z'), price: 4 }], 'mempool', 'day'); const rijen = harness.exports.all(`SELECT fiatPrice FROM PriceHistory ORDER BY observedAt`); assert.deepEqual(rijen.map(r => r.fiatPrice), [4]); }); it('slaat een punt zonder positieve koers over, ook een lege string', async () => { await upsertPriceHistory(btc, [ { time: unix('2012-03-05T00:00:00Z'), price: 0 }, { time: unix('2012-03-06T00:00:00Z'), price: '' }, { time: unix('2012-03-07T00:00:00Z'), price: -3 }, ], 'mempool', 'day'); assert.equal(priceHistoryCount(btc), 0); }); it('laat een opgeslagen koers staan als dezelfde dag later met nul terugkomt', async () => { const t = unix('2024-03-05T00:00:00Z'); await upsertPriceHistory(btc, [{ time: t, price: 1000 }], 'mempool', 'day'); await upsertPriceHistory(btc, [{ time: t, price: 0 }], 'mempool', 'day'); const r = harness.exports.one(`SELECT fiatPrice FROM PriceHistory`); assert.equal(r.fiatPrice, 1000); }); }); describe('listPriceHistory / priceHistoryCount / getPricePoint', () => { it('sorteert nieuwste eerst en filtert op asset', () => { punt({ observedAt: '2024-01-01T00:00:00Z' }); punt({ observedAt: '2024-03-01T00:00:00Z' }); punt({ assetId: eth, observedAt: '2024-02-01T00:00:00Z' }); assert.deepEqual(listPriceHistory(btc).map(r => r.observedAt), ['2024-03-01T00:00:00Z', '2024-01-01T00:00:00Z']); }); it('filtert vanaf een datum, grens inbegrepen', () => { punt({ observedAt: '2024-01-01T00:00:00Z' }); punt({ observedAt: '2024-02-01T00:00:00Z' }); assert.equal(listPriceHistory(btc, '2024-02-01').length, 1); assert.equal(listPriceHistory(btc, '2024-01-01').length, 2, 'de begindag telt mee'); }); it('telt per asset', () => { punt({ observedAt: '2024-01-01T00:00:00Z' }); punt({ assetId: eth, observedAt: '2024-01-01T00:00:00Z' }); assert.equal(priceHistoryCount(btc), 1); assert.equal(priceHistoryCount(eth), 1); }); it('getPricePoint vindt een dagpunt op datum', () => { punt({ observedAt: '2024-01-01T00:00:00Z', fiatPrice: 42000 }); assert.equal(getPricePoint(btc, '2024-01-01').fiatPrice, 42000); assert.equal(getPricePoint(btc, '2024-01-02'), null); }); it('getPricePoint valt zonder dagpunt terug op de laatste spot van die dag, gemarkeerd', () => { punt({ observedAt: '2024-01-01T09:00:00Z', resolution: 'timestamp', fiatPrice: 1 }); punt({ observedAt: '2024-01-01T21:00:00Z', resolution: 'timestamp', fiatPrice: 2 }); const p = getPricePoint(btc, '2024-01-01'); assert.equal(p.fiatPrice, 2, 'de LAATSTE spot van de dag'); assert.equal(p.derivedFrom, 'spot'); }); it('getPricePoint laat een echt dagpunt voorgaan, en geeft met derived:false alleen dat', () => { punt({ observedAt: '2024-01-01T21:00:00Z', resolution: 'timestamp', fiatPrice: 2 }); assert.equal(getPricePoint(btc, '2024-01-01', { derived: false }), null); punt({ observedAt: '2024-01-01T00:00:00Z', fiatPrice: 5 }); const p = getPricePoint(btc, '2024-01-01'); assert.equal(p.fiatPrice, 5); assert.equal(p.derivedFrom, undefined); }); it('getPricePoint neemt bij meerdere bronnen de laatst toegevoegde rij', () => { punt({ observedAt: '2024-01-01T00:00:00Z', fiatPrice: 100, source: 'mempool' }); punt({ observedAt: '2024-01-01T00:00:00Z', fiatPrice: 200, source: 'bitvavo' }); assert.equal(getPricePoint(btc, '2024-01-01').fiatPrice, 200); }); }); describe('getPriceAt', () => { const MOMENT = '2024-06-15T12:00:00Z'; it('geeft null zonder enig punt', () => { assert.equal(getPriceAt(btc, unix(MOMENT)), null); }); it('kiest een timestamp-punt binnen de drempel', () => { punt({ observedAt: vanaf(MOMENT, -30), resolution: 'timestamp', fiatPrice: 111 }); const r = getPriceAt(btc, unix(MOMENT)); assert.equal(r.fiatPrice, 111); assert.equal(r.resolution, 'timestamp'); assert.equal(r.dist_seconds, 30, 'de afstand wordt meegegeven'); }); it('neemt een timestamp-punt buiten de drempel niet als moment-koers, wel als koers van die dag', () => { punt({ observedAt: new Date((unix(MOMENT) - DREMPEL - 60) * 1000).toISOString(), resolution: 'timestamp', fiatPrice: 111 }); const p = getPriceAt(btc, unix(MOMENT)); assert.equal(p.fiatPrice, 111); assert.equal(p.derivedFrom, 'spot'); assert.equal(p.dist_seconds, undefined, 'geen moment-treffer, dus geen afstand'); }); it('geeft niets als er op die dag geen enkele koers is', () => { punt({ observedAt: '2020-01-01T12:00:00Z', resolution: 'timestamp', fiatPrice: 111 }); assert.equal(getPriceAt(btc, unix(MOMENT)), null); }); it('kiest van twee timestamp-punten het naaste, ook als dat later ligt', () => { punt({ observedAt: vanaf(MOMENT, -Math.floor(DREMPEL * 0.8)), resolution: 'timestamp', fiatPrice: 100 }); punt({ observedAt: vanaf(MOMENT, Math.floor(DREMPEL * 0.3)), resolution: 'timestamp', fiatPrice: 200 }); assert.equal(getPriceAt(btc, unix(MOMENT)).fiatPrice, 200, 'dichterbij weegt zwaarder dan eerder'); }); it('valt terug op een dagpunt van dezelfde kalenderdag', () => { punt({ observedAt: '2024-06-15T00:00:00Z', fiatPrice: 300 }); const r = getPriceAt(btc, unix(MOMENT)); assert.equal(r.fiatPrice, 300); assert.equal(r.resolution, 'day'); assert.equal(r.dist_seconds, null, 'een dagpunt heeft geen afstand'); }); it('gebruikt geen dagpunt van een andere dag', () => { punt({ observedAt: '2024-06-14T00:00:00Z', fiatPrice: 300 }); assert.equal(getPriceAt(btc, unix(MOMENT)), null); }); it('geeft een timestamp-punt voorrang boven een dagpunt', () => { punt({ observedAt: '2024-06-15T00:00:00Z', fiatPrice: 300 }); punt({ observedAt: vanaf(MOMENT, -30), resolution: 'timestamp', fiatPrice: 111 }); assert.equal(getPriceAt(btc, unix(MOMENT)).fiatPrice, 111); }); describe('voorkeursbron', () => { it('kiest de voorkeursbron ook als een andere bron naderbij ligt', () => { punt({ observedAt: vanaf(MOMENT, Math.floor(DREMPEL * 0.3)), resolution: 'timestamp', fiatPrice: 100, source: 'mempool' }); punt({ observedAt: vanaf(MOMENT, -Math.floor(DREMPEL * 0.8)), resolution: 'timestamp', fiatPrice: 200, source: 'bitvavo' }); assert.equal(getPriceAt(btc, unix(MOMENT), 'bitvavo').fiatPrice, 200, 'de bron-matrix bepaalt welke provider leidend is'); }); it('valt binnen dezelfde resolutie terug op een andere bron', () => { punt({ observedAt: vanaf(MOMENT, -30), resolution: 'timestamp', fiatPrice: 100, source: 'mempool' }); assert.equal(getPriceAt(btc, unix(MOMENT), 'bitvavo').fiatPrice, 100, 'liever een punt van een andere bron dan geen koers'); }); it('gaat niet van resolutie af om de voorkeursbron te volgen', () => { punt({ observedAt: vanaf(MOMENT, -30), resolution: 'timestamp', fiatPrice: 100, source: 'mempool' }); punt({ observedAt: '2024-06-15T00:00:00Z', fiatPrice: 999, source: 'bitvavo' }); assert.equal(getPriceAt(btc, unix(MOMENT), 'bitvavo').fiatPrice, 100); }); }); }); describe('latestPricePoint', () => { it('geeft het nieuwste punt, ongeacht de resolutie', () => { punt({ observedAt: '2026-01-01T00:00:00Z', fiatPrice: 100 }); punt({ observedAt: '2026-03-01T12:00:00Z', resolution: 'timestamp', fiatPrice: 300 }); punt({ observedAt: '2026-02-01T00:00:00Z', fiatPrice: 200 }); assert.equal(latestPricePoint(btc).fiatPrice, 300); }); it('geeft null als er niets staat, en niet nul', () => { assert.equal(latestPricePoint(eth), null); }); it('kijkt niet over de asset-grens heen', () => { punt({ assetId: eth, observedAt: '2027-01-01T00:00:00Z', fiatPrice: 999 }); punt({ observedAt: '2026-01-01T00:00:00Z', fiatPrice: 100 }); assert.equal(latestPricePoint(btc).fiatPrice, 100, 'het nieuwere punt van een ANDER asset hoort niet mee'); }); it('kiest bij twee punten op hetzelfde moment de laatst geschreven rij', () => { punt({ observedAt: '2026-01-01T00:00:00Z', fiatPrice: 100, source: 'mempool' }); punt({ observedAt: '2026-01-01T00:00:00Z', fiatPrice: 111, source: 'bitvavo' }); assert.equal(latestPricePoint(btc).fiatPrice, 111); }); }); describe('latestSpotPoint', () => { it('geeft de nieuwste spot van die ene bron', () => { punt({ observedAt: '2026-09-25T08:00:00.000Z', resolution: 'timestamp', fiatPrice: 1 }); punt({ observedAt: '2026-09-25T09:00:00.000Z', resolution: 'timestamp', fiatPrice: 2 }); assert.equal(latestSpotPoint(btc, 'mempool').fiatPrice, 2); }); it('neemt de spot van een andere bron niet, ook niet als die nieuwer is', () => { punt({ observedAt: '2026-09-25T08:00:00.000Z', resolution: 'timestamp', fiatPrice: 1 }); punt({ observedAt: '2026-09-25T09:00:00.000Z', resolution: 'timestamp', fiatPrice: 2, source: 'bitvavo' }); assert.equal(latestSpotPoint(btc, 'mempool').fiatPrice, 1); }); it('neemt geen dagrij, geen ander asset en geen koers van nul', () => { punt({ observedAt: '2026-09-25T00:00:00Z', fiatPrice: 5 }); punt({ assetId: eth, observedAt: '2026-09-25T09:00:00.000Z', resolution: 'timestamp', fiatPrice: 6 }); punt({ observedAt: '2026-09-25T10:00:00.000Z', resolution: 'timestamp', fiatPrice: 0 }); assert.equal(latestSpotPoint(btc, 'mempool'), null); }); it('geeft null bij een onbekende bron in plaats van te gooien', () => { assert.equal(latestSpotPoint(btc, 'bestaatniet'), null); }); }); describe('listAllPriceHistory', () => { beforeEach(() => { punt({ observedAt: '2024-01-01T00:00:00Z', fiatPrice: 1 }); punt({ observedAt: '2024-03-01T00:00:00Z', fiatPrice: 2 }); punt({ assetId: eth, observedAt: '2024-02-01T00:00:00Z', fiatPrice: 3 }); }); it('geeft zonder filter alles, nieuwste eerst, met assetsymbool', () => { const rijen = listAllPriceHistory(); assert.equal(rijen.length, 3); assert.deepEqual(rijen.map(r => r.fiatPrice), [2, 3, 1]); assert.equal(rijen[0].asset_symbol, 'BTC'); }); it('filtert op asset', () => { assert.deepEqual(listAllPriceHistory({ assetId: eth }).map(r => r.fiatPrice), [3]); }); it('filtert vanaf een datum', () => { assert.deepEqual(listAllPriceHistory({ fromDate: '2024-02-01' }).map(r => r.fiatPrice), [2, 3]); }); it('combineert beide filters', () => { assert.deepEqual(listAllPriceHistory({ assetId: btc, fromDate: '2024-02-01' }).map(r => r.fiatPrice), [2]); }); }); describe('dekkingsguard', () => { const vreemdeRij = (extra = {}) => { harness.exports.run(`DELETE FROM ProviderCoverage WHERE providerId = ? AND symbol = 'ETH'`, [providerId('mempool')]); return punt({ assetId: eth, observedAt: '2024-06-15T00:00:00Z', fiatPrice: 74500, source: 'mempool', ...extra }); }; it('laat een rij van een niet-dekkende bron geen dagpunt worden', () => { vreemdeRij(); assert.equal(getPricePoint(eth, '2024-06-15'), null, 'mempool dekt geen ETH, dus deze rij is geen ETH-koers'); }); it('laat zo n rij ook getPriceAt niet winnen, op geen van beide resoluties', () => { vreemdeRij(); vreemdeRij({ observedAt: '2024-06-15T12:00:00Z', resolution: 'timestamp' }); assert.equal(getPriceAt(eth, unix('2024-06-15T12:00:00Z')), null); }); it('houdt hem uit listPriceHistory, en daarmee uit het daggrid van signalen en de spot-widget', () => { vreemdeRij(); assert.deepEqual(listPriceHistory(eth), []); assert.deepEqual(listPriceHistory(eth, '2024-01-01'), [], 'ook met een vanaf-datum'); }); it('houdt hem uit latestPricePoint', () => { vreemdeRij(); assert.equal(latestPricePoint(eth), null); }); it('laat een dekkende bron er wél doorheen, ook als de niet-dekkende nieuwer is', () => { vreemdeRij({ observedAt: '2024-06-16T00:00:00Z' }); punt({ assetId: eth, observedAt: '2024-06-15T00:00:00Z', fiatPrice: 3000, source: 'bitvavo' }); assert.equal(latestPricePoint(eth).fiatPrice, 3000, 'de nieuwere rij van de niet-dekkende bron telt niet'); }); it('LAAT EEN HANDMATIG PUNT STAAN, ook al heeft de systeembron geen enkele dekkingsrij', () => { systeemProvider('manual'); punt({ assetId: eth, observedAt: '2024-06-15T00:00:00Z', fiatPrice: 3000, source: 'manual' }); assert.equal(getPricePoint(eth, '2024-06-15').fiatPrice, 3000); assert.equal(latestPricePoint(eth).fiatPrice, 3000); assert.equal(listPriceHistory(eth).length, 1); assert.equal(getPriceAt(eth, unix('2024-06-15T12:00:00Z')).fiatPrice, 3000); }); it('toont de foute rij wél in de beheertabel, want daar wordt hij verwijderd', () => { vreemdeRij(); assert.deepEqual(listAllPriceHistory().map(r => r.fiatPrice), [74500]); assert.equal(priceHistoryCount(eth), 1, 'de telling gaat over wat er staat, niet over wat meetelt'); }); }); describe('listPriceGaps', () => { const VANDAAG = '2026-09-24'; const boeking = (assetId, dag) => harness.exports.run( `INSERT INTO Event (assetId, amount, type, source, occurredAt, createdAt) VALUES (?, 100, 'transfer', 'manual', ?, ?)`, [assetId, `${dag}T10:00:00Z`, `${dag}T10:00:00Z`]); const dagen = (assetId, van, tot, source = 'mempool') => { for (let d = van; d <= tot; d = new Date(Date.parse(`${d}T00:00:00Z`) + 86400000).toISOString().slice(0, 10)) { punt({ assetId, observedAt: `${d}T00:00:00Z`, source }); } }; it('vindt een gat in het midden, met de juiste eerste en laatste dag', () => { boeking(btc, '2026-09-01'); dagen(btc, '2026-09-01', '2026-09-05'); dagen(btc, '2026-09-15', '2026-09-23'); assert.deepEqual(listPriceGaps({ today: VANDAAG }).rows, [{ assetId: btc, symbol: 'BTC', from: '2026-09-06', until: '2026-09-14', days: 9 }]); }); it('vindt de kop (koersen beginnen na de eerste boeking) en de staart (tot en met gisteren)', () => { boeking(btc, '2026-08-01'); dagen(btc, '2026-09-01', '2026-09-10'); assert.deepEqual(listPriceGaps({ today: VANDAAG }).rows.map(g => [g.from, g.until, g.days]), [['2026-08-01', '2026-08-31', 31], ['2026-09-11', '2026-09-23', 13]]); }); it('telt een gat tot de drempel niet, zoals een weekend met een feestdag', () => { boeking(btc, '2026-09-01'); dagen(btc, '2026-09-01', '2026-09-10'); dagen(btc, '2026-09-17', '2026-09-23'); assert.equal(listPriceGaps({ today: VANDAAG }).total, 0); }); it('laat een bron zonder dekking geen gat dichten', () => { harness.exports.run(`DELETE FROM ProviderCoverage WHERE providerId = ? AND symbol = 'BTC'`, [providerId('bitvavo')]); boeking(btc, '2026-09-01'); dagen(btc, '2026-09-01', '2026-09-05'); dagen(btc, '2026-09-06', '2026-09-14', 'bitvavo'); dagen(btc, '2026-09-15', '2026-09-23'); assert.equal(listPriceGaps({ today: VANDAAG }).rows[0].from, '2026-09-06'); }); describe('een gat negeren', () => { it('laat een genegeerd gat weg, en terugzetten brengt het terug', async () => { boeking(btc, '2026-09-01'); dagen(btc, '2026-09-01', '2026-09-05'); dagen(btc, '2026-09-15', '2026-09-23'); const id = await ignorePriceGap({ assetId: btc, from: '2026-09-06', until: '2026-09-14' }); assert.equal(listPriceGaps({ today: VANDAAG }).total, 0); assert.equal(listIgnoredPriceGaps()[0].symbol, 'BTC'); await unignorePriceGap(id); assert.equal(listPriceGaps({ today: VANDAAG }).total, 1); }); it('toont bij een staart die doorgroeit alleen het nieuwe stuk, en pas als dat de drempel haalt', async () => { boeking(btc, '2026-09-01'); dagen(btc, '2026-09-01', '2026-09-05'); await ignorePriceGap({ assetId: btc, from: '2026-09-06', until: '2026-09-20' }); assert.equal(listPriceGaps({ today: '2026-09-24' }).total, 0, 'drie nieuwe dagen haalt de drempel niet'); assert.deepEqual(listPriceGaps({ today: '2026-10-01' }).rows.map(g => [g.from, g.until, g.days]), [['2026-09-21', '2026-09-30', 10]], 'tien nieuwe dagen wel, en alleen die'); }); it('weigert een onbekend asset en een bereik dat geen bereik is', async () => { await assert.rejects(() => ignorePriceGap({ assetId: 9999, from: '2026-09-01', until: '2026-09-02' }), /does not exist/); await assert.rejects(() => ignorePriceGap({ assetId: btc, from: '2026-09-05', until: '2026-09-01' }), /not a range/); await assert.rejects(() => ignorePriceGap({ assetId: btc, from: 'gisteren', until: '2026-09-01' }), /not a range/); assert.deepEqual(listIgnoredPriceGaps(), []); }); }); describe('dagen zonder koers per bron', () => { it('onthoudt een dag één keer per bron, en levert hem vanaf een startdag terug', async () => { await recordNoPriceDays(btc, 'mempool', ['2025-12-25', '2025-12-26']); await recordNoPriceDays(btc, 'mempool', ['2025-12-26']); await recordNoPriceDays(btc, 'bitvavo', ['2025-12-26']); assert.deepEqual(listNoPriceDays(btc).map(r => r.day), ['2025-12-25', '2025-12-26', '2025-12-26'], 'twee bronnen op dezelfde dag zijn twee rijen, dezelfde bron twee keer is er één'); assert.deepEqual(listNoPriceDays(btc, '2025-12-26'), [{ providerId: providerId('mempool'), day: '2025-12-26' }, { providerId: providerId('bitvavo'), day: '2025-12-26' }] .sort((a, b) => a.providerId - b.providerId)); assert.deepEqual(listNoPriceDays(eth), [], 'per asset'); }); it('weigert een onbekend asset, een onbekende bron en iets wat geen dag is', async () => { await assert.rejects(() => recordNoPriceDays(9999, 'mempool', ['2025-12-25']), /does not exist/); await assert.rejects(() => recordNoPriceDays(btc, 'bestaat-niet', ['2025-12-25']), /Unknown price source/); await assert.rejects(() => recordNoPriceDays(btc, 'mempool', ['25-12-2025']), /Not a list of days/); await assert.rejects(() => recordNoPriceDays(btc, 'mempool', '2025-12-25'), /Not a list of days/); assert.deepEqual(listNoPriceDays(btc), []); }); it('voegt opeenvolgende dagen per asset en bron samen tot een periode', async () => { await recordNoPriceDays(btc, 'mempool', ['2025-12-24', '2025-12-25', '2025-12-26', '2026-01-01']); await recordNoPriceDays(btc, 'bitvavo', ['2025-12-25']); await recordNoPriceDays(eth, 'mempool', ['2025-12-26']); const r = listPriceAbsences().map(a => [a.assetId, a.providerName, a.fromDay, a.untilDay, a.days]); assert.deepEqual(r, [ [btc, 'bitvavo', '2025-12-25', '2025-12-25', 1], [btc, 'mempool', '2025-12-24', '2025-12-26', 3], [btc, 'mempool', '2026-01-01', '2026-01-01', 1], [eth, 'mempool', '2025-12-26', '2025-12-26', 1], ]); }); it('vergeet alleen de periode van die bron, en weigert iets wat geen periode is', async () => { await recordNoPriceDays(btc, 'mempool', ['2025-12-24', '2025-12-25', '2026-01-01']); await recordNoPriceDays(btc, 'bitvavo', ['2025-12-25']); await deletePriceAbsences({ assetId: btc, providerId: providerId('mempool'), from: '2025-12-24', until: '2025-12-25' }); assert.deepEqual(listNoPriceDays(btc).map(r => [r.providerId, r.day]), [[providerId('bitvavo'), '2025-12-25'], [providerId('mempool'), '2026-01-01']]); await assert.rejects(() => deletePriceAbsences({ assetId: btc, providerId: providerId('mempool'), from: '2026-01-02', until: '2026-01-01' }), /not a range/); await assert.rejects(() => deletePriceAbsences({ assetId: btc, from: '2026-01-01', until: '2026-01-01' }), /required/); assert.equal(listNoPriceDays(btc).length, 2, 'een geweigerde aanroep wist niets'); }); }); it('negeert een gat vóór de eerste boeking, een asset zonder boeking, en fiat', () => { dagen(btc, '2026-06-01', '2026-06-02'); boeking(btc, '2026-09-01'); dagen(btc, '2026-09-01', '2026-09-23'); dagen(eth, '2026-09-01', '2026-09-02'); harness.exports.run(`UPDATE Asset SET kind = 'fiat' WHERE id = ?`, [eth]); boeking(eth, '2026-01-01'); assert.equal(listPriceGaps({ today: VANDAAG }).total, 0); }); }); describe('recordCurrentSpot', () => { it('bewaart de spot als timestamp-punt', async () => { await recordCurrentSpot(btc, 'mempool', { price: 55000 }); const r = harness.exports.one(`SELECT resolution, fiatPrice FROM PriceHistory`); assert.equal(r.resolution, 'timestamp'); assert.equal(r.fiatPrice, 55000); }); it('houdt één punt per asset+bron per dag en werkt dat BIJ', async () => { await recordCurrentSpot(btc, 'mempool', { price: 55000 }); await recordCurrentSpot(btc, 'mempool', { price: 56000 }); assert.equal(priceHistoryCount(btc), 1, 'nog steeds één rij'); assert.equal(harness.exports.one(`SELECT fiatPrice FROM PriceHistory`).fiatPrice, 56000, 'maar met de LAATST geziene koers, want een slotkoers staat pas vast als de dag om is'); }); it('schuift het moment mee met de koers', async () => { await recordCurrentSpot(btc, 'mempool', { price: 55000 }); const eerst = harness.exports.one(`SELECT observedAt FROM PriceHistory`).observedAt; await recordCurrentSpot(btc, 'mempool', { price: 56000 }); const daarna = harness.exports.one(`SELECT observedAt FROM PriceHistory`).observedAt; assert.ok(daarna >= eerst, 'het moment loopt niet terug'); assert.equal(daarna.slice(0, 10), new Date().toISOString().slice(0, 10), 'en blijft op vandaag'); }); it('bewaart wél een punt per bron', async () => { await recordCurrentSpot(btc, 'mempool', { price: 55000 }); await recordCurrentSpot(btc, 'bitvavo', { price: 55100 }); assert.equal(priceHistoryCount(btc), 2); }); it('doet niets zonder bruikbare koers', async () => { await recordCurrentSpot(btc, 'mempool', null); await recordCurrentSpot(btc, 'mempool', {}); await recordCurrentSpot(btc, 'mempool', { price: NaN }); await recordCurrentSpot(btc, 'mempool', { price: Infinity }); assert.equal(priceHistoryCount(btc), 0); }); }); describe('addManualPricePoint', () => { const morgen = () => new Date(Date.now() + 36 * 3600 * 1000).toISOString().slice(0, 10); beforeEach(() => { systeemProvider('manual'); }); it('bewaart een dagpunt op de systeembron Handmatig', async () => { const id = await addManualPricePoint({ assetId: btc, observedAt: '1994-06-15', fiatPrice: 315.5 }); assert.ok(id, 'het id komt terug, zodat de pagina de nieuwe rij kan aanwijzen'); const r = harness.exports.one(`SELECT ph.*, p.key AS bron FROM PriceHistory ph JOIN Provider p ON p.id = ph.providerId WHERE ph.id = ?`, [id]); assert.equal(r.fiatPrice, 315.5); assert.equal(r.resolution, 'day'); assert.equal(r.observedAt, '1994-06-15T00:00:00Z', 'een dagpunt staat op middernacht UTC'); assert.equal(r.bron, 'manual', 'geen nieuwe systeembron: deze betekent al "met de hand"'); }); it('weigert een moment in de TOEKOMST', async () => { await assert.rejects(() => addManualPricePoint({ assetId: btc, observedAt: morgen(), fiatPrice: 100 }), /future/i); assert.equal(priceHistoryCount(btc), 0); }); it('weigert een koers van nul of lager', async () => { await assert.rejects(() => addManualPricePoint({ assetId: btc, observedAt: '2024-01-01', fiatPrice: 0 }), /above zero/); await assert.rejects(() => addManualPricePoint({ assetId: btc, observedAt: '2024-01-01', fiatPrice: -5 }), /above zero/); await assert.rejects(() => addManualPricePoint({ assetId: btc, observedAt: '2024-01-01', fiatPrice: 'veel' }), /above zero/); assert.equal(priceHistoryCount(btc), 0); }); it('weigert een onleesbare datum en een onbekend asset', async () => { await assert.rejects(() => addManualPricePoint({ assetId: btc, observedAt: 'ooit', fiatPrice: 100 }), /valid moment/); await assert.rejects(() => addManualPricePoint({ assetId: 99999, observedAt: '2024-01-01', fiatPrice: 100 }), /does not exist/); assert.equal(priceHistoryCount(btc), 0); }); it('overschrijft hetzelfde punt in plaats van er een tweede naast te zetten', async () => { const a = await addManualPricePoint({ assetId: btc, observedAt: '2024-01-01', fiatPrice: 100 }); const b = await addManualPricePoint({ assetId: btc, observedAt: '2024-01-01', fiatPrice: 120 }); assert.equal(a, b, 'dezelfde rij'); assert.equal(priceHistoryCount(btc), 1); assert.equal(harness.exports.one(`SELECT fiatPrice FROM PriceHistory`).fiatPrice, 120); }); it('weigert een resolutie die niet bestaat', async () => { await assert.rejects(() => addManualPricePoint({ assetId: btc, observedAt: '2024-01-01', fiatPrice: 100, resolution: 'uur' })); }); }); describe('addManualPricePoint: de eenheid', () => { let goud; beforeEach(() => { systeemProvider('manual'); harness.exports.run( `INSERT INTO Asset (symbol, name, searchName, kind, unit, subunit, decimals, displayUnit) VALUES ('XAU','Goud','goud','metal','ozt','g',4,'unit')`); goud = harness.exports.lastId(); }); it('rekent een ouncerprijs om naar de gramprijs van de opslag', async () => { const id = await addManualPricePoint({ assetId: goud, observedAt: '1994-06-15', fiatPrice: 315.5, unit: 'ozt' }); const r = harness.exports.one(`SELECT fiatPrice FROM PriceHistory WHERE id = ?`, [id]); assert.equal(r.fiatPrice, 315.5 / CONFIG.METALS.UNITS.ozt); assert.ok(r.fiatPrice > 10 && r.fiatPrice < 11, 'ruim tien euro per gram, en niet ruim driehonderd'); }); it('laat een gramprijs staan zoals hij is', async () => { const id = await addManualPricePoint({ assetId: goud, observedAt: '1994-06-15', fiatPrice: 10.14, unit: 'g' }); assert.equal(harness.exports.one(`SELECT fiatPrice FROM PriceHistory WHERE id = ?`, [id]).fiatPrice, 10.14); }); it('neemt zonder eenheid de HOOFDEENHEID van het asset en niet de weergave-eenheid', async () => { harness.exports.run(`UPDATE Asset SET displayUnit = 'subunit' WHERE id = ?`, [goud]); const id = await addManualPricePoint({ assetId: goud, observedAt: '1994-06-15', fiatPrice: 315.5 }); assert.equal(harness.exports.one(`SELECT fiatPrice FROM PriceHistory WHERE id = ?`, [id]).fiatPrice, 315.5 / CONFIG.METALS.UNITS.ozt, 'ozt, ook al staat de weergave op gram'); }); it('laat CRYPTO ongemoeid, wat er ook als eenheid meekomt', async () => { const id = await addManualPricePoint({ assetId: btc, observedAt: '2024-01-01', fiatPrice: 40000, unit: 'ozt' }); assert.equal(harness.exports.one(`SELECT fiatPrice FROM PriceHistory WHERE id = ?`, [id]).fiatPrice, 40000); }); it('weigert een eenheid die de config niet kent', async () => { await assert.rejects( () => addManualPricePoint({ assetId: goud, observedAt: '2024-01-01', fiatPrice: 100, unit: 'pond' }), /Unknown metal unit/); }); });