110 lines
6.1 KiB
TypeScript
110 lines
6.1 KiB
TypeScript
import { EventEmitter } from "node:events";
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { xml, type Client, type Element } from "@xmpp/client";
|
|
import type { ChatDB } from "../src/store/db.ts";
|
|
import { Muc, accountIdentity, type RoomState } from "../src/sync/muc.ts";
|
|
|
|
class FakeClient extends EventEmitter {
|
|
sent: Element[] = [];
|
|
|
|
async send(stanza: Element): Promise<void> {
|
|
this.sent.push(stanza);
|
|
}
|
|
}
|
|
|
|
test("mediated invite has an id, does not edit affiliation, and surfaces a matching room error", async () => {
|
|
const client = new FakeClient();
|
|
const muc = new Muc(client as unknown as Client, {} as ChatDB, "me@0ff.space", "muc.0ff.space", { room() {} });
|
|
|
|
const invited = muc.invite("test@muc.0ff.space", "friend@example.org");
|
|
await Promise.resolve();
|
|
|
|
assert.equal(client.sent.length, 1);
|
|
const stanza = client.sent[0];
|
|
assert.equal(stanza.name, "message");
|
|
assert.equal(stanza.attrs.to, "test@muc.0ff.space");
|
|
assert.ok(stanza.attrs.id);
|
|
assert.equal(stanza.getChild("query", "http://jabber.org/protocol/muc#admin"), undefined);
|
|
assert.equal(stanza.getChild("x", "http://jabber.org/protocol/muc#user")?.getChild("invite")?.attrs.to, "friend@example.org");
|
|
|
|
client.emit(
|
|
"stanza",
|
|
xml(
|
|
"message",
|
|
{ from: "test@muc.0ff.space", id: stanza.attrs.id, type: "error" },
|
|
xml("error", { type: "auth" }, xml("forbidden", { xmlns: "urn:ietf:params:xml:ns:xmpp-stanzas" })),
|
|
),
|
|
);
|
|
await assert.rejects(invited, /not allowed to invite/i);
|
|
});
|
|
|
|
test("a disconnect while inviting rejects instead of silently counting as sent", async () => {
|
|
const client = new FakeClient();
|
|
const muc = new Muc(client as unknown as Client, {} as ChatDB, "me@0ff.space", "muc.0ff.space", { room() {} });
|
|
|
|
const invited = muc.invite("test@muc.0ff.space", "friend@example.org");
|
|
await Promise.resolve();
|
|
muc.reset();
|
|
await assert.rejects(invited, /disconnected/);
|
|
});
|
|
|
|
const room = 'test@muc.0ff.space';
|
|
const presence = (nick: string, attrs: Record<string,string> = {}, codes: string[] = [], type?: string) =>
|
|
xml('presence',{from:room+'/'+nick,...(type?{type}:{})},xml('x',{xmlns:'http://jabber.org/protocol/muc#user'},xml('item',attrs),...codes.map(code=>xml('status',{code}))));
|
|
|
|
test('a transport closing during join rejects promptly without an unhandled send',async()=>{
|
|
const client = new FakeClient();
|
|
client.send=async()=>{throw Error('Connection is closing');};
|
|
const muc=new Muc(client as unknown as Client,{} as ChatDB,'me@0ff.space','muc.0ff.space',{room(){}});
|
|
await assert.rejects(muc.join(room,'Me'),/Connection closed while joining/);
|
|
});
|
|
|
|
test('participant records track roles, disclosed identities, changes, rename and reconnect without counting unknown presence',async()=>{
|
|
const client = new FakeClient();
|
|
let state: RoomState | undefined;
|
|
const muc = new Muc(client as unknown as Client,{} as ChatDB,'me@0ff.space','muc.0ff.space',{room(_jid,s){state=s;}});
|
|
const joining=muc.join(room,'Me');
|
|
client.emit('stanza',presence('Me',{role:'moderator',affiliation:'owner',jid:'me@0ff.space/web'},['110']));
|
|
await joining;
|
|
client.emit('stanza',presence('Peer',{role:'participant',affiliation:'member',jid:'peer@0ff.space/mobile'}));
|
|
assert.equal(state!.occupants,2);assert.equal(state!.participants[1].jid,'peer@0ff.space');
|
|
client.emit('stanza',presence('Peer',{role:'moderator',affiliation:'admin'}));
|
|
assert.equal(state!.participants[1].role,'moderator');
|
|
assert.equal(state!.participants[1].jid,undefined); // no stale identity after disclosure changes
|
|
client.emit('stanza',xml('presence',{from:room+'/unknown'}));
|
|
assert.equal(state!.occupants,2);
|
|
client.emit('stanza',presence('Peer',{nick:'New nick',role:'moderator',affiliation:'admin'},['303'],'unavailable'));
|
|
assert.deepEqual(state!.participants.map(p=>p.nick),['Me','New nick']);
|
|
client.emit('stanza',presence('New nick',{role:'participant',affiliation:'member'}));
|
|
assert.equal(state!.occupants,2);
|
|
client.emit('stanza',presence('Me',{nick:'Me too',role:'moderator',affiliation:'owner'},['110','303'],'unavailable'));
|
|
assert.equal(state!.nick,'Me too');assert.equal(state!.joined,true);
|
|
client.emit('stanza',presence('New nick',{},[],'unavailable'));assert.equal(state!.occupants,1);
|
|
muc.reset();assert.equal(state!.occupants,0);assert.deepEqual(state!.participants,[]);assert.equal(state!.joined,false);
|
|
const again=muc.join(room,'Me too');client.emit('stanza',presence('Me too',{role:'moderator',affiliation:'owner'},['110']));await again;
|
|
client.emit('stanza',presence('Me too',{},['110','307'],'unavailable'));
|
|
assert.equal(state!.joined,false);assert.deepEqual(state!.participants,[]);
|
|
});
|
|
|
|
test('all three affiliation lists are requested separately; forbidden, malformed and empty differ',async()=>{
|
|
const client = new FakeClient() as FakeClient & {iqCaller:Client['iqCaller']};
|
|
const requests:string[]=[];
|
|
client.iqCaller={get:async(query,to)=>{
|
|
assert.equal(to,room);assert.equal(query.attrs.xmlns,'http://jabber.org/protocol/muc#admin');
|
|
const affiliation=query.getChild('item')!.attrs.affiliation;requests.push(affiliation);
|
|
if(affiliation==='admin')throw {condition:'forbidden',message:'denied'};
|
|
return xml('query',{xmlns:'http://jabber.org/protocol/muc#admin'},
|
|
...(affiliation==='member'?[xml('item',{affiliation,jid:'absent@0ff.space/phone'}),xml('item',{affiliation,jid:'absent@0ff.space'})]:[]));
|
|
},set:async()=>undefined,request:async()=>xml('iq')};
|
|
const muc=new Muc(client,{} as ChatDB,'me@0ff.space','muc.0ff.space',{room(){}});
|
|
const lists=await muc.memberLists(room);
|
|
assert.deepEqual(requests,['member','admin','owner']);
|
|
assert.deepEqual(lists.map(l=>l.state),['ready','forbidden','ready']);
|
|
assert.deepEqual(lists[0].members,[{jid:'absent@0ff.space',nick:undefined,affiliation:'member'}]);
|
|
assert.deepEqual(lists[2].members,[]);
|
|
client.iqCaller.get=async()=>xml('query',{xmlns:'http://jabber.org/protocol/muc#admin'},xml('item',{jid:'bad'}));
|
|
assert.ok((await muc.memberLists(room)).every(l=>l.state==='error'));
|
|
assert.equal(accountIdentity('room-nickname'),undefined);
|
|
assert.equal(accountIdentity('peer@example.org/Resource'),'peer@example.org');
|
|
});
|