-
Notifications
You must be signed in to change notification settings - Fork 52
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
- Loading branch information
Showing
1 changed file
with
205 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
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" | ||
) | ||
|
||
// 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 unread 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) | ||
} | ||
} | ||
|
||
// Test behavior of threaded receipts and notifications. | ||
// | ||
// 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 timeline like: | ||
// | ||
// A<--B<--C [thread] | ||
// ^ | ||
// +---D [main timeline] | ||
// | ||
// 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) | ||
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) | ||
|
||
// 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. | ||
eventA := alice.SendEventSynced(t, roomID, b.Event{ | ||
Type: "m.room.message", | ||
Content: map[string]interface{}{ | ||
"msgtype": "m.text", | ||
"body": "Hello world!", | ||
}, | ||
}) | ||
|
||
// Create a thread from the above message and send both two messages in it, | ||
// the second of which is a mention (causing a highlight). | ||
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": eventA, | ||
"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": eventA, | ||
"rel_type": "m.thread", | ||
}, | ||
}, | ||
}) | ||
|
||
// Send an additional unthreaded message, which is a mention (causing a highlight). | ||
eventD := 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, which should include all previously | ||
// sent messages. | ||
bob.MustSyncUntil( | ||
t, client.SyncReq{Since: bobNextBatch}, | ||
client.SyncTimelineHas(roomID, func(r gjson.Result) bool { | ||
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() | ||
}), | ||
) | ||
bob.MustSyncUntil( | ||
t, | ||
client.SyncReq{Since: bobNextBatch, Filter: threadFilter}, | ||
client.SyncTimelineHas(roomID, func(r gjson.Result) bool { | ||
return r.Get("event_id").Str == eventD | ||
}), | ||
syncHasUnreadNotifs(roomID, func(r gjson.Result, t gjson.Result) bool { | ||
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 | ||
}), | ||
) | ||
|
||
// 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", 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 == 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() | ||
}), | ||
) | ||
bob.MustSyncUntil( | ||
t, | ||
client.SyncReq{Since: bobNextBatch, Filter: threadFilter}, | ||
client.SyncTimelineHas(roomID, func(r gjson.Result) bool { | ||
return r.Get("event_id").Str == eventD | ||
}), | ||
syncHasUnreadNotifs(roomID, func(r gjson.Result, t gjson.Result) bool { | ||
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 | ||
}), | ||
) | ||
|
||
// 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", 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 == 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() | ||
}), | ||
) | ||
bob.MustSyncUntil( | ||
t, | ||
client.SyncReq{Since: bobNextBatch, Filter: threadFilter}, | ||
client.SyncTimelineHas(roomID, func(r gjson.Result) bool { | ||
return r.Get("event_id").Str == eventD | ||
}), | ||
syncHasUnreadNotifs(roomID, func(r gjson.Result, t gjson.Result) bool { | ||
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 | ||
}), | ||
) | ||
|
||
// 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", 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 == 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() | ||
}), | ||
) | ||
bob.MustSyncUntil( | ||
t, | ||
client.SyncReq{Since: bobNextBatch, Filter: threadFilter}, | ||
client.SyncTimelineHas(roomID, func(r gjson.Result) bool { | ||
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() | ||
}), | ||
) | ||
} |