sms-gateway/test/db.test.js
2026-03-04 19:54:03 +01:00

227 lines
8.8 KiB
JavaScript

/**
* Tests for the database layer (src/db.js).
* Uses an in-memory database so no files are created.
*/
const { test, before, after } = require('node:test');
const assert = require('node:assert/strict');
// Point to an in-memory DB for tests
process.env.DB_PATH = ':memory:';
process.env.ADMIN_USERNAME = 'testadmin';
process.env.ADMIN_PASSWORD = 'testpass';
const db = require('../src/db');
before(() => {
db.initDB();
});
// ── Config ────────────────────────────────────────────────────────────────────
test('config: getConfig returns null for missing key', () => {
assert.equal(db.getConfig('nonexistent'), null);
});
test('config: setConfig and getConfig round-trip', () => {
db.setConfig('test_key', 'hello');
assert.equal(db.getConfig('test_key'), 'hello');
});
test('config: setConfig overwrites existing value', () => {
db.setConfig('test_key', 'updated');
assert.equal(db.getConfig('test_key'), 'updated');
});
test('config: getAllConfig returns object with seeded keys', () => {
const cfg = db.getAllConfig();
assert.equal(typeof cfg, 'object');
assert.ok('device_ip' in cfg);
assert.ok('poll_interval' in cfg);
});
// ── Admins ────────────────────────────────────────────────────────────────────
test('admins: getAdmin returns seeded admin', () => {
const admin = db.getAdmin('testadmin');
assert.ok(admin);
assert.equal(admin.username, 'testadmin');
assert.ok(admin.password.startsWith('$2')); // bcrypt hash
});
test('admins: getAdmin returns null for unknown user', () => {
assert.equal(db.getAdmin('nobody'), null);
});
test('admins: updateAdminPassword changes the hash', () => {
db.updateAdminPassword('testadmin', 'newpass123');
const admin = db.getAdmin('testadmin');
const bcrypt = require('bcryptjs');
assert.ok(bcrypt.compareSync('newpass123', admin.password));
});
// ── API Tokens ─────────────────────────────────────────────────────────────────
test('tokens: createToken returns token record with token string', () => {
const tok = db.createToken('Test App');
assert.ok(tok.id);
assert.equal(tok.name, 'Test App');
assert.ok(tok.token.length >= 32);
assert.equal(tok.active, 1);
});
test('tokens: getTokenByValue returns active token', () => {
const tok = db.createToken('App2');
const found = db.getTokenByValue(tok.token);
assert.ok(found);
assert.equal(found.id, tok.id);
});
test('tokens: getTokenByValue returns null for unknown token', () => {
assert.equal(db.getTokenByValue('no-such-token'), null);
});
test('tokens: toggleToken deactivates token', () => {
const tok = db.createToken('Deactivate Me');
db.toggleToken(tok.id, false);
assert.equal(db.getTokenByValue(tok.token), null);
});
test('tokens: toggleToken reactivates token', () => {
const tok = db.createToken('Reactivate Me');
db.toggleToken(tok.id, false);
db.toggleToken(tok.id, true);
assert.ok(db.getTokenByValue(tok.token));
});
test('tokens: deleteToken removes it', () => {
const tok = db.createToken('Delete Me');
db.deleteToken(tok.id);
assert.equal(db.getTokenByValue(tok.token), null);
});
test('tokens: listTokens returns all tokens', () => {
const before = db.listTokens().length;
db.createToken('Listed');
assert.equal(db.listTokens().length, before + 1);
});
// ── Webhooks ──────────────────────────────────────────────────────────────────
test('webhooks: createWebhook generates token and stores record', () => {
const hook = db.createWebhook({ name: 'My Hook', phone_number: '+46701234567' });
assert.ok(hook.id);
assert.equal(hook.name, 'My Hook');
assert.equal(hook.phone_number, '+46701234567');
assert.ok(hook.token && hook.token.length > 0, 'token should be generated');
assert.equal(hook.message_template, '{{msg}}');
assert.equal(hook.active, 1);
});
test('webhooks: createWebhook with custom message_template', () => {
const hook = db.createWebhook({
name: 'Templated', phone_number: '+46709999999', message_template: 'Alert: {{monitor.name}} is {{heartbeat.status}}',
});
assert.equal(hook.message_template, 'Alert: {{monitor.name}} is {{heartbeat.status}}');
});
test('webhooks: getWebhookByToken finds the hook', () => {
const hook = db.createWebhook({ name: 'Findable', phone_number: '+1234567890' });
const found = db.getWebhookByToken(hook.token);
assert.ok(found);
assert.equal(found.id, hook.id);
});
test('webhooks: getWebhookByToken returns null for unknown token', () => {
assert.equal(db.getWebhookByToken('nonexistenttoken'), null);
});
test('webhooks: updateWebhook changes allowed fields', () => {
const hook = db.createWebhook({ name: 'Old Name', phone_number: '+1111111111' });
const updated = db.updateWebhook(hook.id, { name: 'New Name', message_template: '{{title}}' });
assert.equal(updated.name, 'New Name');
assert.equal(updated.message_template, '{{title}}');
});
test('webhooks: deleteWebhook removes it', () => {
const hook = db.createWebhook({ name: 'Gone', phone_number: '+2222222222' });
const before = db.listWebhooks().length;
db.deleteWebhook(hook.id);
assert.equal(db.listWebhooks().length, before - 1);
});
test('webhooks: inactive webhook not found by token lookup (active flag)', () => {
const hook = db.createWebhook({ name: 'Disabled', phone_number: '+3333333333' });
db.updateWebhook(hook.id, { active: 0 });
const found = db.getWebhookByToken(hook.token);
// getWebhookByToken returns regardless of active flag — receiver checks active
assert.ok(found);
assert.equal(found.active, 0);
});
// ── Messages ──────────────────────────────────────────────────────────────────
test('messages: saveMessage stores and returns record', () => {
const msg = db.saveMessage({
direction: 'received', phone_number: '+46701111111', message: 'Hello test', status: 'received',
});
assert.ok(msg.id);
assert.equal(msg.direction, 'received');
assert.equal(msg.message, 'Hello test');
});
test('messages: messageExistsByDeviceId detects duplicates', () => {
db.saveMessage({
direction: 'received', phone_number: '+1', message: 'dup', device_msg_id: 'DEV-42',
});
assert.ok(db.messageExistsByDeviceId('DEV-42'));
assert.ok(!db.messageExistsByDeviceId('DEV-99'));
});
test('messages: updateMessageStatus changes status', () => {
const msg = db.saveMessage({ direction: 'sent', phone_number: '+2', message: 'hi', status: 'pending' });
db.updateMessageStatus(msg.id, 'sent');
const rows = db.listMessages({ direction: 'sent' });
const found = rows.find(r => r.id === msg.id);
assert.equal(found.status, 'sent');
});
test('messages: listMessages filters by direction', () => {
db.saveMessage({ direction: 'received', phone_number: '+3', message: 'rx' });
db.saveMessage({ direction: 'sent', phone_number: '+3', message: 'tx' });
const rx = db.listMessages({ direction: 'received' });
const tx = db.listMessages({ direction: 'sent' });
assert.ok(rx.every(m => m.direction === 'received'));
assert.ok(tx.every(m => m.direction === 'sent'));
});
test('messages: listMessages filters by phone_number', () => {
const phone = '+46799988877';
db.saveMessage({ direction: 'received', phone_number: phone, message: 'hi' });
const rows = db.listMessages({ phone_number: phone });
assert.ok(rows.length >= 1);
assert.ok(rows.every(m => m.phone_number === phone));
});
test('messages: listMessages respects limit and offset', () => {
// Insert 5 messages with known phone
const phone = '+46700000099';
for (let i = 0; i < 5; i++) {
db.saveMessage({ direction: 'received', phone_number: phone, message: `msg ${i}` });
}
const page1 = db.listMessages({ phone_number: phone, limit: 3, offset: 0 });
const page2 = db.listMessages({ phone_number: phone, limit: 3, offset: 3 });
assert.equal(page1.length, 3);
assert.ok(page2.length >= 1 && page2.length <= 3);
// No overlap
const ids1 = new Set(page1.map(m => m.id));
assert.ok(page2.every(m => !ids1.has(m.id)));
});
test('messages: getMessageStats returns counts', () => {
const stats = db.getMessageStats();
assert.ok(typeof stats.sent === 'number');
assert.ok(typeof stats.received === 'number');
assert.ok(typeof stats.failed === 'number');
assert.ok(stats.sent >= 0 && stats.received >= 0 && stats.failed >= 0);
});