// Tests für @hightrusted/capture // Node.js native test runner (Node 18+) import { test } from 'node:test'; import assert from 'node:assert/strict'; import { createHmac } from 'node:crypto'; import { Client, HightrustedError, InvalidApiKeyError, QuotaExceededError, RateLimitedError, verifyWebhookSignature, } from '../src/index.mjs'; // ───────────────────────────────────────────────────────────────────── // Mock-fetch helper // ───────────────────────────────────────────────────────────────────── function mockFetch(handler) { const calls = []; const fetch = async (url, init) => { calls.push({ url, init }); return handler(url, init); }; return { fetch, calls }; } function jsonResponse(body, { status = 200, headers = {} } = {}) { return new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json', ...headers }, }); } // ───────────────────────────────────────────────────────────────────── test('Client wirft, wenn kein API-Key gesetzt ist', () => { const old = process.env.HIGHTRUSTED_API_KEY; delete process.env.HIGHTRUSTED_API_KEY; assert.throws(() => new Client(), /API-Key fehlt/); if (old) process.env.HIGHTRUSTED_API_KEY = old; }); test('Client liest API-Key aus ENV', () => { const old = process.env.HIGHTRUSTED_API_KEY; process.env.HIGHTRUSTED_API_KEY = 'ht_test_env'; const c = new Client({ fetch: () => {} }); assert.equal(c.apiKey, 'ht_test_env'); if (old) process.env.HIGHTRUSTED_API_KEY = old; else delete process.env.HIGHTRUSTED_API_KEY; }); test('capture() sendet Bearer-Token', async () => { const { fetch, calls } = mockFetch(() => jsonResponse({ id: '550e', status: 'ready', verify_url: 'https://verify.hightrusted.net/c/550e', created_at: '2026-04-25T00:00:00Z', }) ); const c = new Client({ apiKey: 'ht_test_xyz', fetch }); const result = await c.capture({ url: 'https://example.com' }); assert.equal(result.id, '550e'); assert.equal(calls[0].init.headers.Authorization, 'Bearer ht_test_xyz'); const body = JSON.parse(calls[0].init.body); assert.equal(body.url, 'https://example.com'); }); test('capture() mit reference + viewport', async () => { const { fetch, calls } = mockFetch(() => jsonResponse({ id: 'x', status: 'ready', created_at: '2026-04-25T00:00:00Z' }) ); const c = new Client({ apiKey: 'ht_test_xyz', fetch }); await c.capture({ url: 'https://example.com', reference: 'case-001', viewport: { width: 1920, height: 1080 }, }); const body = JSON.parse(calls[0].init.body); assert.equal(body.reference, 'case-001'); assert.equal(body.viewport.width, 1920); }); test('verify() ohne Argumente wirft', async () => { const c = new Client({ apiKey: 'ht_test_xyz', fetch: () => {} }); await assert.rejects(() => c.verify(), /Genau einer/); await assert.rejects(() => c.verify({ source: 'x', pdf: Buffer.from('y') }), /Genau einer/); }); test('verify() per ID', async () => { const { fetch, calls } = mockFetch(() => jsonResponse({ valid: true, capture_id: '550e' })); const c = new Client({ apiKey: 'ht_test_xyz', fetch }); const result = await c.verify({ source: '550e8400-e29b-41d4-a716-446655440000' }); assert.equal(result.valid, true); const body = JSON.parse(calls[0].init.body); assert.equal(body.source, '550e8400-e29b-41d4-a716-446655440000'); }); test('InvalidApiKeyError bei 401', async () => { const { fetch } = mockFetch(() => jsonResponse( { error: { code: 'invalid_api_key', message: 'Bad key', request_id: 'r1' } }, { status: 401 } ) ); const c = new Client({ apiKey: 'ht_test_xyz', fetch, maxRetries: 0 }); await assert.rejects(() => c.capture({ url: 'https://example.com' }), (err) => { assert.ok(err instanceof InvalidApiKeyError); assert.equal(err.code, 'invalid_api_key'); assert.equal(err.requestId, 'r1'); return true; }); }); test('QuotaExceededError bei 402', async () => { const { fetch } = mockFetch(() => jsonResponse( { error: { code: 'quota_exceeded', message: 'X', request_id: 'r1' } }, { status: 402 } ) ); const c = new Client({ apiKey: 'ht_test_xyz', fetch, maxRetries: 0 }); await assert.rejects(() => c.capture({ url: 'https://example.com' }), QuotaExceededError); }); test('RateLimitedError mit retryAfterSeconds', async () => { const { fetch } = mockFetch(() => jsonResponse( { error: { code: 'rate_limited', message: 'X', request_id: 'r1' } }, { status: 429, headers: { 'Retry-After': '12' } } ) ); const c = new Client({ apiKey: 'ht_test_xyz', fetch, maxRetries: 0 }); await assert.rejects(() => c.capture({ url: 'https://example.com' }), (err) => { assert.ok(err instanceof RateLimitedError); assert.equal(err.retryAfterSeconds, 12); return true; }); }); // ───────────────────────────────────────────────────────────────────── // Webhook-Signatur // ───────────────────────────────────────────────────────────────────── test('verifyWebhookSignature akzeptiert korrekte Signatur', () => { const body = Buffer.from('{"event":"capture.ready"}'); const secret = 'wh_secret_test'; const sig = 'sha256=' + createHmac('sha256', secret).update(body).digest('hex'); assert.equal(verifyWebhookSignature(body, sig, secret), true); }); test('verifyWebhookSignature lehnt falsche Signatur ab', () => { const body = Buffer.from('{"event":"capture.ready"}'); const badSig = 'sha256=' + '0'.repeat(64); assert.equal(verifyWebhookSignature(body, badSig, 'secret'), false); }); test('verifyWebhookSignature lehnt leere Inputs ab', () => { assert.equal(verifyWebhookSignature('', 'sha256=x', 'secret'), false); assert.equal(verifyWebhookSignature('x', '', 'secret'), false); assert.equal(verifyWebhookSignature('x', 'sha256=x', ''), false); }); test('verifyWebhookSignature akzeptiert auch String-Body', () => { const body = '{"event":"capture.ready"}'; const secret = 'wh_secret_test'; const sig = 'sha256=' + createHmac('sha256', secret).update(body).digest('hex'); assert.equal(verifyWebhookSignature(body, sig, secret), true); });