monal-rebuild/test/parse.test.ts
pijukka 411c3b32ab Milestone 4: rooms
- Bookmarks (XEP-0402) fetched on every sync; autojoin rooms are joined
  (no MUC history requested) and caught up from the room's MAM archive
  (newest 200 on first join, then from the last archive id). Joining a
  room from "+" bookmarks it with autojoin.
- Join handling: own presence (110) confirms, server nick changes (210)
  are adopted, nick conflicts retry with a suffix, join errors and
  kicks/bans show in the header; rooms re-join after reconnect.
- Own room messages recognised by nick and marked delivered on
  reflection; mentions of our nick highlighted and flagged in the list;
  "nick: message" previews; room names from bookmarks.
- Private messages through a room get their own conversation (full
  JID), fixing the gap noted in milestone 2.
- Rooms get no typing states, no contact actions; the composer waits for
  the join.
- 3 more parser tests (11).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:59:18 +02:00

105 lines
6.2 KiB
TypeScript

// Run: npm test (Node strips the TypeScript types itself)
import { test } from "node:test";
import assert from "node:assert/strict";
import parse from "ltx/lib/parse.js";
import { parseAck, parseMessage, parseOwnDisplayed } from "../src/xmpp/parse.ts";
const me = "lukas@0ff.space";
const el = (s: string) => parse(s) as any;
test("live incoming message", () => {
const m = parseMessage(el(`<message from="anna@0ff.space/phone" to="lukas@0ff.space/web" type="chat" id="m1">
<body>hi</body><origin-id xmlns="urn:xmpp:sid:0" id="o1"/>
<stanza-id xmlns="urn:xmpp:sid:0" by="lukas@0ff.space" id="A1"/>
<request xmlns="urn:xmpp:receipts"/><markable xmlns="urn:xmpp:chat-markers:0"/></message>`), { me })!;
assert.equal(m.peer, "anna@0ff.space");
assert.equal(m.dir, "in");
assert.equal(m.key, "anna@0ff.space|anna@0ff.space|o1");
assert.equal(m.archiveId, "A1");
assert.equal(m.receiptRequested, true);
assert.equal(m.markable, true);
});
test("stanza-id stamped by someone else is ignored", () => {
const m = parseMessage(el(`<message from="anna@0ff.space/phone" type="chat" id="m1"><body>x</body>
<stanza-id xmlns="urn:xmpp:sid:0" by="anna@0ff.space" id="FORGED"/></message>`), { me })!;
assert.equal(m.archiveId, undefined);
});
test("sent carbon and MAM copy of the same message share one key", () => {
const inner = `<message from="lukas@0ff.space/monal" to="anna@0ff.space" type="chat" id="m2"><body>yo</body></message>`;
const fwd = el(`<forwarded xmlns="urn:xmpp:forward:0"><delay xmlns="urn:xmpp:delay" stamp="2026-09-28T10:00:00Z"/>${inner}</forwarded>`);
const carbon = parseMessage(el(inner), { me })!;
const mam = parseMessage(fwd.getChild("message"), { me, archiveId: "A2", forwarded: fwd })!;
assert.equal(carbon.dir, "out");
assert.equal(carbon.peer, "anna@0ff.space");
assert.equal(carbon.key, mam.key);
assert.equal(mam.archiveId, "A2");
assert.equal(mam.ts, Date.parse("2026-09-28T10:00:00Z"));
assert.equal(mam.status, "sent");
});
test("groupchat message keys by room/nick and trusts the room's stanza-id", () => {
const m = parseMessage(el(`<message from="lobby@muc.0ff.space/anna" type="groupchat" id="g1"><body>hey</body>
<stanza-id xmlns="urn:xmpp:sid:0" by="lobby@muc.0ff.space" id="R1"/></message>`), { me })!;
assert.equal(m.peer, "lobby@muc.0ff.space");
assert.equal(m.key, "lobby@muc.0ff.space|lobby@muc.0ff.space/anna|g1");
assert.equal(m.archiveId, "R1");
});
test("non-messages are skipped", () => {
assert.equal(parseMessage(el(`<message from="anna@0ff.space" type="chat"><composing xmlns="http://jabber.org/protocol/chatstates"/></message>`), { me }), null);
assert.equal(parseMessage(el(`<message from="anna@0ff.space" type="error" id="x"><body>b</body></message>`), { me }), null);
});
test("receipts and markers point at our outgoing message", () => {
assert.deepEqual(parseAck(el(`<message from="anna@0ff.space/phone"><received xmlns="urn:xmpp:receipts" id="m2"/></message>`), me),
{ key: "anna@0ff.space|lukas@0ff.space|m2", status: "delivered" });
assert.deepEqual(parseAck(el(`<message from="anna@0ff.space/phone" type="chat"><displayed xmlns="urn:xmpp:chat-markers:0" id="m2"/></message>`), me),
{ key: "anna@0ff.space|lukas@0ff.space|m2", status: "read" });
// Our own receipt echoed as a carbon is not about our messages.
assert.equal(parseAck(el(`<message from="lukas@0ff.space/monal" to="anna@0ff.space"><received xmlns="urn:xmpp:receipts" id="m1"/></message>`), me), null);
});
test("our own displayed marker from another device marks the peer's message read", () => {
// Inner message of a sent carbon: Monal told anna it displayed her message m1.
assert.deepEqual(parseOwnDisplayed(el(`<message from="lukas@0ff.space/monal" to="anna@0ff.space" type="chat"><displayed xmlns="urn:xmpp:chat-markers:0" id="m1"/></message>`), me),
{ peer: "anna@0ff.space", key: "anna@0ff.space|anna@0ff.space|m1" });
// Anna's marker is not ours.
assert.equal(parseOwnDisplayed(el(`<message from="anna@0ff.space/p" to="lukas@0ff.space" type="chat"><displayed xmlns="urn:xmpp:chat-markers:0" id="m2"/></message>`), me), null);
});
test("id-less messages get a stable key, so redelivery does not duplicate", () => {
const motd = `<message from="0ff.space" to="lukas@0ff.space/web"><body>Welcome!</body></message>`;
const a = parseMessage(el(motd), { me })!;
const b = parseMessage(el(motd), { me })!;
assert.equal(a.key, b.key);
assert.equal(a.peer, "0ff.space");
});
test("our own room message is recognised by nick and counts as delivered", () => {
const m = parseMessage(el(`<message from="lobby@muc.0ff.space/lukas" type="groupchat" id="g2"><body>hi all</body></message>`), { me, nick: "lukas" })!;
assert.equal(m.dir, "out");
assert.equal(m.status, "delivered");
// Same key the client stores when sending as lukas in that room.
assert.equal(m.key, "lobby@muc.0ff.space|lobby@muc.0ff.space/lukas|g2");
});
test("mentions of our nick in rooms, whole word, any case", () => {
const msg = (body: string) => parseMessage(el(`<message from="lobby@muc.0ff.space/anna" type="groupchat" id="x"><body>${body}</body></message>`), { me, nick: "lukas" })!;
assert.equal(msg("@Lukas can you check?").mention, true);
assert.equal(msg("lukas: ping").mention, true);
assert.equal(msg("lukasz is here").mention, undefined);
assert.equal(parseMessage(el(`<message from="lobby@muc.0ff.space/lukas" type="groupchat" id="y"><body>lukas here</body></message>`), { me, nick: "lukas" })!.mention, undefined);
});
test("private messages through a room are their own conversation", () => {
const isRoom = (j: string) => j.endsWith("@muc.0ff.space");
const incoming = parseMessage(el(`<message from="lobby@muc.0ff.space/anna" to="lukas@0ff.space/web" type="chat" id="p1"><body>psst</body></message>`), { me, isRoom })!;
assert.equal(incoming.peer, "lobby@muc.0ff.space/anna");
assert.equal(incoming.dir, "in");
// Our reply as a carbon from another device lands in the same conversation.
const out = parseMessage(el(`<message from="lukas@0ff.space/monal" to="lobby@muc.0ff.space/anna" type="chat" id="p2"><body>yes?</body></message>`), { me, isRoom })!;
assert.equal(out.peer, "lobby@muc.0ff.space/anna");
assert.equal(out.key, "lobby@muc.0ff.space/anna|lukas@0ff.space|p2");
});