free-score-api.test.js
1 /** 2 * Tests for src/api/free-score-api.js 3 * 4 * Covers: 5 * - fetchPendingScans: no WORKER_URL → returns [] 6 * - fetchPendingScans: non-ok response → throws 7 * - fetchPendingScans: empty scans array (data.scans missing) → returns [] 8 * - fetchPendingScans: returns scans array 9 * - acknowledgeScans: calls DELETE for each key 10 * - acknowledgeScans: logs warning on non-ok response (does not throw) 11 * - acknowledgeScans: empty kvKeys → no fetch calls 12 */ 13 14 import { test, describe, before, after } from 'node:test'; 15 import assert from 'node:assert/strict'; 16 import Database from 'better-sqlite3'; 17 import { randomUUID } from 'crypto'; 18 19 // ── fetchPendingScans ───────────────────────────────────────────────────────── 20 21 describe('fetchPendingScans — missing WORKER_URL', () => { 22 let savedUrl; 23 24 before(() => { 25 savedUrl = process.env.API_WORKER_URL; 26 delete process.env.API_WORKER_URL; 27 }); 28 29 after(() => { 30 if (savedUrl !== undefined) process.env.API_WORKER_URL = savedUrl; 31 else delete process.env.API_WORKER_URL; 32 }); 33 34 test('returns empty array when API_WORKER_URL not set', async () => { 35 const { fetchPendingScans } = await import(`../../src/api/free-score-api.js?ts=${Date.now()}`); 36 const result = await fetchPendingScans(); 37 assert.deepEqual(result, []); 38 }); 39 }); 40 41 describe('fetchPendingScans — with mocked fetch', () => { 42 let savedUrl; 43 let savedSecret; 44 let savedFetch; 45 46 before(() => { 47 savedUrl = process.env.API_WORKER_URL; 48 savedSecret = process.env.API_WORKER_SECRET; 49 savedFetch = global.fetch; 50 process.env.API_WORKER_URL = 'https://fake-worker.example.com'; 51 process.env.API_WORKER_SECRET = 'fake-secret'; 52 }); 53 54 after(() => { 55 if (savedUrl !== undefined) process.env.API_WORKER_URL = savedUrl; 56 else delete process.env.API_WORKER_URL; 57 if (savedSecret !== undefined) process.env.API_WORKER_SECRET = savedSecret; 58 else delete process.env.API_WORKER_SECRET; 59 global.fetch = savedFetch; 60 }); 61 62 test('throws when response is not ok', async () => { 63 global.fetch = async () => ({ 64 ok: false, 65 status: 401, 66 text: async () => 'Unauthorized', 67 json: async () => ({}), 68 }); 69 70 const { fetchPendingScans } = await import(`../../src/api/free-score-api.js?ts=${Date.now()}a`); 71 await assert.rejects(fetchPendingScans, /Worker responded 401/); 72 }); 73 74 test('returns scans array from worker response', async () => { 75 const scans = [ 76 { scan_id: randomUUID(), url: 'https://example.com', domain: 'example.com', score: 72 }, 77 { scan_id: randomUUID(), url: 'https://test.com', domain: 'test.com', score: 50 }, 78 ]; 79 global.fetch = async () => ({ 80 ok: true, 81 json: async () => ({ scans }), 82 }); 83 84 const { fetchPendingScans } = await import(`../../src/api/free-score-api.js?ts=${Date.now()}b`); 85 const result = await fetchPendingScans(); 86 assert.equal(result.length, 2); 87 assert.equal(result[0].domain, 'example.com'); 88 }); 89 90 test('returns empty array when scans key missing from response', async () => { 91 global.fetch = async () => ({ 92 ok: true, 93 json: async () => ({ message: 'no scans today' }), 94 }); 95 96 const { fetchPendingScans } = await import(`../../src/api/free-score-api.js?ts=${Date.now()}c`); 97 const result = await fetchPendingScans(); 98 assert.deepEqual(result, []); 99 }); 100 }); 101 102 // ── acknowledgeScans ────────────────────────────────────────────────────────── 103 104 describe('acknowledgeScans — with mocked fetch', () => { 105 let savedUrl; 106 let savedSecret; 107 let savedFetch; 108 109 before(() => { 110 savedUrl = process.env.API_WORKER_URL; 111 savedSecret = process.env.API_WORKER_SECRET; 112 savedFetch = global.fetch; 113 process.env.API_WORKER_URL = 'https://fake-worker.example.com'; 114 process.env.API_WORKER_SECRET = 'fake-secret'; 115 }); 116 117 after(() => { 118 if (savedUrl !== undefined) process.env.API_WORKER_URL = savedUrl; 119 else delete process.env.API_WORKER_URL; 120 if (savedSecret !== undefined) process.env.API_WORKER_SECRET = savedSecret; 121 else delete process.env.API_WORKER_SECRET; 122 global.fetch = savedFetch; 123 }); 124 125 test('calls DELETE for each kv key', async () => { 126 const calls = []; 127 global.fetch = async (url, opts) => { 128 calls.push({ url, method: opts.method }); 129 return { ok: true }; 130 }; 131 132 const { acknowledgeScans } = await import(`../../src/api/free-score-api.js?ts=${Date.now()}d`); 133 await acknowledgeScans(['scan:abc', 'scan:def']); 134 135 assert.equal(calls.length, 2); 136 assert.ok(calls[0].method === 'DELETE'); 137 assert.ok(calls[0].url.includes('scan%3Aabc') || calls[0].url.includes('scan:abc')); 138 }); 139 140 test('does not throw when DELETE response is not ok', async () => { 141 global.fetch = async () => ({ ok: false, status: 404 }); 142 143 const { acknowledgeScans } = await import(`../../src/api/free-score-api.js?ts=${Date.now()}e`); 144 // Should not throw 145 await assert.doesNotReject(() => acknowledgeScans(['scan:missing'])); 146 }); 147 148 test('handles empty kvKeys array without calling fetch', async () => { 149 const calls = []; 150 global.fetch = async () => { 151 calls.push(1); 152 return { ok: true }; 153 }; 154 155 const { acknowledgeScans } = await import(`../../src/api/free-score-api.js?ts=${Date.now()}f`); 156 await acknowledgeScans([]); 157 assert.equal(calls.length, 0); 158 }); 159 });