-
Notifications
You must be signed in to change notification settings - Fork 66
Add tests for MSC4429: Profile Updates for Legacy Sync #849
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package tests | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/matrix-org/complement" | ||
| ) | ||
|
|
||
| func TestMain(m *testing.M) { | ||
| complement.TestMain(m, "msc4429") | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,207 @@ | ||||||||||||||||
| package tests | ||||||||||||||||
|
|
||||||||||||||||
| import ( | ||||||||||||||||
| "encoding/json" | ||||||||||||||||
| "fmt" | ||||||||||||||||
| "testing" | ||||||||||||||||
|
|
||||||||||||||||
| "github.com/tidwall/gjson" | ||||||||||||||||
|
|
||||||||||||||||
| "github.com/matrix-org/complement" | ||||||||||||||||
| "github.com/matrix-org/complement/client" | ||||||||||||||||
| "github.com/matrix-org/complement/helpers" | ||||||||||||||||
| "github.com/matrix-org/complement/match" | ||||||||||||||||
| "github.com/matrix-org/complement/must" | ||||||||||||||||
| ) | ||||||||||||||||
|
|
||||||||||||||||
| const ( | ||||||||||||||||
| msc4429UsersStable = "users" | ||||||||||||||||
| msc4429UsersUnstable = "org\\.matrix\\.msc4429\\.users" | ||||||||||||||||
| ) | ||||||||||||||||
|
|
||||||||||||||||
| func TestMSC4429ProfileUpdates(t *testing.T) { | ||||||||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Half-Shot mentioned on the MSC that it would be good for the homeserver to send down It would be good to add a test case for this, ideally replacing the profile object with just a |
||||||||||||||||
| deployment := complement.Deploy(t, 1) | ||||||||||||||||
| defer deployment.Destroy(t) | ||||||||||||||||
|
|
||||||||||||||||
| t.Run("Initial sync includes requested profile fields and filters others", func(t *testing.T) { | ||||||||||||||||
| alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "alice-initial"}) | ||||||||||||||||
| bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "bob-initial"}) | ||||||||||||||||
|
|
||||||||||||||||
| mustCreateSharedRoom(t, alice, bob) | ||||||||||||||||
|
|
||||||||||||||||
| bob.MustSetDisplayName(t, "Bob Display") | ||||||||||||||||
| mustSetProfileField(t, bob, "m.status", map[string]interface{}{ | ||||||||||||||||
| "text": "busy", | ||||||||||||||||
| "emoji": "🛑", | ||||||||||||||||
| }) | ||||||||||||||||
|
|
||||||||||||||||
| // Exclude 'displayname' | ||||||||||||||||
| filter := mustBuildMSC4429Filter(t, []string{"m.status"}) | ||||||||||||||||
| res, _ := alice.MustSync(t, client.SyncReq{Filter: filter}) | ||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be flaky if we don't use a sync until utility for profile updates to show up. (read after write consistency) The worker processing profile updates might not be the same one that serves |
||||||||||||||||
|
|
||||||||||||||||
| update, ok := getProfileUpdate(res, bob.UserID, "m.status") | ||||||||||||||||
| if !ok { | ||||||||||||||||
| t.Fatalf("missing m.status profile update for %s in initial sync: %s", bob.UserID, res.Raw) | ||||||||||||||||
| } | ||||||||||||||||
| must.MatchGJSON(t, update, match.JSONKeyEqual("", map[string]interface{}{ | ||||||||||||||||
| "text": "busy", | ||||||||||||||||
| "emoji": "🛑", | ||||||||||||||||
| })) | ||||||||||||||||
| assertNoProfileUpdate(t, res, bob.UserID, "displayname") | ||||||||||||||||
| }) | ||||||||||||||||
|
|
||||||||||||||||
| t.Run("No updates without profile_fields filter", func(t *testing.T) { | ||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Explain why, "opt-in" per MSC4429 |
||||||||||||||||
| alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "alice-nofilter"}) | ||||||||||||||||
| bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "bob-nofilter"}) | ||||||||||||||||
|
|
||||||||||||||||
| mustCreateSharedRoom(t, alice, bob) | ||||||||||||||||
|
|
||||||||||||||||
| // No filter = no profile fields returned. | ||||||||||||||||
| _, since := alice.MustSync(t, client.SyncReq{}) | ||||||||||||||||
|
Comment on lines
+59
to
+60
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't actually assert anything for the initial sync request |
||||||||||||||||
| mustSetProfileField(t, bob, "m.status", map[string]interface{}{ | ||||||||||||||||
| "text": "away", | ||||||||||||||||
| }) | ||||||||||||||||
|
Comment on lines
+59
to
+63
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As a sanity check, we should have another observer user that sees the
Comment on lines
+61
to
+63
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Separate action from previous request
Suggested change
|
||||||||||||||||
|
|
||||||||||||||||
| res, _ := alice.MustSync(t, client.SyncReq{Since: since}) | ||||||||||||||||
| assertNoProfileUpdate(t, res, bob.UserID, "m.status") | ||||||||||||||||
| }) | ||||||||||||||||
|
|
||||||||||||||||
| t.Run("Incremental sync returns the latest update", func(t *testing.T) { | ||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps we should have another test to make sure more subsequent incremental This ensures that any caching (remembering which users were sent down before) is correct.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess, we kinda have this already with "Cleared profile field is returned as null" |
||||||||||||||||
| alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "alice-latest"}) | ||||||||||||||||
| bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "bob-latest"}) | ||||||||||||||||
|
|
||||||||||||||||
| mustCreateSharedRoom(t, alice, bob) | ||||||||||||||||
|
|
||||||||||||||||
| filter := mustBuildMSC4429Filter(t, []string{"m.status"}) | ||||||||||||||||
| _, since := alice.MustSync(t, client.SyncReq{Filter: filter}) | ||||||||||||||||
|
|
||||||||||||||||
| mustSetProfileField(t, bob, "m.status", map[string]interface{}{ | ||||||||||||||||
| "text": "first", | ||||||||||||||||
| }) | ||||||||||||||||
| mustSetProfileField(t, bob, "m.status", map[string]interface{}{ | ||||||||||||||||
| "text": "second", | ||||||||||||||||
| }) | ||||||||||||||||
|
|
||||||||||||||||
| alice.MustSyncUntil( | ||||||||||||||||
| t, | ||||||||||||||||
| client.SyncReq{Since: since, Filter: filter}, | ||||||||||||||||
| syncHasProfileUpdate(bob.UserID, "m.status", map[string]interface{}{ | ||||||||||||||||
| "text": "second", | ||||||||||||||||
| }), | ||||||||||||||||
| ) | ||||||||||||||||
| }) | ||||||||||||||||
|
|
||||||||||||||||
| t.Run("Cleared profile field is returned as null", func(t *testing.T) { | ||||||||||||||||
| alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "alice-clear"}) | ||||||||||||||||
| bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{LocalpartSuffix: "bob-clear"}) | ||||||||||||||||
|
|
||||||||||||||||
| mustCreateSharedRoom(t, alice, bob) | ||||||||||||||||
|
|
||||||||||||||||
| filter := mustBuildMSC4429Filter(t, []string{"m.status"}) | ||||||||||||||||
| _, since := alice.MustSync(t, client.SyncReq{Filter: filter}) | ||||||||||||||||
|
|
||||||||||||||||
| mustSetProfileField(t, bob, "m.status", map[string]interface{}{ | ||||||||||||||||
| "text": "busy", | ||||||||||||||||
| }) | ||||||||||||||||
| since = alice.MustSyncUntil( | ||||||||||||||||
| t, | ||||||||||||||||
| client.SyncReq{Since: since, Filter: filter}, | ||||||||||||||||
| syncHasProfileUpdate(bob.UserID, "m.status", map[string]interface{}{ | ||||||||||||||||
| "text": "busy", | ||||||||||||||||
| }), | ||||||||||||||||
| ) | ||||||||||||||||
|
|
||||||||||||||||
| mustSetProfileField(t, bob, "m.status", nil) | ||||||||||||||||
| alice.MustSyncUntil( | ||||||||||||||||
| t, | ||||||||||||||||
| client.SyncReq{Since: since, Filter: filter}, | ||||||||||||||||
| syncHasProfileUpdate(bob.UserID, "m.status", nil), | ||||||||||||||||
| ) | ||||||||||||||||
| }) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // mustBuildMSC4429Filter builds a filter that can be used to limit the field | ||||||||||||||||
| // IDs returned in a `/sync` response. | ||||||||||||||||
| func mustBuildMSC4429Filter(t *testing.T, ids []string) string { | ||||||||||||||||
| t.Helper() | ||||||||||||||||
| filter := map[string]interface{}{ | ||||||||||||||||
| "profile_fields": map[string]interface{}{ | ||||||||||||||||
| "ids": ids, | ||||||||||||||||
| }, | ||||||||||||||||
| "org.matrix.msc4429.profile_fields": map[string]interface{}{ | ||||||||||||||||
| "ids": ids, | ||||||||||||||||
| }, | ||||||||||||||||
| } | ||||||||||||||||
| encoded, err := json.Marshal(filter) | ||||||||||||||||
| if err != nil { | ||||||||||||||||
| t.Fatalf("failed to marshal MSC4429 filter: %s", err) | ||||||||||||||||
| } | ||||||||||||||||
| return string(encoded) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // mustCreateSharedRoom creates a shared room between `alice` and `bob` and returns the | ||||||||||||||||
| // room ID. | ||||||||||||||||
| func mustCreateSharedRoom(t *testing.T, alice *client.CSAPI, bob *client.CSAPI) string { | ||||||||||||||||
| t.Helper() | ||||||||||||||||
| roomID := alice.MustCreateRoom(t, map[string]interface{}{ | ||||||||||||||||
| "preset": "public_chat", | ||||||||||||||||
| }) | ||||||||||||||||
| alice.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(alice.UserID, roomID)) | ||||||||||||||||
| bob.MustJoinRoom(t, roomID, nil) | ||||||||||||||||
| alice.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(bob.UserID, roomID)) | ||||||||||||||||
| bob.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(bob.UserID, roomID)) | ||||||||||||||||
| return roomID | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // mustSetProfileField sets the given profile field ID to the given value on the given user's | ||||||||||||||||
| // profile. | ||||||||||||||||
| func mustSetProfileField(t *testing.T, user *client.CSAPI, field string, value interface{}) { | ||||||||||||||||
| t.Helper() | ||||||||||||||||
| user.MustDo(t, "PUT", []string{"_matrix", "client", "v3", "profile", user.UserID, field}, | ||||||||||||||||
| client.WithJSONBody(t, map[string]interface{}{ | ||||||||||||||||
| field: value, | ||||||||||||||||
| }), | ||||||||||||||||
| ) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // getProfileUpdate extracts the given profile updates for a given user by field | ||||||||||||||||
| // ID from a legacy `/sync` response. | ||||||||||||||||
| func getProfileUpdate(res gjson.Result, userID, field string) (gjson.Result, bool) { | ||||||||||||||||
| stablePath := msc4429UsersStable + "." + client.GjsonEscape(userID) + ".profile_updates." + client.GjsonEscape(field) | ||||||||||||||||
| stableRes := res.Get(stablePath) | ||||||||||||||||
| if stableRes.Exists() { | ||||||||||||||||
| return stableRes, true | ||||||||||||||||
| } | ||||||||||||||||
| unstablePath := msc4429UsersUnstable + "." + client.GjsonEscape(userID) + ".profile_updates." + client.GjsonEscape(field) | ||||||||||||||||
| unstableRes := res.Get(unstablePath) | ||||||||||||||||
| if unstableRes.Exists() { | ||||||||||||||||
| return unstableRes, true | ||||||||||||||||
| } | ||||||||||||||||
| return gjson.Result{}, false | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| func assertNoProfileUpdate(t *testing.T, res gjson.Result, userID, field string) { | ||||||||||||||||
| t.Helper() | ||||||||||||||||
| if update, ok := getProfileUpdate(res, userID, field); ok { | ||||||||||||||||
| t.Fatalf("unexpected profile update for %s %s: %s", userID, field, update.Raw) | ||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is correct but the language is ambiguous whether we expected no profile update or just a different profile update. |
||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| func syncHasProfileUpdate(userID, field string, expected interface{}) client.SyncCheckOpt { | ||||||||||||||||
| return func(clientUserID string, topLevelSyncJSON gjson.Result) error { | ||||||||||||||||
| update, ok := getProfileUpdate(topLevelSyncJSON, userID, field) | ||||||||||||||||
| if !ok { | ||||||||||||||||
| return fmt.Errorf("missing profile update for %s %s", userID, field) | ||||||||||||||||
| } | ||||||||||||||||
| if expected == nil { | ||||||||||||||||
| if update.Type != gjson.Null { | ||||||||||||||||
| return fmt.Errorf("expected null profile update for %s %s, got %s", userID, field, update.Type) | ||||||||||||||||
| } | ||||||||||||||||
| return nil | ||||||||||||||||
| } | ||||||||||||||||
| if err := match.JSONKeyEqual("", expected)(update); err != nil { | ||||||||||||||||
| return fmt.Errorf("profile update mismatch for %s %s: %w", userID, field, err) | ||||||||||||||||
| } | ||||||||||||||||
| return nil | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems pre-mature to support stable given matrix-org/matrix-spec-proposals#4429 is no where near that point 🤔
Perhaps we just comment that part out for now. Useful for our future reference when we stabilize