/** * Phase 8.3 (intégration) — le câblage HTTP des feature flags. * * Les tests unitaires de `feature-flags.mjs` prouvent la sémantique des flags ; * ceux-ci prouvent qu'ils sont *branchés* : que le provider éteint n'est pas * appelé, qu'il est signalé dans la réponse, et que le diagnostic de santé ne le * sonde pas. * * Méthode : on démarre le serveur avec les SIX flags à `0`. Un fan-out normal * appellerait six upstreams ; ici il n'y a donc **aucun accès réseau**, ce qui * rend le test déterministe (et le prouve : voir l'assertion de durée). */ import { describe, it, before, after } from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import net from 'node:net'; import { spawn } from 'node:child_process'; const ALL = ['yt', 'dm', 'tw', 'pt', 'od', 'ru']; const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'newtube-ff-')); const PORT = await new Promise((resolve) => { const s = net.createServer(); s.listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => resolve(p)); }); }); const base = `http://127.0.0.1:${PORT}`; const server = spawn(process.execPath, ['./server/index.mjs'], { cwd: path.resolve(import.meta.dirname, '..', '..'), env: { ...process.env, PORT: String(PORT), NEWTUBE_DB_FILE: path.join(tmpDir, 'ff.db'), JWT_SECRET: 'ff-test-secret', NODE_ENV: 'test', // Tous les providers éteints -> aucun appel upstream attendu. ...Object.fromEntries(ALL.map((p) => [`FF_${p.toUpperCase()}`, '0'])), // Coupe aussi l'enrichissement web des suggestions (sinon : réseau). SUGGEST_WEB_ENABLED: '0', }, stdio: ['ignore', 'pipe', 'pipe'], }); let logs = ''; server.stdout.on('data', (d) => { logs += d; }); server.stderr.on('data', (d) => { logs += d; }); const J = async (url) => { const r = await fetch(url); return { status: r.status, body: await r.json().catch(() => ({})) }; }; describe('Phase 8.3 — feature flags câblés sur le HTTP', () => { before(async () => { const t0 = Date.now(); while (Date.now() - t0 < 25000) { try { const r = await fetch(`${base}/api/health`); if (r.status < 500) return; } catch {} await new Promise((r) => setTimeout(r, 300)); } assert.fail(`API ne démarre pas:\n${logs.slice(-3000)}`); }); after(() => { server.kill(); try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {} }); it('/api/search exclut les providers éteints du fan-out et les signale', async () => { const t0 = Date.now(); const { status, body } = await J(`${base}/api/search?q=test&providers=${ALL.join(',')}`); const elapsed = Date.now() - t0; assert.equal(status, 200); assert.equal(body.v, 2, 'la version de contrat reste annoncée'); assert.deepEqual(body.providers, [], 'aucun provider ne doit rester dans le fan-out'); for (const p of ALL) { assert.deepEqual(body.groups[p], [], `${p} doit avoir un groupe vide et non absent`); assert.equal(body.errors[p].code, 'disabled_by_ff', `${p} doit être signalé disabled_by_ff`); assert.match(body.errors[p].message, new RegExp(`FF_${p.toUpperCase()}`), 'le message nomme le flag'); } // Preuve indirecte qu'aucun upstream n'a été contacté : un fan-out réel // part en parallèle sur 6 services et dépasse largement ce seuil. assert.ok(elapsed < 2000, `réponse en ${elapsed}ms : un provider éteint est probablement encore appelé`); }); it('un provider éteint est absent de `providers` mais garde sa colonne', async () => { const { body } = await J(`${base}/api/search?q=test&providers=yt,ru,od`); assert.deepEqual(body.providers, [], 'tous les providers demandés sont éteints dans ce test'); assert.ok('yt' in body.groups && 'ru' in body.groups && 'od' in body.groups, 'le front doit pouvoir afficher une colonne pour chacun des providers demandés'); }); it('/api/suggest applique les mêmes flags et signale la raison', async () => { const { status, body } = await J(`${base}/api/search/suggest?q=test&providers=${ALL.join(',')}`); assert.equal(status, 200); assert.equal(body.errors.ru.code, 'disabled_by_ff'); for (const p of ALL) assert.deepEqual(body.groups[p], [], `${p} doit être vide`); }); it('/api/providers/health distingue « éteint » de « en panne »', async () => { const { body } = await J(`${base}/api/providers/health?provider=ru`); const ru = body.providers.ru; assert.equal(ru.disabled, true); assert.equal(ru.flag, 'FF_RU'); assert.equal(ru.flagValue, '0'); assert.equal(ru.lastError, 'disabled_by_ff'); // Un provider éteint n'est pas « cassé » : ni erreur, ni latence mesurée. assert.equal(ru.consecutiveFailures, 0); assert.equal(ru.errorRate, 0); assert.equal(ru.latencyMs, 0); }); });