interacmanagernew / scripts /test-api.ts
MichaelEdou
Initial commit β€” ICC Interac Manager full-stack app
149698e
#!/usr/bin/env node
// scripts/test-api.ts
//
// ICC API Endpoint Test Suite
// Run: npx tsx scripts/test-api.ts
//
// Requires the server to be running on PORT (default 3001).
import 'dotenv/config';
import jwt from 'jsonwebtoken';
const PORT = process.env.PORT || '3001';
const BASE = `http://localhost:${PORT}`;
const JWT_SECRET = process.env.JWT_SECRET || 'icc-dev-secret-change-in-production';
// Create a test JWT (simulates a logged-in user)
const TEST_USER_ID = 'test-user-healthcheck';
const testToken = jwt.sign(
{ userId: TEST_USER_ID, email: 'test@iccameriques.org', name: 'Test User' },
JWT_SECRET,
{ expiresIn: '1h' }
);
const PASS = '\x1b[32mβœ…\x1b[0m';
const FAIL = '\x1b[31m❌\x1b[0m';
let passed = 0;
let failed = 0;
const failures: string[] = [];
async function test(name: string, fn: () => Promise<void>) {
try {
await fn();
console.log(` ${PASS} ${name}`);
passed++;
} catch (error: any) {
console.log(` ${FAIL} ${name}`);
console.log(` β†’ ${error.message}`);
failed++;
failures.push(`${name}: ${error.message}`);
}
}
function assert(condition: boolean, msg: string) {
if (!condition) throw new Error(msg);
}
async function fetchJSON(path: string, options: RequestInit = {}) {
const res = await fetch(`${BASE}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${testToken}`,
...(options.headers as Record<string, string>),
},
});
return { res, data: await res.json().catch(() => null) };
}
async function main() {
console.log('\n\x1b[1mπŸ§ͺ ICC API Endpoint Tests\x1b[0m');
console.log(` Server: ${BASE}`);
console.log('═'.repeat(50));
// Check server is running
try {
await fetch(`${BASE}/api/health`, { signal: AbortSignal.timeout(2000) });
} catch {
console.log(`\n ${FAIL} Server not running on ${BASE}`);
console.log(' β†’ Start with: cd packages/server && npx tsx src/index.ts\n');
process.exit(1);
}
// ─── HEALTH ───
console.log('\n\x1b[1mHealth\x1b[0m');
await test('GET /api/health β†’ 200 + status ok', async () => {
const { res, data } = await fetchJSON('/api/health');
assert(res.status === 200, `Expected 200, got ${res.status}`);
assert(data?.status === 'ok', `Expected status=ok, got ${data?.status}`);
assert(!!data?.timestamp, 'Missing timestamp');
});
// ─── AUTH ───
console.log('\n\x1b[1mAuthentication\x1b[0m');
await test('GET /api/auth/google β†’ 302 redirect to Google', async () => {
const res = await fetch(`${BASE}/api/auth/google`, { redirect: 'manual' });
assert(res.status === 302 || res.status === 301, `Expected 302, got ${res.status}`);
const loc = res.headers.get('location') || '';
assert(loc.includes('accounts.google.com') || loc.includes('google'), `Redirect not to Google: ${loc.substring(0, 60)}`);
});
await test('GET /api/auth/me β†’ 401 without token', async () => {
const res = await fetch(`${BASE}/api/auth/me`);
assert(res.status === 401, `Expected 401, got ${res.status}`);
});
await test('GET /api/auth/me β†’ 404 with test token (user not in DB)', async () => {
const { res } = await fetchJSON('/api/auth/me');
assert(res.status === 404 || res.status === 200, `Expected 404 or 200, got ${res.status}`);
});
await test('Auth middleware rejects expired token', async () => {
const expiredToken = jwt.sign({ userId: 'x' }, JWT_SECRET, { expiresIn: '-1h' });
const res = await fetch(`${BASE}/api/transactions`, {
headers: { Authorization: `Bearer ${expiredToken}` },
});
assert(res.status === 401, `Expected 401, got ${res.status}`);
});
await test('Auth middleware rejects garbage token', async () => {
const res = await fetch(`${BASE}/api/transactions`, {
headers: { Authorization: 'Bearer this-is-not-a-valid-jwt' },
});
assert(res.status === 401, `Expected 401, got ${res.status}`);
});
// ─── SCAN ───
console.log('\n\x1b[1mScan Endpoints\x1b[0m');
await test('POST /api/scan/start β†’ 401 without auth', async () => {
const res = await fetch(`${BASE}/api/scan/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ preset: 'today' }),
});
assert(res.status === 401, `Expected 401, got ${res.status}`);
});
await test('POST /api/scan/start β†’ 400 with invalid preset', async () => {
const { res, data } = await fetchJSON('/api/scan/start', {
method: 'POST',
body: JSON.stringify({ preset: 'invalid_preset' }),
});
assert(res.status === 400, `Expected 400, got ${res.status}`);
});
await test('POST /api/scan/start β†’ 400 custom without dates', async () => {
const { res } = await fetchJSON('/api/scan/start', {
method: 'POST',
body: JSON.stringify({ preset: 'custom' }),
});
// custom preset requires resolveScanDates which throws β†’ 500
assert(res.status === 400 || res.status === 500, `Expected 400 or 500, got ${res.status}`);
});
await test('GET /api/scan/history β†’ 200 + response object', async () => {
const { res, data } = await fetchJSON('/api/scan/history');
assert(res.status === 200, `Expected 200, got ${res.status}`);
assert(data?.scans !== undefined || Array.isArray(data), 'Expected scans in response');
});
await test('GET /api/scan/status/nonexistent β†’ 404', async () => {
const { res } = await fetchJSON('/api/scan/status/fake-job-id');
assert(res.status === 404, `Expected 404, got ${res.status}`);
});
// ─── TRANSACTIONS ───
console.log('\n\x1b[1mTransaction Endpoints\x1b[0m');
await test('GET /api/transactions β†’ 200 + paginated response', async () => {
const { res, data } = await fetchJSON('/api/transactions');
assert(res.status === 200, `Expected 200, got ${res.status}`);
// Response format: { transactions, total, page, limit, totalPages }
assert(typeof data?.total === 'number', `Missing total, got keys: ${Object.keys(data || {})}`);
assert(Array.isArray(data?.transactions), 'transactions should be array');
assert(typeof data?.page === 'number', 'Missing page number');
assert(typeof data?.limit === 'number', 'Missing limit');
});
await test('GET /api/transactions?page=1&limit=5 β†’ respects pagination', async () => {
const { res, data } = await fetchJSON('/api/transactions?page=1&limit=5');
assert(res.status === 200, `Expected 200, got ${res.status}`);
assert(data?.limit === 5, `Expected limit=5, got ${data?.limit}`);
assert(data?.transactions?.length <= 5, `Expected <=5 items, got ${data?.transactions?.length}`);
});
await test('GET /api/transactions?limit=999 β†’ caps at 500', async () => {
const { res, data } = await fetchJSON('/api/transactions?limit=999');
assert(res.status === 200, `Expected 200, got ${res.status}`);
assert(data?.limit <= 500, `Expected limit<=500, got ${data?.limit}`);
});
await test('GET /api/transactions/stats β†’ 200 + totals', async () => {
const { res, data } = await fetchJSON('/api/transactions/stats');
assert(res.status === 200, `Expected 200, got ${res.status}`);
// Response format: { totalCount, totalAmount, byStatus: { deposited, ... }, byBranch, byMonth }
assert(typeof data?.totalCount === 'number', `Missing totalCount, got keys: ${Object.keys(data || {})}`);
assert(typeof data?.totalAmount === 'number', 'Missing totalAmount');
assert(typeof data?.byStatus === 'object', 'Missing byStatus object');
assert(Array.isArray(data?.byBranch), 'Missing byBranch array');
});
await test('GET /api/transactions β†’ 401 without auth', async () => {
const res = await fetch(`${BASE}/api/transactions`);
assert(res.status === 401, `Expected 401, got ${res.status}`);
});
// ─── CHAT ───
console.log('\n\x1b[1mChat Endpoint\x1b[0m');
await test('POST /api/chat/message β†’ 401 without auth', async () => {
const res = await fetch(`${BASE}/api/chat/message`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: 'bonjour' }),
});
assert(res.status === 401, `Expected 401, got ${res.status}`);
});
await test('POST /api/chat/message β†’ 400 with empty message', async () => {
const { res } = await fetchJSON('/api/chat/message', {
method: 'POST',
body: JSON.stringify({ message: '' }),
});
assert(res.status === 400, `Expected 400, got ${res.status}`);
});
await test('POST /api/chat/message β†’ 400 with no body', async () => {
const { res } = await fetchJSON('/api/chat/message', {
method: 'POST',
body: JSON.stringify({}),
});
assert(res.status === 400, `Expected 400, got ${res.status}`);
});
// Only test AI-powered chat if API key is available
if (process.env.GROQ_API_KEY || process.env.MISTRAL_API_KEY) {
await test('POST /api/chat/message β†’ responds to "bonjour"', async () => {
const { res, data } = await fetchJSON('/api/chat/message', {
method: 'POST',
body: JSON.stringify({ message: 'Bonjour' }),
});
assert(res.status === 200, `Expected 200, got ${res.status}`);
assert(typeof data?.message === 'string' || typeof data?.response === 'string', 'Missing response message');
});
} else {
console.log(' ⏭️ Skipping AI chat tests (no API key set)');
}
// ═══ SUMMARY ═══
console.log('\n' + '═'.repeat(50));
console.log(`\x1b[1mπŸ“‹ Results: ${passed} passed, ${failed} failed\x1b[0m`);
if (failures.length > 0) {
console.log('\n\x1b[31mFailures:\x1b[0m');
failures.forEach((f) => console.log(` β†’ ${f}`));
}
console.log('═'.repeat(50) + '\n');
process.exit(failed > 0 ? 1 : 0);
}
main().catch(console.error);