import { test } from 'node:test'; import assert from 'node:assert/strict'; import { MAX_TEXT_BYTES, nextDraft, nextStatus, validateContextUrl } from '../src/core/drafts.ts'; import type { SaveDraftInput } from '../src/core/types.ts'; const input = (override: Partial = {}): SaveDraftInput => ({ id: 'test-draft', accountId: 'local', kind: 'post', title: '', text: 'Merhaba 🐟', contextUrl: '', expectedRevision: 0, source: 'local', ...override }); const now = '2026-09-27T00:00:00.000Z'; test('committed revisions preserve Unicode, empty text and context', () => { const first = nextDraft(undefined, input(), now); const next = nextDraft(first.draft, input({ expectedRevision: 1, text: '', contextUrl: 'https://x.com/user/status/123' }), now); assert.equal(first.revision.text, 'Merhaba 🐟'); assert.equal(next.revision.text, ''); assert.equal(next.draft.revision, 2); assert.equal(next.draft.createdAt, now); }); test('stale revision rejects instead of overwriting newer text', () => { const { draft } = nextDraft(undefined, input(), now); assert.throws(() => nextDraft(draft, input({ text: 'stale' }), now), /^Error: CONFLICT:/); assert.equal(draft.text, 'Merhaba 🐟'); }); test('account switch cannot transfer an existing draft', () => { const { draft } = nextDraft(undefined, input(), now); assert.throws(() => nextDraft(draft, input({ accountId: 'different', expectedRevision: 1 }), now), /^Error: ACCOUNT:/); }); test('archive is reversible and participates in revision checks', () => { const { draft } = nextDraft(undefined, input(), now); const archived = nextStatus(draft, 1, 'archived', now); assert.equal(archived.text, draft.text); assert.throws(() => nextStatus(archived, 1, 'active', now), /CONFLICT/); assert.equal(nextStatus(archived, 2, 'active', now).status, 'active'); }); test('context rejects credential tricks, alternate ports, insecure URLs and lookalike domains', () => { for (const url of ['javascript:alert(1)', 'https://x.com.evil.test/a', 'https://evil@x.com/a', 'http://x.com/a', 'https://x.com:9443/a']) assert.throws(() => validateContextUrl(url)); validateContextUrl('https://twitter.com/user/status/123'); }); test('UTF-8 byte limit applies before persistence and rejects invalid identity/revisions', () => { assert.throws(() => nextDraft(undefined, input({ text: '🐟'.repeat(MAX_TEXT_BYTES / 4 + 1) }), now), /256 KiB/); assert.throws(() => nextDraft(undefined, input({ id: '../other' }), now), /Invalid id/); assert.throws(() => nextDraft(undefined, input({ expectedRevision: NaN }), now), /Invalid revision/); });