import test from 'node:test'; import assert from 'node:assert/strict'; import { world, SCOPES } from './world.mjs'; import { MISSING, parseVersion } from '../src/startup.mjs'; const started = async (t, edit) => { const w = await world(t, edit); await w.adapter.start('demo'); return w; }; const state = (w) => w.adapter.status()[0]; test('a correct install starts, and the first reconcile records tasks that already exist', async (t) => { const w = await world(t); await w.ui('POST', `/projects/${w.project}/tasks`, { title: 'made in the UI' }); await w.adapter.start('demo'); assert.equal(state(w).state, 'ready'); assert.equal(state(w).untested, false); const s = w.snapshots(`vikunja:${w.project}/1`); assert.equal(s.length, 1); assert.equal(s[0].via, 'reconcile'); assert.equal(s[0].fields.bucket, w.installed.buckets.todo); // The probes: missing-task reads and write probes, all on the fixed missing id. const probes = w.fake.requests.filter((r) => r.path.includes(String(MISSING))); assert.deepEqual( probes.map((r) => `${r.method} ${r.status}`), ['PATCH 401', 'GET 404', 'DELETE 401', 'GET 404', 'DELETE 401', 'POST 401', 'GET 404', 'DELETE 401', 'POST 401'], ); }); test('verbs refuse while a business is starting and after startup refused it', async (t) => { const w = await world(t, { scopes: { sync: '*' } }); const pm = w.agent('pm'); await assert.rejects(w.call(pm, 'task.close', { task_ref: 'vikunja:1/1', verdict: 'v' }), /tracker-starting/); await w.adapter.start('demo'); assert.equal(state(w).refused, 'scope-too-broad'); await assert.rejects(w.call(pm, 'task.close', { task_ref: 'vikunja:1/1', verdict: 'v' }), /scope-too-broad/); assert.deepEqual(w.log, ['tasks demo startup scope-too-broad']); // The refusal is evidence too. assert.equal(w.events('action.refused').at(-1).body.code, 'scope-too-broad'); }); test('startup refuses a token that can do more than its role needs', async (t) => { for (const [scopes, code] of [ [{ sync: { ...SCOPES.sync, tasks: ['read_all', 'read_one', 'update'] } }, 'scope-too-broad'], [{ pm: { ...SCOPES.pm, tasks: ['read_one', 'create', 'update', 'delete'] } }, 'scope-too-broad'], [{ coder: { ...SCOPES.worker, tasks_labels: ['create'] } }, 'scope-too-broad'], [{ coder: { ...SCOPES.worker, tasks: ['update'] } }, 'tracker-unauthorized'], ]) { const w = await started(t, { scopes }); assert.equal(state(w).refused, code, JSON.stringify(scopes)); } }); test('startup refuses an unsupported version and flags an untested one', async (t) => { assert.equal(state(await started(t, { fake: { version: 'v1.0.0' } })).refused, 'tracker-version'); assert.equal(state(await started(t, { fake: { version: 'v2.3.9' } })).refused, 'tracker-version'); const w = await started(t, { fake: { version: 'v2.8.1' } }); assert.equal(state(w).state, 'ready'); assert.equal(state(w).untested, true); assert.deepEqual(w.log, ['tasks demo startup untested-version']); assert.deepEqual(parseVersion('v2.7.0-rc1'), [2, 7, 0]); assert.equal(parseVersion('unstable'), null); }); test('startup refuses a board that the runbook did not install', async (t) => { assert.equal(state(await started(t, { install: false })).refused, 'tracker-install'); const extra = await world(t); extra.fake.addView(extra.project, 'kanban'); await extra.adapter.start('demo'); assert.equal(state(extra).refused, 'tracker-install'); const renamed = await world(t); renamed.fake.rename(renamed.installed.buckets.blocked, 'on-hold'); await renamed.adapter.start('demo'); assert.equal(state(renamed).refused, 'tracker-install'); const wiring = await world(t); wiring.fake.view(wiring.installed.view).done_bucket_id = wiring.installed.buckets['in-review']; await wiring.adapter.start('demo'); assert.equal(state(wiring).refused, 'tracker-install'); }); test('startup refuses a project the sync bot cannot read', async (t) => { const w = await started(t, { tracker: { project: 99 } }); assert.equal(state(w).refused, 'tracker-project'); }); test('startup refuses a configured label the pm bot cannot see', async (t) => { const w = await started(t, { business: (b, { fake }) => { const stranger = fake.user('someone'); b.tracker.labels.private = fake.label('private', stranger); }, }); assert.equal(state(w).refused, 'tracker-labels'); }); test('startup refuses an expired credential and a missing sync credential', async (t) => { assert.equal(state(await started(t, { expires: { coder: '2001-01-01' } })).refused, 'credential-expired'); const w = await started(t, { business: (b) => delete b.tracker.sync.credentials }); assert.equal(state(w).refused, 'credential-unavailable'); }); test('an unreachable tracker refuses with tracker-unavailable', async (t) => { const w = await started(t, { fetch: async () => { throw new TypeError('fetch failed'); }, }); assert.equal(state(w).refused, 'tracker-unavailable'); });