import "fake-indexeddb/auto"; import { test } from "node:test"; import assert from "node:assert/strict"; import parse from "ltx/lib/parse.js"; import { ChatDB, type StoredMessage } from "../src/store/db.ts"; import { parseMessage } from "../src/xmpp/parse.ts"; import { composeReply, displayQuote, matchesReply, parseReply, replyBody, replyDraft } from "../src/xmpp/replies.ts"; const me = "me@example.org", peer = "you@example.org"; const el = (s: string) => parse(s) as any; const original: StoredMessage = { key: "original", peer, type: "chat", dir: "in", from: peer + "/phone", id: "sender-id", originId: "origin-id", body: "Original 🐱\nsecond line", ts: 1, encrypted: true }; test("reply wire uses origin IDs and Unicode codepoint fallback; unsupported clients retain one useful quote", () => { const target = replyDraft(original)!; const content = composeReply("Answer šŸŽ‰", target); const stanza = el(`${content.body}${content.elements.join("")}`); const parsed = parseMessage(stanza, { me })!; assert.equal(parsed.body, "Answer šŸŽ‰"); assert.equal(parsed.reply?.id, "origin-id"); assert.equal(parsed.reply?.quote, "you@example.org:\nOriginal 🐱\nsecond line"); assert.equal((content.body.match(/Original/g) ?? []).length, 1); assert.equal(content.elements[1].getChild("body")?.attrs.end, String(Array.from(content.body.slice(0, content.body.indexOf("Answer"))).length)); }); test("group replies require room-stamped IDs, distinct from the archive cursor", () => { const room = "room@conference.example.org"; const group = (extra: string, archiveId?: string) => parseMessage(el(`hello${extra}`), { me, archiveId })!; assert.equal(replyDraft(group("", "mam-cursor")), undefined); assert.equal(replyDraft(group(``)), undefined); assert.equal(replyDraft(group(``)), undefined); const valid = group(``, "mam-cursor"); assert.equal(replyDraft(valid)?.id, "room-id"); assert.equal(valid.archiveId, "mam-cursor"); }); test("malformed, unrelated and ambiguous reply metadata cannot hide text or choose another conversation", async () => { const xml = (metadata: string) => el(`${metadata}`); assert.equal(parseReply(xml(''), peer, me, false), undefined); assert.equal(parseReply(xml(''), peer, me, false), undefined); const reply = { id: "origin-id", to: peer }; for (const range of ['start="0" end="999"', 'start="0" end="3.5"', 'start="0"', 'start="3" end="4"', 'start="0" end="5"']) { assert.equal(replyBody(xml(``), "hello", reply).body, "hello"); } assert.equal(matchesReply(original, "elsewhere@example.org", reply), false); const db = await ChatDB.open("reply-isolation@example.org"); try { await db.putMessage(original, false); await db.putMessage({ ...original, key: "collision", from: me, dir: "out" }, false); assert.equal(await db.resolveReply(peer, { id: "origin-id" }), undefined); assert.equal((await db.resolveReply(peer, reply))?.key, original.key); assert.equal(await db.resolveReply("elsewhere@example.org", reply), undefined); } finally { db.close(); await ChatDB.destroy("reply-isolation@example.org"); } }); test("encrypted parsing defers fallback until real plaintext and refuses arbitrary ciphertext-metadata stripping", () => { const content = composeReply("Secret answer", replyDraft(original)!); const stanza = el(`This message is encrypted${content.elements.join("")}`); const parsed = parseMessage(stanza, { me })!; assert.equal(parsed.body, "This message is encrypted"); assert.equal(parsed.reply?.quote, undefined); assert.deepEqual(replyBody(stanza, content.body, parsed.reply), { body: "Secret answer", reply: content.reply }); const hostile = el(''); assert.equal(replyBody(hostile, "Secret answer", parsed.reply).body, "Secret answer"); assert.equal(replyDraft({ ...original, undecryptable: true }), undefined); }); test("reply drafts persist per chat through restart/cancel and survive competing send completion", async () => { const account = "reply-draft@example.org", target = replyDraft(original)!; let db = await ChatDB.open(account); try { await db.saveDraft(peer, "chat", { text: "", reply: target, revision: "quote-only" }); db.close(); db = await ChatDB.open(account); assert.deepEqual((await db.getDraft(peer))?.reply, target); assert.equal((await db.getConversations())[0].draft, "Reply draft"); assert.equal(await db.getDraft("elsewhere@example.org"), undefined); await db.saveDraft(peer, "chat", { text: "text while sending", reply: target, revision: "new" }); assert.equal(await db.clearSentDraft(peer, "quote-only"), false); assert.deepEqual((await db.getDraft(peer))?.reply, target); await db.saveDraft(peer, "chat", { text: "text while sending", revision: "cancel" }); assert.equal((await db.getDraft(peer))?.reply, undefined); assert.equal((await db.getDraft(peer))?.text, "text while sending"); } finally { db.close(); await ChatDB.destroy(account); } }); test("an edited original retains its reference; room reflection supplies the local echo's stable ID", async () => { const room = "room@conference.example.org", account = "reply-reflection@example.org"; const message: StoredMessage = { key: "room-original", peer: room, type: "groupchat", dir: "out", from: room + "/Me", body: "before edit", ts: 1, id: "sender-id" }; const db = await ChatDB.open(account); try { await db.putMessage(message, false); assert.equal(replyDraft(message), undefined); await db.putMessage({ ...message, stanzaId: "room-stable-id", archiveId: "cursor" }, false); const reflected = (await db.messagesByKeys([message.key]))[0]; const editedDraft = replyDraft(reflected, "after edit")!; assert.equal(editedDraft.id, "room-stable-id"); assert.equal(editedDraft.text, "after edit"); await db.putMessage({ ...message, key: "correction", id: "correction-id", replaceId: "sender-id", body: "after edit", ts: 2 }, false); assert.equal((await db.resolveReply(room, { id: "room-stable-id", to: message.from }))?.key, message.key); assert.equal(composeReply("yes", editedDraft).reply?.quote, "Me:\nafter edit"); } finally { db.close(); await ChatDB.destroy(account); } }); test("quoting an attachment names it; an encrypted one never carries its key into the quote", () => { const key = "76396656ed08305a82762013b3722d174cdaa876c450176cf4a5b7065eec5352adebe8c9a38a49e48e34242d"; const voice = `aesgcm://upload.0ff.space/file_share/01a11c4b/C8331F4B.m4a#${key}`; const original: StoredMessage = { key: "k", peer, type: "chat", dir: "in", from: peer + "/phone", id: "i", originId: "o", body: voice, ts: 1, encrypted: true, }; const target = replyDraft(original)!; assert.equal(target.text, "šŸŽ¤ Voice message"); const content = composeReply("Nice one", target); // The fragment is the file key; it has no business in a second message. assert.equal(content.body.includes(key), false); assert.equal(content.body.includes("aesgcm://"), false); assert.equal(content.reply?.quote?.includes(key), false); assert.ok(content.body.startsWith(`> ${peer}:\n> šŸŽ¤ Voice message\n\n`)); // A photo and a plaintext upload are named too, from their own shapes. const photo = { ...original, body: "aesgcm://upload.0ff.space/f/x.jpg#" + key }; assert.equal(replyDraft(photo)!.text, "šŸ“· Photo"); const plain = { ...original, body: "https://upload.0ff.space/f/notes.pdf", oob: "https://upload.0ff.space/f/notes.pdf", encrypted: undefined }; assert.equal(replyDraft(plain)!.text, "šŸ“Ž notes.pdf"); // Ordinary text, including a bare link somebody typed, is quoted as written. const text = { ...original, body: "look at https://example.org/thing", encrypted: undefined }; assert.equal(replyDraft(text)!.text, "look at https://example.org/thing"); }); test("a quote that already holds an aesgcm URL is named at render time, not shown", () => { const key = "a".repeat(64); // History written before quotedText existed, and anything a remote client sends. assert.equal(displayQuote(`${peer}:\naesgcm://upload.0ff.space/f/v.m4a#${key}`), `${peer}:\nšŸŽ¤ Voice message`); assert.equal(displayQuote(`aesgcm://upload.0ff.space/f/p.png#${key}`), "šŸ“· Photo"); // A plain link may just be a link; it stays as written. assert.equal(displayQuote("see https://example.org/x"), "see https://example.org/x"); assert.equal(displayQuote("https://example.org/x"), "https://example.org/x"); assert.equal(displayQuote("nothing special"), "nothing special"); });