41 lines
2.6 KiB
TypeScript
41 lines
2.6 KiB
TypeScript
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> = {}): 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/);
|
|
});
|