git » sdk » commit 81a4502

Fix bug in SQLite version of getMembers

author Eric Roberts
2026-08-26 20:00:10 UTC
committer Stephen Paul Weber
2026-08-26 20:02:12 UTC
parent 074248bbc0c8852bcc9497e7cee021210b650f04

Fix bug in SQLite version of getMembers

Sqlite version considered empty presence map to be online, which is wrong. The
IDB version had the test that surfaced this, when moved into the shared tests
the SQLite executions of it failed.

Fixed the code in SQLite and added a unit test there as well. There are now no
unique tests in sqlite.spec.ts.

borogove/persistence/Sqlite.hx +3 -1
test/TestSqlite.hx +27 -0
test/idb.spec.ts +0 -80
test/persistence-tests.ts +94 -0
test/sqlite.spec.ts +0 -83

diff --git a/borogove/persistence/Sqlite.hx b/borogove/persistence/Sqlite.hx
index 73463e6..1b485ee 100644
--- a/borogove/persistence/Sqlite.hx
+++ b/borogove/persistence/Sqlite.hx
@@ -674,7 +674,9 @@ class Sqlite implements Persistence implements KeyValueStore {
 					(SELECT MAX(CASE value->>'$.id' WHEN 'owner' THEN 4 WHEN 'admin' THEN 3 WHEN 'none' THEN 1 WHEN 'outcast' THEN 0 ELSE 2 END) FROM json_each(roles)),
 					2
 				) AS role_rank,
-				CASE WHEN json(presence) NOT LIKE '% type=\\\"unavailable\\\"%' THEN 1 ELSE 0 END AS is_online
+				CASE WHEN EXISTS (SELECT 1 FROM json_each(presence))
+					AND json(presence) NOT LIKE '% type=\\\"unavailable\\\"%'
+					THEN 1 ELSE 0 END AS is_online
 			FROM members
 			WHERE
 				account_id=? AND chat_id=?
diff --git a/test/TestSqlite.hx b/test/TestSqlite.hx
index 44b23d2..06a4742 100644
--- a/test/TestSqlite.hx
+++ b/test/TestSqlite.hx
@@ -1212,6 +1212,33 @@ class TestSqlite extends utest.Test {
 		});
 	}
 
+	public function testGetMembersTreatsEmptyPresenceAsOffline(async: Async) {
+		final account = "alice@example.com";
+		final chat = new Channel(cast null, cast null, persistence, "room-members-empty-presence@example.com");
+		chat.displayName = "A Chat";
+
+		persistence.storeMembers(account, chat.chatId, [
+			new Member(
+				"room-members-empty-presence@example.com/guest",
+				"Guest",
+				null,
+				false,
+				[new Role("none", "Guest")],
+				JID.parse("guest@example.com"),
+				new Map(),
+				new AvailableChat("guest@example.com", "Guest", "", new borogove.Caps("", [], [], []))
+			)
+		]).then(_ ->
+			persistence.getMembers(account, chat, false)
+		).then(result -> {
+			Assert.equals(0, result.length);
+			async.done();
+		}).catchError(e -> {
+			Assert.fail(Std.string(e));
+			async.done();
+		});
+	}
+
 	public function testGetMembersIncludesModeratorVisibleRows(async: Async) {
 		final account = "alice@example.com";
 		final chat = new Channel(cast null, cast null, persistence, "room-members-6@example.com");
diff --git a/test/idb.spec.ts b/test/idb.spec.ts
index ee2e55c..2155f45 100644
--- a/test/idb.spec.ts
+++ b/test/idb.spec.ts
@@ -57,86 +57,6 @@ test("hydrate message with incomplete replyToMessage", async ({
 	expect(result).toBe("Hello");
 });
 
-test("getMembers filters hidden rows for non-moderators", async ({
-	page,
-	borogove,
-	persistence,
-}) => {
-	const result = await page.evaluate(
-		async ({ borogove, persistence }) => {
-			const chat = Object.create(borogove.Channel.prototype);
-			chat.chatId = "room-members-5@example.com";
-			chat.getDisplayName = () => "Tea Room";
-
-			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,
-			);
-
-			return normal.map((m) => m.displayName);
-		},
-		{ borogove, persistence },
-	);
-
-	expect(result).toEqual(["Zulu", "Alpha"]);
-});
-
 test("hydrate message with incomplete replyToMessage keys", async ({
 	page,
 	borogove,
diff --git a/test/persistence-tests.ts b/test/persistence-tests.ts
index eee2ea3..f198e79 100644
--- a/test/persistence-tests.ts
+++ b/test/persistence-tests.ts
@@ -1464,6 +1464,100 @@ export function sharedPersistenceTests(test: PersistenceTest) {
 		expect(result.chat2PresenceKeys).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,
+				);
+
+				return normal.map((m) => m.displayName);
+			},
+			{ borogove, persistence },
+		);
+
+		expect(result).toEqual(["Zulu", "Alpha"]);
+	});
+
 	test("getMembers includes moderator-visible rows", async ({
 		page,
 		borogove,
diff --git a/test/sqlite.spec.ts b/test/sqlite.spec.ts
index d60e4a5..299e75f 100644
--- a/test/sqlite.spec.ts
+++ b/test/sqlite.spec.ts
@@ -8,87 +8,4 @@ test.describe("not webkit", () => {
 	);
 
 	sharedPersistenceTests(test);
-
-	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 = "A Chat";
-				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/admin",
-						displayName: "Alpha",
-						photoUri: null,
-						isSelf: false,
-						roles: [{ id: "admin", title: "Admin" }],
-						jid: borogove.JID.parse("alpha@example.com"),
-						presence: new Map([
-							["desk", borogove.Stanza.parse("<presence />")],
-						]),
-						chat: { chatId: "alpha@example.com" },
-					},
-				]);
-
-				const members = await persistence.getMembers(
-					"alice@example.com",
-					chat,
-					false,
-				);
-				return members.map((m) => m.displayName);
-			},
-			{ borogove, persistence },
-		);
-
-		expect(result).toEqual(["Zulu", "Alpha"]);
-	});
 });