234 lines
9 KiB
JavaScript
234 lines
9 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 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);
|
|
});
|