import Database from 'better-sqlite3'; import { randomUUID } from 'crypto'; const API_URL = process.env.API_URL || 'http://localhost:3001'; const TOKEN = process.env.TEST_TOKEN; if (!TOKEN) { console.error('ERROR: TEST_TOKEN environment variable is required'); process.exit(1); } interface TestResult { name: string; passed: boolean; error?: string; } const results: TestResult[] = []; async function request( method: string, path: string, body?: unknown, expectStatus = 200, useToken = true ): Promise<{ status: number; data: unknown; headers: Headers }> { const headers: Record = { 'Content-Type': 'application/json', }; if (useToken) { headers['Authorization'] = `Bearer ${TOKEN}`; } const response = await fetch(`${API_URL}${path}`, { method, headers, body: body ? JSON.stringify(body) : undefined, }); const data = response.headers.get('content-type')?.includes('application/json') ? await response.json() : null; if (response.status !== expectStatus) { throw new Error(`Expected ${expectStatus}, got ${response.status}: ${JSON.stringify(data)}`); } return { status: response.status, data, headers: response.headers }; } async function test(name: string, fn: () => Promise) { try { await fn(); results.push({ name, passed: true }); console.log(` ✓ ${name}`); } catch (error) { const message = error instanceof Error ? error.message : String(error); results.push({ name, passed: false, error: message }); console.log(` ✗ ${name}`); console.log(` ${message}`); } } async function runTests() { console.log('\n=== Kaboot Payments API Tests ===\n'); console.log(`API: ${API_URL}`); console.log(''); console.log('Payment Config Tests:'); await test('GET /api/payments/config returns configuration', async () => { const res = await fetch(`${API_URL}/api/payments/config`); const data = await res.json(); if (typeof data.configured !== 'boolean') { throw new Error('Missing configured field'); } if (typeof data.generationLimit !== 'number') { throw new Error('Missing generationLimit field'); } if (data.generationLimit !== 250) { throw new Error(`Expected generationLimit 250, got ${data.generationLimit}`); } }); console.log('\nPayment Status Tests:'); await test('GET /api/payments/status without auth returns 401', async () => { const res = await fetch(`${API_URL}/api/payments/status`); if (res.status !== 401) { throw new Error(`Expected 401, got ${res.status}`); } }); await test('GET /api/payments/status with invalid token returns 401', async () => { const res = await fetch(`${API_URL}/api/payments/status`, { headers: { Authorization: 'Bearer invalid-token-here' }, }); if (res.status !== 401) { throw new Error(`Expected 401, got ${res.status}`); } }); await test('GET /api/payments/status with valid token returns status', async () => { const { data } = await request('GET', '/api/payments/status'); const status = data as Record; if (typeof status.hasAccess !== 'boolean') { throw new Error('Missing hasAccess field'); } if (!['group', 'subscription', 'none'].includes(status.accessType as string)) { throw new Error(`Invalid accessType: ${status.accessType}`); } if (!['none', 'active', 'past_due', 'canceled'].includes(status.status as string)) { throw new Error(`Invalid status: ${status.status}`); } }); console.log('\nCheckout Tests:'); await test('POST /api/payments/checkout without auth returns 401', async () => { const res = await fetch(`${API_URL}/api/payments/checkout`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ planType: 'monthly', successUrl: 'http://test.com/success', cancelUrl: 'http://test.com/cancel' }), }); if (res.status !== 401) { throw new Error(`Expected 401, got ${res.status}`); } }); await test('POST /api/payments/checkout without planType returns 400', async () => { const { status, data } = await request('POST', '/api/payments/checkout', { successUrl: 'http://test.com/success', cancelUrl: 'http://test.com/cancel', }, 400); const error = data as { error: string }; if (!error.error.includes('plan')) { throw new Error(`Expected plan type error, got: ${error.error}`); } }); await test('POST /api/payments/checkout with invalid planType returns 400', async () => { const { data } = await request('POST', '/api/payments/checkout', { planType: 'invalid', successUrl: 'http://test.com/success', cancelUrl: 'http://test.com/cancel', }, 400); const error = data as { error: string }; if (!error.error.includes('monthly') && !error.error.includes('yearly')) { throw new Error(`Expected plan type validation error, got: ${error.error}`); } }); await test('POST /api/payments/checkout without successUrl returns 400', async () => { const { data } = await request('POST', '/api/payments/checkout', { planType: 'monthly', cancelUrl: 'http://test.com/cancel', }, 400); const error = data as { error: string }; if (!error.error.includes('successUrl')) { throw new Error(`Expected successUrl error, got: ${error.error}`); } }); await test('POST /api/payments/checkout without cancelUrl returns 400', async () => { const { data } = await request('POST', '/api/payments/checkout', { planType: 'monthly', successUrl: 'http://test.com/success', }, 400); const error = data as { error: string }; if (!error.error.includes('cancelUrl')) { throw new Error(`Expected cancelUrl error, got: ${error.error}`); } }); console.log('\nPortal Tests:'); await test('POST /api/payments/portal without auth returns 401', async () => { const res = await fetch(`${API_URL}/api/payments/portal`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ returnUrl: 'http://test.com' }), }); if (res.status !== 401) { throw new Error(`Expected 401, got ${res.status}`); } }); await test('POST /api/payments/portal without returnUrl returns 400', async () => { const { data } = await request('POST', '/api/payments/portal', {}, 400); const error = data as { error: string }; if (!error.error.includes('returnUrl')) { throw new Error(`Expected returnUrl error, got: ${error.error}`); } }); console.log('\nWebhook Tests:'); await test('POST /api/payments/webhook without signature returns 400', async () => { const res = await fetch(`${API_URL}/api/payments/webhook`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'test' }), }); if (res.status !== 400 && res.status !== 503) { throw new Error(`Expected 400 or 503, got ${res.status}`); } }); await test('POST /api/payments/webhook with invalid signature returns 400', async () => { const res = await fetch(`${API_URL}/api/payments/webhook`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'stripe-signature': 'invalid-signature', }, body: JSON.stringify({ type: 'test' }), }); if (res.status !== 400 && res.status !== 503) { throw new Error(`Expected 400 or 503, got ${res.status}`); } }); console.log('\n=== Results ===\n'); const passed = results.filter((r) => r.passed).length; const failed = results.filter((r) => !r.passed).length; console.log(`Passed: ${passed}`); console.log(`Failed: ${failed}`); console.log(`Total: ${results.length}`); if (failed > 0) { console.log('\nFailed tests:'); results .filter((r) => !r.passed) .forEach((r) => console.log(` - ${r.name}: ${r.error}`)); process.exit(1); } } runTests().catch((err) => { console.error('Test runner error:', err); process.exit(1); });