| author | Eric Roberts
<eric@devl.me> 2026-09-10 19:47:57 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2026-09-14 14:02:44 UTC |
| parent | 49f911a2d199d6ce74b5a8c7d45878e232c56583 |
| .gitignore | +2 | -0 |
| Makefile | +7 | -6 |
| npm/package.json | +5 | -2 |
| playwright.config.ts | +1 | -3 |
| test/browser-test.ts | +0 | -202 |
| test/idb.spec.ts | +115 | -82 |
| test/persistence-tests.ts | +1546 | -2082 |
| test/sqlite.spec.ts | +62 | -1 |
| vitest.config.mts | +39 | -0 |
diff --git a/.gitignore b/.gitignore index c6137e0..b8ba41e 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ docs/js/borogove*.md /blob-report/ /playwright/.cache/ /playwright/.auth/ + +.vitest diff --git a/Makefile b/Makefile index dd67f5a..b9299b4 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,14 @@ HAXE_PATH=$$HOME/Software/haxe-4.3.1/hxnodejs/12,1,0/src CJSTOESM=npx -p typescript@6 -p cjstoesm cjstoesm -.PHONY: all test doc format format-check hx-build-dep cpp/libborogove.dso npm/borogove-browser.js npm/borogove.js cpp playwright ci +.PHONY: all test doc format format-check hx-build-dep cpp/libborogove.dso npm/borogove-browser.js npm/borogove.js cpp browser-tests ci all: npm libborogove.batteriesincluded.so .WAIT libborogove.so libborogove.a test: haxe test.hxml -ci: format-check lint test playwright +ci: format-check lint test browser-tests mkdir -p .cache haxe testjs.hxml haxe testcpp.hxml @@ -64,9 +64,9 @@ npm: format-check lint npm/borogove-browser.js npm/borogove.js borogove/persiste cp borogove/persistence/MediaStoreCache.js npm cp borogove/persistence/sqlite-worker1.mjs npm $(RM) npm/calls.js npm/persistence.js npm/persistence-browser.js npm/borogove-enums.js - -cd npm && npx tsc --esModuleInterop --lib esnext,dom --target esnext --preserveConstEnums --allowJs --checkJs -d index.ts > /dev/null - cd npm && npx tsc --esModuleInterop --lib esnext,dom --target esnext --preserveConstEnums --allowJs --checkJs -d index.ts - cd npm && npx tsc --esModuleInterop --lib esnext,dom --target esnext --preserveConstEnums --allowJs --checkJs -d persistence-browser.ts + -cd npm && npx tsc --esModuleInterop --lib esnext,dom --target esnext --moduleResolution node --preserveConstEnums --allowJs --checkJs -d index.ts > /dev/null + cd npm && npx tsc --esModuleInterop --lib esnext,dom --target esnext --moduleResolution node --preserveConstEnums --allowJs --checkJs -d index.ts + cd npm && npx tsc --esModuleInterop --lib esnext,dom --target esnext --moduleResolution node --preserveConstEnums --allowJs --checkJs -d persistence-browser.ts playwright/.cache/borogove.js: npm cd npm && esbuild index.js --bundle --format=esm "--alias:node:dns=./test/no-dns.mjs" "--footer:js=export { borogove_JID as JID, borogove_Stanza as Stanza, borogove_ReactionUpdate as ReactionUpdate, borogove_MemberUpdate as MemberUpdate }" --outfile=../$@ @@ -84,8 +84,9 @@ playwright/.cache/sqlite-worker1.js: npm $(RM) $@.mjs $(RM) $@.mjs.bak -playwright: playwright/.cache/borogove.js playwright/.cache/sqlite-wasm.js playwright/.cache/sqlite-worker1.js +browser-tests: playwright/.cache/borogove.js playwright/.cache/sqlite-wasm.js playwright/.cache/sqlite-worker1.js npx playwright test + cd npm && npm run test:vitest-browser cpp/libborogove.dso: haxe cpp.hxml diff --git a/npm/package.json b/npm/package.json index 9479a79..26fd6be 100644 --- a/npm/package.json +++ b/npm/package.json @@ -23,7 +23,7 @@ "./sqlite-wasm": "./sqlite-wasm.js" }, "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test:vitest-browser": "vitest run --config ../vitest.config.mts" }, "author": "", "license": "Apache-2.0", @@ -40,8 +40,11 @@ "devDependencies": { "@playwright/test": "1.60.0", "@sqlite.org/sqlite-wasm": "^3.53.0-build1", + "@vitest/browser": "^5.0.0", + "@vitest/browser-playwright": "^5.0.0", "oxlint": "^1.82.0", "oxfmt": "^0.67.0", - "typescript": "^5.4.5" + "typescript": "^5.4.5", + "vitest": "^5.0.0" } } diff --git a/playwright.config.ts b/playwright.config.ts index 730adc1..656f03a 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -14,6 +14,7 @@ import { defineConfig, devices } from "@playwright/test"; export default defineConfig({ timeout: 260000, testDir: "./test", + testMatch: ["**/fast-auth.spec.ts"], webServer: { command: "node playwright/server.mjs", url: "http://127.0.0.1:49276", @@ -51,9 +52,6 @@ export default defineConfig({ { name: "webkit", use: { ...devices["Desktop Safari"] }, - // Skip sqlite tests on webkit because the - // version in playwright lacks OPFS - testIgnore: "sqlite.spec.ts", }, ], }); diff --git a/test/browser-test.ts b/test/browser-test.ts deleted file mode 100644 index 036e096..0000000 --- a/test/browser-test.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { - test as base, - expect, - type JSHandle, - type Page, -} from "@playwright/test"; -import type { borogove_Persistence } from "../npm/borogove-browser"; -import type { borogove_persistence_Sqlite } from "../npm/sqlite-wasm"; - -type PageKeyPair = { - privKey: number[]; - pubKey: number[]; -}; - -type PageHelpers = { - keyToBuffer(key: number[]): ArrayBuffer; - bufferToKey(key: ArrayBuffer): number[]; - keyPairToBuffers(keyPair: PageKeyPair): { - privKey: ArrayBuffer; - pubKey: ArrayBuffer; - }; - buffersToKeyPair(keyPair: { - privKey: ArrayBuffer; - pubKey: ArrayBuffer; - }): PageKeyPair; -}; - -type BrowserFixtures = { - borogove: JSHandle<any>; - createChannel: JSHandle<any>; - pageHelpers: JSHandle<PageHelpers>; - persistence: JSHandle<borogove_Persistence>; - storeIncompleteMember: JSHandle<any>; -}; - -type SqliteFixtures = BrowserFixtures & { - sqlite: JSHandle<any>; -}; - -declare global { - interface Window { - borogove: any; - sqlite: any; - sqliteWorker1Url: URL; - } -} - -const pageHelpers = async ( - { page }: { page: Page }, - use: (helpers: JSHandle<PageHelpers>) => Promise<void>, -) => { - const helpers = await page.evaluateHandle(() => ({ - keyToBuffer: (key: number[]) => new Uint8Array(key).buffer, - bufferToKey: (key: ArrayBuffer) => [...new Uint8Array(key)], - keyPairToBuffers: (keyPair: PageKeyPair) => ({ - privKey: new Uint8Array(keyPair.privKey).buffer, - pubKey: new Uint8Array(keyPair.pubKey).buffer, - }), - buffersToKeyPair: (keyPair: { - privKey: ArrayBuffer; - pubKey: ArrayBuffer; - }) => ({ - privKey: [...new Uint8Array(keyPair.privKey)], - pubKey: [...new Uint8Array(keyPair.pubKey)], - }), - })); - await use(helpers); -}; - -export const idbTest = base.extend<BrowserFixtures>({ - page: async ({ page }, use) => { - await page.goto("/idb"); - await use(page); - }, - borogove: async ({ page }, use) => { - const borogove = await page.evaluateHandle(() => window.borogove); - await use(borogove); - }, - pageHelpers, - 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"); - return borogove.persistence.IDB("snikket", mediaStore); - }, 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>({ - page: async ({ page }, use) => { - await page.goto("/sqlite"); - await use(page); - }, - borogove: async ({ page }, use) => { - const borogove = await page.evaluateHandle(() => window.borogove); - await use(borogove); - }, - pageHelpers, - sqlite: async ({ page }, use) => { - 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 }) => { - const mediaStore = - await borogove.persistence.MediaStoreCache("snikket"); - return new sqlite.borogove_persistence_Sqlite("snikket", mediaStore); - }, - { 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 420e047..36df8c5 100644 --- a/test/idb.spec.ts +++ b/test/idb.spec.ts @@ -1,90 +1,123 @@ -import { idbTest as test, expect } from "./browser-test"; +import { expect, test } from "vitest"; +import * as borogove from "../playwright/.cache/borogove.js"; import { sharedPersistenceTests } from "./persistence-tests"; -sharedPersistenceTests(test); - -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(); +async function idbFixture() { + const databaseName = `idb-${crypto.randomUUID()}`; + const mediaStore = await borogove.persistence.MediaStoreCache( + `${databaseName}-media`, + ); + const persistence = await borogove.persistence.IDB(databaseName, mediaStore); + const createChannel = (p: any, chatId: string) => + new borogove.Channel(null, null, p, chatId); + const storeIncompleteMember = async (chatId: string) => { + const request = indexedDB.open(databaseName); + 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<void>((resolve, reject) => { + transaction.oncomplete = () => resolve(); + transaction.onerror = () => reject(transaction.error); + }); + }; + return { + borogove, + persistence, + createChannel, + storeIncompleteMember, + databaseName, + }; +} - await persistence.storeMessages("alice@example.com", [ - parentMsg, - childMsg, - ]); +function idbTest(name: string, fn: (fixture: any) => Promise<void>) { + test("idb: " + name, async () => { + const current = await idbFixture(); + await fn(current); + }); +} - 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); - }, - ); +sharedPersistenceTests(idbTest); - rawChild.replyToMessage = ["alice@example.com", "parent", "", ""]; +idbTest( + "hydrate message with incomplete replyToMessage keys", + async ({ borogove, persistence, databaseName }) => { + 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 new Promise<void>((resolve) => { - const req = store.put(rawChild); - req.onsuccess = () => resolve(); - }); - await new Promise<void>((resolve) => { - tx.oncomplete = () => resolve(); - }); + 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(); - 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 }, - ); + await persistence.storeMessages("alice@example.com", [parentMsg, childMsg]); + const db = await new Promise<IDBDatabase>((resolve, reject) => { + const request = indexedDB.open(databaseName); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + const transaction = db.transaction(["messages"], "readwrite"); + const store = transaction.objectStore("messages"); + const rawChild = await new Promise<{ replyToMessage: string[] }>( + (resolve, reject) => { + const request = store.get([ + "alice@example.com", + "child", + "hatter@example.com", + "loc2", + ]); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }, + ); + rawChild.replyToMessage = ["alice@example.com", "parent", "", ""]; + store.put(rawChild); + await new Promise<void>((resolve, reject) => { + transaction.oncomplete = () => resolve(); + transaction.onerror = () => reject(transaction.error); + }); - expect(result.hasReply).toBe(true); - expect(result.replyServerId).toBe("parent"); -}); + const retrievedChild = await persistence.getMessage( + "alice@example.com", + "hatter@example.com", + "child", + "loc2", + ); + expect(!!retrievedChild.replyToMessage).toBe(true); + expect(retrievedChild.replyToMessage?.serverId).toBe("parent"); + }, +); diff --git a/test/persistence-tests.ts b/test/persistence-tests.ts index 0a4ef6c..658b65f 100644 --- a/test/persistence-tests.ts +++ b/test/persistence-tests.ts @@ -1,388 +1,288 @@ -import { idbTest, sqliteTest, expect } from "./browser-test"; -import { randomBytes } from "node:crypto"; +import { expect } from "vitest"; -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 }, +export function sharedPersistenceTests(test) { + test("storeChats and getChats", async ({ borogove, persistence }) => { + const chat = new borogove.DirectChat( + null, + null, + persistence, + "hatter@example.com", ); - - 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"); + 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"); + expect(chats.length).toBe(1); + expect(chats[0]?.chatId).toBe("hatter@example.com"); + expect(chats[0]?.displayName).toBe("The Mad Hatter"); + expect(chats[0]?.trusted).toBe(true); + expect(chats[0]?.klass).toBe("DirectChat"); + expect(chats[0]?.threads?.get(null)).toBe("Tea Time"); + expect(chats[0]?.threads?.get("thread-1")).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 }, + const chat = new borogove.DirectChat( + null, + null, + persistence, + "hatter@example.com", ); - - expect(result.length).toBe(1); - expect(result.chatId).toBe("hatter@example.com"); - expect(result.statusEmoji).toBe("🎩"); - expect(result.statusText).toBe("Time for tea!"); + 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"); + expect(chats.length).toBe(1); + expect(chats[0]?.chatId).toBe("hatter@example.com"); + expect(chats[0]?.status?.emoji).toBe("🎩"); + expect(chats[0]?.status?.text).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 }, + const chat = new borogove.DirectChat( + null, + null, + persistence, + "hatter@example.com", ); + chat.displayName = "The Mad Hatter"; + chat.trusted = true; - expect(result).toEqual(["phone"]); + 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"); + expect([...stored.presence.keys()]).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 }, - ); + const chat = createChannel(persistence, "room-chat-hydrate@example.com"); + chat.displayName = "Tea Room"; + chat.trusted = true; + chat.mavUntil = "2024-05-01T12:00:00Z"; - expect(result.mavUntil).toBe("2024-05-01T12:00:00Z"); - expect(result.membersForName).toEqual(["Alpha", "Zulu"]); - expect(result.presenceKeys).toEqual(["desk"]); + 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"); + expect(stored.mavUntil).toBe("2024-05-01T12:00:00Z"); + expect(stored.membersForName.map((m) => m.displayName)).toEqual([ + "Alpha", + "Zulu", + ]); + expect([...stored.presence.keys()]).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"); + 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(), + ]); + expect(childStored.replyToMessage.body().toPlainText()).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 }, + 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, ); - - expect(result.byServerId).not.toBeNull(); - expect(result.byServerId.serverId).toBe("srv1"); - expect(result.byLocalId).not.toBeNull(); - expect(result.byLocalId.localId).toBe("loc1"); + const byLocalId = await persistence.getMessage( + "alice@example.com", + "hatter@example.com", + null, + "loc1", + ); + expect(byServerId).not.toBeNull(); + expect(byServerId.serverId).toBe("srv1"); + expect(byLocalId).not.toBeNull(); + expect(byLocalId.localId).toBe("loc1"); }); test("persists message encryption information", async ({ - page, borogove, persistence, }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const account = "encryption-alice@example.com"; - const chatId = "encryption-hatter@example.com"; - const builder = new borogove.ChatMessageBuilder({ - serverId: "encrypted-message", - serverIdBy: account, - senderId: chatId, - direction: 0, - }); - builder.sortId = "encrypted-a0"; - builder.syncPoint = true; - builder.text = "Encrypted persistence marker"; - builder.to = borogove.JID.parse(account); - builder.from = borogove.JID.parse(chatId); - builder.recipients = [builder.to]; - builder.replyTo = [builder.from]; - builder.encryption = { - status: borogove.EncryptionStatus.DecryptionFailure, - method: "urn:xmpp:omemo:2", - methodName: "OMEMO 2", - reason: "invalid-key", - reasonText: "The sender key was invalid", - }; - - const [stored] = await persistence.storeMessages(account, [ - builder.build(), - ]); - const fetched = await persistence.getMessage( - account, - chatId, - "encrypted-message", - null, - ); - const [searched] = await persistence.searchMessages( - account, - chatId, - "persistence marker", - ); - const [paged] = await persistence.getMessagesBefore(account, chatId); - const syncPoint = await persistence.syncPoint(account, null); - - const fields = (message) => ({ - status: message?.encryption?.status, - method: message?.encryption?.method, - methodName: message?.encryption?.methodName, - reason: message?.encryption?.reason, - reasonText: message?.encryption?.reasonText, - }); - return { - stored: fields(stored), - fetched: fields(fetched), - searched: fields(searched), - paged: fields(paged), - syncPoint: fields(syncPoint), - }; - }, - { borogove, persistence }, - ); + const account = "encryption-alice@example.com"; + const chatId = "encryption-hatter@example.com"; + const builder = new borogove.ChatMessageBuilder({ + serverId: "encrypted-message", + serverIdBy: account, + senderId: chatId, + direction: 0, + }); + builder.sortId = "encrypted-a0"; + builder.syncPoint = true; + builder.text = "Encrypted persistence marker"; + builder.to = borogove.JID.parse(account); + builder.from = borogove.JID.parse(chatId); + builder.recipients = [builder.to]; + builder.replyTo = [builder.from]; + builder.encryption = { + status: borogove.EncryptionStatus.DecryptionFailure, + method: "urn:xmpp:omemo:2", + methodName: "OMEMO 2", + reason: "invalid-key", + reasonText: "The sender key was invalid", + }; + const [stored] = await persistence.storeMessages(account, [ + builder.build(), + ]); + const fetched = await persistence.getMessage( + account, + chatId, + "encrypted-message", + null, + ); + const [searched] = await persistence.searchMessages( + account, + chatId, + "persistence marker", + ); + const [paged] = await persistence.getMessagesBefore(account, chatId); + const syncPoint = await persistence.syncPoint(account, null); + + const fields = (message) => ({ + status: message?.encryption?.status, + method: message?.encryption?.method, + methodName: message?.encryption?.methodName, + reason: message?.encryption?.reason, + reasonText: message?.encryption?.reasonText, + }); const expected = { status: 1, method: "urn:xmpp:omemo:2", @@ -390,58 +290,50 @@ export function sharedPersistenceTests(test: PersistenceTest) { reason: "invalid-key", reasonText: "The sender key was invalid", }; - expect(result.stored).toEqual(expected); - expect(result.fetched).toEqual(expected); - expect(result.searched).toEqual(expected); - expect(result.paged).toEqual(expected); - expect(result.syncPoint).toEqual(expected); + expect(fields(stored)).toEqual(expected); + expect(fields(fetched)).toEqual(expected); + expect(fields(searched)).toEqual(expected); + expect(fields(paged)).toEqual(expected); + expect(fields(syncPoint)).toEqual(expected); }); test("preserves encryption information when updating message status", async ({ - page, borogove, persistence, }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const account = "encryption-status-alice@example.com"; - const builder = new borogove.ChatMessageBuilder({ - localId: "encrypted-outgoing", - senderId: account, - direction: 1, - }); - builder.sortId = "encrypted-b0"; - builder.to = borogove.JID.parse("encryption-hatter@example.com"); - builder.from = borogove.JID.parse(account); - builder.recipients = [builder.to]; - builder.replyTo = [builder.from]; - builder.encryption = { - status: borogove.EncryptionStatus.DecryptionSuccess, - method: "eu.siacs.conversations.axolotl", - methodName: "OMEMO", - reason: null, - reasonText: null, - }; - - await persistence.storeMessages(account, [builder.build()]); - const updated = await persistence.updateMessageStatus( - account, - "encrypted-outgoing", - 1, - "Delivered", - ); - return { - status: updated.encryption?.status, - method: updated.encryption?.method, - methodName: updated.encryption?.methodName, - reason: updated.encryption?.reason, - reasonText: updated.encryption?.reasonText, - }; - }, - { borogove, persistence }, - ); + const account = "encryption-status-alice@example.com"; + const builder = new borogove.ChatMessageBuilder({ + localId: "encrypted-outgoing", + senderId: account, + direction: 1, + }); + builder.sortId = "encrypted-b0"; + builder.to = borogove.JID.parse("encryption-hatter@example.com"); + builder.from = borogove.JID.parse(account); + builder.recipients = [builder.to]; + builder.replyTo = [builder.from]; + builder.encryption = { + status: borogove.EncryptionStatus.DecryptionSuccess, + method: "eu.siacs.conversations.axolotl", + methodName: "OMEMO", + reason: null, + reasonText: null, + }; - expect(result).toEqual({ + await persistence.storeMessages(account, [builder.build()]); + const updated = await persistence.updateMessageStatus( + account, + "encrypted-outgoing", + 1, + "Delivered", + ); + expect({ + status: updated.encryption?.status, + method: updated.encryption?.method, + methodName: updated.encryption?.methodName, + reason: updated.encryption?.reason, + reasonText: updated.encryption?.reasonText, + }).toEqual({ status: 0, method: "eu.siacs.conversations.axolotl", methodName: "OMEMO", @@ -451,79 +343,71 @@ export function sharedPersistenceTests(test: PersistenceTest) { }); test("preserves encryption information for corrected message versions", async ({ - page, borogove, persistence, }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const account = "encryption-correction-alice@example.com"; - const chatId = "encryption-correction-hatter@example.com"; - const originalBuilder = new borogove.ChatMessageBuilder({ - localId: "encrypted-original", - senderId: account, - direction: 1, - timestamp: "2026-08-26T12:00:00Z", - }); - originalBuilder.sortId = "encrypted-c0"; - originalBuilder.text = "Original encrypted text"; - originalBuilder.to = borogove.JID.parse(chatId); - originalBuilder.from = borogove.JID.parse(account); - originalBuilder.recipients = [originalBuilder.to]; - originalBuilder.replyTo = [originalBuilder.from]; - originalBuilder.encryption = { - status: borogove.EncryptionStatus.DecryptionSuccess, - method: "urn:xmpp:omemo:1", - methodName: "OMEMO 1", - reason: null, - reasonText: null, - }; - const original = originalBuilder.build(); - await persistence.storeMessages(account, [original]); - - const correctionBuilder = new borogove.ChatMessageBuilder({ - localId: "encrypted-correction", - senderId: account, - direction: 1, - timestamp: "2026-08-26T12:01:00Z", - }); - correctionBuilder.sortId = "encrypted-c0"; - correctionBuilder.text = "Corrected encrypted text"; - correctionBuilder.to = borogove.JID.parse(chatId); - correctionBuilder.from = borogove.JID.parse(account); - correctionBuilder.recipients = [correctionBuilder.to]; - correctionBuilder.replyTo = [correctionBuilder.from]; - correctionBuilder.encryption = { - status: borogove.EncryptionStatus.DecryptionFailure, - method: "urn:xmpp:omemo:2", - methodName: "OMEMO 2", - reason: "invalid-key", - reasonText: "Correction could not be decrypted", - }; - const correctionVersion = correctionBuilder.build(); - correctionBuilder.versions = [correctionVersion]; - correctionBuilder.localId = original.localId; - - const [stored] = await persistence.storeMessages(account, [ - correctionBuilder.build(), - ]); - const [fetched] = await persistence.getMessagesBefore(account, chatId); - const summarize = (message) => ({ - text: message.text, - method: message.encryption?.method, - reason: message.encryption?.reason, - versions: message.versions.map((version) => ({ - text: version.text, - method: version.encryption?.method, - reason: version.encryption?.reason, - })), - }); - return { stored: summarize(stored), fetched: summarize(fetched) }; - }, - { borogove, persistence }, - ); - - for (const message of [result.stored, result.fetched]) { + const account = "encryption-correction-alice@example.com"; + const chatId = "encryption-correction-hatter@example.com"; + const originalBuilder = new borogove.ChatMessageBuilder({ + localId: "encrypted-original", + senderId: account, + direction: 1, + timestamp: "2026-08-26T12:00:00Z", + }); + originalBuilder.sortId = "encrypted-c0"; + originalBuilder.text = "Original encrypted text"; + originalBuilder.to = borogove.JID.parse(chatId); + originalBuilder.from = borogove.JID.parse(account); + originalBuilder.recipients = [originalBuilder.to]; + originalBuilder.replyTo = [originalBuilder.from]; + originalBuilder.encryption = { + status: borogove.EncryptionStatus.DecryptionSuccess, + method: "urn:xmpp:omemo:1", + methodName: "OMEMO 1", + reason: null, + reasonText: null, + }; + const original = originalBuilder.build(); + await persistence.storeMessages(account, [original]); + + const correctionBuilder = new borogove.ChatMessageBuilder({ + localId: "encrypted-correction", + senderId: account, + direction: 1, + timestamp: "2026-08-26T12:01:00Z", + }); + correctionBuilder.sortId = "encrypted-c0"; + correctionBuilder.text = "Corrected encrypted text"; + correctionBuilder.to = borogove.JID.parse(chatId); + correctionBuilder.from = borogove.JID.parse(account); + correctionBuilder.recipients = [correctionBuilder.to]; + correctionBuilder.replyTo = [correctionBuilder.from]; + correctionBuilder.encryption = { + status: borogove.EncryptionStatus.DecryptionFailure, + method: "urn:xmpp:omemo:2", + methodName: "OMEMO 2", + reason: "invalid-key", + reasonText: "Correction could not be decrypted", + }; + const correctionVersion = correctionBuilder.build(); + correctionBuilder.versions = [correctionVersion]; + correctionBuilder.localId = original.localId; + + const [stored] = await persistence.storeMessages(account, [ + correctionBuilder.build(), + ]); + const [fetched] = await persistence.getMessagesBefore(account, chatId); + const summarize = (message) => ({ + text: message.text, + method: message.encryption?.method, + reason: message.encryption?.reason, + versions: message.versions.map((version) => ({ + text: version.text, + method: version.encryption?.method, + reason: version.encryption?.reason, + })), + }); + for (const message of [summarize(stored), summarize(fetched)]) { expect(message.text).toBe("Corrected encrypted text"); expect(message.method).toBe("urn:xmpp:omemo:2"); expect(message.reason).toBe("invalid-key"); @@ -542,1572 +426,1235 @@ export function sharedPersistenceTests(test: PersistenceTest) { } }); - 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 }, + test("storeReaction", 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, ); - expect(result.reactions.length).toBe(1); - expect(result.reactions[0].key).toBe("👍"); - expect(result.reactions[0].count).toBe(1); + const msg = await persistence.storeReaction("alice@example.com", update); + const reactions = [...msg.reactions.entries()].map(([k, v]) => ({ + key: k, + count: v.length, + })); + expect(reactions.length).toBe(1); + expect(reactions[0].key).toBe("👍"); + expect(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 }, + test("searchMessages", 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", ); - - expect(result.length).toBe(1); - expect(result[0]).toBe("Hello world"); + expect(results.length).toBe(1); + expect(results[0].text).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 }, + test("1:1 come back ordered by sortId", 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(), + ]); + const messages = await persistence.getMessagesBefore( + "alice@example.com", + "hatter@example.com", ); - - expect(result.length).toBe(2); - expect(result[0].serverId).toBe("1"); - expect(result[1].serverId).toBe("2"); + expect(messages.length).toBe(2); + expect(messages[0].serverId).toBe("1"); + expect(messages[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 }, + 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(), + ]); + const messages = await persistence.getMessagesBefore( + "alice@example.com", + "teaparty@example.com", ); - - expect(result.length).toBe(3); - expect(result[0].serverId).toBe("1"); - expect(result[1].serverId).toBe("2"); - expect(result[2].serverId).toBe("3"); + expect(messages.length).toBe(3); + expect(messages[0].serverId).toBe("1"); + expect(messages[1].serverId).toBe("2"); + expect(messages[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 }, + 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(), + ]); + const messages = await persistence.getMessagesBefore( + "alice@example.com", + "teaparty@example.com", + builder4.build(), ); - - expect(result.length).toBe(3); - expect(result[0].serverId).toBe("1"); - expect(result[1].serverId).toBe("2"); - expect(result[2].serverId).toBe("3"); + expect(messages.length).toBe(3); + expect(messages[0].serverId).toBe("1"); + expect(messages[1].serverId).toBe("2"); + expect(messages[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 }, + test("getMessagesBefore a PM", 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(), + ]); + const messages = await persistence.getMessagesBefore( + "alice@example.com", + "teaparty@example.com", + builder3.build(), ); - - expect(result.length).toBe(2); - expect(result[0].serverId).toBe("1"); - expect(result[1].serverId).toBe("2"); + expect(messages.length).toBe(2); + expect(messages[0].serverId).toBe("1"); + expect(messages[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 }, + 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(), + ]); + const messages = await persistence.getMessagesAfter( + "alice@example.com", + "teaparty@example.com", ); - - expect(result.length).toBe(3); - expect(result[0].serverId).toBe("1"); - expect(result[1].serverId).toBe("2"); - expect(result[2].serverId).toBe("3"); + expect(messages.length).toBe(3); + expect(messages[0].serverId).toBe("1"); + expect(messages[1].serverId).toBe("2"); + expect(messages[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 }, + 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(), + ]); + const messages = await persistence.getMessagesAfter( + "alice@example.com", + "teaparty@example.com", + builder.build(), ); - - expect(result.length).toBe(3); - expect(result[0].serverId).toBe("2"); - expect(result[1].serverId).toBe("3"); - expect(result[2].serverId).toBe("4"); + expect(messages.length).toBe(3); + expect(messages[0].serverId).toBe("2"); + expect(messages[1].serverId).toBe("3"); + expect(messages[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 }, + test("getMessagesAfter a PM", 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(), + ]); + const messages = await persistence.getMessagesAfter( + "alice@example.com", + "teaparty@example.com", + builder3.build(), ); - - expect(result.length).toBe(1); - expect(result[0].serverId).toBe("4"); + expect(messages.length).toBe(1); + expect(messages[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("updateMessageStatus", 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 + expect(updated.status).toBe(1); + expect(updated.statusText).toBe("Delivered"); }); - test("removeAccount and listAccounts", async ({ - page, - borogove, - persistence, - }) => { - const result = await page.evaluate( - async ({ 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("removeAccount and listAccounts", async ({ 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(); + expect(accountsBefore).toContain("alice@example.com"); + expect(accountsBefore).toContain("bob@example.com"); + expect(accountsAfter).not.toContain("alice@example.com"); + expect(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 }, + test("getChatUnreadDetails", 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(), + ]); + const unreadDetails = await persistence.getChatUnreadDetails( + "alice@example.com", + chat, ); - - expect(result.unreadCount).toBe(1); - expect(result.message.serverId).toBe("srv2"); + expect(unreadDetails.unreadCount).toBe(1); + expect(unreadDetails.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 }, - ); + test("media storage functions", 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; + } + }, + }); + } - expect(result.hasBefore).toBe( + 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); + expect(hasBefore).toBe( "/.well-known/ni/sha-256/A5BYxvLAy0ksUzsKTRTvd8wPeKvMztUofYShogEc-4E", ); - expect(result.hasAfter).toBe(null); + expect(hasAfter).toBe(null); }); test("storeStreamManamagement and getStreamManagement", async ({ - page, - borogove, persistence, }) => { - const result = await page.evaluate( - async ({ 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 }, + 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", ); - - expect(result.smIsEq).toBe(0); - expect(result.smIsArrayBuffer).toBe(true); - expect(result.sortId).toBe("ZZ"); + const streamManagement = + await persistence.getStreamManagement("alice@example.com"); + expect(streamManagement.sm instanceof ArrayBuffer).toBe(true); + expect( + streamManagement.sm + ? indexedDB.cmp( + streamManagement.sm, + new Uint8Array([1, 2, 0, 4]).buffer, + ) + : "null", + ).toBe(0); + expect(streamManagement.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 }, - ); + 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" }, + }; - 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); + await persistence.storeMembers("alice@example.com", chat.chatId, [member]); + const [stored] = await persistence.getMembers( + "alice@example.com", + chat, + false, + ); + expect(stored.id).toBe("room-members-1@example.com/occ-1"); + expect(stored.displayName).toBe("Alice"); + expect(stored.chat?.chatId).toBe("alice@example.com"); + expect(stored.roles.map((r) => r.id)).toEqual(["admin"]); + expect([...stored.presence.keys()]).toEqual(["laptop"]); + expect(stored.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)); - }, + const chat = createChannel(persistence, "room-members-7@example.com"); + chat.displayName = "A Chat"; + chat.trusted = true; + + await persistence.storeMembers("alice@example.com", chat.chatId, [ { - borogove, - createChannel, - persistence, - storeIncompleteMember, + 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", + ], ); - - expect(result).toEqual(["Alpha", null]); + expect(details.map((m) => (m ? m.displayName : null))).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, - ); + const chat = Object.create(borogove.Channel.prototype); + chat.chatId = "room-members-2@example.com"; + chat.getDisplayName = () => "Tea Room"; - return { - updatedRoleIds: updated[0].roles.map((r) => r.id), - updatedPresenceKeys: [...updated[0].presence.keys()].sort(), - updatedDisplayName: updated[0].displayName, - }; + 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" }, }, - { borogove, persistence }, - ); + ]); + + 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 />")]]), + ), + ]; - expect(result.updatedRoleIds).toEqual(["urn:xmpp:hats:test"]); - expect(result.updatedPresenceKeys).toEqual(["desk", "mobile"]); - expect(result.updatedDisplayName).toBe("Alice Cooper"); + const updated = await persistence.storeMemberUpdates( + "alice@example.com", + chat, + updates, + false, + ); + expect(updated[0].roles.map((r) => r.id)).toEqual(["urn:xmpp:hats:test"]); + expect([...updated[0].presence.keys()].sort()).toEqual(["desk", "mobile"]); + expect(updated[0].displayName).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); + 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" }, }, - { borogove, persistence }, + { + 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, ); - - expect(result).toEqual([]); + const members = await persistence.getMembers( + "alice@example.com", + chat, + true, + ); + expect(members.find((m) => m.id.endsWith("occ-2")).roles).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; + 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" }, }, - { borogove, persistence }, - ); + { + 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" }, + }, + ]); - expect(result).toBe("Alice Renamed"); + 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"], + ); + expect(chat1Member.displayName).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()], - }; + 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" }, }, - { borogove, persistence }, - ); + ]); + 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" }, + }, + ]); - expect(result.chat1PresenceKeys).toEqual([]); - expect(result.chat2PresenceKeys).toEqual(["phone"]); + 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"], + ); + expect([...chat1Member.presence.keys()]).toEqual([]); + expect([...chat2Member.presence.keys()]).toEqual(["phone"]); }); test("getMembers filters hidden rows for non-moderators", async ({ - page, borogove, persistence, }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const chat = new borogove.Channel( - null, - null, - persistence, - "room-members-5@example.com", - ); - chat.displayName = "Tea Room"; - chat.trusted = true; - - await persistence.storeMembers("alice@example.com", chat.chatId, [ - { - id: "room-members-5@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-5@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-5@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-5@example.com/guest-offline2", - displayName: "Guest2", - photoUri: null, - isSelf: false, - roles: [{ id: "none", title: "Guest" }], - jid: borogove.JID.parse("guest2@example.com"), - presence: new Map(), - chat: { chatId: "guest2@example.com" }, - }, - { - id: "room-members-5@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 normal = await persistence.getMembers( - "alice@example.com", - chat, - false, - ); + const chat = new borogove.Channel( + null, + null, + persistence, + "room-members-5@example.com", + ); + chat.displayName = "Tea Room"; + chat.trusted = true; - return normal.map((m) => m.displayName); + await persistence.storeMembers("alice@example.com", chat.chatId, [ + { + id: "room-members-5@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" }, }, - { borogove, persistence }, - ); + { + id: "room-members-5@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-5@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-5@example.com/guest-offline2", + displayName: "Guest2", + photoUri: null, + isSelf: false, + roles: [{ id: "none", title: "Guest" }], + jid: borogove.JID.parse("guest2@example.com"), + presence: new Map(), + chat: { chatId: "guest2@example.com" }, + }, + { + id: "room-members-5@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" }, + }, + ]); - expect(result).toEqual(["Zulu", "Alpha"]); + const normal = await persistence.getMembers( + "alice@example.com", + chat, + false, + ); + expect(normal.map((m) => m.displayName)).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); + 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" }, }, - { borogove, persistence }, - ); + { + 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" }, + }, + ]); - expect(result).toEqual(["Zulu", "Alpha", "Banned"]); + const moderator = await persistence.getMembers( + "alice@example.com", + chat, + true, + ); + expect(moderator.map((m) => m.displayName)).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, - ); + const chat = Object.create(borogove.Channel.prototype); + chat.chatId = "room-voice-requests@example.com"; + chat.getDisplayName = () => "Tea Room"; - return { - requests1: requests1.map((m) => m.displayName).sort(), - requests2: requests2.map((m) => m.displayName).sort(), - }; + 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" }, }, - { borogove, persistence }, + ]); + + 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, ); - expect(result.requests1).toEqual(["Bob", "Charlie"]); - expect(result.requests2).toEqual(["Charlie"]); + await persistence.storeVoiceRequest( + "alice@example.com", + chat, + "bob@example.com", + false, + ); + + const requests2 = await persistence.listVoiceRequests( + "alice@example.com", + chat, + ); + expect(requests1.map((m) => m.displayName).sort()).toEqual([ + "Bob", + "Charlie", + ]); + expect(requests2.map((m) => m.displayName).sort()).toEqual(["Charlie"]); }); test("getOmemoId returns no ID when none is stored", async ({ - page, persistence, }) => { - const result = await page.evaluate( - async (persistence) => - persistence.getOmemoId("omemo-not-found@example.com"), - persistence, - ); - - expect(result).toBeNull(); + expect( + await persistence.getOmemoId("omemo-not-found@example.com"), + ).toBeNull(); }); - test("storeOmemoId stores the ID", async ({ page, persistence }) => { + test("storeOmemoId stores the ID", async ({ persistence }) => { const account = "omemo-existing@example.com"; const omemoId = 12345; - const result = await page.evaluate( - async ({ persistence, account, omemoId }) => { - await persistence.storeOmemoId(account, omemoId); - return persistence.getOmemoId(account); - }, - { persistence, account, omemoId }, - ); - expect(result).toBe(omemoId); + await persistence.storeOmemoId(account, omemoId); + expect(await persistence.getOmemoId(account)).toBe(omemoId); }); test("getOmemoIdentityKey returns null when none is stored", async ({ - page, persistence, }) => { - const result = await page.evaluate( - async (persistence) => - persistence.getOmemoIdentityKey("omemo-identity-not-found@example.com"), - persistence, - ); - - expect(result).toBeNull(); + expect( + await persistence.getOmemoIdentityKey( + "omemo-identity-not-found@example.com", + ), + ).toBeNull(); }); - test("storeOmemoIdentityKey stores the key pair", async ({ - page, - pageHelpers, - persistence, - }) => { + test("storeOmemoIdentityKey stores the key pair", async ({ persistence }) => { const account = "omemo-identity-existing@example.com"; const keyPair = makeKeyPair(); - const result = await page.evaluate( - async ({ persistence, account, keyPair, pageHelpers }) => { - await persistence.storeOmemoIdentityKey( - account, - pageHelpers.keyPairToBuffers(keyPair), - ); - const loadedKeyPair = await persistence.getOmemoIdentityKey(account); + await persistence.storeOmemoIdentityKey(account, keyPair); + const loadedKeyPair = await persistence.getOmemoIdentityKey(account); - return pageHelpers.buffersToKeyPair(loadedKeyPair); - }, - { persistence, account, keyPair, pageHelpers }, - ); - - expectKeyPair(result, keyPair); + expectKeyPair(loadedKeyPair, keyPair); }); test("getOmemoDeviceList returns an empty list when none is stored", async ({ - page, persistence, }) => { - const result = await page.evaluate( - async (persistence) => - persistence.getOmemoDeviceList("omemo-devices-not-found@example.com"), - persistence, - ); - - expect(result).toEqual([]); + expect( + await persistence.getOmemoDeviceList( + "omemo-devices-not-found@example.com", + ), + ).toEqual([]); }); test("storeOmemoDeviceList replaces and clears the device list", async ({ - page, persistence, }) => { const identifier = "omemo-devices-existing@example.com"; const initialDeviceIds = [12345, 67890]; const replacementDeviceIds = [24680]; - const result = await page.evaluate( - async ({ - persistence, - identifier, - initialDeviceIds, - replacementDeviceIds, - }) => { - await persistence.storeOmemoDeviceList(identifier, initialDeviceIds); - const initial = await persistence.getOmemoDeviceList(identifier); - await persistence.storeOmemoDeviceList( - identifier, - replacementDeviceIds, - ); - const afterReplace = await persistence.getOmemoDeviceList(identifier); - await persistence.storeOmemoDeviceList(identifier, []); - const afterClear = await persistence.getOmemoDeviceList(identifier); - - return { initial, afterReplace, afterClear }; - }, - { - persistence, - identifier, - initialDeviceIds, - replacementDeviceIds, - }, - ); - expect(result.initial).toEqual(initialDeviceIds); - expect(result.afterReplace).toEqual(replacementDeviceIds); - expect(result.afterClear).toEqual([]); + await persistence.storeOmemoDeviceList(identifier, initialDeviceIds); + const initial = await persistence.getOmemoDeviceList(identifier); + await persistence.storeOmemoDeviceList(identifier, replacementDeviceIds); + const afterReplace = await persistence.getOmemoDeviceList(identifier); + await persistence.storeOmemoDeviceList(identifier, []); + const afterClear = await persistence.getOmemoDeviceList(identifier); + expect(initial).toEqual(initialDeviceIds); + expect(afterReplace).toEqual(replacementDeviceIds); + expect(afterClear).toEqual([]); }); test("getOmemoPreKey returns null when none is stored", async ({ - page, persistence, }) => { const identifier = "omemo-prekey-not-found@example.com"; const keyId = 1; - const result = await page.evaluate( - async ({ persistence, identifier, keyId }) => - persistence.getOmemoPreKey(identifier, keyId), - { persistence, identifier, keyId }, - ); - - expect(result).toBeNull(); + expect(await persistence.getOmemoPreKey(identifier, keyId)).toBeNull(); }); test("storeOmemoPreKey stores a removable pre-key", async ({ - page, - pageHelpers, persistence, }) => { const identifier = "omemo-prekey-existing@example.com"; const keyId = 42; const keyPair = makeKeyPair(); - const result = await page.evaluate( - async ({ persistence, identifier, keyId, keyPair, pageHelpers }) => { - await persistence.storeOmemoPreKey(identifier, keyId, { - ...pageHelpers.keyPairToBuffers(keyPair), - }); - const loadedKeyPair = await persistence.getOmemoPreKey( - identifier, - keyId, - ); - await persistence.removeOmemoPreKey(identifier, keyId); - const afterRemove = await persistence.getOmemoPreKey(identifier, keyId); - - return { - loadedKeyPair: pageHelpers.buffersToKeyPair(loadedKeyPair), - afterRemove, - }; - }, - { persistence, identifier, keyId, keyPair, pageHelpers }, - ); - expectKeyPair(result.loadedKeyPair, keyPair); - expect(result.afterRemove).toBeNull(); + await persistence.storeOmemoPreKey(identifier, keyId, { + ...keyPair, + }); + const loadedKeyPair = await persistence.getOmemoPreKey(identifier, keyId); + await persistence.removeOmemoPreKey(identifier, keyId); + const afterRemove = await persistence.getOmemoPreKey(identifier, keyId); + expectKeyPair(loadedKeyPair, keyPair); + expect(afterRemove).toBeNull(); }); - test("getOmemoPreKeys lists stored pre-keys", async ({ - page, - pageHelpers, - persistence, - }) => { + test("getOmemoPreKeys lists stored pre-keys", async ({ persistence }) => { const identifier = "omemo-prekeys-existing@example.com"; const preKeys = [ { @@ -2119,30 +1666,19 @@ export function sharedPersistenceTests(test: PersistenceTest) { keyPair: makeKeyPair(), }, ]; - const result = await page.evaluate( - async ({ persistence, identifier, preKeys, pageHelpers }) => { - for (const preKey of preKeys) { - await persistence.storeOmemoPreKey( - identifier, - preKey.keyId, - pageHelpers.keyPairToBuffers(preKey.keyPair), - ); - } - const loaded = await persistence.getOmemoPreKeys(identifier); - return loaded.map((preKey) => ({ - keyId: preKey.keyId, - keyPair: pageHelpers.buffersToKeyPair(preKey.keyPair), - })); - }, - { persistence, identifier, preKeys, pageHelpers }, - ); - expect(result).toEqual(preKeys); + for (const preKey of preKeys) { + await persistence.storeOmemoPreKey( + identifier, + preKey.keyId, + preKey.keyPair, + ); + } + const loaded = await persistence.getOmemoPreKeys(identifier); + expect(loaded).toEqual(preKeys); }); test("storeOmemoSignedPreKey and getOmemoSignedPreKey", async ({ - page, - pageHelpers, persistence, }) => { const identifier = "omemo-signed-prekey-existing@example.com"; @@ -2152,127 +1688,85 @@ export function sharedPersistenceTests(test: PersistenceTest) { keyPair: makeKeyPair(), signature: makeKey(), }; - const result = await page.evaluate( - async ({ persistence, identifier, signedPreKey, pageHelpers }) => { - await persistence.storeOmemoSignedPreKey(identifier, { - keyId: signedPreKey.keyId, - keyPair: pageHelpers.keyPairToBuffers(signedPreKey.keyPair), - signature: pageHelpers.keyToBuffer(signedPreKey.signature), - }); - const loaded = await persistence.getOmemoSignedPreKey( - identifier, - signedPreKey.keyId, - ); - - return { - keyId: loaded.keyId, - keyPair: pageHelpers.buffersToKeyPair(loaded.keyPair), - signature: pageHelpers.bufferToKey(loaded.signature), - }; - }, - { persistence, identifier, signedPreKey, pageHelpers }, - ); - expect(result.keyId).toBe(keyId); - expectKeyPair(result.keyPair, signedPreKey.keyPair); - expectKey(result.signature, signedPreKey.signature); + await persistence.storeOmemoSignedPreKey(identifier, { + keyId: signedPreKey.keyId, + keyPair: signedPreKey.keyPair, + signature: signedPreKey.signature, + }); + const loaded = await persistence.getOmemoSignedPreKey( + identifier, + signedPreKey.keyId, + ); + expect(loaded.keyId).toBe(keyId); + expectKeyPair(loaded.keyPair, signedPreKey.keyPair); + expectKey(loaded.signature, signedPreKey.signature); }); test("getOmemoSignedPreKey returns null when none is stored", async ({ - page, persistence, }) => { - const result = await page.evaluate( - async ({ persistence }) => - persistence.getOmemoSignedPreKey( - "omemo-signed-prekey-not-found@example.com", - 1, - ), - { persistence }, - ); - - expect(result).toBeNull(); + expect( + await persistence.getOmemoSignedPreKey( + "omemo-signed-prekey-not-found@example.com", + 1, + ), + ).toBeNull(); }); test("getOmemoContactIdentityKey returns null when none is stored", async ({ - page, persistence, }) => { - const result = await page.evaluate( - async ({ persistence }) => - persistence.getOmemoContactIdentityKey( - "omemo-contact-not-found@example.com", - "contact@example.com/1", - ), - { persistence }, - ); - - expect(result).toBeNull(); + expect( + await persistence.getOmemoContactIdentityKey( + "omemo-contact-not-found@example.com", + "contact@example.com/1", + ), + ).toBeNull(); }); test("storeOmemoContactIdentityKey and getOmemoContactIdentityKey", async ({ - page, - pageHelpers, persistence, }) => { const account = "omemo-contact-existing@example.com"; const address = "contact@example.com/1"; const identityKey = makeKey(); - const result = await page.evaluate( - async ({ persistence, account, address, identityKey, pageHelpers }) => { - await persistence.storeOmemoContactIdentityKey( - account, - address, - pageHelpers.keyToBuffer(identityKey), - ); - const loaded = await persistence.getOmemoContactIdentityKey( - account, - address, - ); - return pageHelpers.bufferToKey(loaded); - }, - { persistence, account, address, identityKey, pageHelpers }, - ); - expect(result).toEqual(identityKey); + await persistence.storeOmemoContactIdentityKey( + account, + address, + identityKey, + ); + const loaded = await persistence.getOmemoContactIdentityKey( + account, + address, + ); + expect(loaded).toEqual(identityKey); }); test("getOmemoSession returns null when none is stored", async ({ - page, persistence, }) => { - const result = await page.evaluate( - async ({ persistence }) => - persistence.getOmemoSession( - "omemo-session-not-found@example.com", - "contact@example.com/1", - ), - { persistence }, - ); - - expect(result).toBeNull(); + expect( + await persistence.getOmemoSession( + "omemo-session-not-found@example.com", + "contact@example.com/1", + ), + ).toBeNull(); }); test("getOmemoMetadata returns null when none is stored", async ({ - page, persistence, }) => { - const result = await page.evaluate( - async ({ persistence }) => - persistence.getOmemoMetadata( - "omemo-metadata-not-found@example.com", - "contact@example.com/1", - ), - { persistence }, - ); - - expect(result).toBeNull(); + expect( + await persistence.getOmemoMetadata( + "omemo-metadata-not-found@example.com", + "contact@example.com/1", + ), + ).toBeNull(); }); - test("storeOmemoMetadata and getOmemoMetadata", async ({ - page, - persistence, - }) => { + test("storeOmemoMetadata and getOmemoMetadata", async ({ persistence }) => { const account = "omemo-metadata-existing@example.com"; const address = "contact@example.com/1"; const metadata = { @@ -2280,181 +1774,151 @@ export function sharedPersistenceTests(test: PersistenceTest) { lastMessageDecryptedOk: false, sentKeyExchange: true, }; - const result = await page.evaluate( - async ({ persistence, account, address, metadata }) => { - await persistence.storeOmemoMetadata(account, address, metadata); - return persistence.getOmemoMetadata(account, address); - }, - { persistence, account, address, metadata }, - ); - expect(result).toEqual(metadata); + await persistence.storeOmemoMetadata(account, address, metadata); + expect(await persistence.getOmemoMetadata(account, address)).toEqual( + metadata, + ); }); test("storeOmemoSession, getOmemoSession, and removeOmemoSession", async ({ - page, persistence, }) => { const account = "omemo-session-existing@example.com"; const address = "contact@example.com/1"; const session = '{"sessions":{},"version":"v1"}'; - const result = await page.evaluate( - async ({ persistence, account, address, session }) => { - await persistence.storeOmemoSession(account, address, session); - const loaded = await persistence.getOmemoSession(account, address); - await persistence.removeOmemoSession(account, address); - const afterRemove = await persistence.getOmemoSession(account, address); - - return { - loaded, - afterRemove, - }; - }, - { persistence, account, address, session }, - ); - expect(result.loaded).toEqual(session); - expect(result.afterRemove).toBeNull(); + await persistence.storeOmemoSession(account, address, session); + const loaded = await persistence.getOmemoSession(account, address); + await persistence.removeOmemoSession(account, address); + const afterRemove = await persistence.getOmemoSession(account, address); + expect(loaded).toEqual(session); + expect(afterRemove).toBeNull(); }); 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: "alice@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: "alice@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"); + const builder = new borogove.ChatMessageBuilder({ + serverId: "parent", + serverIdBy: "alice@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: "alice@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, + ]); + expect(childStored.replyToMessage.body().toPlainText()).toBe("Hello"); }); test("allows rescinding a custom reaction", async ({ - page, borogove, persistence, }) => { - const result = await page.evaluate( - async ({ borogove, persistence }) => { - const account = "alice@example.com"; - const chatId = "hatter@example.com"; - const targetLocalId = "target-message"; - const reactionMessageId = "reaction-message"; - const timestamp = "2026-09-09T12:00:00Z"; - - const targetBuilder = new borogove.ChatMessageBuilder({ - localId: targetLocalId, - senderId: account, - direction: 1, - }); - targetBuilder.sortId = "a0"; - targetBuilder.to = borogove.JID.parse(chatId); - targetBuilder.from = borogove.JID.parse(account); - targetBuilder.recipients = [targetBuilder.to]; - targetBuilder.replyTo = [targetBuilder.from]; - - await persistence.storeMessages(account, [targetBuilder.build()]); - - const reaction = new borogove.CustomEmojiReaction( - account, - timestamp, - "tada", - "https://example.com/tada.png", - reactionMessageId, - ); - const update = new borogove.ReactionUpdate( - reactionMessageId, - null, - null, - targetLocalId, - chatId, - account, - timestamp, - [reaction], - borogove.ReactionUpdateKind.AppendReactions, - ); - - const withReaction = await persistence.storeReaction(account, update); - - const emptyBuilder = new borogove.ChatMessageBuilder({ - localId: reactionMessageId, - senderId: account, - direction: 1, - }); - emptyBuilder.sortId = "a1"; - emptyBuilder.to = borogove.JID.parse(chatId); - emptyBuilder.from = borogove.JID.parse(account); - emptyBuilder.recipients = [emptyBuilder.to]; - emptyBuilder.replyTo = [emptyBuilder.from]; - - const [stored] = await persistence.storeMessages(account, [ - emptyBuilder.build(), - ]); - - return { - before: [...withReaction!.reactions.keys()], - after: [...stored.reactions.keys()], - }; - }, - { borogove, persistence }, + const account = "alice@example.com"; + const chatId = "hatter@example.com"; + const targetLocalId = "target-message"; + const reactionMessageId = "reaction-message"; + const timestamp = "2026-09-09T12:00:00Z"; + + const targetBuilder = new borogove.ChatMessageBuilder({ + localId: targetLocalId, + senderId: account, + direction: 1, + }); + targetBuilder.sortId = "a0"; + targetBuilder.to = borogove.JID.parse(chatId); + targetBuilder.from = borogove.JID.parse(account); + targetBuilder.recipients = [targetBuilder.to]; + targetBuilder.replyTo = [targetBuilder.from]; + + await persistence.storeMessages(account, [targetBuilder.build()]); + + const reaction = new borogove.CustomEmojiReaction( + account, + timestamp, + "tada", + "https://example.com/tada.png", + reactionMessageId, ); + const update = new borogove.ReactionUpdate( + reactionMessageId, + null, + null, + targetLocalId, + chatId, + account, + timestamp, + [reaction], + borogove.ReactionUpdateKind.AppendReactions, + ); + + const withReaction = await persistence.storeReaction(account, update); - expect(result.before).toEqual(["https://example.com/tada.png"]); - expect(result.after).toEqual([]); + const emptyBuilder = new borogove.ChatMessageBuilder({ + localId: reactionMessageId, + senderId: account, + direction: 1, + }); + emptyBuilder.sortId = "a1"; + emptyBuilder.to = borogove.JID.parse(chatId); + emptyBuilder.from = borogove.JID.parse(account); + emptyBuilder.recipients = [emptyBuilder.to]; + emptyBuilder.replyTo = [emptyBuilder.from]; + + const [stored] = await persistence.storeMessages(account, [ + emptyBuilder.build(), + ]); + + expect([...withReaction!.reactions.keys()]).toEqual([ + "https://example.com/tada.png", + ]); + expect([...stored.reactions.keys()]).toEqual([]); }); } type TestKeyPair = { - privKey: number[]; - pubKey: number[]; + privKey: ArrayBuffer; + pubKey: ArrayBuffer; }; -const makeKey = (): number[] => [...randomBytes(32)]; +const makeKey = (): ArrayBuffer => { + const bytes = new Uint8Array(32); + crypto.getRandomValues(bytes); + return bytes.buffer; +}; const makeKeyPair = (): TestKeyPair => ({ privKey: makeKey(), - pubKey: makeKey().reverse(), + pubKey: makeKey(), }); function expectKeyPair(actual: TestKeyPair, expected: TestKeyPair) { @@ -2462,6 +1926,6 @@ function expectKeyPair(actual: TestKeyPair, expected: TestKeyPair) { expectKey(actual.pubKey, expected.pubKey); } -function expectKey(actual: number[], expected: number[]) { +function expectKey(actual: ArrayBuffer, expected: ArrayBuffer) { expect(actual).toEqual(expected); } diff --git a/test/sqlite.spec.ts b/test/sqlite.spec.ts index d124d0f..d34b33f 100644 --- a/test/sqlite.spec.ts +++ b/test/sqlite.spec.ts @@ -1,4 +1,65 @@ -import { sqliteTest } from "./browser-test"; +import { test } from "vitest"; +import * as borogove from "../playwright/.cache/borogove.js"; +import * as sqlite from "../playwright/.cache/sqlite-wasm.js"; import { sharedPersistenceTests } from "./persistence-tests"; +async function sqliteFixture() { + const databaseName = "sqlite"; + const mediaStore = await borogove.persistence.MediaStoreCache( + `${databaseName}-media`, + ); + window.sqliteWorker1Url = new URL("/sqlite-worker1.js", location.href); + const persistence = new sqlite.borogove_persistence_Sqlite( + databaseName, + mediaStore, + ); + const storeChats = persistence.storeChats.bind(persistence); + // 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. + persistence.storeChats = (...args) => { + storeChats(...args); + return new Promise((resolve) => setTimeout(resolve, 200)); + }; + const createChannel = (p: any, chatId: string) => + new sqlite.Channel(null, null, p, chatId); + const storeIncompleteMember = async (chatId: string) => { + 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, + "{}", + "[]", + "{}", + "", + ], + ); + }; + return { borogove, persistence, createChannel, storeIncompleteMember }; +} + +const sqlitePersistence = sqliteFixture(); +function sqliteTest(name: string, fn: (fixture: any) => Promise<void>) { + test("sqlite: " + name, async () => { + const current = await sqlitePersistence; + await current.persistence.db.exec( + "DELETE FROM messages; DELETE FROM chats; DELETE FROM keyvaluepairs; DELETE FROM caps; DELETE FROM services; DELETE FROM accounts; DELETE FROM reactions; DELETE FROM members; DELETE FROM omemo_identity_keys; DELETE FROM omemo_devices; DELETE FROM omemo_prekeys; DELETE FROM omemo_signed_prekeys; DELETE FROM omemo_contact_identity_keys; DELETE FROM omemo_sessions; DELETE FROM omemo_sessions_meta;", + ); + await fn(current); + }); +} + sharedPersistenceTests(sqliteTest); diff --git a/vitest.config.mts b/vitest.config.mts new file mode 100644 index 0000000..c006a39 --- /dev/null +++ b/vitest.config.mts @@ -0,0 +1,39 @@ +import { defineConfig } from "vitest/config"; +import { playwright } from "@vitest/browser-playwright"; +import { createReadStream } from "node:fs"; + +export default defineConfig({ + root: "..", + plugins: [ + { + name: "serve-sqlite-worker", + configureServer(server) { + server.middlewares.use("/sqlite-worker1.js", (_req, res) => { + res.setHeader("Content-Type", "text/javascript; charset=utf-8"); + createReadStream( + new URL("playwright/.cache/sqlite-worker1.js", import.meta.url), + ).pipe(res); + }); + }, + }, + ], + test: { + include: ["test/idb.spec.ts", "test/sqlite.spec.ts"], + browser: { + enabled: true, + provider: playwright(), + headless: true, + instances: [ + { + browser: "chromium", + include: ["test/idb.spec.ts", "test/sqlite.spec.ts"], + }, + { + browser: "firefox", + include: ["test/idb.spec.ts", "test/sqlite.spec.ts"], + }, + { browser: "webkit", include: ["test/idb.spec.ts"] }, + ], + }, + }, +});