/** * 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 stores and returns record', () => { const hook = db.createWebhook({ name: 'My Hook', url: 'https://example.com/hook' }); assert.ok(hook.id); assert.equal(hook.name, 'My Hook'); assert.equal(hook.url, 'https://example.com/hook'); assert.equal(hook.events, 'received'); assert.equal(hook.active, 1); assert.equal(hook.phone_number, null); }); test('webhooks: createWebhook with phone filter', () => { const hook = db.createWebhook({ name: 'Filtered Hook', url: 'https://example.com', phone_number: '+46701234567', }); assert.equal(hook.phone_number, '+46701234567'); }); test('webhooks: updateWebhook changes fields', () => { const hook = db.createWebhook({ name: 'Old Name', url: 'https://a.com' }); const updated = db.updateWebhook(hook.id, { name: 'New Name', events: 'both' }); assert.equal(updated.name, 'New Name'); assert.equal(updated.events, 'both'); }); test('webhooks: deleteWebhook removes it', () => { const hook = db.createWebhook({ name: 'Gone', url: 'https://gone.com' }); const before = db.listWebhooks().length; db.deleteWebhook(hook.id); assert.equal(db.listWebhooks().length, before - 1); }); test('webhooks: getActiveWebhooksForEvent returns matching hooks', () => { db.createWebhook({ name: 'Catch-all', url: 'https://all.com', events: 'received' }); const hooks = db.getActiveWebhooksForEvent('received', '+46700000000'); assert.ok(hooks.length >= 1); }); test('webhooks: getActiveWebhooksForEvent respects phone filter', () => { db.createWebhook({ name: 'Specific', url: 'https://specific.com', phone_number: '+46709999999', events: 'received', }); const matchHooks = db.getActiveWebhooksForEvent('received', '+46709999999'); const noMatch = db.getActiveWebhooksForEvent('received', '+46700000001'); // Specific hook should only appear for its number (catch-alls still included) const specificInMatch = matchHooks.some(h => h.name === 'Specific'); const specificInNone = noMatch.some(h => h.name === 'Specific'); assert.ok(specificInMatch); assert.ok(!specificInNone); }); test('webhooks: inactive webhooks not returned for events', () => { const hook = db.createWebhook({ name: 'Inactive', url: 'https://inactive.com', events: 'received' }); db.updateWebhook(hook.id, { active: 0 }); const hooks = db.getActiveWebhooksForEvent('received', '+0'); assert.ok(!hooks.some(h => h.id === hook.id)); }); // ── 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); });