Skip to content

Commit e7f6921

Browse files
author
David Robertson
authored
Test that invites from ignored users are omitted from incremental syncs (#267)
* Change SyncUntil* functions to return next_batch * Add a matcher which checks that a key is missing * Reproduce matrix-org/synapse#11506 * Blocklist this test on Dendrite
1 parent 5c13563 commit e7f6921

File tree

3 files changed

+145
-6
lines changed

3 files changed

+145
-6
lines changed

internal/client/client.go

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,22 +134,41 @@ func (c *CSAPI) SendEventSynced(t *testing.T, roomID string, e b.Event) string {
134134
// - we see an event in the room for which the `check` function returns True
135135
// If the `check` function fails the test, the failing event will be automatically logged.
136136
// Will time out after CSAPI.SyncUntilTimeout.
137-
func (c *CSAPI) SyncUntilTimelineHas(t *testing.T, roomID string, check func(gjson.Result) bool) {
137+
//
138+
// Returns the `next_batch` token from the last /sync response. This can be passed as
139+
// `since` to sync from this point forward only.
140+
func (c *CSAPI) SyncUntilTimelineHas(t *testing.T, roomID string, check func(gjson.Result) bool) string {
141+
t.Helper()
142+
return c.SyncUntil(t, "", "", "rooms.join."+GjsonEscape(roomID)+".timeline.events", check)
143+
}
144+
145+
// SyncUntilGlobalAccountDataHas is a wrapper around `SyncUntil`.
146+
// It blocks and continually calls `/sync` until
147+
// - we an event in the global account data for which the `check` function returns True
148+
// If the `check` function fails the test, the failing event will be automatically logged.
149+
// Will time out after CSAPI.SyncUntilTimeout.
150+
//
151+
// Returns the `next_batch` token from the last /sync response. This can be passed as
152+
// `since` to sync from this point forward only.
153+
func (c *CSAPI) SyncUntilGlobalAccountDataHas(t *testing.T, check func(gjson.Result) bool) string {
138154
t.Helper()
139-
c.SyncUntil(t, "", "", "rooms.join."+GjsonEscape(roomID)+".timeline.events", check)
155+
return c.SyncUntil(t, "", "", "account_data.events", check)
140156
}
141157

142158
// SyncUntilInvitedTo is a wrapper around SyncUntil.
143159
// It blocks and continually calls `/sync` until we've been invited to the given room.
144160
// Will time out after CSAPI.SyncUntilTimeout.
145-
func (c *CSAPI) SyncUntilInvitedTo(t *testing.T, roomID string) {
161+
//
162+
// Returns the `next_batch` token from the last /sync response. This can be passed as
163+
// `since` to sync from this point forward only.
164+
func (c *CSAPI) SyncUntilInvitedTo(t *testing.T, roomID string) string {
146165
t.Helper()
147166
check := func(event gjson.Result) bool {
148167
return event.Get("type").Str == "m.room.member" &&
149168
event.Get("content.membership").Str == "invite" &&
150169
event.Get("state_key").Str == c.UserID
151170
}
152-
c.SyncUntil(t, "", "", "rooms.invite."+GjsonEscape(roomID)+".invite_state.events", check)
171+
return c.SyncUntil(t, "", "", "rooms.invite."+GjsonEscape(roomID)+".invite_state.events", check)
153172
}
154173

155174
// SyncUntilJoined is a wrapper around SyncUntil.
@@ -170,7 +189,10 @@ func (c *CSAPI) SyncUntilJoined(t *testing.T, roomID string) {
170189
// - some element in that array makes the `check` function return true.
171190
// If the `check` function fails the test, the failing event will be automatically logged.
172191
// Will time out after CSAPI.SyncUntilTimeout.
173-
func (c *CSAPI) SyncUntil(t *testing.T, since, filter, key string, check func(gjson.Result) bool) {
192+
//
193+
// Returns the `next_batch` token from the last /sync response. This can be passed as
194+
// `since` to sync from this point forward only.
195+
func (c *CSAPI) SyncUntil(t *testing.T, since, filter, key string, check func(gjson.Result) bool) string {
174196
t.Helper()
175197
start := time.Now()
176198
checkCounter := 0
@@ -212,7 +234,7 @@ func (c *CSAPI) SyncUntil(t *testing.T, since, filter, key string, check func(gj
212234
for i, ev := range events {
213235
lastEvent = &events[i]
214236
if check(ev) {
215-
return
237+
return since
216238
}
217239
wasFailed = t.Failed()
218240
checkCounter++

internal/match/json.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,18 @@ func JSONKeyPresent(wantKey string) JSON {
4141
}
4242
}
4343

44+
// JSONKeyMissing returns a matcher which will check that `forbiddenKey` is not present in the JSON object.
45+
// `forbiddenKey` can be nested, see https://godoc.org/github.com/tidwall/gjson#Get for details.
46+
func JSONKeyMissing(forbiddenKey string) JSON {
47+
return func(body []byte) error {
48+
res := gjson.GetBytes(body, forbiddenKey)
49+
if res.Exists() {
50+
return fmt.Errorf("key '%s' present", forbiddenKey)
51+
}
52+
return nil
53+
}
54+
}
55+
4456
// JSONKeyTypeEqual returns a matcher which will check that `wantKey` is present and its value is of the type `wantType`.
4557
// `wantKey` can be nested, see https://godoc.org/github.com/tidwall/gjson#Get for details.
4658
func JSONKeyTypeEqual(wantKey string, wantType gjson.Type) JSON {

tests/csapi/ignored_users_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// +build !dendrite_blacklist
2+
3+
// Rationale for being included in Dendrite's blacklist: https://github.com/matrix-org/dendrite/issues/600
4+
package csapi_tests
5+
6+
import (
7+
"net/url"
8+
"testing"
9+
10+
"github.com/tidwall/gjson"
11+
12+
"github.com/matrix-org/complement/internal/b"
13+
"github.com/matrix-org/complement/internal/client"
14+
"github.com/matrix-org/complement/internal/match"
15+
"github.com/matrix-org/complement/internal/must"
16+
)
17+
18+
// The Spec says here
19+
// https://spec.matrix.org/v1.1/client-server-api/#server-behaviour-13
20+
// that
21+
// > Servers must not send room invites from ignored users to clients.
22+
//
23+
// This is a regression test for
24+
// https://github.com/matrix-org/synapse/issues/11506
25+
// to ensure that Synapse complies with this part of the spec.
26+
func TestInviteFromIgnoredUsersDoesNotAppearInSync(t *testing.T) {
27+
deployment := Deploy(t, b.BlueprintCleanHS)
28+
defer deployment.Destroy(t)
29+
alice := deployment.RegisterUser(t, "hs1", "alice", "sufficiently_long_password_alice")
30+
bob := deployment.RegisterUser(t, "hs1", "bob", "sufficiently_long_password_bob")
31+
chris := deployment.RegisterUser(t, "hs1", "chris", "sufficiently_long_password_chris")
32+
33+
// Alice creates a room for herself.
34+
publicRoom := alice.CreateRoom(t, map[string]interface{}{
35+
"preset": "public_chat",
36+
})
37+
38+
// Alice waits to see the join event.
39+
alice.SyncUntilTimelineHas(
40+
t, publicRoom, func(ev gjson.Result) bool {
41+
return ev.Get("type").Str == "m.room.member" &&
42+
ev.Get("state_key").Str == alice.UserID &&
43+
ev.Get("content.membership").Str == "join"
44+
},
45+
)
46+
47+
// Alice ignores Bob.
48+
alice.MustDoFunc(
49+
t,
50+
"PUT",
51+
[]string{"_matrix", "client", "r0", "user", alice.UserID, "account_data", "m.ignored_user_list"},
52+
client.WithJSONBody(t, map[string]interface{}{
53+
"ignored_users": map[string]interface{}{
54+
bob.UserID: map[string]interface{}{},
55+
},
56+
}),
57+
)
58+
59+
// Alice waits to see that the ignore was successful.
60+
sinceJoinedAndIgnored := alice.SyncUntilGlobalAccountDataHas(
61+
t,
62+
func(ev gjson.Result) bool {
63+
t.Logf(ev.Raw + "\n")
64+
return ev.Get("type").Str == "m.ignored_user_list" &&
65+
ev.Get("content.ignored_users."+client.GjsonEscape(bob.UserID)).Exists()
66+
},
67+
)
68+
69+
// Bob invites Alice to a private room.
70+
bobRoom := bob.CreateRoom(t, map[string]interface{}{
71+
"preset": "private_chat",
72+
"invite": []string{alice.UserID},
73+
})
74+
75+
// So does Chris.
76+
chrisRoom := chris.CreateRoom(t, map[string]interface{}{
77+
"preset": "private_chat",
78+
"invite": []string{alice.UserID},
79+
})
80+
81+
// Alice waits until she's seen Chris's invite.
82+
alice.SyncUntilInvitedTo(t, chrisRoom)
83+
84+
// We re-request the sync with a `since` token. We should see Chris's invite, but not Bob's.
85+
queryParams := url.Values{
86+
"since": {sinceJoinedAndIgnored},
87+
"timeout": {"0"},
88+
}
89+
// Note: SyncUntil only runs its callback on array elements. I want to investigate an object.
90+
// So let's make the HTTP request more directly.
91+
response := alice.MustDoFunc(
92+
t,
93+
"GET",
94+
[]string{"_matrix", "client", "r0", "sync"},
95+
client.WithQueries(queryParams),
96+
)
97+
bobRoomPath := "rooms.invite." + client.GjsonEscape(bobRoom)
98+
chrisRoomPath := "rooms.invite." + client.GjsonEscape(chrisRoom)
99+
must.MatchResponse(t, response, match.HTTPResponse{
100+
JSON: []match.JSON{
101+
match.JSONKeyMissing(bobRoomPath),
102+
match.JSONKeyPresent(chrisRoomPath),
103+
},
104+
})
105+
}

0 commit comments

Comments
 (0)