/** * Integration tests for the REST API routes. * Spins up the Express app against an in-memory DB. */ const { test, before, after } = require('node:test'); const assert = require('node:assert/strict'); const http = require('node:http'); // Use in-memory DB for tests process.env.DB_PATH = ':memory:'; process.env.ADMIN_USERNAME = 'admin'; process.env.ADMIN_PASSWORD = 'testpass123'; process.env.SESSION_SECRET = 'test-secret'; process.env.DEVICE_IP = ''; // No real device – send/poll will fail gracefully const { initDB } = require('../src/db'); const apiRoutes = require('../src/routes/api'); // Build a minimal Express app for testing const express = require('express'); const session = require('express-session'); const app = express(); app.use(express.json()); app.use(session({ secret: 'test-secret', resave: false, saveUninitialized: false })); app.use('/api', apiRoutes); let server; let baseURL; // Cookie jar for session let sessionCookie = ''; before(async () => { initDB(); server = http.createServer(app); await new Promise(r => server.listen(0, '127.0.0.1', r)); const { port } = server.address(); baseURL = `http://127.0.0.1:${port}`; }); after(() => { server?.close(); }); // ── Helpers ─────────────────────────────────────────────────────────────────── async function req(method, path, body, extraHeaders = {}) { const url = baseURL + '/api' + path; const headers = { 'Content-Type': 'application/json', ...extraHeaders }; if (sessionCookie) headers['Cookie'] = sessionCookie; const response = await fetch(url, { method, headers, body: body !== undefined ? JSON.stringify(body) : undefined, }); // Capture Set-Cookie const setCookie = response.headers.get('set-cookie'); if (setCookie) sessionCookie = setCookie.split(';')[0]; const text = await response.text(); let json; try { json = JSON.parse(text); } catch { json = text; } return { status: response.status, body: json }; } // ── Auth ────────────────────────────────────────────────────────────────────── test('auth: /me returns unauthenticated when no session', async () => { sessionCookie = ''; const { status, body } = await req('GET', '/auth/me'); assert.equal(status, 200); assert.equal(body.authenticated, false); }); test('auth: login with wrong password returns 401', async () => { const { status } = await req('POST', '/auth/login', { username: 'admin', password: 'wrong' }); assert.equal(status, 401); }); test('auth: login with correct credentials returns 200', async () => { const { status, body } = await req('POST', '/auth/login', { username: 'admin', password: 'testpass123' }); assert.equal(status, 200); assert.equal(body.success, true); }); test('auth: /me returns authenticated after login', async () => { const { status, body } = await req('GET', '/auth/me'); assert.equal(status, 200); assert.equal(body.authenticated, true); assert.equal(body.username, 'admin'); }); test('auth: change password with wrong current password returns 401', async () => { const { status } = await req('PUT', '/auth/password', { currentPassword: 'wrongpass', newPassword: 'newpass123', }); assert.equal(status, 401); }); test('auth: change password succeeds with correct current password', async () => { const { status } = await req('PUT', '/auth/password', { currentPassword: 'testpass123', newPassword: 'newpass456', }); assert.equal(status, 200); // Change back await req('PUT', '/auth/password', { currentPassword: 'newpass456', newPassword: 'testpass123' }); }); test('auth: logout destroys session', async () => { await req('POST', '/auth/logout'); const { body } = await req('GET', '/auth/me'); assert.equal(body.authenticated, false); // Log back in for subsequent tests await req('POST', '/auth/login', { username: 'admin', password: 'testpass123' }); }); // ── Config ──────────────────────────────────────────────────────────────────── test('config: GET /config requires session', async () => { const saved = sessionCookie; sessionCookie = ''; const { status } = await req('GET', '/config'); assert.equal(status, 401); sessionCookie = saved; }); test('config: GET /config returns config object', async () => { const { status, body } = await req('GET', '/config'); assert.equal(status, 200); assert.ok('device_ip' in body); assert.ok('poll_interval' in body); }); test('config: PUT /config updates values', async () => { const { status } = await req('PUT', '/config', { device_ip: '10.0.0.1', poll_interval: '60' }); assert.equal(status, 200); const { body } = await req('GET', '/config'); assert.equal(body.device_ip, '10.0.0.1'); assert.equal(body.poll_interval, '60'); }); // ── Stats ───────────────────────────────────────────────────────────────────── test('stats: GET /stats returns numeric counts', async () => { const { status, body } = await req('GET', '/stats'); assert.equal(status, 200); assert.ok(typeof body.sent === 'number'); assert.ok(typeof body.received === 'number'); assert.ok(typeof body.failed === 'number'); }); // ── API Tokens ───────────────────────────────────────────────────────────────── test('tokens: POST /tokens creates token', async () => { const { status, body } = await req('POST', '/tokens', { name: 'Test Token' }); assert.equal(status, 201); assert.ok(body.id); assert.equal(body.name, 'Test Token'); assert.ok(body.token.length >= 32); }); test('tokens: GET /tokens lists tokens', async () => { const { status, body } = await req('GET', '/tokens'); assert.equal(status, 200); assert.ok(Array.isArray(body)); assert.ok(body.length >= 1); }); test('tokens: PATCH /tokens/:id toggles active state', async () => { const { body: tok } = await req('POST', '/tokens', { name: 'Toggle Me' }); const { status } = await req('PATCH', `/tokens/${tok.id}`, { active: false }); assert.equal(status, 200); }); test('tokens: DELETE /tokens/:id removes token', async () => { const { body: tok } = await req('POST', '/tokens', { name: 'Delete Me' }); const { status } = await req('DELETE', `/tokens/${tok.id}`); assert.equal(status, 200); }); test('tokens: API token auth works on /sms/messages', async () => { const { body: tok } = await req('POST', '/tokens', { name: 'API Client' }); const saved = sessionCookie; sessionCookie = ''; const { status } = await req('GET', '/sms/messages', undefined, { Authorization: `Bearer ${tok.token}`, }); assert.equal(status, 200); sessionCookie = saved; }); test('tokens: invalid Bearer token returns 401', async () => { const saved = sessionCookie; sessionCookie = ''; const { status } = await req('GET', '/sms/messages', undefined, { Authorization: 'Bearer invalid-token-xyz', }); assert.equal(status, 401); sessionCookie = saved; }); // ── Webhooks ────────────────────────────────────────────────────────────────── test('webhooks: POST /webhooks creates webhook', async () => { const { status, body } = await req('POST', '/webhooks', { name: 'My Hook', url: 'https://example.com/hook', }); assert.equal(status, 201); assert.ok(body.id); assert.equal(body.name, 'My Hook'); }); test('webhooks: POST /webhooks requires name and url', async () => { const { status } = await req('POST', '/webhooks', { name: 'No URL' }); assert.equal(status, 400); }); test('webhooks: GET /webhooks lists webhooks', async () => { const { status, body } = await req('GET', '/webhooks'); assert.equal(status, 200); assert.ok(Array.isArray(body)); }); test('webhooks: PUT /webhooks/:id updates webhook', async () => { const { body: hook } = await req('POST', '/webhooks', { name: 'Original', url: 'https://a.com' }); const { status, body } = await req('PUT', `/webhooks/${hook.id}`, { name: 'Updated' }); assert.equal(status, 200); assert.equal(body.name, 'Updated'); }); test('webhooks: DELETE /webhooks/:id removes webhook', async () => { const { body: hook } = await req('POST', '/webhooks', { name: 'Gone', url: 'https://b.com' }); const { status } = await req('DELETE', `/webhooks/${hook.id}`); assert.equal(status, 200); }); // ── SMS Messages ────────────────────────────────────────────────────────────── test('sms: GET /sms/messages returns array', async () => { const { status, body } = await req('GET', '/sms/messages'); assert.equal(status, 200); assert.ok(Array.isArray(body)); }); test('sms: GET /sms/messages/received returns only received', async () => { const { body } = await req('GET', '/sms/messages/received'); assert.ok(Array.isArray(body)); assert.ok(body.every(m => m.direction === 'received')); }); test('sms: GET /sms/messages/sent returns only sent', async () => { const { body } = await req('GET', '/sms/messages/sent'); assert.ok(Array.isArray(body)); assert.ok(body.every(m => m.direction === 'sent')); }); test('sms: POST /sms/send with no device configured returns 502', async () => { // Device IP is empty – should fail gracefully const { status, body } = await req('POST', '/sms/send', { number: '+46701234567', message: 'Test message', }); assert.equal(status, 502); assert.ok(body.error); }); test('sms: POST /sms/send missing fields returns 400', async () => { const { status } = await req('POST', '/sms/send', { number: '+46701234567' }); assert.equal(status, 400); }); test('sms: POST /sms/send unauthenticated returns 401', async () => { const saved = sessionCookie; sessionCookie = ''; const { status } = await req('POST', '/sms/send', { number: '+1', message: 'hi' }); assert.equal(status, 401); sessionCookie = saved; });