135 lines
5.7 KiB
JavaScript
135 lines
5.7 KiB
JavaScript
// Generated, do not edit. sha256:80f49e74154e65b0c30e365129d8a9f192b6481371b78383fd6a55befe29b11a
|
|||
|
|
import { describe, it, beforeEach, mock } from 'node:test';
|
||
|
|
import assert from 'node:assert/strict';
|
||
|
|
|
||
|
|
import { createDbHarness } from '../helpers/db-node.mjs';
|
||
|
|
|
||
|
|
const harness = createDbHarness();
|
||
|
|
mock.module('../../server/db.js', { namedExports: harness.exports });
|
||
|
|
|
||
|
|
for (const mod of ['providers', 'price-source', 'price-history', 'api-metrics', 'preferences']) {
|
||
|
|
mock.module(`../../client/data/${mod}.js`, { namedExports: await import(`../../server/data/${mod}.js`) });
|
||
|
|
}
|
||
|
|
|
||
|
|
const facade = await import('../../client/price/providers.js');
|
||
|
|
|
||
|
|
let fetchCalls = [];
|
||
|
|
let antwoord = { price: 100 };
|
||
|
|
globalThis.fetch = async (url, opties) => {
|
||
|
|
fetchCalls.push({ url, body: opties?.body });
|
||
|
|
return { ok: true, status: 200, json: async () => antwoord };
|
||
|
|
};
|
||
|
|
|
||
|
|
const { run, lastId, one } = harness.exports;
|
||
|
|
let goud;
|
||
|
|
|
||
|
|
function provider(key, { capability = 'spot', interval = null } = {}) {
|
||
|
|
run(`INSERT INTO Provider (key, name, searchName, baseUrl, authenticationKind, authenticationRequired,
|
||
|
|
isActive, isSeed, isSystem, createdAt)
|
||
|
|
VALUES (?,?,?, 'https://voorbeeld.test','none',0,1,1,0,'2026-09-21T10:00:00.000Z')`, [key, key, key]);
|
||
|
|
const id = lastId();
|
||
|
|
run(`INSERT INTO ProviderCoverage (providerId, symbol) VALUES (?, 'XAU')`, [id]);
|
||
|
|
run(`INSERT INTO ProviderEndpoint (providerId, capability, method, pathTemplate, queryTemplate, minIntervalSeconds)
|
||
|
|
VALUES (?, ?, 'GET', '/prijs', NULL, ?)`, [id, capability, interval]);
|
||
|
|
return id;
|
||
|
|
}
|
||
|
|
function bronkeuze(providerId, capability = 'spot') {
|
||
|
|
run(`INSERT INTO PriceSource (assetId, capability, providerId, rank, status)
|
||
|
|
VALUES (?, ?, ?, 1, 'confirmed')`, [goud, capability === 'spot' ? 'spot' : 'history', providerId]);
|
||
|
|
}
|
||
|
|
function spotRij(providerId, minutenGeleden, eur) {
|
||
|
|
const moment = new Date(Date.now() - minutenGeleden * 60000).toISOString();
|
||
|
|
run(`INSERT INTO PriceHistory (assetId, providerId, fiatPrice, resolution, observedAt, createdAt, updatedAt)
|
||
|
|
VALUES (?, ?, ?, 'timestamp', ?, ?, ?)`, [goud, providerId, eur, moment, moment, moment]);
|
||
|
|
}
|
||
|
|
const asset = () => one(`SELECT * FROM Asset WHERE id = ?`, [goud]);
|
||
|
|
const dag = n => new Date(Date.now() + n * 86400000).toISOString().slice(0, 10);
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
harness.recreate();
|
||
|
|
facade.clearSpotCache();
|
||
|
|
facade.forgetSpotAnswers();
|
||
|
|
facade.forgetCallTimes();
|
||
|
|
fetchCalls = [];
|
||
|
|
antwoord = { price: 100 };
|
||
|
|
run(`INSERT INTO Asset (symbol, name, searchName, kind, unit, subunit, decimals, displayUnit)
|
||
|
|
VALUES ('XAU','Goud','goud','metal','g','mg',3,'unit')`);
|
||
|
|
goud = lastId();
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('het interval van een spot', () => {
|
||
|
|
it('vraagt binnen het interval niet opnieuw, ook niet na de tik van de app', async () => {
|
||
|
|
bronkeuze(provider('goldapi', { interval: 600 }));
|
||
|
|
await facade.liveSpotFor(asset());
|
||
|
|
facade.clearSpotCache();
|
||
|
|
antwoord = { price: 999 };
|
||
|
|
assert.deepEqual(await facade.liveSpotFor(asset()), { price: 100 }, 'het bewaarde antwoord');
|
||
|
|
assert.equal(fetchCalls.length, 1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('vraagt zonder interval na de tik wél opnieuw, zoals voorheen', async () => {
|
||
|
|
bronkeuze(provider('bron'));
|
||
|
|
await facade.liveSpotFor(asset());
|
||
|
|
facade.clearSpotCache();
|
||
|
|
await facade.liveSpotFor(asset());
|
||
|
|
assert.equal(fetchCalls.length, 2);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('overleeft een herstart via de laatste spotrij van die bron', async () => {
|
||
|
|
const p = provider('goldapi', { interval: 600 });
|
||
|
|
bronkeuze(p);
|
||
|
|
spotRij(p, 5, 123);
|
||
|
|
assert.deepEqual(await facade.liveSpotFor(asset()), { price: 123 });
|
||
|
|
assert.equal(fetchCalls.length, 0, 'geen verzoek');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('vraagt wél opnieuw als die rij ouder is dan het interval', async () => {
|
||
|
|
const p = provider('goldapi', { interval: 600 });
|
||
|
|
bronkeuze(p);
|
||
|
|
spotRij(p, 20, 123);
|
||
|
|
assert.deepEqual(await facade.liveSpotFor(asset()), { price: 100 });
|
||
|
|
assert.equal(fetchCalls.length, 1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('telt de spotrij van een ándere bron niet mee', async () => {
|
||
|
|
const p = provider('goldapi', { interval: 600 });
|
||
|
|
const ander = provider('metalsdev');
|
||
|
|
bronkeuze(p);
|
||
|
|
spotRij(ander, 1, 123);
|
||
|
|
await facade.liveSpotFor(asset());
|
||
|
|
assert.equal(fetchCalls.length, 1, 'die rij zegt niets over wanneer goldapi gevraagd is');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('het interval van een koers per datum', () => {
|
||
|
|
it('wacht het interval af tussen twee verzoeken', async () => {
|
||
|
|
provider('perdatum', { capability: 'history_point', interval: 1 });
|
||
|
|
antwoord = { time: 1, price: 100 };
|
||
|
|
const t0 = Date.now();
|
||
|
|
await facade.fetchPriceVia('perdatum', asset(), 'history_point', dag(-3));
|
||
|
|
await facade.fetchPriceVia('perdatum', asset(), 'history_point', dag(-2));
|
||
|
|
assert.equal(fetchCalls.length, 2);
|
||
|
|
assert.ok(Date.now() - t0 >= 900, `het tweede verzoek wachtte (${Date.now() - t0} ms)`);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('wacht niet zonder interval', async () => {
|
||
|
|
provider('perdatum', { capability: 'history_point' });
|
||
|
|
antwoord = { time: 1, price: 100 };
|
||
|
|
const t0 = Date.now();
|
||
|
|
await facade.fetchPriceVia('perdatum', asset(), 'history_point', dag(-3));
|
||
|
|
await facade.fetchPriceVia('perdatum', asset(), 'history_point', dag(-2));
|
||
|
|
assert.ok(Date.now() - t0 < 500);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('breekt het wachten af met de stopknop, zonder verzoek', async () => {
|
||
|
|
bronkeuze(provider('perdatum', { capability: 'history_point', interval: 3600 }), 'history_point');
|
||
|
|
antwoord = { time: 1, price: 100 };
|
||
|
|
const stop = new AbortController();
|
||
|
|
const t0 = Date.now();
|
||
|
|
setTimeout(() => stop.abort(), 100);
|
||
|
|
const uit = await facade.fillClosesFor(asset(), dag(-7), dag(-1), { signal: stop.signal });
|
||
|
|
assert.equal(uit.stopped, true);
|
||
|
|
assert.equal(fetchCalls.length, 1, 'alleen het eerste verzoek ging weg');
|
||
|
|
assert.ok(Date.now() - t0 < 2000);
|
||
|
|
});
|
||
|
|
});
|