package setup_test import ( "net/http" "net/url" "testing" "github.com/dariuszjaniak/warp/server/internal/setup" ) // threadCounts is the part of a sync the per-thread count test reads. type threadCounts struct { NotificationCount int `json:"notification_count"` HighlightCount int `json:"highlight_count"` } type threadSync struct { Rooms struct { Join map[string]struct { UnreadNotifications *threadCounts `json:"unread_notifications"` UnreadThreadNotifications map[string]threadCounts `json:"unread_thread_notifications"` } `json:"join"` } `json:"rooms"` } // threadSyncOnce is an initial sync, with the per-thread counts asked for // or not. func threadSyncOnce(t *testing.T, srv *setup.Server, token string, byThread bool) threadSync { t.Helper() path := "/_matrix/client/v3/sync?timeout=0" if byThread { path += "&filter=" + url.QueryEscape(`{"room":{"timeline":{"unread_thread_notifications":true}}}`) } rec := getWithToken(t, srv, path, token) if rec.Code != http.StatusOK { t.Fatalf("sync: %d %s", rec.Code, rec.Body.String()) } return decode[threadSync](t, rec) } // TestThreadNotificationCountsThroughTheAPI follows notifications into a // thread and out again: counted under the thread with the filter flag and // in the room total without it, and read by a thread receipt, a main // receipt and an unthreaded one, each clearing only its own part. func TestThreadNotificationCountsThroughTheAPI(t *testing.T) { srv := newSigningServer(t, nil) alice := registerThrough(t, srv, "alice") bob := registerThrough(t, srv, "bob") roomID := createPublicRoom(t, srv, alice) mustOK(t, post(t, srv, "/_matrix/client/v3/rooms/"+url.PathEscape(roomID)+"/join", map[string]any{}, bob), "join") waitForPush(t, srv) inThread := func(root string) map[string]any { return map[string]any{"rel_type": "m.thread", "event_id": root} } root := sendText(t, srv, bob, roomID, "r", map[string]any{"msgtype": "m.text", "body": "a thread"}) sendText(t, srv, bob, roomID, "a", map[string]any{"msgtype": "m.text", "body": "first reply", "m.relates_to": inThread(root)}) reply := sendText(t, srv, bob, roomID, "b", map[string]any{"msgtype": "m.text", "body": "second reply", "m.mentions": map[string]any{"user_ids": []string{"@alice:warp.example"}}, "m.relates_to": inThread(root)}) sendText(t, srv, bob, roomID, "c", map[string]any{"msgtype": "m.text", "body": "back in the room"}) waitForPush(t, srv) // Without the flag the room counts everything, as before threads. plain := threadSyncOnce(t, srv, alice, false).Rooms.Join[roomID] if c := plain.UnreadNotifications; c == nil || *c != (threadCounts{NotificationCount: 4, HighlightCount: 1}) { t.Errorf("room total = %+v", c) } if plain.UnreadThreadNotifications != nil { t.Errorf("thread counts without the flag: %+v", plain.UnreadThreadNotifications) } // With it the root and the later message are the main timeline's, and // the replies are the thread's. split := threadSyncOnce(t, srv, alice, true).Rooms.Join[roomID] if c := split.UnreadNotifications; c == nil || *c != (threadCounts{NotificationCount: 2}) { t.Errorf("main timeline = %+v", c) } if got := split.UnreadThreadNotifications; len(got) != 1 || got[root] != (threadCounts{NotificationCount: 2, HighlightCount: 1}) { t.Errorf("thread counts = %+v", got) } receipt := func(eventID string, body map[string]any) { t.Helper() mustOK(t, post(t, srv, "/_matrix/client/v3/rooms/"+url.PathEscape(roomID)+"/receipt/m.read/"+url.PathEscape(eventID), body, alice), "receipt") } // Reading the thread leaves the main timeline unread. receipt(reply, map[string]any{"thread_id": root}) split = threadSyncOnce(t, srv, alice, true).Rooms.Join[roomID] if c := split.UnreadNotifications; c == nil || c.NotificationCount != 2 || len(split.UnreadThreadNotifications) != 0 { t.Errorf("after the thread receipt: %+v, %+v", c, split.UnreadThreadNotifications) } if c := threadSyncOnce(t, srv, alice, false).Rooms.Join[roomID].UnreadNotifications; c == nil || c.NotificationCount != 2 { t.Errorf("room total after the thread receipt = %+v", c) } // A new reply, then a main receipt on the root: the root is read, the // later message and the new reply are not. third := sendText(t, srv, bob, roomID, "d", map[string]any{"msgtype": "m.text", "body": "third reply", "m.relates_to": inThread(root)}) waitForPush(t, srv) receipt(root, map[string]any{"thread_id": "main"}) split = threadSyncOnce(t, srv, alice, true).Rooms.Join[roomID] if c := split.UnreadNotifications; c == nil || c.NotificationCount != 1 || split.UnreadThreadNotifications[root].NotificationCount != 1 { t.Errorf("after the main receipt: %+v, %+v", c, split.UnreadThreadNotifications) } // An unthreaded receipt reads the lot up to its event, whichever // thread that event is in: here the newest, a reply. receipt(third, map[string]any{}) split = threadSyncOnce(t, srv, alice, true).Rooms.Join[roomID] if c := split.UnreadNotifications; c == nil || c.NotificationCount != 0 || len(split.UnreadThreadNotifications) != 0 { t.Errorf("after the unthreaded receipt: %+v, %+v", c, split.UnreadThreadNotifications) } }