diff --git a/internal/client/client.go b/internal/client/client.go index cd5a5b80..0d9ce3c3 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -333,6 +333,30 @@ func (c *CSAPI) MustSyncUntil(t *testing.T, syncReq SyncReq, checks ...SyncCheck } } +// LoginUser will log in to a homeserver and create a new device on an existing user. +func (c *CSAPI) LoginUser(t *testing.T, localpart, password string) (userID, accessToken, deviceID string) { + t.Helper() + reqBody := map[string]interface{}{ + "identifier": map[string]interface{}{ + "type": "m.id.user", + "user": localpart, + }, + "password": password, + "type": "m.login.password", + } + res := c.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "login"}, WithJSONBody(t, reqBody)) + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatalf("unable to read response body: %v", err) + } + + userID = gjson.GetBytes(body, "user_id").Str + accessToken = gjson.GetBytes(body, "access_token").Str + deviceID = gjson.GetBytes(body, "device_id").Str + return userID, accessToken, deviceID +} + //RegisterUser will register the user with given parameters and // return user ID & access token, and fail the test on network error func (c *CSAPI) RegisterUser(t *testing.T, localpart, password string) (userID, accessToken, deviceID string) { diff --git a/internal/docker/deployment.go b/internal/docker/deployment.go index cc0b53e1..effcd523 100644 --- a/internal/docker/deployment.go +++ b/internal/docker/deployment.go @@ -118,6 +118,30 @@ func (d *Deployment) RegisterUser(t *testing.T, hsName, localpart, password stri return client } +// LoginUser within a homeserver and return an authenticatedClient. Fails the test if the hsName is not found. +// Note that this will not change the access token of the client that is returned by `deployment.Client`. +func (d *Deployment) LoginUser(t *testing.T, hsName, localpart, password string) *client.CSAPI { + t.Helper() + dep, ok := d.HS[hsName] + if !ok { + t.Fatalf("Deployment.Client - HS name '%s' not found", hsName) + return nil + } + client := &client.CSAPI{ + BaseURL: dep.BaseURL, + Client: client.NewLoggedClient(t, hsName, nil), + SyncUntilTimeout: 5 * time.Second, + Debug: d.Deployer.debugLogging, + } + dep.CSAPIClients = append(dep.CSAPIClients, client) + userID, accessToken, deviceID := client.LoginUser(t, localpart, password) + + client.UserID = userID + client.AccessToken = accessToken + client.DeviceID = deviceID + return client +} + // Restart a deployment. func (d *Deployment) Restart(t *testing.T) error { t.Helper() diff --git a/tests/msc3391_test.go b/tests/msc3391_test.go index c797bc50..e74f9703 100644 --- a/tests/msc3391_test.go +++ b/tests/msc3391_test.go @@ -84,7 +84,7 @@ func createUserAccountData(t *testing.T, c *client.CSAPI) { func(body []byte) error { if !match.JSONDeepEqual(body, testAccountDataContent) { return fmt.Errorf( - "Expected %s for room account data content when, got '%s'", + "Expected %s for room account data content, got '%s'", testAccountDataType, string(body), ) @@ -124,7 +124,7 @@ func createRoomAccountData(t *testing.T, c *client.CSAPI, roomID string) { func(body []byte) error { if !match.JSONDeepEqual(body, testAccountDataContent) { return fmt.Errorf( - "Expected %s for room account data content when, got '%s'", + "Expected %s for room account data content, got '%s'", testAccountDataType, string(body), ) diff --git a/tests/msc3890_test.go b/tests/msc3890_test.go new file mode 100644 index 00000000..9cb4df4c --- /dev/null +++ b/tests/msc3890_test.go @@ -0,0 +1,93 @@ +//go:build msc3890 +// +build msc3890 + +// This file contains tests for local notification settings as +// defined by MSC3890, which you can read here: +// https://github.com/matrix-org/matrix-doc/pull/3890 + +package 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" + "github.com/tidwall/gjson" +) + +func TestDeletingDeviceRemovesDeviceLocalNotificationSettings(t *testing.T) { + // Create a deployment with a single user + deployment := Deploy(t, b.BlueprintCleanHS) + defer deployment.Destroy(t) + + // Create a user which we can log in to multiple times + aliceLocalpart := "alice" + alicePassword := "hunter2" + aliceDeviceOne := deployment.RegisterUser(t, "hs1", aliceLocalpart, alicePassword, false) + + // Log in to another device on this user + aliceDeviceTwo := deployment.LoginUser(t, "hs1", aliceLocalpart, alicePassword) + + accountDataType := "org.matrix.msc3890.local_notification_settings." + aliceDeviceTwo.DeviceID + accountDataContent := map[string]interface{}{"is_silenced": true} + + // Test deleting global account data. + t.Run("Deleting a user's device should delete any local notification settings entries from their account data", func(t *testing.T) { + // Retrieve a sync token for this user + _, nextBatchToken := aliceDeviceOne.MustSync( + t, + client.SyncReq{}, + ) + + // Using the first device, create some local notification settings in the user's account data for the second device. + aliceDeviceOne.SetGlobalAccountData( + t, + accountDataType, + accountDataContent, + ) + + checkAccountDataContent := func(r gjson.Result) bool { + // Only listen for our test type + if r.Get("type").Str != accountDataType { + return false + } + content := r.Get("content") + + // Ensure the content of this account data type is as we expect + return match.JSONDeepEqual([]byte(content.Raw), accountDataContent) + } + + // Check that the content of the user account data for this type has been set successfully + aliceDeviceOne.MustSyncUntil( + t, + client.SyncReq{ + Since: nextBatchToken, + }, + client.SyncGlobalAccountDataHas(checkAccountDataContent), + ) + // Also check via the dedicated account data endpoint to ensure the similar check later is not 404'ing for some other reason. + // Using `MustDoFunc` ensures that the response code is 2xx. + res := aliceDeviceOne.MustDoFunc(t, "GET", []string{"_matrix", "client", "v3", "user", aliceDeviceOne.UserID, "account_data", accountDataType}) + must.MatchResponse(t, res, match.HTTPResponse{ + JSON: []match.JSON{ + match.JSONKeyEqual("is_silenced", true), + }, + }) + + // Log out the second device + aliceDeviceTwo.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "logout"}) + + // Using the first device, check that the local notification setting account data for the deleted device was removed. + res = aliceDeviceOne.DoFunc(t, "GET", []string{"_matrix", "client", "v3", "user", aliceDeviceOne.UserID, "account_data", accountDataType}) + must.MatchResponse(t, res, match.HTTPResponse{ + StatusCode: 404, + JSON: []match.JSON{ + // A 404 can be generated for missing endpoints as well (which would have an errcode of `M_UNRECOGNIZED`). + // Ensure we're getting the error we expect. + match.JSONKeyEqual("errcode", "M_NOT_FOUND"), + }, + }) + }) +}