Skip to content

Commit 83b4571

Browse files
author
David Robertson
authored
Add test case reproducing matrix-org/synapse#5677 for local users (#199)
* Mark MustDo as Deprecated * Introduce SyncUntilInvitedTo * match: use rs.Exists() in JSONKeyEqual, for consistency with other matchers * match: matcher that seeks an array of a fixed size * Introduce `AnyOf` matcher * Fix PUT call to set displayname
1 parent f56ed8e commit 83b4571

File tree

5 files changed

+282
-3
lines changed

5 files changed

+282
-3
lines changed

.golangci.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,11 @@ issues:
438438
- G107
439439
# gosec: Error on TLS InsecureSkipVerify set to true.
440440
- G402
441+
# staticcheck: Using a deprecated function, variable, constant or field
442+
# Tried to make this a warning rather than error in the severity section.
443+
# I failed. But it's nice to have goland know that certain things are deprecated
444+
# so it can strike them through.
445+
- SA1019
441446

442447
# Excluding configuration per-path, per-linter, per-text and per-source
443448
exclude-rules:

internal/client/client.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,15 +127,34 @@ func (c *CSAPI) SendEventSynced(t *testing.T, roomID string, e b.Event) string {
127127
return eventID
128128
}
129129

130-
// SyncUntilTimelineHas blocks and continually calls /sync until the `check` function returns true.
130+
// SyncUntilTimelineHas is a wrapper around `SyncUntil`.
131+
// It blocks and continually calls `/sync` until
132+
// - we have joined the given room
133+
// - we see an event in the room for which the `check` function returns True
131134
// If the `check` function fails the test, the failing event will be automatically logged.
132135
// Will time out after CSAPI.SyncUntilTimeout.
133136
func (c *CSAPI) SyncUntilTimelineHas(t *testing.T, roomID string, check func(gjson.Result) bool) {
134137
t.Helper()
135138
c.SyncUntil(t, "", "", "rooms.join."+GjsonEscape(roomID)+".timeline.events", check)
136139
}
137140

138-
// SyncUntil blocks and continually calls /sync until the `check` function returns true.
141+
// SyncUntilInvitedTo is a wrapper around SyncUntil.
142+
// It blocks and continually calls `/sync` until we've been invited to the given room.
143+
// Will time out after CSAPI.SyncUntilTimeout.
144+
func (c *CSAPI) SyncUntilInvitedTo(t *testing.T, roomID string) {
145+
t.Helper()
146+
check := func(event gjson.Result) bool {
147+
return event.Get("type").Str == "m.room.member" &&
148+
event.Get("content.membership").Str == "invite" &&
149+
event.Get("state_key").Str == c.UserID
150+
}
151+
c.SyncUntil(t, "", "", "rooms.invite."+GjsonEscape(roomID)+".invite_state.events", check)
152+
}
153+
154+
// SyncUntil blocks and continually calls /sync until
155+
// - the response contains a particular `key`, and
156+
// - its corresponding value is an array
157+
// - some element in that array makes the `check` function return true.
139158
// If the `check` function fails the test, the failing event will be automatically logged.
140159
// Will time out after CSAPI.SyncUntilTimeout.
141160
func (c *CSAPI) SyncUntil(t *testing.T, since, filter, key string, check func(gjson.Result) bool) {
@@ -213,6 +232,9 @@ func (c *CSAPI) RegisterUser(t *testing.T, localpart, password string) (userID,
213232
}
214233

215234
// MustDo will do the HTTP request and fail the test if the response is not 2xx
235+
//
236+
// Deprecated: Prefer MustDoFunc. MustDo is the older format which doesn't allow for vargs
237+
// and will be removed in the future. MustDoFunc also logs HTTP response bodies on error.
216238
func (c *CSAPI) MustDo(t *testing.T, method string, paths []string, jsonBody interface{}) *http.Response {
217239
t.Helper()
218240
res := c.DoFunc(t, method, paths, WithJSONBody(t, jsonBody))

internal/instruction/runner.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,9 @@ func calculateUserInstructionSets(r *Runner, hs b.Homeserver) [][]instruction {
289289
instrs = append(instrs, instructionLogin(hs, user))
290290
} else {
291291
instrs = append(instrs, instructionRegister(hs, user))
292+
if user.DisplayName != "" {
293+
instrs = append(instrs, instructionDisplayName(hs, user))
294+
}
292295
}
293296
createdUsers[user.Localpart] = true
294297

@@ -440,6 +443,21 @@ func instructionRegister(hs b.Homeserver, user b.User) instruction {
440443
}
441444
}
442445

446+
func instructionDisplayName(hs b.Homeserver, user b.User) instruction {
447+
body := map[string]interface{}{
448+
"displayname": user.DisplayName,
449+
}
450+
return instruction{
451+
method: "PUT",
452+
path: fmt.Sprintf(
453+
"/_matrix/client/r0/profile/@%s:%s/displayname",
454+
user.Localpart, hs.Name,
455+
),
456+
accessToken: fmt.Sprintf("user_@%s:%s", user.Localpart, hs.Name),
457+
body: body,
458+
}
459+
}
460+
443461
func instructionLogin(hs b.Homeserver, user b.User) instruction {
444462
body := map[string]interface{}{
445463
"type": "m.login.password",

internal/match/json.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package match
33
import (
44
"fmt"
55
"reflect"
6+
"strings"
67

78
"github.com/tidwall/gjson"
89
)
@@ -17,7 +18,7 @@ type JSON func(body []byte) error
1718
func JSONKeyEqual(wantKey string, wantValue interface{}) JSON {
1819
return func(body []byte) error {
1920
res := gjson.GetBytes(body, wantKey)
20-
if res.Index == 0 {
21+
if !res.Exists() {
2122
return fmt.Errorf("key '%s' missing", wantKey)
2223
}
2324
gotValue := res.Value()
@@ -55,6 +56,26 @@ func JSONKeyTypeEqual(wantKey string, wantType gjson.Type) JSON {
5556
}
5657
}
5758

59+
// JSONKeyArrayOfSize returns a matcher which will check that `wantKey` is present and
60+
// its value is an array with the given size.
61+
// `wantKey` can be nested, see https://godoc.org/github.com/tidwall/gjson#Get for details.
62+
func JSONKeyArrayOfSize(wantKey string, wantSize int) JSON {
63+
return func(body []byte) error {
64+
res := gjson.GetBytes(body, wantKey)
65+
if !res.Exists() {
66+
return fmt.Errorf("key '%s' missing", wantKey)
67+
}
68+
if !res.IsArray() {
69+
return fmt.Errorf("key '%s' is not an array", wantKey)
70+
}
71+
entries := res.Array()
72+
if len(entries) != wantSize {
73+
return fmt.Errorf("key '%s' is an array of the wrong size, got %v want %v", wantKey, len(entries), wantSize)
74+
}
75+
return nil
76+
}
77+
}
78+
5879
func jsonCheckOffInternal(wantKey string, wantItems []interface{}, allowUnwantedItems bool, mapper func(gjson.Result) interface{}, fn func(interface{}, gjson.Result) error) JSON {
5980
return func(body []byte) error {
6081
res := gjson.GetBytes(body, wantKey)
@@ -204,3 +225,29 @@ func JSONMapEach(wantKey string, fn func(k, v gjson.Result) error) JSON {
204225
return err
205226
}
206227
}
228+
229+
// AnyOf takes 1 or more `checkers`, and builds a new checker which accepts a given
230+
// json body iff it's accepted by at least one of the original `checkers`.
231+
func AnyOf(checkers ...JSON) JSON {
232+
return func(body []byte) error {
233+
if len(checkers) == 0 {
234+
return fmt.Errorf("must provide at least one checker to AnyOf")
235+
}
236+
237+
errors := make([]error, len(checkers))
238+
for i, check := range checkers {
239+
errors[i] = check(body)
240+
if errors[i] == nil {
241+
return nil
242+
}
243+
}
244+
245+
builder := strings.Builder{}
246+
builder.WriteString("all checks failed:")
247+
for _, err := range errors {
248+
builder.WriteString("\n ")
249+
builder.WriteString(err.Error())
250+
}
251+
return fmt.Errorf(builder.String())
252+
}
253+
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
// +build !dendrite_blacklist
2+
3+
// Rationale for being included in Dendrite's blacklist: https://github.com/matrix-org/complement/pull/199#issuecomment-904852233
4+
package csapi_tests
5+
6+
import (
7+
"testing"
8+
9+
"github.com/matrix-org/complement/internal/b"
10+
"github.com/matrix-org/complement/internal/client"
11+
"github.com/matrix-org/complement/internal/match"
12+
"github.com/matrix-org/complement/internal/must"
13+
)
14+
15+
const aliceUserID = "@alice:hs1"
16+
const alicePublicName = "Alice Cooper"
17+
const alicePrivateName = "Freddy"
18+
19+
var justAliceByPublicName = []match.JSON{
20+
match.JSONKeyArrayOfSize("results", 1),
21+
match.JSONKeyEqual("results.0.display_name", alicePublicName),
22+
match.JSONKeyEqual("results.0.user_id", aliceUserID),
23+
}
24+
25+
var noResults = []match.JSON{
26+
match.JSONKeyArrayOfSize("results", 0),
27+
}
28+
29+
func setupUsers(t *testing.T) (*client.CSAPI, *client.CSAPI, *client.CSAPI, func(*testing.T)) {
30+
// Originally written to reproduce https://github.com/matrix-org/synapse/issues/5677
31+
// In that bug report,
32+
// - Bob knows about Alice, and
33+
// - Alice has revealed a private name to another friend X,
34+
// - Bob can see that private name when he shouldn't be able to.
35+
//
36+
// I've tweaked the names to be more traditional:
37+
// - Eve knows about Alice,
38+
// - Alice reveals a private name to another friend Bob
39+
// - Eve shouldn't be able to see that private name via the directory.
40+
deployment := Deploy(t, b.BlueprintAlice)
41+
cleanup := func(t *testing.T) {
42+
deployment.Destroy(t)
43+
}
44+
45+
alice := deployment.Client(t, "hs1", aliceUserID)
46+
bob := deployment.RegisterUser(t, "hs1", "bob", "bob-has-a-very-secret-pw")
47+
eve := deployment.RegisterUser(t, "hs1", "eve", "eve-has-a-very-secret-pw")
48+
49+
// Alice sets her profile displayname. This ensures that her
50+
// public name, private name and userid localpart are all
51+
// distinguishable, even case-insensitively.
52+
alice.MustDoFunc(
53+
t,
54+
"PUT",
55+
[]string{"_matrix", "client", "r0", "profile", alice.UserID, "displayname"},
56+
client.WithJSONBody(t, map[string]interface{}{
57+
"displayname": alicePublicName,
58+
}),
59+
)
60+
61+
// Alice creates a public room (so when Eve searches, she can see that Alice exists)
62+
alice.CreateRoom(t, map[string]interface{}{"visibility": "public"})
63+
return alice, bob, eve, cleanup
64+
}
65+
66+
func checkExpectations(t *testing.T, bob, eve *client.CSAPI) {
67+
t.Run("Eve can find Alice by profile display name", func(t *testing.T) {
68+
res := eve.MustDoFunc(
69+
t,
70+
"POST",
71+
[]string{"_matrix", "client", "r0", "user_directory", "search"},
72+
client.WithJSONBody(t, map[string]interface{}{
73+
"search_term": alicePublicName,
74+
}),
75+
)
76+
must.MatchResponse(t, res, match.HTTPResponse{JSON: justAliceByPublicName})
77+
})
78+
79+
t.Run("Eve can find Alice by mxid", func(t *testing.T) {
80+
res := eve.MustDoFunc(
81+
t,
82+
"POST",
83+
[]string{"_matrix", "client", "r0", "user_directory", "search"},
84+
client.WithJSONBody(t, map[string]interface{}{
85+
"search_term": aliceUserID,
86+
}),
87+
)
88+
must.MatchResponse(t, res, match.HTTPResponse{JSON: justAliceByPublicName})
89+
})
90+
91+
t.Run("Eve cannot find Alice by room-specific name that Eve is not privy to", func(t *testing.T) {
92+
res := eve.MustDoFunc(
93+
t,
94+
"POST",
95+
[]string{"_matrix", "client", "r0", "user_directory", "search"},
96+
client.WithJSONBody(t, map[string]interface{}{
97+
"search_term": alicePrivateName,
98+
}),
99+
)
100+
must.MatchResponse(t, res, match.HTTPResponse{JSON: noResults})
101+
})
102+
103+
t.Run("Bob can find Alice by profile display name", func(t *testing.T) {
104+
res := bob.MustDoFunc(
105+
t,
106+
"POST",
107+
[]string{"_matrix", "client", "r0", "user_directory", "search"},
108+
client.WithJSONBody(t, map[string]interface{}{
109+
"search_term": alicePublicName,
110+
}),
111+
)
112+
must.MatchResponse(t, res, match.HTTPResponse{
113+
JSON: justAliceByPublicName,
114+
})
115+
})
116+
117+
t.Run("Bob can find Alice by mxid", func(t *testing.T) {
118+
res := bob.MustDoFunc(
119+
t,
120+
"POST",
121+
[]string{"_matrix", "client", "r0", "user_directory", "search"},
122+
client.WithJSONBody(t, map[string]interface{}{
123+
"search_term": aliceUserID,
124+
}),
125+
)
126+
must.MatchResponse(t, res, match.HTTPResponse{
127+
JSON: justAliceByPublicName,
128+
})
129+
})
130+
}
131+
132+
func TestRoomSpecificUsernameChange(t *testing.T) {
133+
alice, bob, eve, cleanup := setupUsers(t)
134+
defer cleanup(t)
135+
136+
// Bob creates a new room and invites Alice.
137+
privateRoom := bob.CreateRoom(t, map[string]interface{}{
138+
"visibility": "private",
139+
"invite": []string{alice.UserID},
140+
})
141+
142+
// Alice waits until she sees the invite, then accepts.
143+
alice.SyncUntilInvitedTo(t, privateRoom)
144+
alice.JoinRoom(t, privateRoom, nil)
145+
146+
// Alice reveals her private name to Bob
147+
alice.MustDoFunc(
148+
t,
149+
"PUT",
150+
[]string{"_matrix", "client", "r0", "rooms", privateRoom, "state", "m.room.member", alice.UserID},
151+
client.WithJSONBody(t, map[string]interface{}{
152+
"displayname": alicePrivateName,
153+
"membership": "join",
154+
}),
155+
)
156+
157+
checkExpectations(t, bob, eve)
158+
}
159+
160+
func TestRoomSpecificUsernameAtJoin(t *testing.T) {
161+
alice, bob, eve, cleanup := setupUsers(t)
162+
defer cleanup(t)
163+
164+
// Bob creates a new room and invites Alice.
165+
privateRoom := bob.CreateRoom(t, map[string]interface{}{
166+
"visibility": "private",
167+
"invite": []string{alice.UserID},
168+
})
169+
170+
// Alice waits until she sees the invite, then accepts.
171+
// When she accepts, she does so with a specific displayname.
172+
alice.SyncUntilInvitedTo(t, privateRoom)
173+
alice.JoinRoom(t, privateRoom, nil)
174+
175+
// Alice reveals her private name to Bob
176+
alice.MustDoFunc(
177+
t,
178+
"PUT",
179+
[]string{"_matrix", "client", "r0", "rooms", privateRoom, "state", "m.room.member", alice.UserID},
180+
client.WithJSONBody(t, map[string]interface{}{
181+
"displayname": alicePrivateName,
182+
"membership": "join",
183+
}),
184+
)
185+
186+
checkExpectations(t, bob, eve)
187+
}

0 commit comments

Comments
 (0)