Skip to content
Merged
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions tests/csapi/thread_notifications_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package csapi_tests

import (
"fmt"
"testing"

"github.com/tidwall/gjson"

"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 {
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, unreadThreadNotifications) {
return nil
}
return fmt.Errorf("syncHasUnreadNotifs(%s): check function did not pass: %v / %v", roomID, unreadNotifications.Raw, unreadThreadNotifications.Raw)
}
}
Copy link
Collaborator

@MadLittleMods MadLittleMods Oct 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could craft much better errors if we changed the check function into a expectedMap and displayed a diff

(however to do this in Go)

syncHasUnreadNotifs(roomID, {
  main: {
    highlight_count: 2,
    notification_count: 4
  },
  [firstEventID]: {
    label: "firstEventIDThreadNotifications",
    highlight_count: 1,
    notification_count: 4
  }
})
  • syncHasUnreadNotifs for main: Expected 2 / 4 but actual was 3 / 4 - Format is (highlights / notifications)
  • syncHasUnreadNotifs for firstEventIDThreadNotifications ($abcdef): Expected 1 / 2 but actual was 1 / 1 - Format is (highlights / notifications)

For comparison as a different, separate example, I made the errors for the MSC3030 tests a bit fancy to show the diff of what was expected vs actual in context of the room timeline so it's much easier to figure out what's going wrong, see #178

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left this somewhat generic because:

  1. It seems to fit better how check functions are done with complement.
  2. I suspect we will eventually want to check multiple threads.


// 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) {
runtime.SkipIf(t, runtime.Dendrite) // not supported
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels a bit icky to couple the test to the implementation, but I suppose we do this everywhere. Might be possible to do something cleaner with build tags, but that might be a faff too (would have to e.g. update Synapse CI and complement.sh...)

This is probably fine as it is.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we only use build tags for unstable features?

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!",
},
})

// 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{
Type: "m.room.message",
Content: map[string]interface{}{
"msgtype": "m.text",
"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 {
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, 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 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, 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 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 {
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, t gjson.Result) bool {
return r.Get("highlight_count").Num == 0 && r.Get("notification_count").Num == 0 && !t.Exists()
}),
)
}