import test from 'node:test'; import assert from 'node:assert/strict'; import { safeFetch, safeFetchText, getFetchMetrics } from '../apis/utils/fetch.mjs'; test('safeFetch reports HTML as degraded JSON response', async () => { const originalFetch = globalThis.fetch; globalThis.fetch = async () => ({ ok: true, status: 200, headers: { get: () => 'text/html' }, text: async () => 'not json', }); try { const data = await safeFetch('https://example.test/json', { retries: 0, source: 'unit' }); assert.match(data.error, /Expected JSON/); assert.ok(getFetchMetrics().bySource.unit.requests >= 1); } finally { globalThis.fetch = originalFetch; } }); test('safeFetchText returns text and byte count', async () => { const originalFetch = globalThis.fetch; globalThis.fetch = async () => ({ ok: true, status: 200, text: async () => 'hello', }); try { const data = await safeFetchText('https://example.test/rss', { retries: 0, source: 'rss-unit' }); assert.equal(data.text, 'hello'); assert.equal(data.bytes, 5); } finally { globalThis.fetch = originalFetch; } }); function jsonResponse(payload, ok = true, status = 200) { return { ok, status, headers: { getSetCookie: () => [], get: () => 'application/json' }, text: async () => JSON.stringify(payload), json: async () => payload, }; } function textResponse(text, ok = false, status = 500) { return { ok, status, headers: { getSetCookie: () => [], get: () => 'text/plain' }, text: async () => text, json: async () => JSON.parse(text), }; } async function withAcledEnv(mockFetch, fn) { const originalFetch = globalThis.fetch; const saved = { ACLED_EMAIL: process.env.ACLED_EMAIL, ACLED_USER: process.env.ACLED_USER, ACLED_USERNAME: process.env.ACLED_USERNAME, ACLED_PASSWORD: process.env.ACLED_PASSWORD, }; globalThis.fetch = mockFetch; delete process.env.ACLED_EMAIL; delete process.env.ACLED_USER; delete process.env.ACLED_USERNAME; delete process.env.ACLED_PASSWORD; const acled = await import('../apis/sources/acled.mjs'); acled.resetAcledSessionForTests(); try { return await fn(acled); } finally { globalThis.fetch = originalFetch; for (const [key, value] of Object.entries(saved)) { if (value === undefined) delete process.env[key]; else process.env[key] = value; } acled.resetAcledSessionForTests(); } } test('ACLED credentialed OAuth success returns live events and supports ACLED_USER', async () => { const responses = [ jsonResponse({ access_token: 'secret-token' }), jsonResponse({ status: 200, data: [{ event_date: '2026-05-17', event_type: 'Protests', sub_event_type: 'Peaceful protest', country: 'Example', region: 'Example Region', location: 'Example City', fatalities: '0', latitude: '1.23', longitude: '4.56', }], }), ]; await withAcledEnv(async () => responses.shift(), async ({ briefing }) => { process.env.ACLED_USER = 'operator@example.test'; process.env.ACLED_PASSWORD = 'password'; const data = await briefing(); assert.equal(data.status, 'live'); assert.equal(data.totalEvents, 1); assert.equal(data.topCountries.Example.count, 1); }); }); test('ACLED rejected credentials return auth_failed diagnostics', async () => { const responses = [ textResponse('invalid credentials', false, 401), textResponse('forbidden', false, 403), ]; await withAcledEnv(async () => responses.shift(), async ({ briefing }) => { process.env.ACLED_EMAIL = 'operator@example.test'; process.env.ACLED_PASSWORD = 'wrong-password'; const data = await briefing(); assert.equal(data.status, 'auth_failed'); assert.match(data.error, /All ACLED auth methods failed/); }); }); test('ACLED token endpoint failure returns api_failed diagnostics', async () => { const responses = [ textResponse('temporary outage', false, 503), textResponse('temporary outage', false, 503), ]; await withAcledEnv(async () => responses.shift(), async ({ briefing }) => { process.env.ACLED_EMAIL = 'operator@example.test'; process.env.ACLED_PASSWORD = 'password'; const data = await briefing(); assert.equal(data.status, 'api_failed'); assert.match(data.error, /All ACLED auth methods failed/); }); }); test('ACLED valid empty response is live with zero events', async () => { const responses = [ jsonResponse({ access_token: 'secret-token' }), jsonResponse({ status: 200, data: [] }), ]; await withAcledEnv(async () => responses.shift(), async ({ briefing }) => { process.env.ACLED_EMAIL = 'operator@example.test'; process.env.ACLED_PASSWORD = 'password'; const data = await briefing(); assert.equal(data.status, 'live'); assert.equal(data.totalEvents, 0); assert.match(data.message, /valid empty/); }); });