CI / build-and-test (push) Successful in 14m43s
7.3: capturedAt/source au registre + 6 adaptateurs + module provenance.ts + ?debug=1 (search-transport.mjs). 7.4: ProviderHealthService + badge source degradee. 7.6: squelettes par provider + snapshots progressifs + transport NDJSON /api/search. 8.1: ProviderAdapter unifie (search enveloppe + channelContent/channelMeta/capabilities) via getProviderAdapter + test de contrat offline.
111 lines
5.4 KiB
JavaScript
111 lines
5.4 KiB
JavaScript
/**
|
|
* Phase 7.3 — mode debug `?debug=1` sur `/api/search`.
|
|
*
|
|
* Ce mode est une barrière de sécurité autant qu'un outil : un `raw` de provider
|
|
* peut contenir une clé d'API, et un payload de debug finit souvent dans les
|
|
* logs de CI. Les tests ci-dessous prouvent donc que les garde-fous TIENNENT :
|
|
* troncature à 600 caractères, 2 items par provider, redaction des secrets
|
|
* (y compris imbriqués dans des tableaux), et aucune fuite quand le `raw` est
|
|
* circulaire ou non sérialisable.
|
|
*/
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { buildDebugPayload, redactDebugSecrets, DEBUG_RAW_MAX_CHARS } from '../search-transport.mjs';
|
|
|
|
const AT = 1700000000000;
|
|
|
|
test('7.3 — le payload reste borné (2 items/provider) et compte le total', () => {
|
|
const items = Array.from({ length: 7 }, (_, i) => ({ id: `i${i}`, title: `t${i}`, source: 'api', capturedAt: AT }));
|
|
const dbg = buildDebugPayload({ yt: items }, {});
|
|
assert.equal(dbg.yt.count, 7, 'le total reste visible même tronqué');
|
|
assert.equal(dbg.yt.items.length, 2);
|
|
assert.equal(dbg.yt.capturedAt, AT, 'provenance du provider agrégée');
|
|
assert.equal(dbg.yt.source, 'api');
|
|
});
|
|
|
|
test('7.3 — les champs mappés sont nommés (pas seulement comptés)', () => {
|
|
const dbg = buildDebugPayload({ dm: [{ id: 'x', title: 'T', views: 12, capturedAt: AT, source: 'api', channelRef: { provider: 'dm' } }] }, {});
|
|
const fields = dbg.dm.items[0].fields;
|
|
assert.deepEqual(fields, ['channelRef', 'title', 'views'], 'noms triés, hors provenance');
|
|
assert.equal(dbg.dm.items[0].id, 'x');
|
|
assert.equal(dbg.dm.items[0].capturedAt, AT);
|
|
});
|
|
|
|
test('7.3 — un champ absent reste absent dans le debug', () => {
|
|
// `views` n'est pas là : c'est l'information à ne pas maquiller en `null`.
|
|
const dbg = buildDebugPayload({ tw: [{ id: 'a', title: 'T', source: 'api', capturedAt: AT }] }, {});
|
|
assert.equal(dbg.tw.items[0].fields.includes('views'), false);
|
|
assert.equal(dbg.tw.items[0].raw, undefined, 'pas de `raw` => pas de champ `raw` dans le payload');
|
|
});
|
|
|
|
test('7.3 — le `raw` est tronqué à 600 caractères', () => {
|
|
const raw = { payload: 'x'.repeat(5000) };
|
|
const dbg = buildDebugPayload({ pt: [{ id: 'p', source: 'api', capturedAt: AT, raw }] }, {});
|
|
const shown = dbg.pt.items[0].raw;
|
|
assert.ok(shown.startsWith('{"payload":"xxx'), 'le début du JSON est visible');
|
|
assert.ok(shown.includes('… ('), 'la troncature est signalée');
|
|
assert.ok(shown.includes('car.)'), 'la taille réelle est annoncée');
|
|
// 600 caractères de JSON + la mention de troncature.
|
|
assert.ok(shown.length < DEBUG_RAW_MAX_CHARS + 60, 'la sortie reste courte');
|
|
assert.ok(shown.length > DEBUG_RAW_MAX_CHARS, 'la troncature a bien eu lieu');
|
|
});
|
|
|
|
test('7.3 — un `raw` court est rendu tel quel', () => {
|
|
const dbg = buildDebugPayload({ od: [{ id: 'o', source: 'api', capturedAt: AT, raw: { title: 'T' } }] }, {});
|
|
assert.equal(dbg.od.items[0].raw, '{"title":"T"}');
|
|
});
|
|
|
|
test('7.3 — redaction des secrets, y compris imbriqués', () => {
|
|
const redacted = redactDebugSecrets({
|
|
apiKey: 'AKIA-REAL',
|
|
api_key: 'x',
|
|
authorization: 'Bearer REAL',
|
|
token: 'REAL',
|
|
cookie: 'sid=REAL',
|
|
password: 'REAL',
|
|
clientSecret: 'REAL',
|
|
nested: { accessToken: 'REAL', keep: 'visible', list: [{ refresh_token: 'REAL' }] },
|
|
// Une clé qui CONTIENT « token » mais n'est pas un secret doit survivre.
|
|
tokenCount: 3,
|
|
});
|
|
const json = JSON.stringify(redacted);
|
|
assert.equal(json.includes('REAL'), false, `aucun secret ne doit subsister : ${json}`);
|
|
assert.equal(redacted.nested.keep, 'visible', 'les données utiles sont conservées');
|
|
assert.equal(redacted.tokenCount, 3, 'une clé non sensible n\'est pas censurée à tort');
|
|
assert.equal(redacted.list, undefined);
|
|
assert.equal(redacted.nested.list[0].refresh_token, '[redacted]', 'redaction dans un tableau imbriqué');
|
|
});
|
|
|
|
test('7.3 — la redaction traverse le `raw` avant sérialisation', () => {
|
|
const dbg = buildDebugPayload({ ru: [{ id: 'r', source: 'api', capturedAt: AT, raw: { authorization: 'Bearer LEAK', ok: 1 } }] }, {});
|
|
assert.equal(dbg.ru.items[0].raw.includes('LEAK'), false, 'le secret ne doit pas apparaître dans le JSON renvoyé');
|
|
assert.ok(dbg.ru.items[0].raw.includes('[redacted]'));
|
|
});
|
|
|
|
test('7.3 — un `raw` circulaire ne fait pas exploser le diagnostic', () => {
|
|
const raw = { name: 'loop' };
|
|
raw.self = raw;
|
|
const dbg = buildDebugPayload({ yt: [{ id: 'y', source: 'api', capturedAt: AT, raw }] }, {});
|
|
assert.equal(dbg.yt.items[0].raw, '[unserialisable]', 'on degrade au lieu de lever');
|
|
});
|
|
|
|
test('7.3 — erreurs de provider remontées, sans division par zéro', () => {
|
|
const dbg = buildDebugPayload(
|
|
{ yt: [], dm: [{ id: 'a', source: 'api', capturedAt: AT }] },
|
|
{ yt: { message: 'HTTP 503 innertube' } },
|
|
);
|
|
assert.equal(dbg.yt.error, 'HTTP 503 innertube', 'le message d\'erreur aide au diagnostic');
|
|
assert.equal(dbg.yt.capturedAt, null, 'aucun item => provenance inconnue');
|
|
assert.equal(dbg.dm.error, null);
|
|
});
|
|
|
|
test('7.3 — entrées dégénérées tolérées', () => {
|
|
const dbg = buildDebugPayload({ yt: null, dm: [null, 'texte', 42] }, null);
|
|
assert.equal(dbg.yt.count, 0);
|
|
assert.deepEqual(dbg.yt.items, []);
|
|
assert.equal(dbg.dm.items[0].value, null);
|
|
assert.equal(dbg.dm.items[1].value, 'texte');
|
|
assert.equal(dbg.dm.count, 3, 'le total reste malgré la troncature à 2');
|
|
assert.deepEqual(buildDebugPayload(undefined, undefined), {});
|
|
});
|