/ tests / api / free-score-api.test.js
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  });