waybetter-for-x/apps/desktop/tests/drafts.test.ts
2026-10-01 06:01:13 +07:00

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/);
});