- ESM-Package für Node.js 18+ - Client mit Sync, Async (mit Polling), Webhook, Verify, Download - Typisierte Error-Klassen - Webhook-Signatur-Verifikation (timingSafeEqual) - 13 Tests mit Node native test runner
172 lines
6.7 KiB
JavaScript
172 lines
6.7 KiB
JavaScript
// 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);
|
|
});
|