280 lines
10 KiB
JavaScript
280 lines
10 KiB
JavaScript
/**
|
||
* 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;
|
||
});
|