Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add test case reproducing matrix-org/synapse#5677 for local users #199

Merged
merged 26 commits into from
Nov 15, 2021
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
cd709e8
Add test case reproducing matrix-org/synapse#5677
DMRobertson Aug 23, 2021
05d148a
goimports
DMRobertson Aug 23, 2021
877f6a1
Mark MustDo as Deprecated
DMRobertson Aug 23, 2021
8209286
Introduce SyncUntilInvitedTo
DMRobertson Aug 24, 2021
07afbaa
client: Helper function SearchUserDirectory
DMRobertson Aug 24, 2021
75ef397
match: use rs.Exists() in JSONKeyEqual
DMRobertson Aug 24, 2021
68cf89a
match: matcher that seeks an array of a fixed size
DMRobertson Aug 24, 2021
ce8f3a9
Update tests after review
DMRobertson Aug 24, 2021
ed7d66e
Fix format string
DMRobertson Aug 24, 2021
5435622
Make lint accept use of deprecated things
DMRobertson Aug 24, 2021
6ce2fc4
Remove from synapse blacklist
DMRobertson Aug 24, 2021
26aad20
Introduce `AnyOf` matcher
DMRobertson Aug 25, 2021
9f4b9a0
Expand test cases to inspect Bob's behaviour too
DMRobertson Aug 25, 2021
a2e0d06
Tweak expected behaviour
DMRobertson Aug 26, 2021
b10655f
Enforce Displaynames in blueprints
DMRobertson Aug 31, 2021
b52c712
Fix typo
DMRobertson Sep 2, 2021
c95b011
Add case for remote user with per-room nickname
DMRobertson Sep 2, 2021
d650c5a
Ensure pub name, priv name & localpart all differ
DMRobertson Sep 2, 2021
815cf7b
Remove testing comment
DMRobertson Sep 2, 2021
4c95d45
Prefer MustDoFunc; test joining with private name
DMRobertson Sep 6, 2021
ef171f3
Fix PUT call to set displayname
DMRobertson Sep 6, 2021
707b530
Cleanup deployment after test, not after setup!
DMRobertson Sep 6, 2021
2b4609a
aliceId -> aliceUserID
DMRobertson Sep 7, 2021
700e300
Allow remote users' displaynames to be localparts
DMRobertson Sep 7, 2021
56bf3e3
Remove the tests which apply over federation
DMRobertson Nov 10, 2021
c0b3ecd
Blacklist tests for dendrite
DMRobertson Nov 10, 2021
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
5 changes: 5 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,11 @@ issues:
- G107
# gosec: Error on TLS InsecureSkipVerify set to true.
- G402
# staticcheck: Using a deprecated function, variable, constant or field
# Tried to make this a warning rather than error in the severity section.
# I failed. But it's nice to have goland know that certain things are deprecated
# so it can strike them through.
- SA1019

# Excluding configuration per-path, per-linter, per-text and per-source
exclude-rules:
Expand Down
41 changes: 39 additions & 2 deletions internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,21 @@ func (c *CSAPI) InviteRoom(t *testing.T, roomID string, userID string) {
c.MustDo(t, "POST", []string{"_matrix", "client", "r0", "rooms", roomID, "invite"}, body)
}

// SearchUserDirectory makes a request to search the user directory with a given query.
// We don't include an explicit limit on the number of results returned, relying on
// the spec's default of 10.
func (c *CSAPI) SearchUserDirectory(t *testing.T, query string) *http.Response {
DMRobertson marked this conversation as resolved.
Show resolved Hide resolved
t.Helper()
return c.MustDoFunc(
t,
"POST",
[]string{"_matrix", "client", "r0", "user_directory", "search"},
WithJSONBody(t, map[string]interface{}{
"search_term": query,
}),
)
}

// SendEventSynced sends `e` into the room and waits for its event ID to come down /sync.
// Returns the event ID of the sent event.
func (c *CSAPI) SendEventSynced(t *testing.T, roomID string, e b.Event) string {
Expand All @@ -127,15 +142,34 @@ func (c *CSAPI) SendEventSynced(t *testing.T, roomID string, e b.Event) string {
return eventID
}

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

// SyncUntil blocks and continually calls /sync until the `check` function returns true.
// SyncUntilInvitedTo is a wrapper around SyncUntil.
// It blocks and continually calls `/sync` until we've been invited to the given room.
// Will time out after CSAPI.SyncUntilTimeout.
func (c *CSAPI) SyncUntilInvitedTo(t *testing.T, roomID string) {
t.Helper()
check := func(event gjson.Result) bool {
return event.Get("type").Str == "m.room.member" &&
event.Get("content.membership").Str == "invite" &&
event.Get("state_key").Str == c.UserID
}
c.SyncUntil(t, "", "", "rooms.invite."+GjsonEscape(roomID)+".invite_state.events", check)
}

// SyncUntil blocks and continually calls /sync until
// - the response contains a particular `key`, and
// - its corresponding value is an array
// - some element in that array makes the `check` function return true.
// If the `check` function fails the test, the failing event will be automatically logged.
// Will time out after CSAPI.SyncUntilTimeout.
func (c *CSAPI) SyncUntil(t *testing.T, since, filter, key string, check func(gjson.Result) bool) {
Expand Down Expand Up @@ -213,6 +247,9 @@ func (c *CSAPI) RegisterUser(t *testing.T, localpart, password string) (userID,
}

// MustDo will do the HTTP request and fail the test if the response is not 2xx
//
// Deprecated: Prefer MustDoFunc. MustDo is the older format which doesn't allow for vargs
// and will be removed in the future. MustDoFunc also logs HTTP response bodies on error.
func (c *CSAPI) MustDo(t *testing.T, method string, paths []string, jsonBody interface{}) *http.Response {
t.Helper()
res := c.DoFunc(t, method, paths, WithJSONBody(t, jsonBody))
Expand Down
49 changes: 48 additions & 1 deletion internal/match/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package match
import (
"fmt"
"reflect"
"strings"

"github.com/tidwall/gjson"
)
Expand All @@ -17,7 +18,7 @@ type JSON func(body []byte) error
func JSONKeyEqual(wantKey string, wantValue interface{}) JSON {
return func(body []byte) error {
res := gjson.GetBytes(body, wantKey)
if res.Index == 0 {
if !res.Exists() {
return fmt.Errorf("key '%s' missing", wantKey)
}
gotValue := res.Value()
Expand Down Expand Up @@ -55,6 +56,26 @@ func JSONKeyTypeEqual(wantKey string, wantType gjson.Type) JSON {
}
}

// JSONKeyArrayOfSize returns a matcher which will check that `wantKey` is present and
// its value is an array with the given size.
// `wantKey` can be nested, see https://godoc.org/github.com/tidwall/gjson#Get for details.
func JSONKeyArrayOfSize(wantKey string, wantSize int) JSON {
return func(body []byte) error {
res := gjson.GetBytes(body, wantKey)
if !res.Exists() {
return fmt.Errorf("key '%s' missing", wantKey)
}
if !res.IsArray() {
return fmt.Errorf("key '%s' is not an array", wantKey)
}
entries := res.Array()
if len(entries) != wantSize {
return fmt.Errorf("key '%s' is an array of the wrong size, got %v want %v", wantKey, len(entries), wantSize)
}
return nil
}
}

func jsonCheckOffInternal(wantKey string, wantItems []interface{}, allowUnwantedItems bool, mapper func(gjson.Result) interface{}, fn func(interface{}, gjson.Result) error) JSON {
return func(body []byte) error {
res := gjson.GetBytes(body, wantKey)
Expand Down Expand Up @@ -204,3 +225,29 @@ func JSONMapEach(wantKey string, fn func(k, v gjson.Result) error) JSON {
return err
}
}

// AnyOf takes 1 or more `checkers`, and builds a new checker which accepts a given
// json body iff it's accepted by at least one of the original `checkers`.
func AnyOf(checkers ...JSON) JSON {
DMRobertson marked this conversation as resolved.
Show resolved Hide resolved
return func(body []byte) error {
if len(checkers) == 0 {
return fmt.Errorf("must provide at least one checker to AnyOf")
}

errors := make([]error, len(checkers))
for i, check := range checkers {
errors[i] = check(body)
if errors[i] == nil {
return nil
}
}

builder := strings.Builder{}
builder.WriteString("all checks failed:")
for _, err := range errors {
builder.WriteString("\n ")
builder.WriteString(err.Error())
}
return fmt.Errorf(builder.String())
}
}
124 changes: 124 additions & 0 deletions tests/csapi/user_directory_display_names_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package csapi_tests

import (
"testing"

"github.com/matrix-org/complement/internal/b"
"github.com/matrix-org/complement/internal/client"
"github.com/matrix-org/complement/internal/match"
"github.com/matrix-org/complement/internal/must"
)

func TestRoomSpecificUsernameHandling(t *testing.T) {
// Originally written to reproduce https://github.com/matrix-org/synapse/issues/5677
// In that bug report,
// - Bob knows about Alice, and
// - Alice has revealed a private name to another friend X,
// - Bob can see that private name when he shouldn't be able to.
//
// I've tweaked the names to be more traditional:
// - Even knows about Alice,
DMRobertson marked this conversation as resolved.
Show resolved Hide resolved
// - Alice reveals a private name to another friend Bob
// - Eve shouldn't be able to see that private name.
deployment := Deploy(t, b.BlueprintAlice)
defer deployment.Destroy(t)

alice := deployment.Client(t, "hs1", "@alice:hs1")
bob := deployment.RegisterUser(t, "hs1", "bob", "bob-has-a-very-secret-pw")
eve := deployment.RegisterUser(t, "hs1", "eve", "eve-has-a-very-secret-pw")

// Alice creates a public room (so Eve can see that Alice exists)
alice.CreateRoom(t, map[string]interface{}{"visibility": "public"})

// Bob creates a new room and invites Alice.
privateRoom := bob.CreateRoom(t, map[string]interface{}{
"visibility": "private",
"invite": []string{alice.UserID},
})

// Alice waits until she sees the invite, then accepts.
alice.SyncUntilInvitedTo(t, privateRoom)
alice.JoinRoom(t, privateRoom, nil)

// Alice reveals her private name to Bob
alice.MustDoFunc(
t,
"PUT",
[]string{"_matrix", "client", "r0", "rooms", privateRoom, "state", "m.room.member", "@alice:hs1"},
client.WithJSONBody(t, map[string]interface{}{
"displayname": "Alice Cooper",
DMRobertson marked this conversation as resolved.
Show resolved Hide resolved
"membership": "join",
}),
)

justAliceByPublicName := []match.JSON{
match.JSONKeyArrayOfSize("results", 1),
match.JSONKeyEqual("results.0.display_name", "alice"),
match.JSONKeyEqual("results.0.user_id", alice.UserID),
}

t.Run("Searcher can find target by display name "+
"when searcher and target have no room in common, share a homeserver, and "+
"target is in a public room on that homeserver",
func(t *testing.T) {
res := eve.SearchUserDirectory(t, "alice")
must.MatchResponse(t, res, match.HTTPResponse{JSON: justAliceByPublicName})
})

t.Run("Searcher can find target by mxid "+
"when searcher and target have no room in common, share a homeserver, and "+
"target is in a public room on that homeserver",
func(t *testing.T) {
res := eve.SearchUserDirectory(t, alice.UserID)
must.MatchResponse(t, res, match.HTTPResponse{JSON: justAliceByPublicName})
})

noResults := []match.JSON{
match.JSONKeyArrayOfSize("results", 0),
}

t.Run("Searcher cannot find target by room-specific name they are not privy to "+
"when searcher and target have no room in common, share a homeserver, and "+
"target is in a public room on that homeserver",
func(t *testing.T) {
res := eve.SearchUserDirectory(t, "Alice Cooper")
must.MatchResponse(t, res, match.HTTPResponse{JSON: noResults})
})

justAliceByPublicOrPrivateName := []match.JSON{
match.JSONKeyArrayOfSize("results", 1),
// TODO should bob find alice by her public or private name?
match.AnyOf(
match.JSONKeyEqual("results.0.display_name", "Alice Cooper"),
match.JSONKeyEqual("results.0.display_name", "alice"),
),
match.JSONKeyEqual("results.0.user_id", alice.UserID),
}

t.Run("Searcher can find target by public display name "+
"when searcher and target share a private room with a specific display_name for the target",
func(t *testing.T) {
res := bob.SearchUserDirectory(t, "alice")
must.MatchResponse(t, res, match.HTTPResponse{
JSON: justAliceByPublicOrPrivateName,
})
})

t.Run("Searcher can find target by mxid "+
"when searcher and target share a private room with a specific display_name for the target",
func(t *testing.T) {
res := bob.SearchUserDirectory(t, alice.UserID)
must.MatchResponse(t, res, match.HTTPResponse{
JSON: justAliceByPublicOrPrivateName,
})
})

t.Run("Searcher can find target by room-specific name"+
"when searcher and target share a private room with a specific display_name for the target",
func(t *testing.T) {
res := bob.SearchUserDirectory(t, "Alice Cooper")
must.MatchResponse(t, res, match.HTTPResponse{
JSON: justAliceByPublicOrPrivateName,
})
})
}