// Generated, do not edit. sha256:5217955238b304b9e0b4f32509659a4a0f009e7d18d50b262129f84c861f0e94 import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { createHmac } from 'node:crypto'; import { resolveScalar, resolveColumn, mapSpot, mapHistoryPoint, mapDailyHistory, hmacHeaders, buildRequest, priceCall, failureEntry, } from '../../server/price/custom.js'; import { CONFIG } from '../../shared/config.js'; import { toUtcIso } from '../../shared/time.js'; const provider = (extra = {}) => ({ key: 'test', baseUrl: 'https://api.example.test', authenticationKind: 'none', authenticationRequired: 0, authenticationHeaderName: null, authenticationParameterName: null, hmac: null, endpoints: [], symbols: [], outputs: [], ...extra, }); const endpoint = (capability, pathTemplate, queryTemplate = null) => ({ capability, method: 'GET', pathTemplate, queryTemplate }); const output = (capability, internalField, jsonPath, transform = 'none') => ({ capability, internalField, jsonPath, transform }); describe('resolveScalar: over geparst JSON, nooit over tekst', () => { const doc = { a: { b: { c: 42 } }, lijst: [{ prijs: 1 }, { prijs: 2 }], BTC: { eur: 7 } }; it('loopt een pad van objectsleutels af', () => { assert.equal(resolveScalar(doc, 'a.b.c'), 42); }); it('leest een numeriek segment als index en niet als sleutel', () => { assert.equal(resolveScalar(doc, 'lijst.1.prijs'), 2); }); it('vult {external_id} in als sleutelnaam', () => { assert.equal(resolveScalar(doc, '{external_id}.eur', 'BTC'), 7); }); it('gooit als het pad {external_id} gebruikt maar er geen is', () => { assert.throws(() => resolveScalar(doc, '{external_id}.eur', null), (e) => e.key === 'integrations.price.externalIdMissing'); }); it('geeft undefined bij een pad dat halverwege doodloopt, en gooit niet', () => { assert.equal(resolveScalar(doc, 'a.bestaat.niet.dieper'), undefined); assert.equal(resolveScalar(doc, 'weg.helemaal'), undefined); }); }); describe('resolveColumn: een kolom uit een lijst', () => { it('leest een kolom uit een array van objecten', () => { const doc = { prices: [{ t: 1, e: 10 }, { t: 2, e: 20 }] }; assert.deepEqual(resolveColumn(doc, 'prices[].t'), [1, 2]); }); it('leest een kolom uit een array van arrays met een numerieke index', () => { const doc = { prices: [[1000, 10], [2000, 20]] }; assert.deepEqual(resolveColumn(doc, 'prices[].1'), [10, 20]); }); it('behandelt de wortel als array bij een pad dat met [] begint', () => { assert.deepEqual(resolveColumn([{ x: 1 }, { x: 2 }], '[].x'), [1, 2]); }); it('geeft de elementen zelf terug als er niets achter [] staat', () => { assert.deepEqual(resolveColumn({ v: [1, 2, 3] }, 'v[]'), [1, 2, 3]); }); it('gooit bij een lijst-pad zonder [], en bij een pad dat geen array aanwijst', () => { assert.throws(() => resolveColumn({ a: [] }, 'a.b'), (e) => e.key === 'integrations.price.listPathWithoutBrackets'); assert.throws(() => resolveColumn({ a: 5 }, 'a[].x'), (e) => e.key === 'integrations.price.expectedArray'); }); }); describe('resolveColumn: een object met de rijen als sleutel', () => { const doc = { rates: { '2026-09-01': { EUR: 0.86 }, '2026-09-02': { EUR: 0.87 } } }; it('geeft met @key de sleutels en met een veld de waarden, in dezelfde volgorde', () => { assert.deepEqual(resolveColumn(doc, 'rates{}.@key'), ['2026-09-01', '2026-09-02']); assert.deepEqual(resolveColumn(doc, 'rates{}.EUR'), [0.86, 0.87]); }); it('geeft undefined voor een rij zonder het gevraagde veld, en gooit niet', () => { assert.deepEqual(resolveColumn({ r: { a: { EUR: 1 }, b: { USD: 2 } } }, 'r{}.EUR'), [1, undefined]); }); it('gooit als het pad geen object aanwijst, ook niet bij een array', () => { assert.throws(() => resolveColumn({ r: [1, 2] }, 'r{}.EUR'), (e) => e.key === 'integrations.price.expectedObject'); assert.throws(() => resolveColumn({ r: 5 }, 'r{}.@key'), (e) => e.key === 'integrations.price.expectedObject'); }); }); describe('mapSpot', () => { it('mapt het ene prijsveld op het interne contract', () => { const p = provider({ outputs: [output('spot', 'price', 'data.eur')] }); assert.deepEqual(mapSpot({ data: { eur: 12.5 } }, p), { price: 12.5 }); }); it('past to_number toe op een prijs die als string binnenkomt', () => { const p = provider({ outputs: [output('spot', 'price', 'data.eur', 'to_number')] }); assert.equal(mapSpot({ data: { eur: '12.5' } }, p).price, 12.5); }); it('rekent troy ounce naar gram met de factor uit CONFIG', () => { const p = provider({ outputs: [output('spot', 'price', 'price', 'troy_ounce_to_gram')] }); const uit = mapSpot({ price: CONFIG.METALS.GRAMS_PER_TROY_OUNCE }, p); assert.equal(uit.price, 1, 'een ounce ter waarde van de gramfactor is precies 1 per gram'); }); it('weigert een respons zonder bruikbare prijs', () => { const p = provider({ outputs: [output('spot', 'price', 'data.eur')] }); assert.throws(() => mapSpot({ data: {} }, p), (e) => e.key === 'integrations.price.noValidPrice'); assert.throws(() => mapSpot({ data: { eur: 'kapot' } }, p), (e) => e.key === 'integrations.price.noValidPrice'); }); it('weigert een provider zonder mapping voor deze capability', () => { assert.throws(() => mapSpot({}, provider()), (e) => e.key === 'integrations.price.noOutputMapping'); }); it('gooit bij een transform die de adapter niet kent', () => { const p = provider({ outputs: [output('spot', 'price', 'e', 'verzonnen')] }); assert.throws(() => mapSpot({ e: 1 }, p), (e) => e.key === 'integrations.price.unknownTransform'); }); }); describe('een endpoint dat in een andere munt antwoordt', () => { const inDollars = (outputs, capability = 'spot') => provider({ endpoints: [{ ...endpoint(capability, '/p'), quoteCurrency: 'USD' }], outputs, }); it('geeft price ongewijzigd door, in de munt van het endpoint', () => { const p = inDollars([output('spot', 'price', 'price')]); assert.deepEqual(mapSpot({ price: 2000 }, p), { price: 2000 }); }); it('weigert een antwoord zonder prijs, met de provider in de parameters', () => { const p = inDollars([output('spot', 'price', 'price')]); assert.throws(() => mapSpot({}, p), (e) => e.key === 'integrations.price.noValidPrice' && e.params.provider === 'test'); }); it('houdt in een reeks de rijen met een prijs en laat de rest vallen', () => { const p = inDollars([output('daily_history', 'time', 'r[].0'), output('daily_history', 'price', 'r[].1')], 'daily_history'); assert.deepEqual(mapDailyHistory({ r: [[100, 5], [200, null]] }, p), [{ time: 100, price: 5 }]); }); it('leest alleen price: een mapping op een ander veld levert geen koers op', () => { const p = provider({ endpoints: [endpoint('spot', '/p')], outputs: [output('spot', 'EUR', 'price')] }); assert.throws(() => mapSpot({ price: 2000 }, p), (e) => e.key === 'integrations.price.noValidPrice'); }); }); describe('mapHistoryPoint', () => { const basis = provider({ outputs: [output('history_point', 'price', 'eur')] }); it('leidt het tijdstip af uit de gevraagde datum als de respons er geen geeft', () => { const uit = mapHistoryPoint({ eur: 3 }, basis, null, '2026-03-01'); assert.equal(uit.time, Math.floor(Date.parse('2026-03-01T00:00:00Z') / 1000)); assert.equal(uit.price, 3); }); it('gebruikt het tijdstip uit de respons als dat er wél is', () => { const p = provider({ outputs: [output('history_point', 'price', 'eur'), output('history_point', 'time', 't')] }); assert.equal(mapHistoryPoint({ eur: 3, t: 99 }, p, null, '2026-03-01').time, 99); }); it('weigert een onleesbare datum als er ook geen tijdstip in de respons staat', () => { assert.throws(() => mapHistoryPoint({ eur: 3 }, basis, null, 'geen-datum'), (e) => e.key === 'integrations.price.invalidDate'); }); }); describe('mapDailyHistory', () => { const kolommen = (extra = []) => provider({ outputs: [output('daily_history', 'time', 'p[].0'), output('daily_history', 'price', 'p[].1'), ...extra], }); it('koppelt de kolommen per index en sorteert oplopend op tijd', () => { const uit = mapDailyHistory({ p: [[300, 3], [100, 1], [200, 2]] }, kolommen()); assert.deepEqual(uit.map(r => r.time), [100, 200, 300]); assert.deepEqual(uit.map(r => r.price), [1, 2, 3]); }); it('laat rijen vallen waarvan tijd of prijs onbruikbaar is', () => { const uit = mapDailyHistory({ p: [[100, 1], [null, 2], [300, 'kapot']] }, kolommen()); assert.deepEqual(uit, [{ time: 100, price: 1 }]); }); it('weigert kolommen van ongelijke lengte', () => { const p = provider({ outputs: [output('daily_history', 'time', 't[]'), output('daily_history', 'price', 'e[]')], }); assert.throws(() => mapDailyHistory({ t: [1, 2, 3], e: [10, 20] }, p), (e) => e.key === 'integrations.price.columnsUneven'); }); it('houdt bij last_per_day alleen het laatste punt van elke kalenderdag', () => { const dag = (iso) => Math.floor(Date.parse(iso) / 1000); const p = provider({ outputs: [output('daily_history', 'time', 'p[].0'), output('daily_history', 'price', 'p[].1', 'last_per_day')], }); const uit = mapDailyHistory({ p: [ [dag('2026-03-01T09:00:00Z'), 1], [dag('2026-03-01T21:00:00Z'), 2], [dag('2026-03-02T09:00:00Z'), 3], ] }, p); assert.deepEqual(uit.map(r => r.price), [2, 3], 'per dag blijft de laatste over'); }); it('leest een reeks met datums als sleutel, en laat een sleutel die geen datum is vallen', () => { const p = provider({ outputs: [output('daily_history', 'time', 'rates{}.@key', 'iso_date_to_s'), output('daily_history', 'price', 'rates{}.EUR')], }); const uit = mapDailyHistory({ rates: { '2026-09-02': { EUR: 0.87 }, '2026-09-01': { EUR: 0.86 }, 'laatste': { EUR: 0.9 }, '2026-09-03': {}, } }, p); assert.deepEqual(uit, [ { time: Math.floor(Date.parse('2026-09-01T00:00:00Z') / 1000), price: 0.86 }, { time: Math.floor(Date.parse('2026-09-02T00:00:00Z') / 1000), price: 0.87 }, ], 'oplopend gesorteerd; geen datum of geen koers is geen rij'); }); it('dateert een slot op middernacht Amsterdam op zijn eigen dag en laat een maandcijfer vallen', () => { const p = provider({ outputs: [output('daily_history', 'time', 'p[].0', 'ms_to_s_local_close'), output('daily_history', 'price', 'p[].1')], endpoints: [{ ...endpoint('daily_history', '/p'), quoteCurrency: 'USD' }], }); const dag = iso => Date.parse(`${iso}T00:00:00Z`) / 1000; const uit = mapDailyHistory({ p: [ [1740697200000, 91.79], [1740787200000, 100.33], [1743631200000, 1.0208], ] }, p); assert.deepEqual(uit.map(r => [r.time, r.price]), [[dag('2025-02-28'), 91.79], [dag('2025-04-03'), 1.0208]]); }); it('geeft rond een maandgrens en een tijdwissel elke dag precies één slot', () => { const p = provider({ outputs: [output('daily_history', 'time', 'p[].0', 'ms_to_s_local_close'), output('daily_history', 'price', 'p[].1')], }); const slotVan = dag => Date.parse(toUtcIso(`${dag}T00:00:00`)); const dagen = (van, n) => Array.from({ length: n }, (_, i) => new Date(Date.parse(`${van}T00:00:00Z`) + i * 86400000).toISOString().slice(0, 10)); for (const week of [dagen('2026-08-29', 7), dagen('2026-10-22', 7), dagen('2026-03-26', 6)]) { const reeks = week.map((d, i) => [slotVan(d), 100 + i]); reeks.push([Date.parse('2026-09-01T00:00:00Z'), 999]); const uit = mapDailyHistory({ p: reeks }, p); assert.deepEqual(uit.map(r => new Date(r.time * 1000).toISOString().slice(0, 10)), week, `de dagen van ${week[0]}`); assert.ok(!uit.some(r => r.price === 999), 'het maandcijfer valt weg'); } }); it('laat een ander tijdstip ongemoeid, zoals de avondpunten van crypto', () => { const p = provider({ outputs: [output('daily_history', 'time', 'p[].0', 'ms_to_s_local_close'), output('daily_history', 'price', 'p[].1')], }); const uit = mapDailyHistory({ p: [[1758859200000, 1], [1759353813000, 2]] }, p); assert.deepEqual(uit.map(r => r.time), [1758859200, 1759353813]); }); }); describe('buildRequest: de poort die weigert zonder sleutel', () => { const metAuth = (kind, extra = {}) => provider({ authenticationKind: kind, authenticationRequired: 1, endpoints: [endpoint('spot', '/v1/prijs')], ...extra, }); it('weigert als de sleutel verplicht is maar ontbreekt', async () => { await assert.rejects(() => buildRequest(metAuth('header_key', { authenticationHeaderName: 'X-Key' }), 'spot', 'BTC', null, null), (e) => e.key === 'integrations.price.keyMissing'); }); it('weigert een HALVE sleutel, en dat is het geval dat telt', async () => { await assert.rejects(() => buildRequest(metAuth('basic'), 'spot', 'BTC', null, { api_key: 'k' }), (e) => e.key === 'integrations.price.keyMissing'); }); it('laat een provider zonder verplichte sleutel gewoon door', async () => { const p = provider({ endpoints: [endpoint('spot', '/v1/prijs')] }); const r = await buildRequest(p, 'spot', 'BTC', null, null); assert.equal(r.url, 'https://api.example.test/v1/prijs'); assert.deepEqual(r.headers, {}); }); it('gooit bij een capability die de provider niet kent, en bij een onbekende capability', async () => { await assert.rejects(() => buildRequest(provider(), 'spot', 'BTC', null, null), (e) => e.key === 'integrations.price.capabilityNotConfigured'); await assert.rejects(() => buildRequest(provider(), 'verzonnen', 'BTC', null, null), (e) => e.key === 'integrations.price.unknownCapability'); }); it('ondersteunt alleen GET', async () => { const p = provider({ endpoints: [{ capability: 'spot', method: 'POST', pathTemplate: '/x', queryTemplate: null }] }); await assert.rejects(() => buildRequest(p, 'spot', 'BTC', null, null), (e) => e.key === 'integrations.price.onlyGet'); }); }); describe('buildRequest: de placeholders in het pad', () => { it('vertaalt het symbool via de symbol-map en valt anders terug op het symbool zelf', async () => { const p = provider({ endpoints: [endpoint('spot', '/v1/{external_id}')], symbols: [{ symbol: 'WXT', externalId: 'wirex' }], }); assert.equal((await buildRequest(p, 'spot', 'WXT', null, null)).url, 'https://api.example.test/v1/wirex'); assert.equal((await buildRequest(p, 'spot', 'XAU', null, null)).url, 'https://api.example.test/v1/XAU'); }); it('codeert de placeholder, zodat een raar extern id het pad niet kan openbreken', async () => { const p = provider({ endpoints: [endpoint('spot', '/v1/{external_id}')], symbols: [{ symbol: 'X', externalId: 'a/b?c' }], }); assert.equal((await buildRequest(p, 'spot', 'X', null, null)).url, 'https://api.example.test/v1/a%2Fb%3Fc'); }); it('zet {date} om naar YYYYMMDD en weigert een onleesbare datum', async () => { const p = provider({ endpoints: [endpoint('history_point', '/v1/{date}')] }); assert.equal((await buildRequest(p, 'history_point', 'X', '2026-03-01', null)).url, 'https://api.example.test/v1/20260301'); await assert.rejects(() => buildRequest(p, 'history_point', 'X', 'rommel', null), (e) => e.key === 'integrations.price.templateDateInvalid'); }); }); describe('buildRequest: de vier auth-soorten', () => { const p = (kind, extra = {}) => provider({ authenticationKind: kind, endpoints: [endpoint('spot', '/v1/p', 'a=1')], ...extra, }); it('zet een header bij header_key', async () => { const r = await buildRequest(p('header_key', { authenticationHeaderName: 'X-Key' }), 'spot', 'X', null, { api_key: 'geheim' }); assert.equal(r.headers['X-Key'], 'geheim'); }); it('zet een Bearer-header bij bearer', async () => { const r = await buildRequest(p('bearer'), 'spot', 'X', null, { api_key: 'geheim' }); assert.equal(r.headers.Authorization, 'Bearer geheim'); }); it('zet een base64-paar bij basic', async () => { const r = await buildRequest(p('basic'), 'spot', 'X', null, { api_key: 'u', api_secret: 'w' }); assert.equal(r.headers.Authorization, 'Basic ' + Buffer.from('u:w').toString('base64')); }); it('plakt de sleutel achter de query bij query_key, gecodeerd en zonder de bestaande query te slopen', async () => { const r = await buildRequest(p('query_key', { authenticationParameterName: 'api key' }), 'spot', 'X', null, { api_key: 'a&b' }); assert.equal(r.url, 'https://api.example.test/v1/p?a=1&api%20key=a%26b'); }); it('gooit als de naam van de header of de queryparameter ontbreekt', async () => { await assert.rejects(() => buildRequest(p('header_key'), 'spot', 'X', null, { api_key: 'k' }), (e) => e.key === 'integrations.price.headerNameMissing'); await assert.rejects(() => buildRequest(p('query_key'), 'spot', 'X', null, { api_key: 'k' }), (e) => e.key === 'integrations.price.parameterNameMissing'); }); it('gooit bij een authenticationKind die niet bestaat', async () => { await assert.rejects(() => buildRequest(p('verzonnen'), 'spot', 'X', null, null), (e) => e.key === 'integrations.price.unknownAuthKind'); }); }); describe('hmacHeaders: het bericht dat ondertekend wordt', () => { const hmac = { messageParts: 'timestamp,method,full_path,body', hashAlgorithm: 'SHA-256', encoding: 'hex', keyHeader: 'X-Key', signatureHeader: 'X-Sig', timestampHeader: 'X-Ts', windowHeader: null, windowMs: null, }; const creds = { api_key: 'sleutel', api_secret: 'geheim' }; it('ondertekent precies de samengevoegde onderdelen, in de opgegeven volgorde', async () => { const h = await hmacHeaders(hmac, 'GET', '/v1/p?a=1', '', creds); const verwacht = createHmac('sha256', creds.api_secret) .update(`${h['X-Ts']}GET/v1/p?a=1`).digest('hex'); assert.equal(h['X-Sig'], verwacht); assert.equal(h['X-Key'], 'sleutel'); assert.match(h['X-Ts'], /^\d{13}$/, 'een tijdstempel in milliseconden'); }); it('volgt de volgorde uit messageParts en niet een vaste volgorde', async () => { const omgekeerd = { ...hmac, messageParts: 'method,timestamp' }; const h = await hmacHeaders(omgekeerd, 'GET', '/x', '', creds); assert.equal(h['X-Sig'], createHmac('sha256', creds.api_secret).update(`GET${h['X-Ts']}`).digest('hex')); }); it('kan base64 in plaats van hex', async () => { const h = await hmacHeaders({ ...hmac, encoding: 'base64' }, 'GET', '/x', '', creds); assert.equal(h['X-Sig'], createHmac('sha256', creds.api_secret).update(`${h['X-Ts']}GET/x`).digest('base64')); }); it('zet de venster-header alleen als hij geconfigureerd is', async () => { assert.equal('X-Win' in await hmacHeaders(hmac, 'GET', '/x', '', creds), false); const h = await hmacHeaders({ ...hmac, windowHeader: 'X-Win', windowMs: 5000 }, 'GET', '/x', '', creds); assert.equal(h['X-Win'], '5000'); }); it('gooit bij een onbekend onderdeel in messageParts', async () => { await assert.rejects(() => hmacHeaders({ ...hmac, messageParts: 'timestamp,verzonnen' }, 'GET', '/x', '', creds), (e) => e.key === 'integrations.price.unknownHmacToken'); }); it('ondertekent het VOLLEDIGE pad inclusief query', async () => { const p = provider({ authenticationKind: 'hmac', hmac, endpoints: [endpoint('spot', '/v1/p', 'a=1')], }); const r = await buildRequest(p, 'spot', 'X', null, creds); assert.equal(r.headers['X-Sig'], createHmac('sha256', creds.api_secret).update(`${r.headers['X-Ts']}GET/v1/p?a=1`).digest('hex')); }); it('gooit als de hmac-config bij de provider ontbreekt', async () => { const p = provider({ authenticationKind: 'hmac', hmac: null, endpoints: [endpoint('spot', '/v1/p')] }); await assert.rejects(() => buildRequest(p, 'spot', 'X', null, creds), (e) => e.key === 'integrations.price.hmacConfigMissing'); }); }); describe('priceCall: de enige functie die het netwerk op gaat', () => { const p = provider({ endpoints: [endpoint('spot', '/v1/prijs')], outputs: [output('spot', 'price', 'data.eur')], }); function metFetch(antwoord, dan) { const origineel = globalThis.fetch; const gezien = {}; globalThis.fetch = async (url, opties) => { gezien.url = url; gezien.opties = opties; return antwoord; }; return Promise.resolve(dan(gezien)).finally(() => { globalThis.fetch = origineel; }); } const respons = (status, body) => new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } }); it('mapt een geslaagde respons op het interne contract', async () => { await metFetch(respons(200, { data: { eur: 42 } }), async (gezien) => { assert.deepEqual(await priceCall(p, 'spot', 'BTC', null, null), { price: 42 }); assert.equal(gezien.url, 'https://api.example.test/v1/prijs'); }); }); it('geeft de ruwe respons erbij als daarom gevraagd wordt', async () => { await metFetch(respons(200, { data: { eur: 42 } }), async () => { const uit = await priceCall(p, 'spot', 'BTC', null, null, { withRaw: true }); assert.deepEqual(uit.result, { price: 42 }); assert.deepEqual(uit.raw, { data: { eur: 42 } }, 'raw is de geparste respons, niet de tekst'); }); }); it('meldt een foutstatus met de code erbij', async () => { await metFetch(respons(429, { fout: 'te veel' }), async () => { await assert.rejects(() => priceCall(p, 'spot', 'BTC', null, null), (e) => e.key === 'integrations.price.httpFailed' && e.params.status === 429); }); }); it('laat een respons die geen JSON is niet stil doorgaan', async () => { const kapot = new Response('storing', { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' } }); await metFetch(kapot, async () => { await assert.rejects(() => priceCall(p, 'spot', 'BTC', null, null), (e) => e.key === 'integrations.price.notJson' && e.params.detail === 'no JSON (text/html, 20 bytes)' && e.httpStatus === 200); }); }); it('beschrijft een foutantwoord dat geen JSON is in plaats van het weg te gooien', async () => { const proxy = new Response('Bad Gateway', { status: 502, headers: { 'Content-Type': 'text/html' } }); await metFetch(proxy, async () => { await assert.rejects(() => priceCall(p, 'spot', 'BTC', null, null), (e) => e.key === 'integrations.price.httpFailed' && e.params.detail === ' no JSON (text/html, 24 bytes)' && e.httpStatus === 502); }); }); it('meldt een netwerkfout als netwerkfout en niet als koersfout', async () => { const origineel = globalThis.fetch; globalThis.fetch = async () => { throw new Error('kabel eruit'); }; try { await assert.rejects(() => priceCall(p, 'spot', 'BTC', null, null), (e) => e.key === 'integrations.price.networkError'); } finally { globalThis.fetch = origineel; } }); }); describe('failureEntry: wat het log van een mislukte koersaanroep krijgt', () => { const p = provider({ authenticationKind: 'query_key', authenticationRequired: 1, authenticationParameterName: 'apikey', endpoints: [endpoint('spot', '/v1/prijs')], outputs: [output('spot', 'price', 'data.eur')], }); const fout = async (antwoord, creds = { api_key: 'GEHEIM123' }) => { const origineel = globalThis.fetch; globalThis.fetch = async () => antwoord; try { await priceCall(p, 'spot', 'BTC', null, creds); } catch (e) { return e; } finally { globalThis.fetch = origineel; } throw new Error('priceCall gooide niet'); }; it('zonder sleutel: "refused", geen tijd, en er ging niets de deur uit', async () => { let gevraagd = false; const origineel = globalThis.fetch; globalThis.fetch = async () => { gevraagd = true; return new Response('{}'); }; let e; try { await priceCall(p, 'spot', 'BTC', null, null); } catch (x) { e = x; } finally { globalThis.fetch = origineel; } assert.equal(gevraagd, false); const rij = failureEntry(p, 'spot', 'BTC', e, 12); assert.deepEqual([rij.outcome, rij.latencyMs, rij.errorKey], ['refused', null, 'integrations.price.keyMissing']); }); it('een foutstatus: "fail" met de status en de sleutel, en de capability als operatie en niet de URL', async () => { const e = await fout(new Response(JSON.stringify({ error: 'bad key GEHEIM123' }), { status: 403 })); const rij = failureEntry(p, 'spot', 'BTC', e, 12); assert.deepEqual({ ...rij, detail: undefined }, { target: 'provider', providerKey: 'test', operation: 'spot', subject: 'BTC', outcome: 'fail', httpStatus: 403, errorKey: 'integrations.price.httpFailed', detail: undefined, latencyMs: 12, }); assert.ok(!JSON.stringify(rij).includes('api.example.test'), 'de URL staat in de rij'); }); it('een antwoord zonder bruikbare prijs: "empty", met status 200', async () => { const e = await fout(new Response(JSON.stringify({ data: { eur: null } }), { status: 200 })); const rij = failureEntry(p, 'spot', 'BTC', e, 5); assert.deepEqual([rij.outcome, rij.httpStatus, rij.errorKey], ['empty', 200, 'integrations.price.noValidPrice']); }); }); describe('één bron voor crypto en metaal: het segment en de eenheid per symbool (migratie 99)', () => { const f = CONFIG.METALS.GRAMS_PER_TROY_OUNCE; const eulerpool = provider({ endpoints: [endpoint('daily_history', '/api/1/{external_id}/quotes/{symbol}')], symbols: [{ symbol: 'BTC', externalId: 'crypto', quoteUnit: null }, { symbol: 'XAU', externalId: 'commodity', quoteUnit: 'ozt' }], outputs: [output('daily_history', 'time', '[].timestamp', 'ms_to_s'), output('daily_history', 'price', '[].price', 'last_per_day')], }); const reeks = [{ timestamp: Date.parse('2026-09-01T00:00:00Z'), price: 3100 }]; async function vraag(symbool) { const origineel = globalThis.fetch; let url = null; globalThis.fetch = async u => { url = u; return new Response(JSON.stringify(reeks), { status: 200 }); }; try { return { rijen: await priceCall(eulerpool, 'daily_history', symbool, null, null), url }; } finally { globalThis.fetch = origineel; } } it('zet het segment van het symbool in het pad', async () => { assert.match((await vraag('BTC')).url, /\/api\/1\/crypto\/quotes\/BTC$/); assert.match((await vraag('XAU')).url, /\/api\/1\/commodity\/quotes\/XAU$/); }); it('rekent alleen het symbool met ozt om naar per gram', async () => { assert.equal((await vraag('BTC')).rijen[0].price, 3100, 'crypto blijft per munt'); assert.equal((await vraag('XAU')).rijen[0].price, 3100 / f, 'metaal wordt per gram'); }); }); describe('de catch-all in de invoermapping (migratie 102)', () => { const f = CONFIG.METALS.GRAMS_PER_TROY_OUNCE; const eulerpool = provider({ endpoints: [endpoint('daily_history', '/api/1/{external_id}/quotes/{symbol}')], symbols: [{ symbol: 'XAU', externalId: 'commodity', quoteUnit: 'ozt', isCatchAll: 0 }, { symbol: null, externalId: 'crypto', quoteUnit: null, isCatchAll: 1 }], outputs: [output('daily_history', 'time', '[].timestamp', 'ms_to_s'), output('daily_history', 'price', '[].price', 'last_per_day')], }); const reeks = [{ timestamp: Date.parse('2026-09-01T00:00:00Z'), price: 3100 }]; async function vraag(p, symbool) { const origineel = globalThis.fetch; let url = null; globalThis.fetch = async u => { url = u; return new Response(JSON.stringify(reeks), { status: 200 }); }; try { return { rijen: await priceCall(p, 'daily_history', symbool, null, null), url }; } finally { globalThis.fetch = origineel; } } it('geeft een munt zonder eigen regel het segment van de catch-all', async () => { assert.match((await vraag(eulerpool, 'DOGE')).url, /\/api\/1\/crypto\/quotes\/DOGE$/); assert.equal((await vraag(eulerpool, 'DOGE')).rijen[0].price, 3100, 'zonder eenheid op de catch-all blijft hij per munt'); }); it('laat een eigen regel voorgaan, ook in de eenheid', async () => { const r = await vraag(eulerpool, 'XAU'); assert.match(r.url, /\/api\/1\/commodity\/quotes\/XAU$/); assert.equal(r.rijen[0].price, 3100 / f); }); it('valt zonder catch-all terug op het symbool zelf, zoals voorheen', async () => { const zonder = { ...eulerpool, symbols: eulerpool.symbols.filter(s => !s.isCatchAll) }; assert.match((await vraag(zonder, 'DOGE')).url, /\/api\/1\/DOGE\/quotes\/DOGE$/); }); });