55 lines
3.8 KiB
JavaScript
55 lines
3.8 KiB
JavaScript
// Filbert, row 38 r2: does create()'s final-read fallback (the create answer plus the labels
|
|
// written, in the default bucket) digest the same as a read on Vikunja v2.7.0? Scratch container on
|
|
// 127.0.0.1 only. The password and token stay in memory; nothing here prints them.
|
|
// Usage: node vkcreate.mjs <origin> <candidate root>
|
|
import { randomBytes } from 'node:crypto';
|
|
const ORIGIN = process.argv[2];
|
|
if (!/^http:\/\/127\.0\.0\.1:\d+$/.test(ORIGIN)) throw new Error('loopback only');
|
|
const { taskFields, digest } = await import(process.argv[3] + '/packages/tasks/src/digest.mjs');
|
|
const { normal } = await import(process.argv[3] + '/packages/tasks/src/sync.mjs');
|
|
const BASE = ORIGIN + '/api/v2';
|
|
async function api(method, path, body, auth) {
|
|
const headers = { 'Content-Type': 'application/json' };
|
|
if (auth) headers.Authorization = `Bearer ${auth}`;
|
|
const r = await fetch(BASE + path, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) });
|
|
const t = await r.text();
|
|
let json = null;
|
|
try { json = JSON.parse(t); } catch {}
|
|
return { status: r.status, json };
|
|
}
|
|
const must = (r, l) => { if (r.status >= 300) throw new Error(`${l}: ${r.status} ${JSON.stringify(r.json)}`); return r.json; };
|
|
const user = 'fp' + randomBytes(4).toString('hex');
|
|
const password = randomBytes(18).toString('base64url');
|
|
must(await api('POST', '/register', { username: user, email: user + '@example.invalid', password }), 'register');
|
|
const O = must(await api('POST', '/login', { username: user, password }), 'login').token;
|
|
const P = must(await api('POST', '/projects', { title: 'probe' }, O), 'project').id;
|
|
const views = must(await api('GET', `/projects/${P}/views`, undefined, O), 'views'); const vl = views.items ?? views;
|
|
const kanban = vl.find((v) => v.view_kind === 'kanban');
|
|
console.log('kanban view', kanban.id, 'default_bucket_id', kanban.default_bucket_id, 'done_bucket_id', kanban.done_bucket_id);
|
|
const L1 = must(await api('POST', '/labels', { title: 'b' }, O), 'label').id;
|
|
const L2 = must(await api('POST', '/labels', { title: 'a' }, O), 'label').id;
|
|
const other = must(await api('POST', `/projects/${P}/tasks`, { title: 'other' }, O), 'other').id;
|
|
const cases = [
|
|
{ title: 'plain' },
|
|
{ title: 'full', description: '<p>d</p>', due_date: '2026-11-01T10:20:30.000Z', priority: 3 },
|
|
{ title: 'labels+relation', due_date: '2026-11-01T10:20:30.000Z', labels: [L1, L2], relation: true },
|
|
];
|
|
for (const c of cases) {
|
|
const { labels = [], relation, ...body } = c;
|
|
const r = await api('POST', `/projects/${P}/tasks`, body, O);
|
|
must(r, 'create');
|
|
for (const l of [...labels].reverse()) must(await api('POST', `/tasks/${r.json.id}/labels`, { label_id: l }, O), 'label');
|
|
if (relation) must(await api('POST', `/tasks/${r.json.id}/relations`, { other_task_id: other, relation_kind: 'related' }, O), 'relation');
|
|
const g = must(await api('GET', `/tasks/${r.json.id}`, undefined, O), 'read');
|
|
const B = kanban.default_bucket_id;
|
|
const fb = { ...taskFields(r.json, B), labels: [...labels].sort((a, b) => a - b) };
|
|
const rd = taskFields(g, B);
|
|
const diff = Object.keys(rd).filter((k) => JSON.stringify(rd[k]) !== JSON.stringify(fb[k]));
|
|
console.log(c.title.padEnd(16), 'digest equal', digest(fb) === digest(rd), 'differing', JSON.stringify(diff),
|
|
'updated answer', normal(r.json.updated), 'read', normal(g.updated), 'answer raw', JSON.stringify(r.json.updated), 'read raw', JSON.stringify(g.updated));
|
|
if (diff.length) console.log(' fallback', JSON.stringify(diff.map((k) => fb[k])), 'read', JSON.stringify(diff.map((k) => rd[k])));
|
|
const bt = must(await api('GET', `/projects/${P}/views/${kanban.id}/buckets/tasks`, undefined, O), 'buckets');
|
|
const where = (bt.items ?? bt).find((b) => (b.tasks ?? []).some((t) => t.id === r.json.id));
|
|
console.log(' placed in bucket', where?.id, 'default', B);
|
|
}
|