43 lines
6.4 KiB
JavaScript
43 lines
6.4 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');
|
|
// the pantry going low, and new podcast episodes (for everyone's rules; the first look only notes what is there)
|
|
assert.equal((await post('/', { when: { type: 'podcast.new', match: '' }, then: { type: 'notify', text: 'x' } })).status, 400, 'a show must be named');
|
|
await post('/', { when: { type: 'pantry.low', match: 'flour' }, then: { type: 'shopping', text: 'Buy {title}' } }); await post('/', { when: { type: 'podcast.new', match: 'daily' }, then: { type: 'task', text: 'Listen: {title} ({show})' } });
|
|
const n1 = tasksOf(ann).length; await auto.fireAll('pantry.low', { title: 'Milk' }); assert.equal(tasksOf(ann).length, n1, 'another item does not match'); await auto.fireAll('pantry.low', { title: 'Flour' }); assert.ok(tasksOf(ann).some(t => t.title === 'Buy Flour'), 'a low item starts the rule');
|
|
let eps = [{ id: '1', title: 'Old', show: 'The Daily' }]; const recent = async () => eps; assert.equal(await auto.checkEpisodes(recent), 0, 'the first look only notes what is there'); assert.ok(!tasksOf(ann).some(t => /^Listen/.test(t.title)));
|
|
eps = [{ id: '2', title: 'Big news', show: 'The Daily' }, { id: '3', title: 'Other', show: 'Cooking Hour' }, ...eps]; assert.equal(await auto.checkEpisodes(recent), 2); assert.deepEqual(tasksOf(ann).filter(t => /^Listen/.test(t.title)).map(t => t.title), ['Listen: Big news (The Daily)'], 'only the show asked for'); assert.equal(await auto.checkEpisodes(recent), 0, 'and not again');
|
|
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();
|
|
});
|