| author | Eric Roberts
<eric@devl.me> 2026-08-25 14:36:46 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2026-08-26 13:25:55 UTC |
| parent | 51690545210e6f55693cbd6ebf8d65728fb07969 |
| borogove/persistence/IDB.js | +1 | -0 |
| test/browser-test.ts | +89 | -0 |
| test/idb.spec.ts | +101 | -1348 |
| test/persistence-tests.ts | +1466 | -0 |
| test/sqlite.spec.ts | +2 | -1668 |
diff --git a/borogove/persistence/IDB.js b/borogove/persistence/IDB.js index 6277596..9bdedb4 100644 --- a/borogove/persistence/IDB.js +++ b/borogove/persistence/IDB.js @@ -481,6 +481,7 @@ export default async (dbname, media, tokenize, stemmer) => { cresult.continue(); } + membersForName?.sort((a, b) => a.displayName.localeCompare(b.displayName)); return [presence, membersForName]; }; diff --git a/test/browser-test.ts b/test/browser-test.ts index 39a2da9..8f49cdd 100644 --- a/test/browser-test.ts +++ b/test/browser-test.ts @@ -1,8 +1,11 @@ import { test as base, expect, type JSHandle } from "@playwright/test"; +import type { borogove_persistence_Sqlite } from "../npm/sqlite-wasm"; type BrowserFixtures = { borogove: JSHandle<any>; + createChannel: JSHandle<any>; persistence: JSHandle<any>; + storeIncompleteMember: JSHandle<any>; }; type SqliteFixtures = BrowserFixtures & { @@ -26,6 +29,14 @@ export const idbTest = base.extend<BrowserFixtures>({ const borogove = await page.evaluateHandle(() => window.borogove); await use(borogove); }, + createChannel: async ({ page, borogove }, use) => { + const createChannel = await page.evaluateHandle( + (borogove) => (persistence, chatId) => + new borogove.Channel(null, null, persistence, chatId), + borogove, + ); + await use(createChannel); + }, persistence: async ({ page, borogove }, use) => { const persistence = await page.evaluateHandle(async (borogove) => { const mediaStore = await borogove.persistence.MediaStoreCache("snikket"); @@ -33,6 +44,35 @@ export const idbTest = base.extend<BrowserFixtures>({ }, borogove); await use(persistence); }, + storeIncompleteMember: async ({ page }, use) => { + const storeIncompleteMember = await page.evaluateHandle( + () => async (chatId) => { + const request = indexedDB.open("snikket"); + const db = await new Promise<IDBDatabase>((resolve, reject) => { + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + const transaction = db.transaction(["members"], "readwrite"); + transaction.objectStore("members").put({ + account: "alice@example.com", + chatId, + id: "room-members-7@example.com/incomplete", + displayName: "", + photoUri: null, + isSelf: 0, + chat: "", + roles: [], + presence: new Map(), + jid: "", + }); + await new Promise((resolve, reject) => { + transaction.oncomplete = () => resolve(null); + transaction.onerror = () => reject(transaction.error); + }); + }, + ); + await use(storeIncompleteMember); + }, }); export const sqliteTest = base.extend<SqliteFixtures>({ @@ -48,6 +88,14 @@ export const sqliteTest = base.extend<SqliteFixtures>({ const sqlite = await page.evaluateHandle(() => window.sqlite); await use(sqlite); }, + createChannel: async ({ page, sqlite }, use) => { + const createChannel = await page.evaluateHandle( + (sqlite) => (persistence, chatId) => + new sqlite.Channel(null, null, persistence, chatId), + sqlite, + ); + await use(createChannel); + }, persistence: async ({ page, borogove, sqlite }, use) => { const persistence = await page.evaluateHandle( async ({ borogove, sqlite }) => { @@ -57,8 +105,49 @@ export const sqliteTest = base.extend<SqliteFixtures>({ }, { borogove, sqlite }, ); + await page.evaluate((persistence: borogove_persistence_Sqlite) => { + // TODO: remove this wrapper + // + // storeChats is not currently awaitable. The Sqlite tests were adding + // manual delays that the IDB tests didn't need. I wanted to share the + // tests, so wrapping the implementation with a delay here lets me do + // that. + // + // Changing storeChats to return a Promise that could be awaited would + // be the better long term solution, but there's already a debounce + // thing in storeChats itself that maybe should be revisited and that + // seemed like a riskier change, so I went with this for now. + const storeChats = persistence.storeChats.bind(persistence); + persistence.storeChats = (...args) => { + storeChats(...args); + return new Promise((resolve) => setTimeout(resolve, 200)); + }; + }, persistence); await use(persistence); }, + storeIncompleteMember: async ({ page, persistence }, use) => { + const storeIncompleteMember = await page.evaluateHandle( + (persistence) => async (chatId) => { + await persistence.db.exec( + "INSERT INTO members(account_id, chat_id, member_id, display_name, photo_uri, is_self, chat, roles, presence, jid) VALUES(?, ?, ?, ?, ?, ?, ?, jsonb(?), jsonb(?), ?)", + [ + "alice@example.com", + chatId, + "room-members-7@example.com/incomplete", + "", + null, + 0, + "{}", + "[]", + "{}", + "", + ], + ); + }, + persistence, + ); + await use(storeIncompleteMember); + }, }); export { expect }; diff --git a/test/idb.spec.ts b/test/idb.spec.ts index c730880..a8e52e0 100644 --- a/test/idb.spec.ts +++ b/test/idb.spec.ts @@ -1,1216 +1,56 @@ import { idbTest as test, expect } from "./browser-test"; +import { sharedPersistenceTests } from "./persistence-tests"; -test("1:1 come back ordered by sortId", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "1", - serverIdBy: "alice@example.com", - senderId: "hatter@example.com", - direction: 0, - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("hatter@example.com"); - builder.replyTo = [builder.from]; - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "2", - serverIdBy: "alice@example.com", - senderId: "hatter@example.com", - direction: 0, - }); - builder2.sortId = "b0"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("hatter@example.com"); - builder2.replyTo = [builder.from]; - - await persistence.storeMessages("alice@example.com", [ - builder2.build(), - builder.build(), - ]); - - return await persistence.getMessagesBefore( - "alice@example.com", - "hatter@example.com", - ); - }, { borogove, persistence }); - - expect(result.length).toBe(2); - expect(result[0].serverId).toBe("1"); - expect(result[1].serverId).toBe("2"); -}); - -test("getMessagesBefore the end: MUC come back ordered by sortId, PM by timestamp", async ({ - page, - borogove, - persistence, -}) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "1", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:01Z", - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder.replyTo = [builder.from.asBare()]; - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "2", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:00Z", - }); - builder2.sortId = "b0"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder2.replyTo = [builder.from.asBare()]; - - const builder3 = new borogove.ChatMessageBuilder({ - serverId: "3", - serverIdBy: "alice@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannelPrivate, - timestamp: "2020-01-01T00:00:03Z", - }); - builder3.sortId = "a0"; - builder3.to = borogove.JID.parse("alice@example.com"); - builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder3.replyTo = [builder.from.asBare()]; - - await persistence.storeMessages("alice@example.com", [ - builder2.build(), - builder3.build(), - builder.build(), - ]); - - return await persistence.getMessagesBefore( - "alice@example.com", - "teaparty@example.com", - ); - }, { borogove, persistence }); - - expect(result.length).toBe(3); - expect(result[0].serverId).toBe("1"); - expect(result[1].serverId).toBe("2"); - expect(result[2].serverId).toBe("3"); -}); - -test("getMessagesBefore some point: MUC come back ordered by sortId, PM by timestamp", async ({ - page, - borogove, - persistence, -}) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "1", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:01Z", - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder.replyTo = [builder.from.asBare()]; - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "2", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:00Z", - }); - builder2.sortId = "b0"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder2.replyTo = [builder.from.asBare()]; - - const builder3 = new borogove.ChatMessageBuilder({ - serverId: "3", - serverIdBy: "alice@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannelPrivate, - timestamp: "2020-01-01T00:00:03Z", - }); - builder3.sortId = "Z~"; - builder3.to = borogove.JID.parse("alice@example.com"); - builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder3.replyTo = [builder.from.asBare()]; - - const builder4 = new borogove.ChatMessageBuilder({ - serverId: "4", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:04Z", - }); - builder4.sortId = "c0"; - builder4.to = borogove.JID.parse("alice@example.com"); - builder4.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder4.replyTo = [builder.from.asBare()]; - - await persistence.storeMessages("alice@example.com", [ - builder2.build(), - builder4.build(), - builder3.build(), - builder.build(), - ]); - - return await persistence.getMessagesBefore( - "alice@example.com", - "teaparty@example.com", - builder4.build(), - ); - }, { borogove, persistence }); - - expect(result.length).toBe(3); - expect(result[0].serverId).toBe("1"); - expect(result[1].serverId).toBe("2"); - expect(result[2].serverId).toBe("3"); -}); - -test("getMessagesBefore a PM", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "1", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:00Z", - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder.replyTo = [builder.from.asBare()]; - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "2", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:01Z", - }); - builder2.sortId = "b0"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder2.replyTo = [builder.from.asBare()]; - - const builder3 = new borogove.ChatMessageBuilder({ - serverId: "3", - serverIdBy: "alice@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannelPrivate, - timestamp: "2020-01-01T00:00:03Z", - }); - builder3.sortId = "Z~"; - builder3.to = borogove.JID.parse("alice@example.com"); - builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder3.replyTo = [builder.from.asBare()]; - - const builder4 = new borogove.ChatMessageBuilder({ - serverId: "4", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:04Z", - }); - builder4.sortId = "c0"; - builder4.to = borogove.JID.parse("alice@example.com"); - builder4.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder4.replyTo = [builder.from.asBare()]; - - await persistence.storeMessages("alice@example.com", [ - builder2.build(), - builder4.build(), - builder3.build(), - builder.build(), - ]); - - return await persistence.getMessagesBefore( - "alice@example.com", - "teaparty@example.com", - builder3.build(), - ); - }, { borogove, persistence }); - - expect(result.length).toBe(2); - expect(result[0].serverId).toBe("1"); - expect(result[1].serverId).toBe("2"); -}); - -test("getMessagesAfter the start: MUC come back ordered by sortId, PM by timestamp", async ({ - page, - borogove, - persistence, -}) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "1", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:00Z", - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder.replyTo = [builder.from.asBare()]; - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "2", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:01Z", - }); - builder2.sortId = "b0"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder2.replyTo = [builder.from.asBare()]; - - const builder3 = new borogove.ChatMessageBuilder({ - serverId: "3", - serverIdBy: "alice@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannelPrivate, - timestamp: "2020-01-01T00:00:03Z", - }); - builder3.sortId = "a1"; - builder3.to = borogove.JID.parse("alice@example.com"); - builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder3.replyTo = [builder.from.asBare()]; - - await persistence.storeMessages("alice@example.com", [ - builder2.build(), - builder3.build(), - builder.build(), - ]); - - return await persistence.getMessagesAfter( - "alice@example.com", - "teaparty@example.com", - ); - }, { borogove, persistence }); - - expect(result.length).toBe(3); - expect(result[0].serverId).toBe("1"); - expect(result[1].serverId).toBe("2"); - expect(result[2].serverId).toBe("3"); -}); - -test("getMessagesAfter some point: MUC come back ordered by sortId, PM by timestamp", async ({ - page, - borogove, - persistence, -}) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "1", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:01Z", - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder.replyTo = [builder.from.asBare()]; - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "2", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:00Z", - }); - builder2.sortId = "b0"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder2.replyTo = [builder.from.asBare()]; - - const builder3 = new borogove.ChatMessageBuilder({ - serverId: "3", - serverIdBy: "alice@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannelPrivate, - timestamp: "2020-01-01T00:00:03Z", - }); - builder3.sortId = "Z~"; - builder3.to = borogove.JID.parse("alice@example.com"); - builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder3.replyTo = [builder.from.asBare()]; - - const builder4 = new borogove.ChatMessageBuilder({ - serverId: "4", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:04Z", - }); - builder4.sortId = "c0"; - builder4.to = borogove.JID.parse("alice@example.com"); - builder4.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder4.replyTo = [builder.from.asBare()]; - - await persistence.storeMessages("alice@example.com", [ - builder2.build(), - builder4.build(), - builder3.build(), - builder.build(), - ]); - - return await persistence.getMessagesAfter( - "alice@example.com", - "teaparty@example.com", - builder.build(), - ); - }, { borogove, persistence }); - - expect(result.length).toBe(3); - expect(result[0].serverId).toBe("2"); - expect(result[1].serverId).toBe("3"); - expect(result[2].serverId).toBe("4"); -}); - -test("getMessagesAfter a PM", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "1", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:00Z", - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder.replyTo = [builder.from.asBare()]; - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "2", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:01Z", - }); - builder2.sortId = "b0"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder2.replyTo = [builder.from.asBare()]; - - const builder3 = new borogove.ChatMessageBuilder({ - serverId: "3", - serverIdBy: "alice@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannelPrivate, - timestamp: "2020-01-01T00:00:03Z", - }); - builder3.sortId = "Z~"; - builder3.to = borogove.JID.parse("alice@example.com"); - builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder3.replyTo = [builder.from.asBare()]; - - const builder4 = new borogove.ChatMessageBuilder({ - serverId: "4", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:04Z", - }); - builder4.sortId = "c0"; - builder4.to = borogove.JID.parse("alice@example.com"); - builder4.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder4.replyTo = [builder.from.asBare()]; - - await persistence.storeMessages("alice@example.com", [ - builder2.build(), - builder4.build(), - builder3.build(), - builder.build(), - ]); - - return await persistence.getMessagesAfter( - "alice@example.com", - "teaparty@example.com", - builder3.build(), - ); - }, { borogove, persistence }); - - expect(result.length).toBe(1); - expect(result[0].serverId).toBe("4"); -}); - -test("storeChats and getChats", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const chat = Object.create(borogove.DirectChat.prototype); - chat.chatId = "hatter@example.com"; - chat.displayName = "The Mad Hatter"; - chat.trusted = true; - chat.presence = new Map(); - chat.threads = new Map([ - [null, "Tea Time"], - ["thread-1", "Introductions"], - ]); - - await persistence.storeChats("alice@example.com", [chat]); - const chats = await persistence.getChats("alice@example.com"); - return { - length: chats.length, - chatId: chats[0]?.chatId, - displayName: chats[0]?.displayName, - trusted: chats[0]?.trusted, - klass: chats[0]?.klass, - channelSubject: chats[0]?.threads?.get(null), - threadSubject: chats[0]?.threads?.get("thread-1"), - }; - }, { borogove, persistence }); - - expect(result.length).toBe(1); - expect(result.chatId).toBe("hatter@example.com"); - expect(result.displayName).toBe("The Mad Hatter"); - expect(result.trusted).toBe(true); - expect(result.klass).toBe("DirectChat"); - expect(result.channelSubject).toBe("Tea Time"); - expect(result.threadSubject).toBe("Introductions"); -}); - -test("getMessage by serverId and localId", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "srv1", - serverIdBy: "hatter@example.com", - localId: "loc1", - senderId: "hatter@example.com", - direction: 0, - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("hatter@example.com"); - builder.recipients = [builder.to]; - builder.replyTo = [builder.from]; - const msg = builder.build(); - - await persistence.storeMessages("alice@example.com", [msg]); - - const byServerId = await persistence.getMessage( - "alice@example.com", - "hatter@example.com", - "srv1", - null, - ); - const byLocalId = await persistence.getMessage( - "alice@example.com", - "hatter@example.com", - null, - "loc1", - ); - - return { - byServerId: byServerId - ? { serverId: byServerId.serverId, localId: byServerId.localId } - : null, - byLocalId: byLocalId - ? { serverId: byLocalId.serverId, localId: byLocalId.localId } - : null, - }; - }, { borogove, persistence }); - - expect(result.byServerId).not.toBeNull(); - expect(result.byServerId.serverId).toBe("srv1"); - expect(result.byLocalId).not.toBeNull(); - expect(result.byLocalId.localId).toBe("loc1"); -}); - -test("storeReaction", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "srv1", - serverIdBy: "hatter@example.com", - senderId: "hatter@example.com", - direction: 0, - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("hatter@example.com"); - builder.recipients = [builder.to]; - builder.replyTo = [builder.from]; - await persistence.storeMessages("alice@example.com", [builder.build()]); - - const reaction = new borogove.Reaction( - "alice@example.com", - "2020-01-01T00:00:01Z", - "👍", - ); - const update = new borogove.ReactionUpdate( - "up1", - "srv1", - "hatter@example.com", - null, - "hatter@example.com", - "alice@example.com", - "2020-01-01T00:00:01Z", - [reaction], - borogove.ReactionUpdateKind.EmojiReactions, - ); - - const msg = await persistence.storeReaction("alice@example.com", update); - return { - reactions: [...msg.reactions.entries()].map(([k, v]) => ({ - key: k, - count: v.length, - })), - }; - }, { borogove, persistence }); - - expect(result.reactions.length).toBe(1); - expect(result.reactions[0].key).toBe("👍"); - expect(result.reactions[0].count).toBe(1); -}); - -test("updateMessageStatus", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - localId: "loc1", - senderId: "alice@example.com", - direction: 1, // MessageSent - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("hatter@example.com"); - builder.from = borogove.JID.parse("alice@example.com"); - builder.recipients = [builder.to]; - builder.replyTo = [builder.from]; - await persistence.storeMessages("alice@example.com", [builder.build()]); - - const updated = await persistence.updateMessageStatus( - "alice@example.com", - "loc1", - 1, - "Delivered", - ); // MessageDelivered - return { status: updated.status, statusText: updated.statusText }; - }, { borogove, persistence }); - - expect(result.status).toBe(1); - expect(result.statusText).toBe("Delivered"); -}); - -test("searchMessages", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "srv1", - serverIdBy: "hatter@example.com", - senderId: "hatter@example.com", - direction: 0, - }); - builder.sortId = "a0"; - builder.text = "Hello world"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("hatter@example.com"); - builder.recipients = [builder.to]; - builder.replyTo = [builder.from]; - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "srv2", - serverIdBy: "hatter@example.com", - senderId: "hatter@example.com", - direction: 0, - }); - builder2.sortId = "a1"; - builder2.text = "Goodbye world"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("hatter@example.com"); - builder2.recipients = [builder2.to]; - builder2.replyTo = [builder2.from]; - - await persistence.storeMessages("alice@example.com", [ - builder.build(), - builder2.build(), - ]); - - const results = await persistence.searchMessages( - "alice@example.com", - "hatter@example.com", - "hello", - ); - return results.map((m) => m.text); - }, { borogove, persistence }); - - expect(result.length).toBe(1); - expect(result[0]).toBe("Hello world"); -}); - -test("removeAccount and listAccounts", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - await persistence.storeLogin("alice@example.com", "client1", "Alice", null); - await persistence.storeLogin("bob@example.com", "client2", "Bob", null); - - const accountsBefore = await persistence.listAccounts(); - await persistence.removeAccount("alice@example.com", true); - const accountsAfter = await persistence.listAccounts(); - - return { accountsBefore, accountsAfter }; - }, { borogove, persistence }); - - expect(result.accountsBefore).toContain("alice@example.com"); - expect(result.accountsBefore).toContain("bob@example.com"); - expect(result.accountsAfter).not.toContain("alice@example.com"); - expect(result.accountsAfter).toContain("bob@example.com"); -}); - -test("getChatUnreadDetails", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const chat = Object.create(borogove.DirectChat.prototype); - chat.chatId = "hatter@example.com"; - chat.readUpToId = "srv1"; - chat.notificationsFiltered = () => false; - - const builder = new borogove.ChatMessageBuilder({ - serverId: "srv1", - serverIdBy: "hatter@example.com", - senderId: "hatter@example.com", - direction: 0, - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("hatter@example.com"); - builder.recipients = [builder.to]; - builder.replyTo = [builder.from]; - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "srv2", - serverIdBy: "hatter@example.com", - senderId: "hatter@example.com", - direction: 0, - }); - builder2.sortId = "a1"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("hatter@example.com"); - builder2.recipients = [builder2.to]; - builder2.replyTo = [builder2.from]; - - await persistence.storeMessages("alice@example.com", [ - builder.build(), - builder2.build(), - ]); - - return await persistence.getChatUnreadDetails("alice@example.com", chat); - }, { borogove, persistence }); - - expect(result.unreadCount).toBe(1); - expect(result.message.serverId).toBe("srv2"); -}); - -test("media storage functions", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - function bufferToByteStream(buffer) { - let done = false; - return new ReadableStream({ - type: "bytes", - async pull(controller) { - if (done) { - controller.close(); - } else { - controller.enqueue(buffer); - done = true; - } - }, - }); - } - - const buffer = new Uint8Array([1, 2, 3]); - const sha256 = await crypto.subtle.digest("SHA-256", buffer.buffer); - await persistence.storeMedia("image/png", bufferToByteStream(buffer)); - const hash = new borogove.Hash("sha-256", sha256); - const hasBefore = await persistence.hasMedia(hash); - await persistence.removeMedia("sha-256", sha256); - const hasAfter = await persistence.hasMedia(hash); - - return { hasBefore, hasAfter }; - }, { borogove, persistence }); - - expect(result.hasBefore).toBe( - "/.well-known/ni/sha-256/A5BYxvLAy0ksUzsKTRTvd8wPeKvMztUofYShogEc-4E", - ); - expect(result.hasAfter).toBe(null); -}); - -test("hydrate message with incomplete replyToMessage keys", async ({ - page, - borogove, - persistence, -}) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "parent", - serverIdBy: "hatter@example.com", - localId: "loc1", - senderId: "hatter@example.com", - direction: 0, - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("hatter@example.com"); - builder.recipients = [builder.to]; - builder.replyTo = [builder.from]; - const parentMsg = builder.build(); - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "child", - serverIdBy: "hatter@example.com", - localId: "loc2", - senderId: "hatter@example.com", - direction: 0, - }); - builder2.sortId = "a1"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("hatter@example.com"); - builder2.recipients = [builder2.to]; - builder2.replyTo = [builder2.from]; - builder2.replyToMessage = parentMsg; - const childMsg = builder2.build(); - - await persistence.storeMessages("alice@example.com", [parentMsg, childMsg]); - - const db = await new Promise<IDBDatabase>((resolve, reject) => { - const req = indexedDB.open("snikket"); - req.onsuccess = () => resolve(req.result); - req.onerror = () => reject(req.error); - }); - const tx = db.transaction(["messages"], "readwrite"); - const store = tx.objectStore("messages"); - const key = ["alice@example.com", "child", "hatter@example.com", "loc2"]; - const rawChild = await new Promise<{ replyToMessage: string[] }>((resolve) => { - const req = store.get(key); - req.onsuccess = () => resolve(req.result); - }); - - rawChild.replyToMessage = ["alice@example.com", "parent", "", ""]; - - await new Promise<void>((resolve) => { - const req = store.put(rawChild); - req.onsuccess = () => resolve(); - }); - await new Promise<void>((resolve) => { - tx.oncomplete = () => resolve(); - }); - - const retrievedChild = await persistence.getMessage( - "alice@example.com", - "hatter@example.com", - "child", - "loc2", - ); - return { - hasReply: !!retrievedChild.replyToMessage, - replyServerId: retrievedChild.replyToMessage - ? retrievedChild.replyToMessage.serverId - : null, - }; - }, { borogove, persistence }); - - expect(result.hasReply).toBe(true); - expect(result.replyServerId).toBe("parent"); -}); - -test("hydrate message with incomplete replyToMessage", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "parent", - serverIdBy: "hatter@example.com", - localId: "loc1", - senderId: "hatter@example.com", - direction: 0, - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("hatter@example.com"); - builder.recipients = [builder.to]; - builder.replyTo = [builder.from]; - const parentStub = builder.build(); - - builder.setBody(borogove.Html.text("Hello")); - const parentMsg = builder.build(); - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "child", - serverIdBy: "hatter@example.com", - localId: "loc2", - senderId: "hatter@example.com", - direction: 0, - }); - builder2.sortId = "a1"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("hatter@example.com"); - builder2.recipients = [builder2.to]; - builder2.replyTo = [builder2.from]; - builder2.replyToMessage = parentStub; - const childMsg = builder2.build(); - - await persistence.storeMessages("alice@example.com", [parentMsg]); - const [childStored] = await persistence.storeMessages("alice@example.com", [ - childMsg, - ]); - - return childStored.replyToMessage.body().toPlainText(); - }, { borogove, persistence }); - - expect(result).toBe("Hello"); -}); - -test("storeChats and getChats with status", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const chat = Object.create(borogove.DirectChat.prototype); - chat.chatId = "hatter@example.com"; - chat.displayName = "The Mad Hatter"; - chat.trusted = true; - chat.presence = new Map(); - chat.threads = new Map(); - chat.status = new borogove.Status("🎩", "Time for tea!"); - - await persistence.storeChats("alice@example.com", [chat]); - const chats = await persistence.getChats("alice@example.com"); - return { - length: chats.length, - chatId: chats[0]?.chatId, - statusEmoji: chats[0]?.status?.emoji, - statusText: chats[0]?.status?.text, - }; - }, { borogove, persistence }); - - expect(result.length).toBe(1); - expect(result.chatId).toBe("hatter@example.com"); - expect(result.statusEmoji).toBe("🎩"); - expect(result.statusText).toBe("Time for tea!"); -}); - -test("storeStreamManamagement and getStreamManagement", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - await persistence.storeLogin("alice@example.com", "", "", null); // or updating with SM may not work - await persistence.storeStreamManagement( - "alice@example.com", - new Uint8Array([1, 2, 0, 4]).buffer, - "ZZ", - ); - const result = await persistence.getStreamManagement("alice@example.com"); - return { - smIsArrayBuffer: result.sm instanceof ArrayBuffer, - smIsEq: result.sm - ? indexedDB.cmp(result.sm, new Uint8Array([1, 2, 0, 4]).buffer) - : "null", - sortId: result.sortId, - }; - }, { borogove, persistence }); - - expect(result.smIsEq).toBe(0); - expect(result.smIsArrayBuffer).toBe(true); - expect(result.sortId).toBe("ZZ"); -}); - -test("getMembers hydrates persisted member data", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const chat = Object.create(borogove.Channel.prototype); - chat.chatId = "room-members-1@example.com"; - chat.getDisplayName = () => "Tea Room"; - - const member = { - id: "room-members-1@example.com/occ-1", - displayName: "Alice", - photoUri: "photo:alice", - isSelf: false, - roles: [{ id: "admin", title: "Admin" }], - jid: borogove.JID.parse("alice@example.com"), - presence: new Map([ - [ - "laptop", - borogove.Stanza.parse("<presence><show>away</show></presence>"), - ], - ]), - chat: { chatId: "alice@example.com" }, - }; - - await persistence.storeMembers("alice@example.com", chat.chatId, [member]); - const [stored] = await persistence.getMembers( - "alice@example.com", - chat, - false, - ); - - return { - id: stored.id, - displayName: stored.displayName, - chatId: stored.chat?.chatId, - roleIds: stored.roles.map((r) => r.id), - presenceKeys: [...stored.presence.keys()], - showPresence: stored.showPresence, - }; - }, { borogove, persistence }); - - expect(result.id).toBe("room-members-1@example.com/occ-1"); - expect(result.displayName).toBe("Alice"); - expect(result.chatId).toBe("alice@example.com"); - expect(result.roleIds).toEqual(["admin"]); - expect(result.presenceKeys).toEqual(["laptop"]); - expect(result.showPresence).toBe(1); -}); - -test("storeMemberUpdates merges existing member data", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const chat = Object.create(borogove.Channel.prototype); - chat.chatId = "room-members-2@example.com"; - chat.getDisplayName = () => "Tea Room"; - - await persistence.storeMembers("alice@example.com", chat.chatId, [ - { - id: "room-members-2@example.com/occ-1", - displayName: "Alice", - photoUri: null, - isSelf: false, - roles: [ - { id: "admin", title: "Admin" }, - { id: "urn:xmpp:hats:test", title: "Tea Host" }, - ], - jid: borogove.JID.parse("alice@example.com"), - presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), - chat: { chatId: "alice@example.com" }, - }, - ]); - - const updates = [ - new borogove.MemberUpdate( - "room-members-2@example.com/occ-1", - borogove.JID.parse("alice@example.com"), - "Alice Cooper", - false, - null, - new Map([["mobile", borogove.Stanza.parse("<presence />")]]), - ), - ]; - - const updated = await persistence.storeMemberUpdates( - "alice@example.com", - chat, - updates, - false, - ); - - return { - updatedRoleIds: updated[0].roles.map((r) => r.id), - updatedPresenceKeys: [...updated[0].presence.keys()].sort(), - updatedDisplayName: updated[0].displayName, - }; - }, { borogove, persistence }); - - expect(result.updatedRoleIds).toEqual(["urn:xmpp:hats:test"]); - expect(result.updatedPresenceKeys).toEqual(["desk", "mobile"]); - expect(result.updatedDisplayName).toBe("Alice Cooper"); -}); - -test("storeMemberUpdates clears omitted full-list affiliations", async ({ - page, - borogove, - persistence, -}) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const chat = Object.create(borogove.Channel.prototype); - chat.chatId = "room-members-2b@example.com"; - chat.getDisplayName = () => "Tea Room"; - - await persistence.storeMembers("alice@example.com", chat.chatId, [ - { - id: "room-members-2b@example.com/occ-1", - displayName: "Alice", - photoUri: null, - isSelf: false, - roles: [{ id: "admin", title: "Admin" }], - jid: borogove.JID.parse("alice@example.com"), - presence: new Map(), - chat: { chatId: "alice@example.com" }, - }, - { - id: "room-members-2b@example.com/occ-2", - displayName: "Bob", - photoUri: null, - isSelf: false, - roles: [{ id: "owner", title: "Owner" }], - jid: borogove.JID.parse("bob@example.com"), - presence: new Map(), - chat: { chatId: "bob@example.com" }, - }, - ]); - - await persistence.storeMemberUpdates( - "alice@example.com", - chat, - [ - new borogove.MemberUpdate( - "room-members-2b@example.com/occ-1", - borogove.JID.parse("alice@example.com"), - "Alice", - false, - null, - new Map(), - ), - ], - true, - ); - const members = await persistence.getMembers( - "alice@example.com", - chat, - true, - ); - return members.find((m) => m.id.endsWith("occ-2")).roles.map((r) => r.id); - }, { borogove, persistence }); - - expect(result).toEqual([]); -}); +sharedPersistenceTests(test); -test("storeMemberUpdates matches existing member by true JID", async ({ - page, - borogove, - persistence, -}) => { +// TODO: Share this with SQLite once direct-chat reply hydration is fixed. +test("hydrate message with incomplete replyToMessage", async ({ page, borogove, persistence }) => { const result = await page.evaluate(async ({ borogove, persistence }) => { - const chat1 = Object.create(borogove.Channel.prototype); - chat1.chatId = "room-members-3@example.com"; - chat1.getDisplayName = () => "Tea Room"; - const chat2 = Object.create(borogove.Channel.prototype); - chat2.chatId = "room-members-4@example.com"; - chat2.getDisplayName = () => "Other Room"; - - await persistence.storeMembers("alice@example.com", chat1.chatId, [ - { - id: "room-members-3@example.com/occ-1", - displayName: "Alice", - photoUri: null, - isSelf: false, - roles: [{ id: "admin", title: "Admin" }], - jid: borogove.JID.parse("alice@example.com"), - presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), - chat: { chatId: "alice@example.com" }, - }, - { - id: "room-members-4@example.com/occ-1", - displayName: "Bob", - photoUri: null, - isSelf: false, - roles: [{ id: "admin", title: "Admin" }], - jid: borogove.JID.parse("bob@example.com"), - presence: new Map([["phone", borogove.Stanza.parse("<presence />")]]), - chat: { chatId: "bob@example.com" }, - }, - ]); - - await persistence.storeMemberUpdates( - "alice@example.com", - chat1, - [ - new borogove.MemberUpdate( - null, - borogove.JID.parse("alice@example.com"), - "Alice Renamed", - false, - null, - new Map(), - ), - ], - false, - ); - const [chat1Member] = await persistence.getMemberDetails( - "alice@example.com", - chat1, - ["room-members-3@example.com/occ-1"], - ); - - return chat1Member.displayName; - }, { borogove, persistence }); + const builder = new borogove.ChatMessageBuilder({ + serverId: "parent", + serverIdBy: "hatter@example.com", + localId: "loc1", + senderId: "hatter@example.com", + direction: 0, + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("hatter@example.com"); + builder.recipients = [builder.to]; + builder.replyTo = [builder.from]; + const parentStub = builder.build(); - expect(result).toBe("Alice Renamed"); -}); + builder.setBody(borogove.Html.text("Hello")); + const parentMsg = builder.build(); -test("clearMemberPresence only clears the targeted chat", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const chat1 = Object.create(borogove.Channel.prototype); - chat1.chatId = "room-members-4a@example.com"; - chat1.getDisplayName = () => "Tea Room"; - const chat2 = Object.create(borogove.Channel.prototype); - chat2.chatId = "room-members-4b@example.com"; - chat2.getDisplayName = () => "Other Room"; + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "child", + serverIdBy: "hatter@example.com", + localId: "loc2", + senderId: "hatter@example.com", + direction: 0, + }); + builder2.sortId = "a1"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("hatter@example.com"); + builder2.recipients = [builder2.to]; + builder2.replyTo = [builder2.from]; + builder2.replyToMessage = parentStub; + const childMsg = builder2.build(); - await persistence.storeMembers("alice@example.com", chat1.chatId, [ - { - id: "room-members-4a@example.com/occ-1", - displayName: "Alice", - photoUri: null, - isSelf: false, - roles: [{ id: "admin", title: "Admin" }], - jid: borogove.JID.parse("alice@example.com"), - presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), - chat: { chatId: "alice@example.com" }, - }, - ]); - await persistence.storeMembers("alice@example.com", chat2.chatId, [ - { - id: "room-members-4b@example.com/occ-1", - displayName: "Bob", - photoUri: null, - isSelf: false, - roles: [{ id: "admin", title: "Admin" }], - jid: borogove.JID.parse("bob@example.com"), - presence: new Map([["phone", borogove.Stanza.parse("<presence />")]]), - chat: { chatId: "bob@example.com" }, - }, + await persistence.storeMessages("alice@example.com", [parentMsg]); + const [childStored] = await persistence.storeMessages("alice@example.com", [ + childMsg, ]); - await persistence.clearMemberPresence("alice@example.com", chat1.chatId); - const [chat1Member] = await persistence.getMemberDetails( - "alice@example.com", - chat1, - ["room-members-4a@example.com/occ-1"], - ); - const [chat2Member] = await persistence.getMemberDetails( - "alice@example.com", - chat2, - ["room-members-4b@example.com/occ-1"], - ); - - return { - chat1PresenceKeys: [...chat1Member.presence.keys()], - chat2PresenceKeys: [...chat2Member.presence.keys()], - }; + return childStored.replyToMessage.body().toPlainText(); }, { borogove, persistence }); - expect(result.chat1PresenceKeys).toEqual([]); - expect(result.chat2PresenceKeys).toEqual(["phone"]); + expect(result).toBe("Hello"); }); + test("getMembers filters hidden rows for non-moderators", async ({ page, borogove, persistence }) => { const result = await page.evaluate(async ({ borogove, persistence }) => { const chat = Object.create(borogove.Channel.prototype); @@ -1284,168 +124,81 @@ test("getMembers filters hidden rows for non-moderators", async ({ page, borogov expect(result).toEqual(["Zulu", "Alpha"]); }); -test("getMembers includes moderator-visible rows", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const chat = Object.create(borogove.Channel.prototype); - chat.chatId = "room-members-6@example.com"; - chat.getDisplayName = () => "Tea Room"; - - await persistence.storeMembers("alice@example.com", chat.chatId, [ - { - id: "room-members-6@example.com/owner", - displayName: "Zulu", - photoUri: null, - isSelf: false, - roles: [{ id: "owner", title: "Owner" }], - jid: borogove.JID.parse("zulu@example.com"), - presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), - chat: { chatId: "zulu@example.com" }, - }, - { - id: "room-members-6@example.com/outcast", - displayName: "Banned", - photoUri: null, - isSelf: false, - roles: [{ id: "outcast", title: "Banned" }], - jid: borogove.JID.parse("banned@example.com"), - presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), - chat: { chatId: "banned@example.com" }, - }, - { - id: "room-members-6@example.com/guest-offline", - displayName: "Guest", - photoUri: null, - isSelf: false, - roles: [{ id: "none", title: "Guest" }], - jid: borogove.JID.parse("guest@example.com"), - presence: new Map([ - ["desk", borogove.Stanza.parse('<presence type="unavailable" />')], - ]), - chat: { chatId: "guest@example.com" }, - }, - { - id: "room-members-6@example.com/admin", - displayName: "Alpha", - photoUri: null, - isSelf: false, - roles: [{ id: "admin", title: "Admin" }], - jid: borogove.JID.parse("alpha@example.com"), - presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), - chat: { chatId: "alpha@example.com" }, - }, - ]); - - const moderator = await persistence.getMembers( - "alice@example.com", - chat, - true, - ); - return moderator.map((m) => m.displayName); - }, { borogove, persistence }); - - expect(result).toEqual(["Zulu", "Alpha", "Banned"]); -}); -test("getMemberDetails returns null for incomplete rows", async ({ page, borogove, persistence }) => { +test("hydrate message with incomplete replyToMessage keys", async ({ + page, + borogove, + persistence, +}) => { const result = await page.evaluate(async ({ borogove, persistence }) => { - const chat = Object.create(borogove.Channel.prototype); - chat.chatId = "room-members-7@example.com"; - chat.getDisplayName = () => "Tea Room"; + const builder = new borogove.ChatMessageBuilder({ + serverId: "parent", + serverIdBy: "hatter@example.com", + localId: "loc1", + senderId: "hatter@example.com", + direction: 0, + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("hatter@example.com"); + builder.recipients = [builder.to]; + builder.replyTo = [builder.from]; + const parentMsg = builder.build(); - await persistence.storeMembers("alice@example.com", chat.chatId, [ - { - id: "room-members-7@example.com/admin", - displayName: "Alpha", - photoUri: null, - isSelf: false, - roles: [{ id: "admin", title: "Admin" }], - jid: borogove.JID.parse("alpha@example.com"), - presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), - chat: { chatId: "alpha@example.com" }, - }, - ]); + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "child", + serverIdBy: "hatter@example.com", + localId: "loc2", + senderId: "hatter@example.com", + direction: 0, + }); + builder2.sortId = "a1"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("hatter@example.com"); + builder2.recipients = [builder2.to]; + builder2.replyTo = [builder2.from]; + builder2.replyToMessage = parentMsg; + const childMsg = builder2.build(); + + await persistence.storeMessages("alice@example.com", [parentMsg, childMsg]); - const tx = indexedDB.open("snikket"); const db = await new Promise<IDBDatabase>((resolve, reject) => { - tx.onsuccess = () => resolve(tx.result); - tx.onerror = () => reject(tx.error); + const req = indexedDB.open("snikket"); + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); + }); + const tx = db.transaction(["messages"], "readwrite"); + const store = tx.objectStore("messages"); + const key = ["alice@example.com", "child", "hatter@example.com", "loc2"]; + const rawChild = await new Promise<{ replyToMessage: string[] }>((resolve) => { + const req = store.get(key); + req.onsuccess = () => resolve(req.result); }); - const write = db.transaction(["members"], "readwrite"); - write.objectStore("members").put({ - account: "alice@example.com", - chatId: chat.chatId, - id: "room-members-7@example.com/incomplete", - displayName: "", - photoUri: null, - isSelf: 0, - chat: "", - roles: [], - presence: new Map(), - jid: "", + + rawChild.replyToMessage = ["alice@example.com", "parent", "", ""]; + + await new Promise<void>((resolve) => { + const req = store.put(rawChild); + req.onsuccess = () => resolve(); }); - await new Promise((resolve, reject) => { - write.oncomplete = () => resolve(null); - write.onerror = () => reject(write.error); + await new Promise<void>((resolve) => { + tx.oncomplete = () => resolve(); }); - const details = await persistence.getMemberDetails( + const retrievedChild = await persistence.getMessage( "alice@example.com", - chat, - [ - "room-members-7@example.com/admin", - "room-members-7@example.com/incomplete", - ], + "hatter@example.com", + "child", + "loc2", ); - return details.map((m) => (m ? m.displayName : null)); - }, { borogove, persistence }); - - expect(result).toEqual(["Alpha", null]); -}); - -test("storeVoiceRequest and listVoiceRequests", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const chat = Object.create(borogove.Channel.prototype); - chat.chatId = "room-voice-requests@example.com"; - chat.getDisplayName = () => "Tea Room"; - - await persistence.storeMembers("alice@example.com", chat.chatId, [ - { - id: "room-voice-requests@example.com/occ-1", - displayName: "Bob", - photoUri: null, - isSelf: false, - roles: [{ id: "none", title: "Participant" }], - jid: borogove.JID.parse("bob@example.com"), - presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), - chat: { chatId: "bob@example.com" }, - }, - { - id: "room-voice-requests@example.com/occ-2", - displayName: "Charlie", - photoUri: null, - isSelf: false, - roles: [{ id: "none", title: "Participant" }], - jid: borogove.JID.parse("charlie@example.com"), - presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), - chat: { chatId: "charlie@example.com" }, - } - ]); - - await persistence.storeVoiceRequest("alice@example.com", chat, "bob@example.com", true); - await persistence.storeVoiceRequest("alice@example.com", chat, "charlie@example.com", true); - - const requests1 = await persistence.listVoiceRequests("alice@example.com", chat); - - await persistence.storeVoiceRequest("alice@example.com", chat, "bob@example.com", false); - - const requests2 = await persistence.listVoiceRequests("alice@example.com", chat); - return { - requests1: requests1.map((m) => m.displayName).sort(), - requests2: requests2.map((m) => m.displayName).sort(), + hasReply: !!retrievedChild.replyToMessage, + replyServerId: retrievedChild.replyToMessage + ? retrievedChild.replyToMessage.serverId + : null, }; }, { borogove, persistence }); - expect(result.requests1).toEqual(["Bob", "Charlie"]); - expect(result.requests2).toEqual(["Charlie"]); + expect(result.hasReply).toBe(true); + expect(result.replyServerId).toBe("parent"); }); diff --git a/test/persistence-tests.ts b/test/persistence-tests.ts new file mode 100644 index 0000000..0e00dac --- /dev/null +++ b/test/persistence-tests.ts @@ -0,0 +1,1466 @@ +import { idbTest, sqliteTest, expect } from "./browser-test"; + +type PersistenceTest = typeof idbTest | typeof sqliteTest; + +export function sharedPersistenceTests(test: PersistenceTest) { + test("storeChats and getChats", async ({ page, borogove, persistence }) => { + const result = await page.evaluate( + async ({ borogove, persistence }) => { + const chat = new borogove.DirectChat( + null, + null, + persistence, + "hatter@example.com", + ); + chat.displayName = "The Mad Hatter"; + chat.trusted = true; + chat.threads = new Map([ + [null, "Tea Time"], + ["thread-1", "Introductions"], + ]); + + await persistence.storeChats("alice@example.com", [chat]); + const chats = await persistence.getChats("alice@example.com"); + return { + length: chats.length, + chatId: chats[0]?.chatId, + displayName: chats[0]?.displayName, + trusted: chats[0]?.trusted, + klass: chats[0]?.klass, + channelSubject: chats[0]?.threads?.get(null), + threadSubject: chats[0]?.threads?.get("thread-1"), + }; + }, + { borogove, persistence }, + ); + + expect(result.length).toBe(1); + expect(result.chatId).toBe("hatter@example.com"); + expect(result.displayName).toBe("The Mad Hatter"); + expect(result.trusted).toBe(true); + expect(result.klass).toBe("DirectChat"); + expect(result.channelSubject).toBe("Tea Time"); + expect(result.threadSubject).toBe("Introductions"); + }); + + test("storeChats and getChats with status", async ({ page, borogove, persistence }) => { + const result = await page.evaluate( + async ({ borogove, persistence }) => { + const chat = new borogove.DirectChat( + null, + null, + persistence, + "hatter@example.com", + ); + chat.displayName = "The Mad Hatter"; + chat.trusted = true; + chat.status = new borogove.Status("🎩", "Time for tea!"); + + await persistence.storeChats("alice@example.com", [chat]); + const chats = await persistence.getChats("alice@example.com"); + return { + length: chats.length, + chatId: chats[0]?.chatId, + statusEmoji: chats[0]?.status?.emoji, + statusText: chats[0]?.status?.text, + }; + }, + { borogove, persistence }, + ); + + expect(result.length).toBe(1); + expect(result.chatId).toBe("hatter@example.com"); + expect(result.statusEmoji).toBe("🎩"); + expect(result.statusText).toBe("Time for tea!"); + }); + + test("getChats uses member presence for direct chats", async ({ page, borogove, persistence }) => { + const result = await page.evaluate( + async ({ borogove, persistence }) => { + const chat = new borogove.DirectChat( + null, + null, + persistence, + "hatter@example.com", + ); + chat.displayName = "The Mad Hatter"; + chat.trusted = true; + + await persistence.storeChats("alice@example.com", [chat]); + await persistence.storeMembers("alice@example.com", chat.chatId, [ + { + id: "hatter@example.com", + displayName: "The Mad Hatter", + photoUri: null, + isSelf: false, + roles: [], + jid: borogove.JID.parse("hatter@example.com"), + presence: new Map([ + ["phone", borogove.Stanza.parse("<presence />")], + ]), + chat: null, + }, + ]); + const [stored] = await persistence.getChats("alice@example.com"); + return [...stored.presence.keys()].sort(); + }, + { borogove, persistence }, + ); + + expect(result).toEqual(["phone"]); + }); + + test("getChats hydrates membersForName and mavUntil from members", async ({ + page, + borogove, + persistence, + createChannel, + }) => { + const result = await page.evaluate( + async ({ borogove, persistence, createChannel }) => { + const chat = createChannel( + persistence, + "room-chat-hydrate@example.com", + ); + chat.displayName = "Tea Room"; + chat.trusted = true; + chat.mavUntil = "2024-05-01T12:00:00Z"; + + await persistence.storeChats("alice@example.com", [chat]); + await persistence.storeMembers("alice@example.com", chat.chatId, [ + { + id: chat.chatId, + displayName: "Tea Room", + photoUri: null, + isSelf: false, + roles: [], + jid: borogove.JID.parse(chat.chatId), + presence: new Map(), + chat: null, + }, + { + id: `${chat.chatId}/self`, + displayName: "Myself", + photoUri: null, + isSelf: true, + roles: [{ id: "owner", title: "Owner" }], + jid: borogove.JID.parse("alice@example.com"), + presence: new Map([ + ["desk", borogove.Stanza.parse("<presence />")], + ]), + chat: null, + }, + { + id: `${chat.chatId}/zulu`, + displayName: "Zulu", + photoUri: null, + isSelf: false, + roles: [{ id: "admin", title: "Admin" }], + jid: borogove.JID.parse("zulu@example.com"), + presence: new Map([ + ["desk", borogove.Stanza.parse("<presence />")], + ]), + chat: { chatId: "zulu@example.com" }, + }, + { + id: `${chat.chatId}/alpha`, + displayName: "Alpha", + photoUri: null, + isSelf: false, + roles: [], + jid: borogove.JID.parse("alpha@example.com"), + presence: new Map([ + ["desk", borogove.Stanza.parse("<presence />")], + ]), + chat: { chatId: "alpha@example.com" }, + }, + { + id: `${chat.chatId}/hidden`, + displayName: "Hidden", + photoUri: null, + isSelf: false, + roles: [{ id: "none", title: "Guest" }], + jid: borogove.JID.parse("hidden@example.com"), + presence: new Map([ + ["desk", borogove.Stanza.parse("<presence />")], + ]), + chat: { chatId: "hidden@example.com" }, + }, + ]); + const [stored] = await persistence.getChats("alice@example.com"); + return { + mavUntil: stored.mavUntil, + membersForName: stored.membersForName.map((m) => m.displayName), + presenceKeys: [...stored.presence.keys()].sort(), + }; + }, + { borogove, persistence, createChannel }, + ); + + expect(result.mavUntil).toBe("2024-05-01T12:00:00Z"); + expect(result.membersForName).toEqual(["Alpha", "Zulu"]); + expect(result.presenceKeys).toEqual(["desk"]); + }); + + + test("hydrate replyToMessage for groupchats", async ({ page, borogove, persistence }) => { + const result = await page.evaluate( + async ({ borogove, persistence }) => { + const builder = new borogove.ChatMessageBuilder({ + serverId: "parent", + serverIdBy: "hatter@example.com", + localId: "loc1", + senderId: "hatter@example.com", + direction: 0, + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("hatter@example.com"); + builder.recipients = [builder.to]; + builder.replyTo = [builder.from]; + builder.type = borogove.MessageType.MessageChannel; + const parentStub = builder.build(); + + builder.setBody(borogove.Html.text("Hello")); + const parentMsg = builder.build(); + + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "child", + serverIdBy: "hatter@example.com", + localId: "loc2", + senderId: "hatter@example.com", + direction: 0, + }); + builder2.sortId = "a1"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("hatter@example.com"); + builder2.recipients = [builder2.to]; + builder2.replyTo = [builder2.from]; + builder2.replyToMessage = parentStub; + builder2.type = borogove.MessageType.MessageChannel; + + await persistence.storeMessages("alice@example.com", [parentMsg]); + const [childStored] = await persistence.storeMessages( + "alice@example.com", + [builder2.build()], + ); + return childStored.replyToMessage.body().toPlainText(); + }, + { borogove, persistence }, + ); + + expect(result).toBe("Hello"); + }); + + test("getMessage by serverId and localId", async ({ + page, + borogove, + persistence, + }) => { + const result = await page.evaluate( + async ({ borogove, persistence }) => { + const builder = new borogove.ChatMessageBuilder({ + serverId: "srv1", + serverIdBy: "hatter@example.com", + localId: "loc1", + senderId: "hatter@example.com", + direction: 0, + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("hatter@example.com"); + builder.recipients = [builder.to]; + builder.replyTo = [builder.from]; + const msg = builder.build(); + + await persistence.storeMessages("alice@example.com", [msg]); + + const byServerId = await persistence.getMessage( + "alice@example.com", + "hatter@example.com", + "srv1", + null, + ); + const byLocalId = await persistence.getMessage( + "alice@example.com", + "hatter@example.com", + null, + "loc1", + ); + + return { + byServerId: byServerId + ? { serverId: byServerId.serverId, localId: byServerId.localId } + : null, + byLocalId: byLocalId + ? { serverId: byLocalId.serverId, localId: byLocalId.localId } + : null, + }; + }, + { borogove, persistence }, + ); + + expect(result.byServerId).not.toBeNull(); + expect(result.byServerId.serverId).toBe("srv1"); + expect(result.byLocalId).not.toBeNull(); + expect(result.byLocalId.localId).toBe("loc1"); + }); + + test("storeReaction", async ({ page, borogove, persistence }) => { + const result = await page.evaluate( + async ({ borogove, persistence }) => { + const builder = new borogove.ChatMessageBuilder({ + serverId: "srv1", + serverIdBy: "hatter@example.com", + senderId: "hatter@example.com", + direction: 0, + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("hatter@example.com"); + builder.recipients = [builder.to]; + builder.replyTo = [builder.from]; + await persistence.storeMessages("alice@example.com", [builder.build()]); + + const reaction = new borogove.Reaction( + "alice@example.com", + "2020-01-01T00:00:01Z", + "👍", + ); + const update = new borogove.ReactionUpdate( + "up1", + "srv1", + "hatter@example.com", + null, + "hatter@example.com", + "alice@example.com", + "2020-01-01T00:00:01Z", + [reaction], + borogove.ReactionUpdateKind.EmojiReactions, + ); + + const msg = await persistence.storeReaction( + "alice@example.com", + update, + ); + return { + reactions: [...msg.reactions.entries()].map(([k, v]) => ({ + key: k, + count: v.length, + })), + }; + }, + { borogove, persistence }, + ); + + expect(result.reactions.length).toBe(1); + expect(result.reactions[0].key).toBe("👍"); + expect(result.reactions[0].count).toBe(1); + }); + + test("searchMessages", async ({ page, borogove, persistence }) => { + const result = await page.evaluate( + async ({ borogove, persistence }) => { + const builder = new borogove.ChatMessageBuilder({ + serverId: "srv1", + serverIdBy: "hatter@example.com", + senderId: "hatter@example.com", + direction: 0, + }); + builder.sortId = "a0"; + builder.text = "Hello world"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("hatter@example.com"); + builder.recipients = [builder.to]; + builder.replyTo = [builder.from]; + + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "srv2", + serverIdBy: "hatter@example.com", + senderId: "hatter@example.com", + direction: 0, + }); + builder2.sortId = "a1"; + builder2.text = "Goodbye world"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("hatter@example.com"); + builder2.recipients = [builder2.to]; + builder2.replyTo = [builder2.from]; + + await persistence.storeMessages("alice@example.com", [ + builder.build(), + builder2.build(), + ]); + + const results = await persistence.searchMessages( + "alice@example.com", + "hatter@example.com", + "hello", + ); + return results.map((m) => m.text); + }, + { borogove, persistence }, + ); + + expect(result.length).toBe(1); + expect(result[0]).toBe("Hello world"); + }); + + test("1:1 come back ordered by sortId", async ({ page, borogove, persistence }) => { + const result = await page.evaluate(async ({ borogove, persistence }) => { + const builder = new borogove.ChatMessageBuilder({ + serverId: "1", + serverIdBy: "alice@example.com", + senderId: "hatter@example.com", + direction: 0, + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("hatter@example.com"); + builder.replyTo = [builder.from]; + + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "2", + serverIdBy: "alice@example.com", + senderId: "hatter@example.com", + direction: 0, + }); + builder2.sortId = "b0"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("hatter@example.com"); + builder2.replyTo = [builder.from]; + + await persistence.storeMessages("alice@example.com", [ + builder2.build(), + builder.build(), + ]); + + return await persistence.getMessagesBefore( + "alice@example.com", + "hatter@example.com", + ); + }, { borogove, persistence }); + + expect(result.length).toBe(2); + expect(result[0].serverId).toBe("1"); + expect(result[1].serverId).toBe("2"); + }); + + test("getMessagesBefore the end: MUC come back ordered by sortId, PM by timestamp", async ({ + page, + borogove, + persistence, + }) => { + const result = await page.evaluate(async ({ borogove, persistence }) => { + const builder = new borogove.ChatMessageBuilder({ + serverId: "1", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:01Z", + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder.replyTo = [builder.from.asBare()]; + + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "2", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:00Z", + }); + builder2.sortId = "b0"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder2.replyTo = [builder.from.asBare()]; + + const builder3 = new borogove.ChatMessageBuilder({ + serverId: "3", + serverIdBy: "alice@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannelPrivate, + timestamp: "2020-01-01T00:00:03Z", + }); + builder3.sortId = "a0"; + builder3.to = borogove.JID.parse("alice@example.com"); + builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder3.replyTo = [builder.from.asBare()]; + + await persistence.storeMessages("alice@example.com", [ + builder2.build(), + builder3.build(), + builder.build(), + ]); + + return await persistence.getMessagesBefore( + "alice@example.com", + "teaparty@example.com", + ); + }, { borogove, persistence }); + + expect(result.length).toBe(3); + expect(result[0].serverId).toBe("1"); + expect(result[1].serverId).toBe("2"); + expect(result[2].serverId).toBe("3"); + }); + + test("getMessagesBefore some point: MUC come back ordered by sortId, PM by timestamp", async ({ + page, + borogove, + persistence, + }) => { + const result = await page.evaluate(async ({ borogove, persistence }) => { + const builder = new borogove.ChatMessageBuilder({ + serverId: "1", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:01Z", + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder.replyTo = [builder.from.asBare()]; + + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "2", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:00Z", + }); + builder2.sortId = "b0"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder2.replyTo = [builder.from.asBare()]; + + const builder3 = new borogove.ChatMessageBuilder({ + serverId: "3", + serverIdBy: "alice@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannelPrivate, + timestamp: "2020-01-01T00:00:03Z", + }); + builder3.sortId = "Z~"; + builder3.to = borogove.JID.parse("alice@example.com"); + builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder3.replyTo = [builder.from.asBare()]; + + const builder4 = new borogove.ChatMessageBuilder({ + serverId: "4", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:04Z", + }); + builder4.sortId = "c0"; + builder4.to = borogove.JID.parse("alice@example.com"); + builder4.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder4.replyTo = [builder.from.asBare()]; + + await persistence.storeMessages("alice@example.com", [ + builder2.build(), + builder4.build(), + builder3.build(), + builder.build(), + ]); + + return await persistence.getMessagesBefore( + "alice@example.com", + "teaparty@example.com", + builder4.build(), + ); + }, { borogove, persistence }); + + expect(result.length).toBe(3); + expect(result[0].serverId).toBe("1"); + expect(result[1].serverId).toBe("2"); + expect(result[2].serverId).toBe("3"); + }); + + test("getMessagesBefore a PM", async ({ page, borogove, persistence }) => { + const result = await page.evaluate(async ({ borogove, persistence }) => { + const builder = new borogove.ChatMessageBuilder({ + serverId: "1", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:00Z", + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder.replyTo = [builder.from.asBare()]; + + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "2", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:01Z", + }); + builder2.sortId = "b0"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder2.replyTo = [builder.from.asBare()]; + + const builder3 = new borogove.ChatMessageBuilder({ + serverId: "3", + serverIdBy: "alice@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannelPrivate, + timestamp: "2020-01-01T00:00:03Z", + }); + builder3.sortId = "Z~"; + builder3.to = borogove.JID.parse("alice@example.com"); + builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder3.replyTo = [builder.from.asBare()]; + + const builder4 = new borogove.ChatMessageBuilder({ + serverId: "4", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:04Z", + }); + builder4.sortId = "c0"; + builder4.to = borogove.JID.parse("alice@example.com"); + builder4.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder4.replyTo = [builder.from.asBare()]; + + await persistence.storeMessages("alice@example.com", [ + builder2.build(), + builder4.build(), + builder3.build(), + builder.build(), + ]); + + return await persistence.getMessagesBefore( + "alice@example.com", + "teaparty@example.com", + builder3.build(), + ); + }, { borogove, persistence }); + + expect(result.length).toBe(2); + expect(result[0].serverId).toBe("1"); + expect(result[1].serverId).toBe("2"); + }); + + test("getMessagesAfter the start: MUC come back ordered by sortId, PM by timestamp", async ({ + page, + borogove, + persistence, + }) => { + const result = await page.evaluate(async ({ borogove, persistence }) => { + const builder = new borogove.ChatMessageBuilder({ + serverId: "1", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:00Z", + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder.replyTo = [builder.from.asBare()]; + + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "2", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:01Z", + }); + builder2.sortId = "b0"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder2.replyTo = [builder.from.asBare()]; + + const builder3 = new borogove.ChatMessageBuilder({ + serverId: "3", + serverIdBy: "alice@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannelPrivate, + timestamp: "2020-01-01T00:00:03Z", + }); + builder3.sortId = "a1"; + builder3.to = borogove.JID.parse("alice@example.com"); + builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder3.replyTo = [builder.from.asBare()]; + + await persistence.storeMessages("alice@example.com", [ + builder2.build(), + builder3.build(), + builder.build(), + ]); + + return await persistence.getMessagesAfter( + "alice@example.com", + "teaparty@example.com", + ); + }, { borogove, persistence }); + + expect(result.length).toBe(3); + expect(result[0].serverId).toBe("1"); + expect(result[1].serverId).toBe("2"); + expect(result[2].serverId).toBe("3"); + }); + + test("getMessagesAfter some point: MUC come back ordered by sortId, PM by timestamp", async ({ + page, + borogove, + persistence, + }) => { + const result = await page.evaluate(async ({ borogove, persistence }) => { + const builder = new borogove.ChatMessageBuilder({ + serverId: "1", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:01Z", + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder.replyTo = [builder.from.asBare()]; + + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "2", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:00Z", + }); + builder2.sortId = "b0"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder2.replyTo = [builder.from.asBare()]; + + const builder3 = new borogove.ChatMessageBuilder({ + serverId: "3", + serverIdBy: "alice@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannelPrivate, + timestamp: "2020-01-01T00:00:03Z", + }); + builder3.sortId = "Z~"; + builder3.to = borogove.JID.parse("alice@example.com"); + builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder3.replyTo = [builder.from.asBare()]; + + const builder4 = new borogove.ChatMessageBuilder({ + serverId: "4", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:04Z", + }); + builder4.sortId = "c0"; + builder4.to = borogove.JID.parse("alice@example.com"); + builder4.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder4.replyTo = [builder.from.asBare()]; + + await persistence.storeMessages("alice@example.com", [ + builder2.build(), + builder4.build(), + builder3.build(), + builder.build(), + ]); + + return await persistence.getMessagesAfter( + "alice@example.com", + "teaparty@example.com", + builder.build(), + ); + }, { borogove, persistence }); + + expect(result.length).toBe(3); + expect(result[0].serverId).toBe("2"); + expect(result[1].serverId).toBe("3"); + expect(result[2].serverId).toBe("4"); + }); + + test("getMessagesAfter a PM", async ({ page, borogove, persistence }) => { + const result = await page.evaluate(async ({ borogove, persistence }) => { + const builder = new borogove.ChatMessageBuilder({ + serverId: "1", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:00Z", + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder.replyTo = [builder.from.asBare()]; + + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "2", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:01Z", + }); + builder2.sortId = "b0"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder2.replyTo = [builder.from.asBare()]; + + const builder3 = new borogove.ChatMessageBuilder({ + serverId: "3", + serverIdBy: "alice@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannelPrivate, + timestamp: "2020-01-01T00:00:03Z", + }); + builder3.sortId = "Z~"; + builder3.to = borogove.JID.parse("alice@example.com"); + builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder3.replyTo = [builder.from.asBare()]; + + const builder4 = new borogove.ChatMessageBuilder({ + serverId: "4", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:04Z", + }); + builder4.sortId = "c0"; + builder4.to = borogove.JID.parse("alice@example.com"); + builder4.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder4.replyTo = [builder.from.asBare()]; + + await persistence.storeMessages("alice@example.com", [ + builder2.build(), + builder4.build(), + builder3.build(), + builder.build(), + ]); + + return await persistence.getMessagesAfter( + "alice@example.com", + "teaparty@example.com", + builder3.build(), + ); + }, { borogove, persistence }); + + expect(result.length).toBe(1); + expect(result[0].serverId).toBe("4"); + }); + + test("updateMessageStatus", async ({ page, borogove, persistence }) => { + const result = await page.evaluate(async ({ borogove, persistence }) => { + const builder = new borogove.ChatMessageBuilder({ + localId: "loc1", + senderId: "alice@example.com", + direction: 1, // MessageSent + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("hatter@example.com"); + builder.from = borogove.JID.parse("alice@example.com"); + builder.recipients = [builder.to]; + builder.replyTo = [builder.from]; + await persistence.storeMessages("alice@example.com", [builder.build()]); + + const updated = await persistence.updateMessageStatus( + "alice@example.com", + "loc1", + 1, + "Delivered", + ); // MessageDelivered + return { status: updated.status, statusText: updated.statusText }; + }, { borogove, persistence }); + + expect(result.status).toBe(1); + expect(result.statusText).toBe("Delivered"); + }); + + test("removeAccount and listAccounts", async ({ page, borogove, persistence }) => { + const result = await page.evaluate(async ({ borogove, persistence }) => { + await persistence.storeLogin("alice@example.com", "client1", "Alice", null); + await persistence.storeLogin("bob@example.com", "client2", "Bob", null); + + const accountsBefore = await persistence.listAccounts(); + await persistence.removeAccount("alice@example.com", true); + const accountsAfter = await persistence.listAccounts(); + + return { accountsBefore, accountsAfter }; + }, { borogove, persistence }); + + expect(result.accountsBefore).toContain("alice@example.com"); + expect(result.accountsBefore).toContain("bob@example.com"); + expect(result.accountsAfter).not.toContain("alice@example.com"); + expect(result.accountsAfter).toContain("bob@example.com"); + }); + + test("getChatUnreadDetails", async ({ page, borogove, persistence }) => { + const result = await page.evaluate(async ({ borogove, persistence }) => { + const chat = Object.create(borogove.DirectChat.prototype); + chat.chatId = "hatter@example.com"; + chat.readUpToId = "srv1"; + chat.notificationsFiltered = () => false; + + const builder = new borogove.ChatMessageBuilder({ + serverId: "srv1", + serverIdBy: "hatter@example.com", + senderId: "hatter@example.com", + direction: 0, + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("hatter@example.com"); + builder.recipients = [builder.to]; + builder.replyTo = [builder.from]; + + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "srv2", + serverIdBy: "hatter@example.com", + senderId: "hatter@example.com", + direction: 0, + }); + builder2.sortId = "a1"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("hatter@example.com"); + builder2.recipients = [builder2.to]; + builder2.replyTo = [builder2.from]; + + await persistence.storeMessages("alice@example.com", [ + builder.build(), + builder2.build(), + ]); + + return await persistence.getChatUnreadDetails("alice@example.com", chat); + }, { borogove, persistence }); + + expect(result.unreadCount).toBe(1); + expect(result.message.serverId).toBe("srv2"); + }); + + test("media storage functions", async ({ page, borogove, persistence }) => { + const result = await page.evaluate(async ({ borogove, persistence }) => { + function bufferToByteStream(buffer) { + let done = false; + return new ReadableStream({ + type: "bytes", + async pull(controller) { + if (done) { + controller.close(); + } else { + controller.enqueue(buffer); + done = true; + } + }, + }); + } + + const buffer = new Uint8Array([1, 2, 3]); + const sha256 = await crypto.subtle.digest("SHA-256", buffer.buffer); + await persistence.storeMedia("image/png", bufferToByteStream(buffer)); + const hash = new borogove.Hash("sha-256", sha256); + const hasBefore = await persistence.hasMedia(hash); + await persistence.removeMedia("sha-256", sha256); + const hasAfter = await persistence.hasMedia(hash); + + return { hasBefore, hasAfter }; + }, { borogove, persistence }); + + expect(result.hasBefore).toBe( + "/.well-known/ni/sha-256/A5BYxvLAy0ksUzsKTRTvd8wPeKvMztUofYShogEc-4E", + ); + expect(result.hasAfter).toBe(null); + }); + + test("storeStreamManamagement and getStreamManagement", async ({ page, borogove, persistence }) => { + const result = await page.evaluate(async ({ borogove, persistence }) => { + await persistence.storeLogin("alice@example.com", "", "", null); // or updating with SM may not work + await persistence.storeStreamManagement( + "alice@example.com", + new Uint8Array([1, 2, 0, 4]).buffer, + "ZZ", + ); + const result = await persistence.getStreamManagement("alice@example.com"); + return { + smIsArrayBuffer: result.sm instanceof ArrayBuffer, + smIsEq: result.sm + ? indexedDB.cmp(result.sm, new Uint8Array([1, 2, 0, 4]).buffer) + : "null", + sortId: result.sortId, + }; + }, { borogove, persistence }); + + expect(result.smIsEq).toBe(0); + expect(result.smIsArrayBuffer).toBe(true); + expect(result.sortId).toBe("ZZ"); + }); + + test("getMembers hydrates persisted member data", async ({ page, borogove, persistence }) => { + const result = await page.evaluate(async ({ borogove, persistence }) => { + const chat = Object.create(borogove.Channel.prototype); + chat.chatId = "room-members-1@example.com"; + chat.getDisplayName = () => "Tea Room"; + + const member = { + id: "room-members-1@example.com/occ-1", + displayName: "Alice", + photoUri: "photo:alice", + isSelf: false, + roles: [{ id: "admin", title: "Admin" }], + jid: borogove.JID.parse("alice@example.com"), + presence: new Map([ + [ + "laptop", + borogove.Stanza.parse("<presence><show>away</show></presence>"), + ], + ]), + chat: { chatId: "alice@example.com" }, + }; + + await persistence.storeMembers("alice@example.com", chat.chatId, [member]); + const [stored] = await persistence.getMembers( + "alice@example.com", + chat, + false, + ); + + return { + id: stored.id, + displayName: stored.displayName, + chatId: stored.chat?.chatId, + roleIds: stored.roles.map((r) => r.id), + presenceKeys: [...stored.presence.keys()], + showPresence: stored.showPresence, + }; + }, { borogove, persistence }); + + expect(result.id).toBe("room-members-1@example.com/occ-1"); + expect(result.displayName).toBe("Alice"); + expect(result.chatId).toBe("alice@example.com"); + expect(result.roleIds).toEqual(["admin"]); + expect(result.presenceKeys).toEqual(["laptop"]); + expect(result.showPresence).toBe(1); + }); + + test("getMemberDetails returns null for incomplete rows", async ({ + page, + borogove, + createChannel, + persistence, + storeIncompleteMember, + }) => { + const result = await page.evaluate( + async ({ + borogove, + createChannel, + persistence, + storeIncompleteMember, + }) => { + const chat = createChannel( + persistence, + "room-members-7@example.com", + ); + chat.displayName = "A Chat"; + chat.trusted = true; + + await persistence.storeMembers("alice@example.com", chat.chatId, [ + { + id: "room-members-7@example.com/admin", + displayName: "Alpha", + photoUri: null, + isSelf: false, + roles: [{ id: "admin", title: "Admin" }], + jid: borogove.JID.parse("alpha@example.com"), + presence: new Map([ + ["desk", borogove.Stanza.parse("<presence />")], + ]), + chat: { chatId: "alpha@example.com" }, + }, + ]); + + await storeIncompleteMember(chat.chatId); + const details = await persistence.getMemberDetails( + "alice@example.com", + chat, + [ + "room-members-7@example.com/admin", + "room-members-7@example.com/incomplete", + ], + ); + return details.map((m) => (m ? m.displayName : null)); + }, + { + borogove, + createChannel, + persistence, + storeIncompleteMember, + }, + ); + + expect(result).toEqual(["Alpha", null]); + }); + + test("storeMemberUpdates merges existing member data", async ({ page, borogove, persistence }) => { + const result = await page.evaluate(async ({ borogove, persistence }) => { + const chat = Object.create(borogove.Channel.prototype); + chat.chatId = "room-members-2@example.com"; + chat.getDisplayName = () => "Tea Room"; + + await persistence.storeMembers("alice@example.com", chat.chatId, [ + { + id: "room-members-2@example.com/occ-1", + displayName: "Alice", + photoUri: null, + isSelf: false, + roles: [ + { id: "admin", title: "Admin" }, + { id: "urn:xmpp:hats:test", title: "Tea Host" }, + ], + jid: borogove.JID.parse("alice@example.com"), + presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), + chat: { chatId: "alice@example.com" }, + }, + ]); + + const updates = [ + new borogove.MemberUpdate( + "room-members-2@example.com/occ-1", + borogove.JID.parse("alice@example.com"), + "Alice Cooper", + false, + null, + new Map([["mobile", borogove.Stanza.parse("<presence />")]]), + ), + ]; + + const updated = await persistence.storeMemberUpdates( + "alice@example.com", + chat, + updates, + false, + ); + + return { + updatedRoleIds: updated[0].roles.map((r) => r.id), + updatedPresenceKeys: [...updated[0].presence.keys()].sort(), + updatedDisplayName: updated[0].displayName, + }; + }, { borogove, persistence }); + + expect(result.updatedRoleIds).toEqual(["urn:xmpp:hats:test"]); + expect(result.updatedPresenceKeys).toEqual(["desk", "mobile"]); + expect(result.updatedDisplayName).toBe("Alice Cooper"); + }); + + test("storeMemberUpdates clears omitted full-list affiliations", async ({ + page, + borogove, + persistence, + }) => { + const result = await page.evaluate(async ({ borogove, persistence }) => { + const chat = Object.create(borogove.Channel.prototype); + chat.chatId = "room-members-2b@example.com"; + chat.getDisplayName = () => "Tea Room"; + + await persistence.storeMembers("alice@example.com", chat.chatId, [ + { + id: "room-members-2b@example.com/occ-1", + displayName: "Alice", + photoUri: null, + isSelf: false, + roles: [{ id: "admin", title: "Admin" }], + jid: borogove.JID.parse("alice@example.com"), + presence: new Map(), + chat: { chatId: "alice@example.com" }, + }, + { + id: "room-members-2b@example.com/occ-2", + displayName: "Bob", + photoUri: null, + isSelf: false, + roles: [{ id: "owner", title: "Owner" }], + jid: borogove.JID.parse("bob@example.com"), + presence: new Map(), + chat: { chatId: "bob@example.com" }, + }, + ]); + + await persistence.storeMemberUpdates( + "alice@example.com", + chat, + [ + new borogove.MemberUpdate( + "room-members-2b@example.com/occ-1", + borogove.JID.parse("alice@example.com"), + "Alice", + false, + null, + new Map(), + ), + ], + true, + ); + const members = await persistence.getMembers( + "alice@example.com", + chat, + true, + ); + + return members.find((m) => m.id.endsWith("occ-2")).roles.map((r) => r.id); + }, { borogove, persistence }); + + expect(result).toEqual([]); + }); + + test("storeMemberUpdates matches existing member by true JID", async ({ + page, + borogove, + persistence, + }) => { + const result = await page.evaluate(async ({ borogove, persistence }) => { + const chat1 = Object.create(borogove.Channel.prototype); + chat1.chatId = "room-members-3@example.com"; + chat1.getDisplayName = () => "Tea Room"; + const chat2 = Object.create(borogove.Channel.prototype); + chat2.chatId = "room-members-4@example.com"; + chat2.getDisplayName = () => "Other Room"; + + await persistence.storeMembers("alice@example.com", chat1.chatId, [ + { + id: "room-members-3@example.com/occ-1", + displayName: "Alice", + photoUri: null, + isSelf: false, + roles: [{ id: "admin", title: "Admin" }], + jid: borogove.JID.parse("alice@example.com"), + presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), + chat: { chatId: "alice@example.com" }, + }, + { + id: "room-members-4@example.com/occ-1", + displayName: "Bob", + photoUri: null, + isSelf: false, + roles: [{ id: "admin", title: "Admin" }], + jid: borogove.JID.parse("bob@example.com"), + presence: new Map([["phone", borogove.Stanza.parse("<presence />")]]), + chat: { chatId: "bob@example.com" }, + }, + ]); + + await persistence.storeMemberUpdates( + "alice@example.com", + chat1, + [ + new borogove.MemberUpdate( + null, + borogove.JID.parse("alice@example.com"), + "Alice Renamed", + false, + null, + new Map(), + ), + ], + false, + ); + const [chat1Member] = await persistence.getMemberDetails( + "alice@example.com", + chat1, + ["room-members-3@example.com/occ-1"], + ); + + return chat1Member.displayName; + }, { borogove, persistence }); + + expect(result).toBe("Alice Renamed"); + }); + + test("clearMemberPresence only clears the targeted chat", async ({ page, borogove, persistence }) => { + const result = await page.evaluate(async ({ borogove, persistence }) => { + const chat1 = Object.create(borogove.Channel.prototype); + chat1.chatId = "room-members-4a@example.com"; + chat1.getDisplayName = () => "Tea Room"; + const chat2 = Object.create(borogove.Channel.prototype); + chat2.chatId = "room-members-4b@example.com"; + chat2.getDisplayName = () => "Other Room"; + + await persistence.storeMembers("alice@example.com", chat1.chatId, [ + { + id: "room-members-4a@example.com/occ-1", + displayName: "Alice", + photoUri: null, + isSelf: false, + roles: [{ id: "admin", title: "Admin" }], + jid: borogove.JID.parse("alice@example.com"), + presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), + chat: { chatId: "alice@example.com" }, + }, + ]); + await persistence.storeMembers("alice@example.com", chat2.chatId, [ + { + id: "room-members-4b@example.com/occ-1", + displayName: "Bob", + photoUri: null, + isSelf: false, + roles: [{ id: "admin", title: "Admin" }], + jid: borogove.JID.parse("bob@example.com"), + presence: new Map([["phone", borogove.Stanza.parse("<presence />")]]), + chat: { chatId: "bob@example.com" }, + }, + ]); + + await persistence.clearMemberPresence("alice@example.com", chat1.chatId); + const [chat1Member] = await persistence.getMemberDetails( + "alice@example.com", + chat1, + ["room-members-4a@example.com/occ-1"], + ); + const [chat2Member] = await persistence.getMemberDetails( + "alice@example.com", + chat2, + ["room-members-4b@example.com/occ-1"], + ); + + return { + chat1PresenceKeys: [...chat1Member.presence.keys()], + chat2PresenceKeys: [...chat2Member.presence.keys()], + }; + }, { borogove, persistence }); + + expect(result.chat1PresenceKeys).toEqual([]); + expect(result.chat2PresenceKeys).toEqual(["phone"]); + }); + + test("getMembers includes moderator-visible rows", async ({ page, borogove, persistence }) => { + const result = await page.evaluate(async ({ borogove, persistence }) => { + const chat = Object.create(borogove.Channel.prototype); + chat.chatId = "room-members-6@example.com"; + chat.getDisplayName = () => "Tea Room"; + + await persistence.storeMembers("alice@example.com", chat.chatId, [ + { + id: "room-members-6@example.com/owner", + displayName: "Zulu", + photoUri: null, + isSelf: false, + roles: [{ id: "owner", title: "Owner" }], + jid: borogove.JID.parse("zulu@example.com"), + presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), + chat: { chatId: "zulu@example.com" }, + }, + { + id: "room-members-6@example.com/outcast", + displayName: "Banned", + photoUri: null, + isSelf: false, + roles: [{ id: "outcast", title: "Banned" }], + jid: borogove.JID.parse("banned@example.com"), + presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), + chat: { chatId: "banned@example.com" }, + }, + { + id: "room-members-6@example.com/guest-offline", + displayName: "Guest", + photoUri: null, + isSelf: false, + roles: [{ id: "none", title: "Guest" }], + jid: borogove.JID.parse("guest@example.com"), + presence: new Map([ + ["desk", borogove.Stanza.parse('<presence type="unavailable" />')], + ]), + chat: { chatId: "guest@example.com" }, + }, + { + id: "room-members-6@example.com/admin", + displayName: "Alpha", + photoUri: null, + isSelf: false, + roles: [{ id: "admin", title: "Admin" }], + jid: borogove.JID.parse("alpha@example.com"), + presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), + chat: { chatId: "alpha@example.com" }, + }, + ]); + + const moderator = await persistence.getMembers( + "alice@example.com", + chat, + true, + ); + return moderator.map((m) => m.displayName); + }, { borogove, persistence }); + + expect(result).toEqual(["Zulu", "Alpha", "Banned"]); + }); + + test("storeVoiceRequest and listVoiceRequests", async ({ page, borogove, persistence }) => { + const result = await page.evaluate(async ({ borogove, persistence }) => { + const chat = Object.create(borogove.Channel.prototype); + chat.chatId = "room-voice-requests@example.com"; + chat.getDisplayName = () => "Tea Room"; + + await persistence.storeMembers("alice@example.com", chat.chatId, [ + { + id: "room-voice-requests@example.com/occ-1", + displayName: "Bob", + photoUri: null, + isSelf: false, + roles: [{ id: "none", title: "Participant" }], + jid: borogove.JID.parse("bob@example.com"), + presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), + chat: { chatId: "bob@example.com" }, + }, + { + id: "room-voice-requests@example.com/occ-2", + displayName: "Charlie", + photoUri: null, + isSelf: false, + roles: [{ id: "none", title: "Participant" }], + jid: borogove.JID.parse("charlie@example.com"), + presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), + chat: { chatId: "charlie@example.com" }, + } + ]); + + await persistence.storeVoiceRequest("alice@example.com", chat, "bob@example.com", true); + await persistence.storeVoiceRequest("alice@example.com", chat, "charlie@example.com", true); + + const requests1 = await persistence.listVoiceRequests("alice@example.com", chat); + + await persistence.storeVoiceRequest("alice@example.com", chat, "bob@example.com", false); + + const requests2 = await persistence.listVoiceRequests("alice@example.com", chat); + + return { + requests1: requests1.map((m) => m.displayName).sort(), + requests2: requests2.map((m) => m.displayName).sort(), + }; + }, { borogove, persistence }); + + expect(result.requests1).toEqual(["Bob", "Charlie"]); + expect(result.requests2).toEqual(["Charlie"]); + }); +} diff --git a/test/sqlite.spec.ts b/test/sqlite.spec.ts index 35f636d..22af01e 100644 --- a/test/sqlite.spec.ts +++ b/test/sqlite.spec.ts @@ -1,4 +1,5 @@ import { sqliteTest as test, expect } from "./browser-test"; +import { sharedPersistenceTests } from "./persistence-tests"; test.describe("not webkit", () => { test.skip( @@ -6,1483 +7,7 @@ test.describe("not webkit", () => { "Skip on webkit because the version in playwright lacks OPFS", ); - test("1:1 come back ordered by sortId", async ({ page, borogove, persistence }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "1", - serverIdBy: "alice@example.com", - senderId: "hatter@example.com", - direction: 0, - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("hatter@example.com"); - builder.recipients = [builder.to]; - builder.replyTo = [builder.from]; - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "2", - serverIdBy: "alice@example.com", - senderId: "hatter@example.com", - direction: 0, - }); - builder2.sortId = "b0"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("hatter@example.com"); - builder2.recipients = [builder2.to]; - builder2.replyTo = [builder2.from]; - - try { - await persistence.storeMessages("alice@example.com", [ - builder2.build(), - builder.build(), - ]); - - return await persistence.getMessagesBefore( - "alice@example.com", - "hatter@example.com", - ); - } catch (e) { - console.error(e, e.result); - throw e.result ? JSON.stringify(e.result) : e.message; - } - }, - { borogove, persistence }, - ); - - expect(result.length).toBe(2); - expect(result[0].serverId).toBe("1"); - expect(result[1].serverId).toBe("2"); - }); - - test("getMessagesBefore the end: MUC come back ordered by sortId, PM by timestamp", async ({ - page, - borogove, - persistence, - }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "1", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:01Z", - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder.replyTo = [builder.from.asBare()]; - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "2", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:00Z", - }); - builder2.sortId = "b0"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder2.replyTo = [builder2.from.asBare()]; - - const builder3 = new borogove.ChatMessageBuilder({ - serverId: "3", - serverIdBy: "alice@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannelPrivate, - timestamp: "2020-01-01T00:00:03Z", - }); - builder3.sortId = "a0"; - builder3.to = borogove.JID.parse("alice@example.com"); - builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder3.replyTo = [builder3.from.asBare()]; - - try { - await persistence.storeMessages("alice@example.com", [ - builder2.build(), - builder3.build(), - builder.build(), - ]); - return await persistence.getMessagesBefore( - "alice@example.com", - "teaparty@example.com", - ); - } catch (e) { - console.error(e, e.result); - throw e.result ? JSON.stringify(e.result) : e.message; - } - }, - { borogove, persistence }, - ); - - expect(result.length).toBe(3); - expect(result[0].serverId).toBe("1"); - expect(result[1].serverId).toBe("2"); - expect(result[2].serverId).toBe("3"); - }); - - test("getMessagesBefore some point: MUC come back ordered by sortId, PM by timestamp", async ({ - page, - borogove, - persistence, - }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "1", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:01Z", - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder.replyTo = [builder.from.asBare()]; - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "2", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:00Z", - }); - builder2.sortId = "b0"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder2.replyTo = [builder2.from.asBare()]; - - const builder3 = new borogove.ChatMessageBuilder({ - serverId: "3", - serverIdBy: "alice@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannelPrivate, - timestamp: "2020-01-01T00:00:03Z", - }); - builder3.sortId = "Z~"; - builder3.to = borogove.JID.parse("alice@example.com"); - builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder3.replyTo = [builder3.from.asBare()]; - - const builder4 = new borogove.ChatMessageBuilder({ - serverId: "4", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:04Z", - }); - builder4.sortId = "c0"; - builder4.to = borogove.JID.parse("alice@example.com"); - builder4.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder4.replyTo = [builder4.from.asBare()]; - - try { - await persistence.storeMessages("alice@example.com", [ - builder2.build(), - builder4.build(), - builder3.build(), - builder.build(), - ]); - return await persistence.getMessagesBefore( - "alice@example.com", - "teaparty@example.com", - builder4.build(), - ); - } catch (e) { - console.error(e, e.result); - throw e.result ? JSON.stringify(e.result) : e.message; - } - }, - { borogove, persistence }, - ); - - expect(result.length).toBe(3); - expect(result[0].serverId).toBe("1"); - expect(result[1].serverId).toBe("2"); - expect(result[2].serverId).toBe("3"); - }); - - test("getMessagesBefore a PM", async ({ page, borogove, persistence }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "1", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:00Z", - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder.replyTo = [builder.from.asBare()]; - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "2", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:01Z", - }); - builder2.sortId = "b0"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder2.replyTo = [builder2.from.asBare()]; - - const builder3 = new borogove.ChatMessageBuilder({ - serverId: "3", - serverIdBy: "alice@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannelPrivate, - timestamp: "2020-01-01T00:00:03Z", - }); - builder3.sortId = "Z~"; - builder3.to = borogove.JID.parse("alice@example.com"); - builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder3.replyTo = [builder3.from.asBare()]; - - const builder4 = new borogove.ChatMessageBuilder({ - serverId: "4", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:04Z", - }); - builder4.sortId = "c0"; - builder4.to = borogove.JID.parse("alice@example.com"); - builder4.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder4.replyTo = [builder4.from.asBare()]; - - try { - await persistence.storeMessages("alice@example.com", [ - builder2.build(), - builder4.build(), - builder3.build(), - builder.build(), - ]); - return await persistence.getMessagesBefore( - "alice@example.com", - "teaparty@example.com", - builder3.build(), - ); - } catch (e) { - console.error(e, e.result); - throw e.result ? JSON.stringify(e.result) : e.message; - } - }, - { borogove, persistence }, - ); - - expect(result.length).toBe(2); - expect(result[0].serverId).toBe("1"); - expect(result[1].serverId).toBe("2"); - }); - - test("getMessagesAfter the start: MUC come back ordered by sortId, PM by timestamp", async ({ - page, - borogove, - persistence, - }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "1", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:00Z", - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder.replyTo = [builder.from.asBare()]; - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "2", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:01Z", - }); - builder2.sortId = "b0"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder2.replyTo = [builder2.from.asBare()]; - - const builder3 = new borogove.ChatMessageBuilder({ - serverId: "3", - serverIdBy: "alice@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannelPrivate, - timestamp: "2020-01-01T00:00:03Z", - }); - builder3.sortId = "a1"; - builder3.to = borogove.JID.parse("alice@example.com"); - builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder3.replyTo = [builder3.from.asBare()]; - - try { - await persistence.storeMessages("alice@example.com", [ - builder2.build(), - builder3.build(), - builder.build(), - ]); - return await persistence.getMessagesAfter( - "alice@example.com", - "teaparty@example.com", - ); - } catch (e) { - console.error(e, e.result); - throw e.result ? JSON.stringify(e.result) : e.message; - } - }, - { borogove, persistence }, - ); - - expect(result.length).toBe(3); - expect(result[0].serverId).toBe("1"); - expect(result[1].serverId).toBe("2"); - expect(result[2].serverId).toBe("3"); - }); - - test("getMessagesAfter some point: MUC come back ordered by sortId, PM by timestamp", async ({ - page, - borogove, - persistence, - }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "1", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:01Z", - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder.replyTo = [builder.from.asBare()]; - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "2", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:00Z", - }); - builder2.sortId = "b0"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder2.replyTo = [builder2.from.asBare()]; - - const builder3 = new borogove.ChatMessageBuilder({ - serverId: "3", - serverIdBy: "alice@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannelPrivate, - timestamp: "2020-01-01T00:00:03Z", - }); - builder3.sortId = "Z~"; - builder3.to = borogove.JID.parse("alice@example.com"); - builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder3.replyTo = [builder3.from.asBare()]; - - const builder4 = new borogove.ChatMessageBuilder({ - serverId: "4", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:04Z", - }); - builder4.sortId = "c0"; - builder4.to = borogove.JID.parse("alice@example.com"); - builder4.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder4.replyTo = [builder4.from.asBare()]; - - try { - await persistence.storeMessages("alice@example.com", [ - builder2.build(), - builder4.build(), - builder3.build(), - builder.build(), - ]); - return await persistence.getMessagesAfter( - "alice@example.com", - "teaparty@example.com", - builder.build(), - ); - } catch (e) { - console.error(e, e.result); - throw e.result ? JSON.stringify(e.result) : e.message; - } - }, - { borogove, persistence }, - ); - - expect(result.length).toBe(3); - expect(result[0].serverId).toBe("2"); - expect(result[1].serverId).toBe("3"); - expect(result[2].serverId).toBe("4"); - }); - - test("getMessagesAfter a PM", async ({ page, borogove, persistence }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "1", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:00Z", - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder.replyTo = [builder.from.asBare()]; - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "2", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:01Z", - }); - builder2.sortId = "b0"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder2.replyTo = [builder2.from.asBare()]; - - const builder3 = new borogove.ChatMessageBuilder({ - serverId: "3", - serverIdBy: "alice@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannelPrivate, - timestamp: "2020-01-01T00:00:03Z", - }); - builder3.sortId = "Z~"; - builder3.to = borogove.JID.parse("alice@example.com"); - builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder3.replyTo = [builder3.from.asBare()]; - - const builder4 = new borogove.ChatMessageBuilder({ - serverId: "4", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:04Z", - }); - builder4.sortId = "c0"; - builder4.to = borogove.JID.parse("alice@example.com"); - builder4.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder4.replyTo = [builder4.from.asBare()]; - - try { - await persistence.storeMessages("alice@example.com", [ - builder2.build(), - builder4.build(), - builder3.build(), - builder.build(), - ]); - return await persistence.getMessagesAfter( - "alice@example.com", - "teaparty@example.com", - builder3.build(), - ); - } catch (e) { - console.error(e, e.result); - throw e.result ? JSON.stringify(e.result) : e.message; - } - }, - { borogove, persistence }, - ); - - expect(result.length).toBe(1); - expect(result[0].serverId).toBe("4"); - }); - - test("storeChats and getChats", async ({ page, borogove, persistence }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const chat = new borogove.DirectChat( - null, - null, - persistence, - "hatter@example.com", - ); - chat.displayName = "The Mad Hatter"; - chat.trusted = true; - chat.threads = new Map([ - [null, "Tea Time"], - ["thread-1", "Introductions"], - ]); - - try { - await persistence.storeChats("alice@example.com", [chat]); - await new Promise((resolve) => setTimeout(resolve, 200)); - const chats = await persistence.getChats("alice@example.com"); - return { - length: chats.length, - chatId: chats[0]?.chatId, - displayName: chats[0]?.displayName, - trusted: chats[0]?.trusted, - klass: chats[0]?.klass, - channelSubject: chats[0]?.threads?.get(null), - threadSubject: chats[0]?.threads?.get("thread-1"), - }; - } catch (e) { - console.error(e, e.result); - throw e.result ? JSON.stringify(e.result) : e.message; - } - }, - { borogove, persistence }, - ); - - expect(result.length).toBe(1); - expect(result.chatId).toBe("hatter@example.com"); - expect(result.displayName).toBe("The Mad Hatter"); - expect(result.trusted).toBe(true); - expect(result.klass).toBe("DirectChat"); - expect(result.channelSubject).toBe("Tea Time"); - expect(result.threadSubject).toBe("Introductions"); - }); - - test("getMessage by serverId and localId", async ({ page, borogove, persistence }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "srv1", - serverIdBy: "hatter@example.com", - localId: "loc1", - senderId: "hatter@example.com", - direction: 0, - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("hatter@example.com"); - builder.recipients = [builder.to]; - builder.replyTo = [builder.from]; - - try { - await persistence.storeMessages("alice@example.com", [ - builder.build(), - ]); - - const byServerId = await persistence.getMessage( - "alice@example.com", - "hatter@example.com", - "srv1", - null, - ); - const byLocalId = await persistence.getMessage( - "alice@example.com", - "hatter@example.com", - null, - "loc1", - ); - return { - byServerId: byServerId - ? { serverId: byServerId.serverId, localId: byLocalId.localId } - : null, - byLocalId: byLocalId - ? { serverId: byLocalId.serverId, localId: byLocalId.localId } - : null, - }; - } catch (e) { - console.error(e, e.result); - throw e.result ? JSON.stringify(e.result) : e.message; - } - }, - { borogove, persistence }, - ); - - expect(result.byServerId).not.toBeNull(); - expect(result.byServerId.serverId).toBe("srv1"); - expect(result.byLocalId).not.toBeNull(); - expect(result.byLocalId.localId).toBe("loc1"); - }); - - test("storeReaction", async ({ page, borogove, persistence }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "srv1", - serverIdBy: "hatter@example.com", - senderId: "hatter@example.com", - direction: 0, - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("hatter@example.com"); - builder.recipients = [builder.to]; - builder.replyTo = [builder.from]; - - try { - await persistence.storeMessages("alice@example.com", [ - builder.build(), - ]); - - const reaction = new borogove.Reaction( - "alice@example.com", - "2020-01-01T00:00:01Z", - "👍", - ); - const update = new borogove.ReactionUpdate( - "up1", - "srv1", - "hatter@example.com", - null, - "hatter@example.com", - "alice@example.com", - "2020-01-01T00:00:01Z", - [reaction], - borogove.ReactionUpdateKind.EmojiReactions, - ); - const msg = await persistence.storeReaction( - "alice@example.com", - update, - ); - return { - reactions: [...msg.reactions.entries()].map(([k, v]) => ({ - key: k, - count: v.length, - })), - }; - } catch (e) { - console.error(e, e.result); - throw e.result ? JSON.stringify(e.result) : e.message; - } - }, - { borogove, persistence }, - ); - - expect(result.reactions.length).toBe(1); - expect(result.reactions[0].key).toBe("👍"); - expect(result.reactions[0].count).toBe(1); - }); - - test("updateMessageStatus", async ({ page, borogove, persistence }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - localId: "loc1", - senderId: "alice@example.com", - direction: 1, - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("hatter@example.com"); - builder.from = borogove.JID.parse("alice@example.com"); - builder.recipients = [builder.to]; - builder.replyTo = [builder.from]; - - try { - await persistence.storeMessages("alice@example.com", [ - builder.build(), - ]); - - const updated = await persistence.updateMessageStatus( - "alice@example.com", - "loc1", - 1, - "Delivered", - ); - return { status: updated.status, statusText: updated.statusText }; - } catch (e) { - console.error(e, e.result); - throw e.result ? JSON.stringify(e.result) : e.message; - } - }, - { borogove, persistence }, - ); - - expect(result.status).toBe(1); - expect(result.statusText).toBe("Delivered"); - }); - - test("searchMessages", async ({ page, borogove, persistence }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "srv1", - serverIdBy: "hatter@example.com", - senderId: "hatter@example.com", - direction: 0, - }); - builder.sortId = "a0"; - builder.text = "Hello world"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("hatter@example.com"); - builder.recipients = [builder.to]; - builder.replyTo = [builder.from]; - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "srv2", - serverIdBy: "hatter@example.com", - senderId: "hatter@example.com", - direction: 0, - }); - builder2.sortId = "a1"; - builder2.text = "Goodbye world"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("hatter@example.com"); - builder2.recipients = [builder2.to]; - builder2.replyTo = [builder2.from]; - - try { - await persistence.storeMessages("alice@example.com", [ - builder.build(), - builder2.build(), - ]); - const results = await persistence.searchMessages( - "alice@example.com", - "hatter@example.com", - "hello", - ); - return results.map((m) => m.text); - } catch (e) { - console.error(e, e.result); - throw e.result ? JSON.stringify(e.result) : e.message; - } - }, - { borogove, persistence }, - ); - - expect(result.length).toBe(1); - expect(result[0]).toBe("Hello world"); - }); - - test("removeAccount and listAccounts", async ({ page, borogove, persistence }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - try { - await persistence.storeLogin( - "alice@example.com", - "client1", - "Alice", - null, - ); - await persistence.storeLogin( - "bob@example.com", - "client2", - "Bob", - null, - ); - const accountsBefore = await persistence.listAccounts(); - await persistence.removeAccount("alice@example.com", true); - const accountsAfter = await persistence.listAccounts(); - return { accountsBefore, accountsAfter }; - } catch (e) { - console.error(e, e.result); - throw e.result ? JSON.stringify(e.result) : e.message; - } - }, - { borogove, persistence }, - ); - - expect(result.accountsBefore).toContain("alice@example.com"); - expect(result.accountsBefore).toContain("bob@example.com"); - expect(result.accountsAfter).not.toContain("alice@example.com"); - expect(result.accountsAfter).toContain("bob@example.com"); - }); - - test("getChatUnreadDetails", async ({ page, borogove, persistence }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const chat = Object.create(borogove.DirectChat.prototype); - chat.constructor = borogove.DirectChat; - chat.chatId = "hatter@example.com"; - chat.readUpToId = "srv1"; - chat.notificationsFiltered = () => false; - - const builder = new borogove.ChatMessageBuilder({ - serverId: "srv1", - serverIdBy: "hatter@example.com", - senderId: "hatter@example.com", - direction: 0, - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("hatter@example.com"); - builder.recipients = [builder.to]; - builder.replyTo = [builder.from]; - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "srv2", - serverIdBy: "hatter@example.com", - senderId: "hatter@example.com", - direction: 0, - }); - builder2.sortId = "a1"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("hatter@example.com"); - builder2.recipients = [builder2.to]; - builder2.replyTo = [builder2.from]; - - try { - await persistence.storeMessages("alice@example.com", [ - builder.build(), - builder2.build(), - ]); - return await persistence.getChatUnreadDetails( - "alice@example.com", - chat, - ); - } catch (e) { - console.error(e, e.result); - throw e.result ? JSON.stringify(e.result) : e.message; - } - }, - { borogove, persistence }, - ); - - expect(result.unreadCount).toBe(1); - expect(result.message.serverId).toBe("srv2"); - }); - - test("media storage functions", async ({ page, borogove, persistence }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - try { - function bufferToByteStream(buffer) { - let done = false; - return new ReadableStream({ - type: "bytes", - async pull(controller) { - if (done) { - controller.close(); - } else { - controller.enqueue(buffer); - done = true; - } - }, - }); - } - - const buffer = new Uint8Array([1, 2, 3]); - const sha256 = await crypto.subtle.digest("SHA-256", buffer.buffer); - await persistence.storeMedia("image/png", bufferToByteStream(buffer)); - const hash = new borogove.Hash("sha-256", sha256); - const hasBefore = await persistence.hasMedia(hash); - await persistence.removeMedia("sha-256", sha256); - const hasAfter = await persistence.hasMedia(hash); - - return { hasBefore, hasAfter }; - } catch (e) { - console.error(e, e.result); - throw e.result ? JSON.stringify(e.result) : e.message; - } - }, - { borogove, persistence }, - ); - - expect(result.hasBefore).toBe( - "/.well-known/ni/sha-256/A5BYxvLAy0ksUzsKTRTvd8wPeKvMztUofYShogEc-4E", - ); - expect(result.hasAfter).toBe(null); - }); - - test("hydrate message with incomplete replyToMessage", async ({ page, borogove, persistence }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "parent", - serverIdBy: "hatter@example.com", - localId: "loc1", - senderId: "hatter@example.com", - direction: 0, - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("hatter@example.com"); - builder.recipients = [builder.to]; - builder.replyTo = [builder.from]; - builder.type = borogove.MessageType.MessageChannel; - const parentStub = builder.build(); - - builder.setBody(borogove.Html.text("Hello")); - const parentMsg = builder.build(); - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "child", - serverIdBy: "hatter@example.com", - localId: "loc2", - senderId: "hatter@example.com", - direction: 0, - }); - builder2.sortId = "a1"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("hatter@example.com"); - builder2.recipients = [builder2.to]; - builder2.replyTo = [builder2.from]; - builder2.replyToMessage = parentStub; - builder2.type = borogove.MessageType.MessageChannel; - const childMsg = builder2.build(); - - await persistence.storeMessages("alice@example.com", [parentMsg]); - const [childStored] = await persistence.storeMessages( - "alice@example.com", - [childMsg], - ); - - return childStored.replyToMessage.body().toPlainText(); - }, - { borogove, persistence }, - ); - - expect(result).toBe("Hello"); - }); - - test("storeChats and getChats with status", async ({ page, borogove, persistence }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const chat = new borogove.DirectChat( - null, - null, - persistence, - "hatter@example.com", - ); - chat.displayName = "The Mad Hatter"; - chat.trusted = true; - chat.status = new borogove.Status("🎩", "Time for tea!"); - - try { - await persistence.storeChats("alice@example.com", [chat]); - await new Promise((resolve) => setTimeout(resolve, 200)); - const chats = await persistence.getChats("alice@example.com"); - return { - length: chats.length, - chatId: chats[0]?.chatId, - statusEmoji: chats[0]?.status?.emoji, - statusText: chats[0]?.status?.text, - }; - } catch (e) { - console.error(e, e.result); - throw e.result ? JSON.stringify(e.result) : e.message; - } - }, - { borogove, persistence }, - ); - - expect(result.length).toBe(1); - expect(result.chatId).toBe("hatter@example.com"); - expect(result.statusEmoji).toBe("🎩"); - expect(result.statusText).toBe("Time for tea!"); - }); - - test("getChats uses member presence for direct chats", async ({ page, borogove, persistence }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const chat = new borogove.DirectChat( - null, - null, - persistence, - "hatter@example.com", - ); - chat.displayName = "The Mad Hatter"; - chat.trusted = true; - - try { - await persistence.storeChats("alice@example.com", [chat]); - await new Promise((resolve) => setTimeout(resolve, 200)); - await persistence.storeMembers("alice@example.com", chat.chatId, [ - { - id: "hatter@example.com", - displayName: "The Mad Hatter", - photoUri: null, - isSelf: false, - roles: [], - jid: borogove.JID.parse("hatter@example.com"), - presence: new Map([ - ["phone", borogove.Stanza.parse("<presence />")], - ]), - chat: null, - }, - ]); - const [stored] = await persistence.getChats("alice@example.com"); - return [...stored.presence.keys()].sort(); - } catch (e) { - console.error(e, e.result); - throw e.result ? JSON.stringify(e.result) : e.message; - } - }, - { borogove, persistence }, - ); - - expect(result).toEqual(["phone"]); - }); - - test("getChats hydrates membersForName and mavUntil from members", async ({ - page, - borogove, - persistence, - sqlite, - }) => { - const result = await page.evaluate( - async ({ borogove, persistence, sqlite }) => { - const chat = new sqlite.Channel( - null, - null, - persistence, - "room-chat-hydrate@example.com", - ); - chat.displayName = "Tea Room"; - chat.trusted = true; - chat.mavUntil = "2024-05-01T12:00:00Z"; - - await persistence.storeChats("alice@example.com", [chat]); - await new Promise((resolve) => setTimeout(resolve, 200)); - await persistence.storeMembers("alice@example.com", chat.chatId, [ - { - id: chat.chatId, - displayName: "Tea Room", - photoUri: null, - isSelf: false, - roles: [], - jid: borogove.JID.parse(chat.chatId), - presence: new Map(), - chat: null, - }, - { - id: `${chat.chatId}/self`, - displayName: "Myself", - photoUri: null, - isSelf: true, - roles: [{ id: "owner", title: "Owner" }], - jid: borogove.JID.parse("alice@example.com"), - presence: new Map([ - ["desk", borogove.Stanza.parse("<presence />")], - ]), - chat: null, - }, - { - id: `${chat.chatId}/zulu`, - displayName: "Zulu", - photoUri: null, - isSelf: false, - roles: [{ id: "admin", title: "Admin" }], - jid: borogove.JID.parse("zulu@example.com"), - presence: new Map([ - ["desk", borogove.Stanza.parse("<presence />")], - ]), - chat: { chatId: "zulu@example.com" }, - }, - { - id: `${chat.chatId}/alpha`, - displayName: "Alpha", - photoUri: null, - isSelf: false, - roles: [], - jid: borogove.JID.parse("alpha@example.com"), - presence: new Map([ - ["desk", borogove.Stanza.parse("<presence />")], - ]), - chat: { chatId: "alpha@example.com" }, - }, - { - id: `${chat.chatId}/hidden`, - displayName: "Hidden", - photoUri: null, - isSelf: false, - roles: [{ id: "none", title: "Guest" }], - jid: borogove.JID.parse("hidden@example.com"), - presence: new Map([ - ["desk", borogove.Stanza.parse("<presence />")], - ]), - chat: { chatId: "hidden@example.com" }, - }, - ]); - const [stored] = await persistence.getChats("alice@example.com"); - return { - mavUntil: stored.mavUntil, - membersForName: stored.membersForName.map((m) => m.displayName), - presenceKeys: [...stored.presence.keys()].sort(), - }; - }, - { borogove, persistence, sqlite }, - ); - - expect(result.mavUntil).toBe("2024-05-01T12:00:00Z"); - expect(result.membersForName).toEqual(["Alpha", "Zulu"]); - expect(result.presenceKeys).toEqual(["desk"]); - }); - - test("storeStreamManamagement and getStreamManagement", async ({ page, borogove, persistence }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - try { - await persistence.storeLogin("alice@example.com", "", "", null); // or updating with SM may not work - await persistence.storeStreamManagement( - "alice@example.com", - new Uint8Array([1, 2, 0, 4]).buffer, - "ZZ", - ); - const result = - await persistence.getStreamManagement("alice@example.com"); - return { - smIsArrayBuffer: result.sm instanceof ArrayBuffer, - smIsEq: result.sm - ? indexedDB.cmp(result.sm, new Uint8Array([1, 2, 0, 4]).buffer) - : "null", - sortId: result.sortId, - }; - } catch (e) { - console.error(e, e.result); - throw e.result ? JSON.stringify(e.result) : e.message; - } - }, - { borogove, persistence }, - ); - - expect(result.smIsEq).toBe(0); - expect(result.smIsArrayBuffer).toBe(true); - expect(result.sortId).toBe("ZZ"); - }); - - test("getMembers hydrates persisted member data", async ({ page, borogove, persistence }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const chat = new borogove.Channel( - null, - null, - persistence, - "room-members-1@example.com", - ); - chat.displayName = "A Chat"; - chat.trusted = true; - - await persistence.storeMembers("alice@example.com", chat.chatId, [ - { - id: "room-members-1@example.com/occ-1", - displayName: "Alice", - photoUri: "photo:alice", - isSelf: false, - roles: [{ id: "admin", title: "Admin" }], - jid: borogove.JID.parse("alice@example.com"), - presence: new Map([ - [ - "laptop", - borogove.Stanza.parse("<presence><show>away</show></presence>"), - ], - ]), - chat: { chatId: "alice@example.com" }, - }, - ]); - - const members = await persistence.getMembers( - "alice@example.com", - chat, - false, - ); - return { - id: members[0]?.id, - displayName: members[0]?.displayName, - chatId: members[0]?.chat?.chatId, - roleIds: members[0]?.roles?.map((r) => r.id), - presenceKeys: members[0] ? [...members[0].presence.keys()] : [], - showPresence: members[0]?.showPresence, - }; - }, - { borogove, persistence }, - ); - - expect(result.id).toBe("room-members-1@example.com/occ-1"); - expect(result.displayName).toBe("Alice"); - expect(result.chatId).toBe("alice@example.com"); - expect(result.roleIds).toEqual(["admin"]); - expect(result.presenceKeys).toEqual(["laptop"]); - expect(result.showPresence).toBe(1); - }); - - test("storeMemberUpdates merges existing member data", async ({ page, borogove, persistence }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const chat = new borogove.Channel( - null, - null, - persistence, - "room-members-2@example.com", - ); - chat.displayName = "A Chat"; - chat.trusted = true; - - await persistence.storeMembers("alice@example.com", chat.chatId, [ - { - id: "room-members-2@example.com/occ-1", - displayName: "Alice", - photoUri: null, - isSelf: false, - roles: [ - { id: "admin", title: "Admin" }, - { id: "urn:xmpp:hats:test", title: "Tea Host" }, - ], - jid: borogove.JID.parse("alice@example.com"), - presence: new Map([ - ["desk", borogove.Stanza.parse("<presence />")], - ]), - chat: { chatId: "alice@example.com" }, - }, - ]); - - const updated = await persistence.storeMemberUpdates( - "alice@example.com", - chat, - [ - new borogove.MemberUpdate( - "room-members-2@example.com/occ-1", - borogove.JID.parse("alice@example.com"), - "Alice Cooper", - false, - null, - new Map([["mobile", borogove.Stanza.parse("<presence />")]]), - ), - ], - false, - ); - - return { - displayName: updated[0]?.displayName, - roleIds: updated[0]?.roles?.map((r) => r.id), - presenceKeys: updated[0] - ? [...updated[0].presence.keys()].sort() - : [], - }; - }, - { borogove, persistence }, - ); - - expect(result.displayName).toBe("Alice Cooper"); - expect(result.roleIds).toEqual(["urn:xmpp:hats:test"]); - expect(result.presenceKeys).toEqual(["desk", "mobile"]); - }); - - test("storeMemberUpdates clears omitted full-list affiliations", async ({ - page, - borogove, - persistence, - }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const chat = new borogove.Channel( - null, - null, - persistence, - "room-members-2b@example.com", - ); - chat.displayName = "A Chat"; - chat.trusted = true; - - await persistence.storeMembers("alice@example.com", chat.chatId, [ - { - id: "room-members-2b@example.com/occ-1", - displayName: "Alice", - photoUri: null, - isSelf: false, - roles: [{ id: "admin", title: "Admin" }], - jid: borogove.JID.parse("alice@example.com"), - presence: new Map(), - chat: { chatId: "alice@example.com" }, - }, - { - id: "room-members-2b@example.com/occ-2", - displayName: "Bob", - photoUri: null, - isSelf: false, - roles: [{ id: "owner", title: "Owner" }], - jid: borogove.JID.parse("bob@example.com"), - presence: new Map(), - chat: { chatId: "bob@example.com" }, - }, - ]); - - await persistence.storeMemberUpdates( - "alice@example.com", - chat, - [ - new borogove.MemberUpdate( - "room-members-2b@example.com/occ-1", - borogove.JID.parse("alice@example.com"), - "Alice", - false, - null, - new Map(), - ), - ], - true, - ); - const members = await persistence.getMembers( - "alice@example.com", - chat, - true, - ); - return members - .find((m) => m.id.endsWith("occ-2")) - ?.roles?.map((r) => r.id); - }, - { borogove, persistence }, - ); - - expect(result).toEqual([]); - }); - - test("storeMemberUpdates matches existing member by true JID", async ({ - page, - borogove, - persistence, - }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const chat = new borogove.Channel( - null, - null, - persistence, - "room-members-3@example.com", - ); - chat.displayName = "A Chat"; - chat.trusted = true; - - await persistence.storeMembers("alice@example.com", chat.chatId, [ - { - id: "room-members-3@example.com/occ-1", - displayName: "Alice", - photoUri: null, - isSelf: false, - roles: [{ id: "admin", title: "Admin" }], - jid: borogove.JID.parse("alice@example.com"), - presence: new Map(), - chat: { chatId: "alice@example.com" }, - }, - ]); - - await persistence.storeMemberUpdates( - "alice@example.com", - chat, - [ - new borogove.MemberUpdate( - null, - borogove.JID.parse("alice@example.com"), - "Alice Renamed", - false, - null, - new Map(), - ), - ], - false, - ); - const [member] = await persistence.getMemberDetails( - "alice@example.com", - chat, - ["room-members-3@example.com/occ-1"], - ); - return member?.displayName; - }, - { borogove, persistence }, - ); - - expect(result).toBe("Alice Renamed"); - }); - - test("clearMemberPresence only clears the targeted chat", async ({ - page, - borogove, - persistence, - }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const chat1 = new borogove.Channel( - null, - null, - persistence, - "room-members-4a@example.com", - ); - chat1.displayName = "A Chat"; - chat1.trusted = true; - const chat2 = new borogove.Channel( - null, - null, - persistence, - "room-members-4b@example.com", - ); - chat2.displayName = "A Chat"; - chat2.trusted = true; - - await persistence.storeMembers("alice@example.com", chat1.chatId, [ - { - id: "room-members-4a@example.com/occ-1", - displayName: "Alice", - photoUri: null, - isSelf: false, - roles: [{ id: "admin", title: "Admin" }], - jid: borogove.JID.parse("alice@example.com"), - presence: new Map([ - ["desk", borogove.Stanza.parse("<presence />")], - ]), - chat: { chatId: "alice@example.com" }, - }, - ]); - await persistence.storeMembers("alice@example.com", chat2.chatId, [ - { - id: "room-members-4b@example.com/occ-1", - displayName: "Bob", - photoUri: null, - isSelf: false, - roles: [{ id: "admin", title: "Admin" }], - jid: borogove.JID.parse("bob@example.com"), - presence: new Map([ - ["phone", borogove.Stanza.parse("<presence />")], - ]), - chat: { chatId: "bob@example.com" }, - }, - ]); - - await persistence.clearMemberPresence( - "alice@example.com", - chat1.chatId, - ); - const [member1] = await persistence.getMemberDetails( - "alice@example.com", - chat1, - ["room-members-4a@example.com/occ-1"], - ); - const [member2] = await persistence.getMemberDetails( - "alice@example.com", - chat2, - ["room-members-4b@example.com/occ-1"], - ); - return { - chat1PresenceKeys: member1 ? [...member1.presence.keys()] : [], - chat2PresenceKeys: member2 ? [...member2.presence.keys()] : [], - }; - }, - { borogove, persistence }, - ); - - expect(result.chat1PresenceKeys).toEqual([]); - expect(result.chat2PresenceKeys).toEqual(["phone"]); - }); + sharedPersistenceTests(test); test("getMembers filters hidden rows for non-moderators", async ({ page, @@ -1567,195 +92,4 @@ test.describe("not webkit", () => { expect(result).toEqual(["Zulu", "Alpha"]); }); - test("getMembers includes moderator-visible rows", async ({ page, borogove, persistence }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const chat = new borogove.Channel( - null, - null, - persistence, - "room-members-6@example.com", - ); - chat.displayName = "A Chat"; - chat.trusted = true; - - await persistence.storeMembers("alice@example.com", chat.chatId, [ - { - id: "room-members-6@example.com/owner", - displayName: "Zulu", - photoUri: null, - isSelf: false, - roles: [{ id: "owner", title: "Owner" }], - jid: borogove.JID.parse("zulu@example.com"), - presence: new Map([ - ["desk", borogove.Stanza.parse("<presence />")], - ]), - chat: { chatId: "zulu@example.com" }, - }, - { - id: "room-members-6@example.com/outcast", - displayName: "Banned", - photoUri: null, - isSelf: false, - roles: [{ id: "outcast", title: "Banned" }], - jid: borogove.JID.parse("banned@example.com"), - presence: new Map([ - ["desk", borogove.Stanza.parse("<presence />")], - ]), - chat: { chatId: "banned@example.com" }, - }, - { - id: "room-members-6@example.com/guest-offline", - displayName: "Guest", - photoUri: null, - isSelf: false, - roles: [{ id: "none", title: "Guest" }], - jid: borogove.JID.parse("guest@example.com"), - presence: new Map([ - [ - "desk", - borogove.Stanza.parse('<presence type="unavailable" />'), - ], - ]), - chat: { chatId: "guest@example.com" }, - }, - { - id: "room-members-6@example.com/admin", - displayName: "Alpha", - photoUri: null, - isSelf: false, - roles: [{ id: "admin", title: "Admin" }], - jid: borogove.JID.parse("alpha@example.com"), - presence: new Map([ - ["desk", borogove.Stanza.parse("<presence />")], - ]), - chat: { chatId: "alpha@example.com" }, - }, - ]); - - const members = await persistence.getMembers( - "alice@example.com", - chat, - true, - ); - return members.map((m) => m.displayName); - }, - { borogove, persistence }, - ); - - expect(result).toEqual(["Zulu", "Alpha", "Banned"]); - }); - - test("getMemberDetails returns null for incomplete rows", async ({ - page, - borogove, - persistence, - }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const chat = new borogove.Channel( - null, - null, - persistence, - "room-members-7@example.com", - ); - chat.displayName = "A Chat"; - chat.trusted = true; - - await persistence.storeMembers("alice@example.com", chat.chatId, [ - { - id: "room-members-7@example.com/admin", - displayName: "Alpha", - photoUri: null, - isSelf: false, - roles: [{ id: "admin", title: "Admin" }], - jid: borogove.JID.parse("alpha@example.com"), - presence: new Map([ - ["desk", borogove.Stanza.parse("<presence />")], - ]), - chat: { chatId: "alpha@example.com" }, - }, - ]); - - await persistence.db.exec( - "INSERT INTO members(account_id, chat_id, member_id, display_name, photo_uri, is_self, chat, roles, presence, jid) VALUES(?, ?, ?, ?, ?, ?, ?, jsonb(?), jsonb(?), ?)", - [ - "alice@example.com", - chat.chatId, - "room-members-7@example.com/incomplete", - "", - null, - 0, - "{}", - "[]", - "{}", - "", - ], - ); - const details = await persistence.getMemberDetails( - "alice@example.com", - chat, - [ - "room-members-7@example.com/admin", - "room-members-7@example.com/incomplete", - ], - ); - return details.map((m) => (m ? m.displayName : null)); - }, - { borogove, persistence }, - ); - - expect(result).toEqual(["Alpha", null]); - }); - - test("storeVoiceRequest and listVoiceRequests", async ({ page, borogove, persistence }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const chat = Object.create(borogove.Channel.prototype); - chat.chatId = "room-voice-requests@example.com"; - chat.getDisplayName = () => "Tea Room"; - - await persistence.storeMembers("alice@example.com", chat.chatId, [ - { - id: "room-voice-requests@example.com/occ-1", - displayName: "Bob", - photoUri: null, - isSelf: false, - roles: [{ id: "none", title: "Participant" }], - jid: borogove.JID.parse("bob@example.com"), - presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), - chat: { chatId: "bob@example.com" }, - }, - { - id: "room-voice-requests@example.com/occ-2", - displayName: "Charlie", - photoUri: null, - isSelf: false, - roles: [{ id: "none", title: "Participant" }], - jid: borogove.JID.parse("charlie@example.com"), - presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), - chat: { chatId: "charlie@example.com" }, - } - ]); - - await persistence.storeVoiceRequest("alice@example.com", chat, "bob@example.com", true); - await persistence.storeVoiceRequest("alice@example.com", chat, "charlie@example.com", true); - - const requests1 = await persistence.listVoiceRequests("alice@example.com", chat); - - await persistence.storeVoiceRequest("alice@example.com", chat, "bob@example.com", false); - - const requests2 = await persistence.listVoiceRequests("alice@example.com", chat); - - return { - requests1: requests1.map((m) => m.displayName).sort(), - requests2: requests2.map((m) => m.displayName).sort(), - }; - }, - { borogove, persistence }, - ); - - expect(result.requests1).toEqual(["Bob", "Charlie"]); - expect(result.requests2).toEqual(["Charlie"]); - }); });