diff --git a/internal/b/alice_and_bob.go b/internal/b/alice_and_bob.go new file mode 100644 index 00000000..19b4bb79 --- /dev/null +++ b/internal/b/alice_and_bob.go @@ -0,0 +1,21 @@ +package b + +// BlueprintAliceAndBob is a two user homeserver +var BlueprintAliceAndBob = MustValidate(Blueprint{ + Name: "alice_and_bob", + Homeservers: []Homeserver{ + { + Name: "hs1", + Users: []User{ + { + Localpart: "@alice", + DisplayName: "Alice", + }, + { + Localpart: "@bob", + DisplayName: "Bob", + }, + }, + }, + }, +}) diff --git a/internal/b/blueprints.go b/internal/b/blueprints.go index 65a8258b..28e9a72c 100644 --- a/internal/b/blueprints.go +++ b/internal/b/blueprints.go @@ -26,6 +26,7 @@ import ( var KnownBlueprints = map[string]*Blueprint{ BlueprintCleanHS.Name: &BlueprintCleanHS, BlueprintAlice.Name: &BlueprintAlice, + BlueprintAliceAndBob.Name: &BlueprintAliceAndBob, BlueprintFederationOneToOneRoom.Name: &BlueprintFederationOneToOneRoom, BlueprintFederationTwoLocalOneRemote.Name: &BlueprintFederationTwoLocalOneRemote, BlueprintHSWithApplicationService.Name: &BlueprintHSWithApplicationService, diff --git a/internal/client/client.go b/internal/client/client.go index a59856b5..98ba90b2 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -112,12 +112,7 @@ func (c *CSAPI) InviteRoom(t *testing.T, roomID string, userID string) { // Returns the event ID of the sent event. func (c *CSAPI) SendEventSynced(t *testing.T, roomID string, e b.Event) string { t.Helper() - c.txnID++ - paths := []string{"_matrix", "client", "r0", "rooms", roomID, "send", e.Type, strconv.Itoa(c.txnID)} - if e.StateKey != nil { - paths = []string{"_matrix", "client", "r0", "rooms", roomID, "state", e.Type, *e.StateKey} - } - res := c.MustDo(t, "PUT", paths, e.Content) + res := c.SendEvent(t, roomID, e) body := ParseJSON(t, res) eventID := GetJSONFieldStr(t, body, "event_id") t.Logf("SendEventSynced waiting for event ID %s", eventID) @@ -127,6 +122,18 @@ func (c *CSAPI) SendEventSynced(t *testing.T, roomID string, e b.Event) string { return eventID } +// SendEventSynced sends `e` into the room. +func (c *CSAPI) SendEvent(t *testing.T, roomID string, e b.Event) *http.Response { + t.Helper() + c.txnID++ + paths := []string{"_matrix", "client", "r0", "rooms", roomID, "send", e.Type, strconv.Itoa(c.txnID)} + if e.StateKey != nil { + paths = []string{"_matrix", "client", "r0", "rooms", roomID, "state", e.Type, *e.StateKey} + } + res := c.DoFunc(t, "PUT", paths, WithJSONBody(t, e.Content)) + return res +} + // SyncUntilTimelineHas blocks and continually calls /sync until 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. diff --git a/internal/must/must.go b/internal/must/must.go index 5b5da72c..87e90965 100644 --- a/internal/must/must.go +++ b/internal/must/must.go @@ -71,30 +71,30 @@ func MatchResponse(t *testing.T, res *http.Response, m match.HTTPResponse) []byt t.Helper() body, err := ioutil.ReadAll(res.Body) if err != nil { - t.Fatalf("MatchResponse: Failed to read response body: %s", err) + t.Errorf("MatchResponse: Failed to read response body: %s", err) } contextStr := fmt.Sprintf("%s => %s", res.Request.URL.String(), string(body)) if m.StatusCode != 0 { if res.StatusCode != m.StatusCode { - t.Fatalf("MatchResponse got status %d want %d - %s", res.StatusCode, m.StatusCode, contextStr) + t.Errorf("MatchResponse got status %d want %d - %s", res.StatusCode, m.StatusCode, contextStr) } } if m.Headers != nil { for name, val := range m.Headers { if res.Header.Get(name) != val { - t.Fatalf("MatchResponse got %s: %s want %s - %s", name, res.Header.Get(name), val, contextStr) + t.Errorf("MatchResponse got %s: %s want %s - %s", name, res.Header.Get(name), val, contextStr) } } } if m.JSON != nil { if !gjson.ValidBytes(body) { - t.Fatalf("MatchResponse response body is not valid JSON - %s", contextStr) + t.Errorf("MatchResponse response body is not valid JSON - %s", contextStr) } for _, jm := range m.JSON { if err = jm(body); err != nil { - t.Fatalf("MatchResponse %s - %s", err, contextStr) + t.Errorf("MatchResponse %s - %s", err, contextStr) } } } diff --git a/tests/client_power_levels_test.go b/tests/client_power_levels_test.go new file mode 100644 index 00000000..8f253bc9 --- /dev/null +++ b/tests/client_power_levels_test.go @@ -0,0 +1,124 @@ +package tests + +import ( + "math/big" + "testing" + + "github.com/matrix-org/complement/internal/b" + "github.com/matrix-org/complement/internal/match" + "github.com/matrix-org/complement/internal/must" +) + +func TestClientSendingPowerLevelsEvents(t *testing.T) { + deployment := Deploy(t, b.BlueprintAliceAndBob) + defer deployment.Destroy(t) + + emptyStateKey := "" + + // Create a client for one local user + aliceUserID := "@alice:hs1" + alice := deployment.Client(t, "hs1", aliceUserID) + + // Create a client for another local user + bobUserID := "@bob:hs1" + bob := deployment.Client(t, "hs1", bobUserID) + + t.Run("Attempt to set valid power levels event", func(t *testing.T) { + roomID := alice.CreateRoom(t, map[string]interface{}{ + "preset": "public_chat", + }) + + alice.SendEventSynced(t, roomID, b.Event{ + Type: "m.room.power_levels", + Sender: alice.UserID, + StateKey: &emptyStateKey, + Content: map[string]interface{}{ + "ban": 50, + "events": map[string]interface{}{ + "m.room.name": 100, + "m.room.power_levels": 100, + }, + "events_default": 0, + "invite": 50, + "kick": 50, + "notifications": map[string]interface{}{ + "room": 20, + }, + "redact": 50, + "state_default": 50, + "users": map[string]interface{}{ + alice.UserID: 100, + bob.UserID: 0, + }, + "users_default": 0, + }, + }) + }) + + t.Run("Attempt to set power levels event with string power level fails", func(t *testing.T) { + roomID := alice.CreateRoom(t, map[string]interface{}{ + "preset": "public_chat", + }) + + res := alice.SendEvent(t, roomID, b.Event{ + Type: "m.room.power_levels", + Sender: alice.UserID, + StateKey: &emptyStateKey, + Content: map[string]interface{}{ + "users_default": "30", + "users": map[string]interface{}{ + alice.UserID: 100, + bob.UserID: 0, + }, + "events": map[string]interface{}{ + "m.room.power_levels": 100, + }, + }, + }) + must.MatchResponse(t, res, match.HTTPResponse{ + StatusCode: 400, + JSON: []match.JSON{ + match.JSONKeyEqual("errcode", "M_BAD_JSON"), + }, + }) + }) + + t.Run("Attempt to set power levels event with unsafe integer fails", func(t *testing.T) { + roomID := alice.CreateRoom(t, map[string]interface{}{ + "preset": "public_chat", + "room_version": "5", // room version from before canonical JSON was enforced + }) + + var smallnum, _ = new(big.Int).SetString("-9007199254740992", 0) + res := alice.SendEvent(t, roomID, b.Event{ + Type: "m.room.power_levels", + Sender: alice.UserID, + StateKey: &emptyStateKey, + Content: map[string]interface{}{ + "users_default": smallnum, + }, + }) + must.MatchResponse(t, res, match.HTTPResponse{ + StatusCode: 400, + JSON: []match.JSON{ + match.JSONKeyEqual("errcode", "M_BAD_JSON"), + }, + }) + + var bignum, _ = new(big.Int).SetString("9007199254740992", 0) + res2 := alice.SendEvent(t, roomID, b.Event{ + Type: "m.room.power_levels", + Sender: alice.UserID, + StateKey: &emptyStateKey, + Content: map[string]interface{}{ + "users_default": bignum, + }, + }) + must.MatchResponse(t, res2, match.HTTPResponse{ + StatusCode: 400, + JSON: []match.JSON{ + match.JSONKeyEqual("errcode", "M_BAD_JSON"), + }, + }) + }) +}