Files
hub/tools/automations-test.js
T

37 lines
5.0 KiB
JavaScript

// Automations: rules are checked when saved, run when their trigger happens (and only then), keep to a daily cap, and belong to one person.
const assert = require('assert'), fs = require('fs'), os = require('os'), path = require('path'), http = require('http');
process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'nehub-auto-'));
const users = require('../users'), auto = require('../automations');
const ann = users.create({ name: 'Ann', role: 'admin', handle: 'YQ' }), bo = users.create({ name: 'Bo', role: 'user', tabs: ['tasks'], handle: 'ZP' });
const tasksOf = u => { try { return JSON.parse(fs.readFileSync(path.join(users.dirOf(u.id), 'tasks.json'), 'utf8')).tasks; } catch { return []; } };
const hub = http.createServer((req, res) => users.runAs(req.headers['x-user'] === 'bo' ? bo : ann, () => auto.handle(req, res))).listen(0, async () => {
const base = 'http://127.0.0.1:' + hub.address().port + '/api/auto', j = r => r.json(), post = (p, o, who) => fetch(base + p, { method: 'POST', body: JSON.stringify(o), headers: who ? { 'x-user': who } : {} }), get = who => fetch(base + '/', { headers: who ? { 'x-user': who } : {} });
try {
// validation
assert.equal((await post('/', { when: { type: 'nope' }, then: { type: 'task', text: 'x' } })).status, 400); assert.equal((await post('/', { when: { type: 'time', at: '25:00' }, then: { type: 'task', text: 'x' } })).status, 400, 'a real time of day');
assert.equal((await post('/', { when: { type: 'mail.new', match: '' }, then: { type: 'notify', text: 'x' } })).status, 400, 'mail rules need something to match, so it is not every email');
assert.equal((await post('/', { when: { type: 'time', at: '08:00' }, then: { type: 'task', text: ' ' } })).status, 400, 'and something to do');
// a daily rule runs once a day, at its time
let r = await j(await post('/', { when: { type: 'time', at: '08:00' }, then: { type: 'task', text: 'Water the plants' } })); assert.equal(r.rules.length, 1); const daily = r.rules[0];
await auto.tickUser(ann, { day: '2026-11-02', hm: '07:59' }); assert.equal(tasksOf(ann).length, 0, 'not before its time');
await auto.tickUser(ann, { day: '2026-11-02', hm: '08:00' }); assert.deepEqual(tasksOf(ann).map(t => t.title), ['Water the plants']);
await auto.tickUser(ann, { day: '2026-11-02', hm: '08:01' }); assert.equal(tasksOf(ann).length, 1, 'once a day'); await auto.tickUser(ann, { day: '2026-11-03', hm: '08:00' }); assert.equal(tasksOf(ann).length, 2, 'and again tomorrow');
await auto.tickUser(ann, { day: '2026-11-04', hm: '15:00' }); assert.equal(tasksOf(ann).length, 2, 'a time that passed hours ago is not caught up');
// a finished task starts matching rules, with the title filled in; the shopping action goes to the Shopping list
await post('/', { when: { type: 'task.done', match: 'milk' }, then: { type: 'task', text: 'Buy more after {title}' } }); await post('/', { when: { type: 'task.done', match: '' }, then: { type: 'notify', text: 'Nice, {title} is done' }, name: 'Cheer' });
await auto.fire(ann.id, 'task.done', { title: 'Buy bread' }); assert.ok(!tasksOf(ann).some(t => /Buy more/.test(t.title)), 'a rule that does not match does nothing');
await auto.fire(ann.id, 'task.done', { title: 'Buy milk' }); assert.ok(tasksOf(ann).some(t => t.title === 'Buy more after Buy milk'));
const inbox = require('../inbox').load(ann.id).filter(x => x.title === 'Cheer'); assert.ok(inbox.length >= 1 && /Nice, Buy (bread|milk) is done/.test(inbox[0].body), 'notify lands in the Notifications list');
// mail
await post('/', { when: { type: 'mail.new', match: 'invoice' }, then: { type: 'shopping', text: 'Pay {subject}' } }); await auto.fire(ann.id, 'mail.new', { from: 'Bob', subject: 'Lunch?' }); assert.ok(!tasksOf(ann).some(t => /^Pay /.test(t.title)));
await auto.fire(ann.id, 'mail.new', { from: 'Acme', subject: 'Your invoice 42' }); assert.ok(tasksOf(ann).some(t => t.title === 'Pay Your invoice 42'));
// off, private, capped
r = await j(await get()); assert.equal(r.rules.length, 4); assert.equal((await j(await get('bo'))).rules.length, 0, 'rules are personal'); assert.equal((await post('/delete', { id: daily.id }, 'bo')).status, 404, 'and cannot be touched by someone else');
const cheer = r.rules.find(x => x.name === 'Cheer'); await post('/toggle', { id: cheer.id, on: false }); const n0 = tasksOf(ann).length; await auto.fire(ann.id, 'task.done', { title: 'x' }); assert.equal(tasksOf(ann).length, n0, 'a rule that is off does not run');
await post('/', { when: { type: 'task.done', match: 'spam' }, then: { type: 'task', text: 'Again' } }); for (let i = 0; i < 30; i++) await auto.fire(ann.id, 'task.done', { title: 'spam' }); assert.equal(tasksOf(ann).filter(t => t.title === 'Again').length, 20, 'a rule runs at most 20 times a day');
assert.equal((await j(await post('/delete', { id: daily.id }))).rules.some(x => x.id === daily.id), false);
console.log('automations test ok');
} catch (e) { console.error(e); process.exitCode = 1; }
hub.close();
});