sms-gateway/test/api.test.js
2026-03-04 19:30:01 +01:00

280 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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;
});