From d5fa161a3fefb84ceb5ba5f9d7c4f0cd06303352 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 11 Oct 2022 11:01:10 -0400 Subject: [PATCH 1/9] Add outline of a test. --- tests/csapi/thread_notifications_test.go | 103 +++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 tests/csapi/thread_notifications_test.go diff --git a/tests/csapi/thread_notifications_test.go b/tests/csapi/thread_notifications_test.go new file mode 100644 index 00000000..d16c6a59 --- /dev/null +++ b/tests/csapi/thread_notifications_test.go @@ -0,0 +1,103 @@ +package csapi_tests + +import ( + "fmt" + "testing" + + "github.com/tidwall/gjson" + + "github.com/matrix-org/complement/internal/b" + "github.com/matrix-org/complement/internal/client" +) + +func syncHasUnreadNotifs(roomID string, check func(gjson.Result) bool) client.SyncCheckOpt { + return func(clientUserID string, topLevelSyncJSON gjson.Result) error { + unreadNotifications := topLevelSyncJSON.Get("rooms.join." + client.GjsonEscape(roomID) + ".unread_notifications") + if !unreadNotifications.Exists() { + return fmt.Errorf("syncHasUnreadNotifs(%s): missing notifications", roomID) + } + if check(unreadNotifications) { + return nil + } + return fmt.Errorf("syncHasUnreadNotifs(%s): check function did not pass: %v", roomID, unreadNotifications.Raw) + } +} + +// Test behavior of threaded receipts and notifications. +// +// Send a series of messages, some of which are in threads. Send combinations of +// threaded and unthreaded receipts and ensure the notification counts are updated +// appropriately. +func TestThreadedReceipts(t *testing.T) { + deployment := Deploy(t, b.BlueprintOneToOneRoom) + defer deployment.Destroy(t) + + // Create a room with alice and bob. + alice := deployment.Client(t, "hs1", "@alice:hs1") + bob := deployment.Client(t, "hs1", "@bob:hs1") + + roomID := alice.CreateRoom(t, map[string]interface{}{"preset": "public_chat"}) + bob.JoinRoom(t, roomID, nil) + + token := bob.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(bob.UserID, roomID)) + + // Send messages as alice and then check the highlight and notification counts from bob. + firstEventID := alice.SendEventSynced(t, roomID, b.Event{ + Type: "m.room.message", + Content: map[string]interface{}{ + "msgtype": "m.text", + "body": "Hello world!", + }, + }) + bob.MustSyncUntil( + t, client.SyncReq{Since: token}, + client.SyncTimelineHas(roomID, func(r gjson.Result) bool { + return r.Get("event_id").Str == firstEventID + }), + syncHasUnreadNotifs(roomID, func(r gjson.Result) bool { + return r.Get("highlight_count").Num == 0 && r.Get("notification_count").Num == 1 + }), + ) + + // Send a highlight message and re-check counts. + secondEventID := alice.SendEventSynced(t, roomID, b.Event{ + Type: "m.room.message", + Content: map[string]interface{}{ + "msgtype": "m.text", + "body": fmt.Sprintf("Hello %s!", bob.UserID), + }, + }) + bob.MustSyncUntil( + t, client.SyncReq{Since: token}, + client.SyncTimelineHas(roomID, func(r gjson.Result) bool { + return r.Get("event_id").Str == secondEventID + }), + syncHasUnreadNotifs(roomID, func(r gjson.Result) bool { + return r.Get("highlight_count").Num == 1 && r.Get("notification_count").Num == 2 + }), + ) + + // Mark the first event as read and check counts. + bob.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "receipt", "m.read", firstEventID}, client.WithJSONBody(t, struct{}{})) + bob.MustSyncUntil( + t, client.SyncReq{Since: token}, + client.SyncTimelineHas(roomID, func(r gjson.Result) bool { + return r.Get("event_id").Str == secondEventID + }), + syncHasUnreadNotifs(roomID, func(r gjson.Result) bool { + return r.Get("highlight_count").Num == 1 && r.Get("notification_count").Num == 1 + }), + ) + + // Mark the second event as read and check counts. + bob.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "receipt", "m.read", secondEventID}, client.WithJSONBody(t, struct{}{})) + bob.MustSyncUntil( + t, client.SyncReq{Since: token}, + client.SyncTimelineHas(roomID, func(r gjson.Result) bool { + return r.Get("event_id").Str == secondEventID + }), + syncHasUnreadNotifs(roomID, func(r gjson.Result) bool { + return r.Get("highlight_count").Num == 0 && r.Get("notification_count").Num == 0 + }), + ) +} From 2c42cf9c43e012ad2555d2e7f4136d16a893b45d Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 11 Oct 2022 12:04:51 -0400 Subject: [PATCH 2/9] Add tests for threaded vs. unthreaded receipts. --- tests/csapi/thread_notifications_test.go | 119 ++++++++++++++++++----- 1 file changed, 96 insertions(+), 23 deletions(-) diff --git a/tests/csapi/thread_notifications_test.go b/tests/csapi/thread_notifications_test.go index d16c6a59..8554942a 100644 --- a/tests/csapi/thread_notifications_test.go +++ b/tests/csapi/thread_notifications_test.go @@ -10,16 +10,17 @@ import ( "github.com/matrix-org/complement/internal/client" ) -func syncHasUnreadNotifs(roomID string, check func(gjson.Result) bool) client.SyncCheckOpt { +func syncHasUnreadNotifs(roomID string, check func(gjson.Result, gjson.Result) bool) client.SyncCheckOpt { return func(clientUserID string, topLevelSyncJSON gjson.Result) error { unreadNotifications := topLevelSyncJSON.Get("rooms.join." + client.GjsonEscape(roomID) + ".unread_notifications") + unreadThreadNotifications := topLevelSyncJSON.Get("rooms.join." + client.GjsonEscape(roomID) + ".unread_thread_notifications") if !unreadNotifications.Exists() { return fmt.Errorf("syncHasUnreadNotifs(%s): missing notifications", roomID) } - if check(unreadNotifications) { + if check(unreadNotifications, unreadThreadNotifications) { return nil } - return fmt.Errorf("syncHasUnreadNotifs(%s): check function did not pass: %v", roomID, unreadNotifications.Raw) + return fmt.Errorf("syncHasUnreadNotifs(%s): check function did not pass: %v / %v", roomID, unreadNotifications.Raw, unreadThreadNotifications.Raw) } } @@ -49,15 +50,30 @@ func TestThreadedReceipts(t *testing.T) { "body": "Hello world!", }, }) - bob.MustSyncUntil( - t, client.SyncReq{Since: token}, - client.SyncTimelineHas(roomID, func(r gjson.Result) bool { - return r.Get("event_id").Str == firstEventID - }), - syncHasUnreadNotifs(roomID, func(r gjson.Result) bool { - return r.Get("highlight_count").Num == 0 && r.Get("notification_count").Num == 1 - }), - ) + + // Create some threaded messages. + firstThreadEventID := alice.SendEventSynced(t, roomID, b.Event{ + Type: "m.room.message", + Content: map[string]interface{}{ + "msgtype": "m.text", + "body": "Start thread!", + "m.relates_to": map[string]interface{}{ + "event_id": firstEventID, + "rel_type": "m.thread", + }, + }, + }) + alice.SendEventSynced(t, roomID, b.Event{ + Type: "m.room.message", + Content: map[string]interface{}{ + "msgtype": "m.text", + "body": fmt.Sprintf("Thread response %s!", bob.UserID), + "m.relates_to": map[string]interface{}{ + "event_id": firstEventID, + "rel_type": "m.thread", + }, + }, + }) // Send a highlight message and re-check counts. secondEventID := alice.SendEventSynced(t, roomID, b.Event{ @@ -67,37 +83,94 @@ func TestThreadedReceipts(t *testing.T) { "body": fmt.Sprintf("Hello %s!", bob.UserID), }, }) + + // A filter to get thread notifications. + threadFilter := `{"room":{"timeline":{"unread_thread_notifications":true}}}` + + // Check the unthreaded and threaded counts. + bob.MustSyncUntil( + t, client.SyncReq{Since: token}, + client.SyncTimelineHas(roomID, func(r gjson.Result) bool { + return r.Get("event_id").Str == secondEventID + }), + syncHasUnreadNotifs(roomID, func(r gjson.Result, t gjson.Result) bool { + return r.Get("highlight_count").Num == 2 && r.Get("notification_count").Num == 4 && !t.Exists() + }), + ) + bob.MustSyncUntil( + t, + client.SyncReq{Since: token, Filter: threadFilter}, client.SyncTimelineHas(roomID, func(r gjson.Result) bool { + return r.Get("event_id").Str == secondEventID + }), + syncHasUnreadNotifs(roomID, func(r gjson.Result, t gjson.Result) bool { + threadNotifications := t.Get(client.GjsonEscape(firstEventID)) + return r.Get("highlight_count").Num == 1 && r.Get("notification_count").Num == 2 && threadNotifications.Get("highlight_count").Num == 1 && threadNotifications.Get("notification_count").Num == 2 + }), + ) + + // Mark the first event as read with a threaded receipt and check counts. + bob.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "receipt", "m.read", firstEventID}, client.WithJSONBody(t, map[string]interface{}{"thread_id": "main"})) bob.MustSyncUntil( t, client.SyncReq{Since: token}, - client.SyncTimelineHas(roomID, func(r gjson.Result) bool { + client.SyncTimelineHas(roomID, func(r gjson.Result) bool { + return r.Get("event_id").Str == secondEventID + }), + syncHasUnreadNotifs(roomID, func(r gjson.Result, t gjson.Result) bool { + return r.Get("highlight_count").Num == 2 && r.Get("notification_count").Num == 3 && !t.Exists() + }), + ) + bob.MustSyncUntil( + t, + client.SyncReq{Since: token, Filter: threadFilter}, client.SyncTimelineHas(roomID, func(r gjson.Result) bool { return r.Get("event_id").Str == secondEventID }), - syncHasUnreadNotifs(roomID, func(r gjson.Result) bool { - return r.Get("highlight_count").Num == 1 && r.Get("notification_count").Num == 2 + syncHasUnreadNotifs(roomID, func(r gjson.Result, t gjson.Result) bool { + threadNotifications := t.Get(client.GjsonEscape(firstEventID)) + return r.Get("highlight_count").Num == 1 && r.Get("notification_count").Num == 1 && threadNotifications.Get("highlight_count").Num == 1 && threadNotifications.Get("notification_count").Num == 2 }), ) - // Mark the first event as read and check counts. - bob.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "receipt", "m.read", firstEventID}, client.WithJSONBody(t, struct{}{})) + // Mark the first thread event as read and check counts. + bob.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "receipt", "m.read", firstThreadEventID}, client.WithJSONBody(t, map[string]interface{}{"thread_id": firstEventID})) bob.MustSyncUntil( t, client.SyncReq{Since: token}, client.SyncTimelineHas(roomID, func(r gjson.Result) bool { return r.Get("event_id").Str == secondEventID }), - syncHasUnreadNotifs(roomID, func(r gjson.Result) bool { - return r.Get("highlight_count").Num == 1 && r.Get("notification_count").Num == 1 + syncHasUnreadNotifs(roomID, func(r gjson.Result, t gjson.Result) bool { + return r.Get("highlight_count").Num == 2 && r.Get("notification_count").Num == 2 && !t.Exists() + }), + ) + bob.MustSyncUntil( + t, + client.SyncReq{Since: token, Filter: threadFilter}, client.SyncTimelineHas(roomID, func(r gjson.Result) bool { + return r.Get("event_id").Str == secondEventID + }), + syncHasUnreadNotifs(roomID, func(r gjson.Result, t gjson.Result) bool { + threadNotifications := t.Get(client.GjsonEscape(firstEventID)) + return r.Get("highlight_count").Num == 1 && r.Get("notification_count").Num == 1 && threadNotifications.Get("highlight_count").Num == 1 && threadNotifications.Get("notification_count").Num == 1 }), ) - // Mark the second event as read and check counts. + // Mark the entire room as read by sending an unthreaded read receipt on the last + // event. bob.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "receipt", "m.read", secondEventID}, client.WithJSONBody(t, struct{}{})) bob.MustSyncUntil( t, client.SyncReq{Since: token}, - client.SyncTimelineHas(roomID, func(r gjson.Result) bool { + client.SyncTimelineHas(roomID, func(r gjson.Result) bool { + return r.Get("event_id").Str == secondEventID + }), + syncHasUnreadNotifs(roomID, func(r gjson.Result, t gjson.Result) bool { + return r.Get("highlight_count").Num == 0 && r.Get("notification_count").Num == 0 && !t.Exists() + }), + ) + bob.MustSyncUntil( + t, + client.SyncReq{Since: token, Filter: threadFilter}, client.SyncTimelineHas(roomID, func(r gjson.Result) bool { return r.Get("event_id").Str == secondEventID }), - syncHasUnreadNotifs(roomID, func(r gjson.Result) bool { - return r.Get("highlight_count").Num == 0 && r.Get("notification_count").Num == 0 + syncHasUnreadNotifs(roomID, func(r gjson.Result, t gjson.Result) bool { + return r.Get("highlight_count").Num == 0 && r.Get("notification_count").Num == 0 && !t.Exists() }), ) } From 2c71289492e6cf719c879492f28066043e0e46c1 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 11 Oct 2022 12:05:48 -0400 Subject: [PATCH 3/9] Not supported on dendrite. --- tests/csapi/thread_notifications_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/csapi/thread_notifications_test.go b/tests/csapi/thread_notifications_test.go index 8554942a..eb22d848 100644 --- a/tests/csapi/thread_notifications_test.go +++ b/tests/csapi/thread_notifications_test.go @@ -8,6 +8,7 @@ import ( "github.com/matrix-org/complement/internal/b" "github.com/matrix-org/complement/internal/client" + "github.com/matrix-org/complement/runtime" ) func syncHasUnreadNotifs(roomID string, check func(gjson.Result, gjson.Result) bool) client.SyncCheckOpt { @@ -30,6 +31,7 @@ func syncHasUnreadNotifs(roomID string, check func(gjson.Result, gjson.Result) b // threaded and unthreaded receipts and ensure the notification counts are updated // appropriately. func TestThreadedReceipts(t *testing.T) { + runtime.SkipIf(t, runtime.Dendrite) // not supported deployment := Deploy(t, b.BlueprintOneToOneRoom) defer deployment.Destroy(t) From 6f31028b42cc1f0245868df297ad2d2f237b4018 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 21 Oct 2022 13:24:55 -0400 Subject: [PATCH 4/9] Clarify comments. --- tests/csapi/thread_notifications_test.go | 44 +++++++++++++++++------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/tests/csapi/thread_notifications_test.go b/tests/csapi/thread_notifications_test.go index eb22d848..54dca1da 100644 --- a/tests/csapi/thread_notifications_test.go +++ b/tests/csapi/thread_notifications_test.go @@ -27,9 +27,19 @@ func syncHasUnreadNotifs(roomID string, check func(gjson.Result, gjson.Result) b // Test behavior of threaded receipts and notifications. // -// Send a series of messages, some of which are in threads. Send combinations of -// threaded and unthreaded receipts and ensure the notification counts are updated -// appropriately. +// 1. Send a series of messages, some of which are in threads. +// 2. Send combinations of threaded and unthreaded receipts. +// 3. Ensure the notification counts are updated appropriately. +// +// This sends four messages as alice creating a DAG somewhat like: +// +// A<--B<--C +// ^ +// +---D +// +// Where C and D generate highlight notifications. +// +// Notification counts and receipts are handled by bob. func TestThreadedReceipts(t *testing.T) { runtime.SkipIf(t, runtime.Dendrite) // not supported deployment := Deploy(t, b.BlueprintOneToOneRoom) @@ -44,7 +54,7 @@ func TestThreadedReceipts(t *testing.T) { token := bob.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(bob.UserID, roomID)) - // Send messages as alice and then check the highlight and notification counts from bob. + // Send an initial message as alice. firstEventID := alice.SendEventSynced(t, roomID, b.Event{ Type: "m.room.message", Content: map[string]interface{}{ @@ -53,7 +63,8 @@ func TestThreadedReceipts(t *testing.T) { }, }) - // Create some threaded messages. + // Create a thread from the above message and send both two messages in it, + // the second of which is a mention (causing a highlight). firstThreadEventID := alice.SendEventSynced(t, roomID, b.Event{ Type: "m.room.message", Content: map[string]interface{}{ @@ -77,7 +88,7 @@ func TestThreadedReceipts(t *testing.T) { }, }) - // Send a highlight message and re-check counts. + // Send an additional unthreaded message, which is a mention (causing a highlight). secondEventID := alice.SendEventSynced(t, roomID, b.Event{ Type: "m.room.message", Content: map[string]interface{}{ @@ -89,7 +100,8 @@ func TestThreadedReceipts(t *testing.T) { // A filter to get thread notifications. threadFilter := `{"room":{"timeline":{"unread_thread_notifications":true}}}` - // Check the unthreaded and threaded counts. + // Check the unthreaded and threaded counts, which should include all previously + // sent messages. bob.MustSyncUntil( t, client.SyncReq{Since: token}, client.SyncTimelineHas(roomID, func(r gjson.Result) bool { @@ -106,11 +118,14 @@ func TestThreadedReceipts(t *testing.T) { }), syncHasUnreadNotifs(roomID, func(r gjson.Result, t gjson.Result) bool { threadNotifications := t.Get(client.GjsonEscape(firstEventID)) - return r.Get("highlight_count").Num == 1 && r.Get("notification_count").Num == 2 && threadNotifications.Get("highlight_count").Num == 1 && threadNotifications.Get("notification_count").Num == 2 + return r.Get("highlight_count").Num == 1 && r.Get("notification_count").Num == 2 && + threadNotifications.Get("highlight_count").Num == 1 && threadNotifications.Get("notification_count").Num == 2 }), ) - // Mark the first event as read with a threaded receipt and check counts. + // Mark the first event as read with a threaded receipt. This causes only the + // notification from that event to be marked as read and only impacts the main + // timeline. bob.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "receipt", "m.read", firstEventID}, client.WithJSONBody(t, map[string]interface{}{"thread_id": "main"})) bob.MustSyncUntil( t, client.SyncReq{Since: token}, @@ -128,11 +143,13 @@ func TestThreadedReceipts(t *testing.T) { }), syncHasUnreadNotifs(roomID, func(r gjson.Result, t gjson.Result) bool { threadNotifications := t.Get(client.GjsonEscape(firstEventID)) - return r.Get("highlight_count").Num == 1 && r.Get("notification_count").Num == 1 && threadNotifications.Get("highlight_count").Num == 1 && threadNotifications.Get("notification_count").Num == 2 + return r.Get("highlight_count").Num == 1 && r.Get("notification_count").Num == 1 && + threadNotifications.Get("highlight_count").Num == 1 && threadNotifications.Get("notification_count").Num == 2 }), ) - // Mark the first thread event as read and check counts. + // Mark the first thread event as read. This causes only the notification from + // that event to be marked as read and only impacts the thread timeline. bob.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "receipt", "m.read", firstThreadEventID}, client.WithJSONBody(t, map[string]interface{}{"thread_id": firstEventID})) bob.MustSyncUntil( t, client.SyncReq{Since: token}, @@ -150,12 +167,13 @@ func TestThreadedReceipts(t *testing.T) { }), syncHasUnreadNotifs(roomID, func(r gjson.Result, t gjson.Result) bool { threadNotifications := t.Get(client.GjsonEscape(firstEventID)) - return r.Get("highlight_count").Num == 1 && r.Get("notification_count").Num == 1 && threadNotifications.Get("highlight_count").Num == 1 && threadNotifications.Get("notification_count").Num == 1 + return r.Get("highlight_count").Num == 1 && r.Get("notification_count").Num == 1 && + threadNotifications.Get("highlight_count").Num == 1 && threadNotifications.Get("notification_count").Num == 1 }), ) // Mark the entire room as read by sending an unthreaded read receipt on the last - // event. + // event. This clears all notification counts. bob.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "receipt", "m.read", secondEventID}, client.WithJSONBody(t, struct{}{})) bob.MustSyncUntil( t, client.SyncReq{Since: token}, From 1124741b18b51c0ba6d06f74a7cfbb06dc51b5c0 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 27 Oct 2022 09:47:33 -0400 Subject: [PATCH 5/9] Clarify comment. Co-authored-by: David Robertson --- tests/csapi/thread_notifications_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/csapi/thread_notifications_test.go b/tests/csapi/thread_notifications_test.go index 54dca1da..30c4620c 100644 --- a/tests/csapi/thread_notifications_test.go +++ b/tests/csapi/thread_notifications_test.go @@ -31,11 +31,11 @@ func syncHasUnreadNotifs(roomID string, check func(gjson.Result, gjson.Result) b // 2. Send combinations of threaded and unthreaded receipts. // 3. Ensure the notification counts are updated appropriately. // -// This sends four messages as alice creating a DAG somewhat like: +// This sends four messages as alice creating a timeline like: // -// A<--B<--C +// A<--B<--C [thread] // ^ -// +---D +// +---D [main timeline] // // Where C and D generate highlight notifications. // From a5add36bbc08926decd32233cad3d50ea00922c7 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 27 Oct 2022 09:52:28 -0400 Subject: [PATCH 6/9] Formatting. --- tests/csapi/thread_notifications_test.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/csapi/thread_notifications_test.go b/tests/csapi/thread_notifications_test.go index 30c4620c..833cbdee 100644 --- a/tests/csapi/thread_notifications_test.go +++ b/tests/csapi/thread_notifications_test.go @@ -113,7 +113,8 @@ func TestThreadedReceipts(t *testing.T) { ) bob.MustSyncUntil( t, - client.SyncReq{Since: token, Filter: threadFilter}, client.SyncTimelineHas(roomID, func(r gjson.Result) bool { + client.SyncReq{Since: token, Filter: threadFilter}, + client.SyncTimelineHas(roomID, func(r gjson.Result) bool { return r.Get("event_id").Str == secondEventID }), syncHasUnreadNotifs(roomID, func(r gjson.Result, t gjson.Result) bool { @@ -138,7 +139,8 @@ func TestThreadedReceipts(t *testing.T) { ) bob.MustSyncUntil( t, - client.SyncReq{Since: token, Filter: threadFilter}, client.SyncTimelineHas(roomID, func(r gjson.Result) bool { + client.SyncReq{Since: token, Filter: threadFilter}, + client.SyncTimelineHas(roomID, func(r gjson.Result) bool { return r.Get("event_id").Str == secondEventID }), syncHasUnreadNotifs(roomID, func(r gjson.Result, t gjson.Result) bool { @@ -162,7 +164,8 @@ func TestThreadedReceipts(t *testing.T) { ) bob.MustSyncUntil( t, - client.SyncReq{Since: token, Filter: threadFilter}, client.SyncTimelineHas(roomID, func(r gjson.Result) bool { + client.SyncReq{Since: token, Filter: threadFilter}, + client.SyncTimelineHas(roomID, func(r gjson.Result) bool { return r.Get("event_id").Str == secondEventID }), syncHasUnreadNotifs(roomID, func(r gjson.Result, t gjson.Result) bool { @@ -186,7 +189,8 @@ func TestThreadedReceipts(t *testing.T) { ) bob.MustSyncUntil( t, - client.SyncReq{Since: token, Filter: threadFilter}, client.SyncTimelineHas(roomID, func(r gjson.Result) bool { + client.SyncReq{Since: token, Filter: threadFilter}, + client.SyncTimelineHas(roomID, func(r gjson.Result) bool { return r.Get("event_id").Str == secondEventID }), syncHasUnreadNotifs(roomID, func(r gjson.Result, t gjson.Result) bool { From fafa3c46bf98e4b8997fb925fb2184087933c7ee Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 27 Oct 2022 09:55:16 -0400 Subject: [PATCH 7/9] Clarify syncHasUnreadNotifs. --- tests/csapi/thread_notifications_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/csapi/thread_notifications_test.go b/tests/csapi/thread_notifications_test.go index 833cbdee..eb575f49 100644 --- a/tests/csapi/thread_notifications_test.go +++ b/tests/csapi/thread_notifications_test.go @@ -11,12 +11,16 @@ import ( "github.com/matrix-org/complement/runtime" ) +// Builds a `SyncCheckOpt` which enforces that a sync result satisfies some `check` function +// on the `unread_notifications` and `unread_thread_notifications` fields for the given room. +// The `unread_notifications` field must exist, or else the overall SyncCheckOpt will be considered +// as failing. func syncHasUnreadNotifs(roomID string, check func(gjson.Result, gjson.Result) bool) client.SyncCheckOpt { return func(clientUserID string, topLevelSyncJSON gjson.Result) error { unreadNotifications := topLevelSyncJSON.Get("rooms.join." + client.GjsonEscape(roomID) + ".unread_notifications") unreadThreadNotifications := topLevelSyncJSON.Get("rooms.join." + client.GjsonEscape(roomID) + ".unread_thread_notifications") if !unreadNotifications.Exists() { - return fmt.Errorf("syncHasUnreadNotifs(%s): missing notifications", roomID) + return fmt.Errorf("syncHasUnreadNotifs(%s): missing unread notifications", roomID) } if check(unreadNotifications, unreadThreadNotifications) { return nil From cc1a3915d1dbbf07b6fb88e50cc7a061b53eeeca Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 27 Oct 2022 09:57:07 -0400 Subject: [PATCH 8/9] Clarify token. --- tests/csapi/thread_notifications_test.go | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/csapi/thread_notifications_test.go b/tests/csapi/thread_notifications_test.go index eb575f49..11b92877 100644 --- a/tests/csapi/thread_notifications_test.go +++ b/tests/csapi/thread_notifications_test.go @@ -56,7 +56,8 @@ func TestThreadedReceipts(t *testing.T) { roomID := alice.CreateRoom(t, map[string]interface{}{"preset": "public_chat"}) bob.JoinRoom(t, roomID, nil) - token := bob.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(bob.UserID, roomID)) + // A next batch token which is past the initial room creation. + bobNextBatch := bob.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(bob.UserID, roomID)) // Send an initial message as alice. firstEventID := alice.SendEventSynced(t, roomID, b.Event{ @@ -107,7 +108,7 @@ func TestThreadedReceipts(t *testing.T) { // Check the unthreaded and threaded counts, which should include all previously // sent messages. bob.MustSyncUntil( - t, client.SyncReq{Since: token}, + t, client.SyncReq{Since: bobNextBatch}, client.SyncTimelineHas(roomID, func(r gjson.Result) bool { return r.Get("event_id").Str == secondEventID }), @@ -117,7 +118,7 @@ func TestThreadedReceipts(t *testing.T) { ) bob.MustSyncUntil( t, - client.SyncReq{Since: token, Filter: threadFilter}, + client.SyncReq{Since: bobNextBatch, Filter: threadFilter}, client.SyncTimelineHas(roomID, func(r gjson.Result) bool { return r.Get("event_id").Str == secondEventID }), @@ -133,7 +134,7 @@ func TestThreadedReceipts(t *testing.T) { // timeline. bob.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "receipt", "m.read", firstEventID}, client.WithJSONBody(t, map[string]interface{}{"thread_id": "main"})) bob.MustSyncUntil( - t, client.SyncReq{Since: token}, + t, client.SyncReq{Since: bobNextBatch}, client.SyncTimelineHas(roomID, func(r gjson.Result) bool { return r.Get("event_id").Str == secondEventID }), @@ -143,7 +144,7 @@ func TestThreadedReceipts(t *testing.T) { ) bob.MustSyncUntil( t, - client.SyncReq{Since: token, Filter: threadFilter}, + client.SyncReq{Since: bobNextBatch, Filter: threadFilter}, client.SyncTimelineHas(roomID, func(r gjson.Result) bool { return r.Get("event_id").Str == secondEventID }), @@ -158,7 +159,7 @@ func TestThreadedReceipts(t *testing.T) { // that event to be marked as read and only impacts the thread timeline. bob.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "receipt", "m.read", firstThreadEventID}, client.WithJSONBody(t, map[string]interface{}{"thread_id": firstEventID})) bob.MustSyncUntil( - t, client.SyncReq{Since: token}, + t, client.SyncReq{Since: bobNextBatch}, client.SyncTimelineHas(roomID, func(r gjson.Result) bool { return r.Get("event_id").Str == secondEventID }), @@ -168,7 +169,7 @@ func TestThreadedReceipts(t *testing.T) { ) bob.MustSyncUntil( t, - client.SyncReq{Since: token, Filter: threadFilter}, + client.SyncReq{Since: bobNextBatch, Filter: threadFilter}, client.SyncTimelineHas(roomID, func(r gjson.Result) bool { return r.Get("event_id").Str == secondEventID }), @@ -183,7 +184,7 @@ func TestThreadedReceipts(t *testing.T) { // event. This clears all notification counts. bob.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "receipt", "m.read", secondEventID}, client.WithJSONBody(t, struct{}{})) bob.MustSyncUntil( - t, client.SyncReq{Since: token}, + t, client.SyncReq{Since: bobNextBatch}, client.SyncTimelineHas(roomID, func(r gjson.Result) bool { return r.Get("event_id").Str == secondEventID }), @@ -193,7 +194,7 @@ func TestThreadedReceipts(t *testing.T) { ) bob.MustSyncUntil( t, - client.SyncReq{Since: token, Filter: threadFilter}, + client.SyncReq{Since: bobNextBatch, Filter: threadFilter}, client.SyncTimelineHas(roomID, func(r gjson.Result) bool { return r.Get("event_id").Str == secondEventID }), From 87fa4e04d6140e7f8e52f815b7e4f1fcd43b8096 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 27 Oct 2022 09:58:54 -0400 Subject: [PATCH 9/9] Rename event variables. --- tests/csapi/thread_notifications_test.go | 38 ++++++++++++------------ 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/csapi/thread_notifications_test.go b/tests/csapi/thread_notifications_test.go index 11b92877..cbfd3515 100644 --- a/tests/csapi/thread_notifications_test.go +++ b/tests/csapi/thread_notifications_test.go @@ -60,7 +60,7 @@ func TestThreadedReceipts(t *testing.T) { bobNextBatch := bob.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(bob.UserID, roomID)) // Send an initial message as alice. - firstEventID := alice.SendEventSynced(t, roomID, b.Event{ + eventA := alice.SendEventSynced(t, roomID, b.Event{ Type: "m.room.message", Content: map[string]interface{}{ "msgtype": "m.text", @@ -70,13 +70,13 @@ func TestThreadedReceipts(t *testing.T) { // Create a thread from the above message and send both two messages in it, // the second of which is a mention (causing a highlight). - firstThreadEventID := alice.SendEventSynced(t, roomID, b.Event{ + eventB := alice.SendEventSynced(t, roomID, b.Event{ Type: "m.room.message", Content: map[string]interface{}{ "msgtype": "m.text", "body": "Start thread!", "m.relates_to": map[string]interface{}{ - "event_id": firstEventID, + "event_id": eventA, "rel_type": "m.thread", }, }, @@ -87,14 +87,14 @@ func TestThreadedReceipts(t *testing.T) { "msgtype": "m.text", "body": fmt.Sprintf("Thread response %s!", bob.UserID), "m.relates_to": map[string]interface{}{ - "event_id": firstEventID, + "event_id": eventA, "rel_type": "m.thread", }, }, }) // Send an additional unthreaded message, which is a mention (causing a highlight). - secondEventID := alice.SendEventSynced(t, roomID, b.Event{ + eventD := alice.SendEventSynced(t, roomID, b.Event{ Type: "m.room.message", Content: map[string]interface{}{ "msgtype": "m.text", @@ -110,7 +110,7 @@ func TestThreadedReceipts(t *testing.T) { bob.MustSyncUntil( t, client.SyncReq{Since: bobNextBatch}, client.SyncTimelineHas(roomID, func(r gjson.Result) bool { - return r.Get("event_id").Str == secondEventID + return r.Get("event_id").Str == eventD }), syncHasUnreadNotifs(roomID, func(r gjson.Result, t gjson.Result) bool { return r.Get("highlight_count").Num == 2 && r.Get("notification_count").Num == 4 && !t.Exists() @@ -120,10 +120,10 @@ func TestThreadedReceipts(t *testing.T) { t, client.SyncReq{Since: bobNextBatch, Filter: threadFilter}, client.SyncTimelineHas(roomID, func(r gjson.Result) bool { - return r.Get("event_id").Str == secondEventID + return r.Get("event_id").Str == eventD }), syncHasUnreadNotifs(roomID, func(r gjson.Result, t gjson.Result) bool { - threadNotifications := t.Get(client.GjsonEscape(firstEventID)) + threadNotifications := t.Get(client.GjsonEscape(eventA)) return r.Get("highlight_count").Num == 1 && r.Get("notification_count").Num == 2 && threadNotifications.Get("highlight_count").Num == 1 && threadNotifications.Get("notification_count").Num == 2 }), @@ -132,11 +132,11 @@ func TestThreadedReceipts(t *testing.T) { // Mark the first event as read with a threaded receipt. This causes only the // notification from that event to be marked as read and only impacts the main // timeline. - bob.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "receipt", "m.read", firstEventID}, client.WithJSONBody(t, map[string]interface{}{"thread_id": "main"})) + bob.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "receipt", "m.read", eventA}, client.WithJSONBody(t, map[string]interface{}{"thread_id": "main"})) bob.MustSyncUntil( t, client.SyncReq{Since: bobNextBatch}, client.SyncTimelineHas(roomID, func(r gjson.Result) bool { - return r.Get("event_id").Str == secondEventID + return r.Get("event_id").Str == eventD }), syncHasUnreadNotifs(roomID, func(r gjson.Result, t gjson.Result) bool { return r.Get("highlight_count").Num == 2 && r.Get("notification_count").Num == 3 && !t.Exists() @@ -146,10 +146,10 @@ func TestThreadedReceipts(t *testing.T) { t, client.SyncReq{Since: bobNextBatch, Filter: threadFilter}, client.SyncTimelineHas(roomID, func(r gjson.Result) bool { - return r.Get("event_id").Str == secondEventID + return r.Get("event_id").Str == eventD }), syncHasUnreadNotifs(roomID, func(r gjson.Result, t gjson.Result) bool { - threadNotifications := t.Get(client.GjsonEscape(firstEventID)) + threadNotifications := t.Get(client.GjsonEscape(eventA)) return r.Get("highlight_count").Num == 1 && r.Get("notification_count").Num == 1 && threadNotifications.Get("highlight_count").Num == 1 && threadNotifications.Get("notification_count").Num == 2 }), @@ -157,11 +157,11 @@ func TestThreadedReceipts(t *testing.T) { // Mark the first thread event as read. This causes only the notification from // that event to be marked as read and only impacts the thread timeline. - bob.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "receipt", "m.read", firstThreadEventID}, client.WithJSONBody(t, map[string]interface{}{"thread_id": firstEventID})) + bob.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "receipt", "m.read", eventB}, client.WithJSONBody(t, map[string]interface{}{"thread_id": eventA})) bob.MustSyncUntil( t, client.SyncReq{Since: bobNextBatch}, client.SyncTimelineHas(roomID, func(r gjson.Result) bool { - return r.Get("event_id").Str == secondEventID + return r.Get("event_id").Str == eventD }), syncHasUnreadNotifs(roomID, func(r gjson.Result, t gjson.Result) bool { return r.Get("highlight_count").Num == 2 && r.Get("notification_count").Num == 2 && !t.Exists() @@ -171,10 +171,10 @@ func TestThreadedReceipts(t *testing.T) { t, client.SyncReq{Since: bobNextBatch, Filter: threadFilter}, client.SyncTimelineHas(roomID, func(r gjson.Result) bool { - return r.Get("event_id").Str == secondEventID + return r.Get("event_id").Str == eventD }), syncHasUnreadNotifs(roomID, func(r gjson.Result, t gjson.Result) bool { - threadNotifications := t.Get(client.GjsonEscape(firstEventID)) + threadNotifications := t.Get(client.GjsonEscape(eventA)) return r.Get("highlight_count").Num == 1 && r.Get("notification_count").Num == 1 && threadNotifications.Get("highlight_count").Num == 1 && threadNotifications.Get("notification_count").Num == 1 }), @@ -182,11 +182,11 @@ func TestThreadedReceipts(t *testing.T) { // Mark the entire room as read by sending an unthreaded read receipt on the last // event. This clears all notification counts. - bob.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "receipt", "m.read", secondEventID}, client.WithJSONBody(t, struct{}{})) + bob.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "receipt", "m.read", eventD}, client.WithJSONBody(t, struct{}{})) bob.MustSyncUntil( t, client.SyncReq{Since: bobNextBatch}, client.SyncTimelineHas(roomID, func(r gjson.Result) bool { - return r.Get("event_id").Str == secondEventID + return r.Get("event_id").Str == eventD }), syncHasUnreadNotifs(roomID, func(r gjson.Result, t gjson.Result) bool { return r.Get("highlight_count").Num == 0 && r.Get("notification_count").Num == 0 && !t.Exists() @@ -196,7 +196,7 @@ func TestThreadedReceipts(t *testing.T) { t, client.SyncReq{Since: bobNextBatch, Filter: threadFilter}, client.SyncTimelineHas(roomID, func(r gjson.Result) bool { - return r.Get("event_id").Str == secondEventID + return r.Get("event_id").Str == eventD }), syncHasUnreadNotifs(roomID, func(r gjson.Result, t gjson.Result) bool { return r.Get("highlight_count").Num == 0 && r.Get("notification_count").Num == 0 && !t.Exists()