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

Convert more tests to Complement #353

Closed
wants to merge 13 commits into from
6 changes: 5 additions & 1 deletion sytest.ignored.list
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,8 @@ Newly created users see their own presence in /initialSync (SYT-34)
Push rules come down in an initial /sync
Guest user calling /events doesn't tightloop
Guest user cannot call /events globally
!53groups
!53groups
Global initialSync
Global initialSync with limit=0 gives no messages
Room initialSync
Room initialSync with limit=0 gives no messages
105 changes: 105 additions & 0 deletions tests/csapi/apidoc_room_levels_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package csapi_tests

import (
"net/http"
"testing"

"github.com/tidwall/gjson"
"github.com/tidwall/sjson"

"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 TestRoomLevels(t *testing.T) {
deployment := Deploy(t, b.BlueprintAlice)
defer deployment.Destroy(t)
alice := deployment.Client(t, "hs1", "@alice:hs1")

successCount := 0
defer func() {
// sytest: Both GET and PUT work
if successCount != 2 {
t.Fatalf("expected GET and PUT to work")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this necessary? t.Run("Parallel", ... will block until the tests complete. Each test can fail the subtest which will have a knock-on effect and fail the outer test, so this seems entirely redundant? We never do this anywhere in Complement AFAICT.

}
}()
t.Run("Parallel", func(t *testing.T) {
// sytest: GET /rooms/:room_id/state/m.room.power_levels can fetch levels
t.Run("GET /rooms/:room_id/state/m.room.power_levels can fetch levels", func(t *testing.T) {
t.Parallel()
roomID := alice.CreateRoom(t, map[string]interface{}{})
alice.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(alice.UserID, roomID))
res := alice.MustDoFunc(t, "GET", []string{"_matrix", "client", "v3", "rooms", roomID, "state", "m.room.power_levels"})

body := gjson.ParseBytes(must.ParseJSON(t, res.Body))
requiredFields := []string{"ban", "kick", "redact", "state_default", "events_default", "events", "users"}
for i := range requiredFields {
if !body.Get(requiredFields[i]).Exists() {
t.Fatalf("expected json field %s, but it does not exist", requiredFields[i])
}
}
users := body.Get("users").Map()
alicePowerLevel, ok := users[alice.UserID]
if !ok {
t.Fatalf("Expected room creator (%s) to exist in user powerlevel list", alice.UserID)
}

userDefaults := body.Get("user_defaults").Int()

if userDefaults > alicePowerLevel.Int() {
t.Fatalf("Expected room creator to have a higher-than-default powerlevel")
}
successCount++
})
// sytest: PUT /rooms/:room_id/state/m.room.power_levels can set levels
t.Run("PUT /rooms/:room_id/state/m.room.power_levels can set levels", func(t *testing.T) {
t.Parallel()
roomID := alice.CreateRoom(t, map[string]interface{}{})
alice.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(alice.UserID, roomID))
res := alice.MustDoFunc(t, "GET", []string{"_matrix", "client", "v3", "rooms", roomID, "state", "m.room.power_levels"})

powerLevels := gjson.ParseBytes(must.ParseJSON(t, res.Body))
changedUser := client.GjsonEscape("@random-other-user:their.home")
alicePowerLevel := powerLevels.Get("users." + alice.UserID).Int()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically alice.UserID should be gjson escaped as well.

pl := map[string]int64{
alice.UserID: alicePowerLevel,
"@random-other-user:their.home": 20,
}
newPowerlevels, err := sjson.Set(powerLevels.Str, "users", pl)
if err != nil {
t.Fatalf("unable to update powerlevel JSON")
}
reqBody := client.WithRawBody([]byte(newPowerlevels))
alice.MustDoFunc(t, "PUT", []string{"_matrix", "client", "v3", "rooms", roomID, "state", "m.room.power_levels"}, reqBody)
res = alice.MustDoFunc(t, "GET", []string{"_matrix", "client", "v3", "rooms", roomID, "state", "m.room.power_levels"})
powerLevels = gjson.ParseBytes(must.ParseJSON(t, res.Body))
if powerLevels.Get("users."+changedUser).Int() != 20 {
t.Fatal("Expected to have set other user's level to 20")
}
successCount++
})
// sytest: PUT power_levels should not explode if the old power levels were empty
t.Run("PUT power_levels should not explode if the old power levels were empty", func(t *testing.T) {
t.Parallel()
roomID := alice.CreateRoom(t, map[string]interface{}{})
alice.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(alice.UserID, roomID))

reqBody := client.WithJSONBody(t, map[string]interface{}{
"users": map[string]int64{
alice.UserID: 100,
},
})
alice.MustDoFunc(t, "PUT", []string{"_matrix", "client", "v3", "rooms", roomID, "state", "m.room.power_levels"}, reqBody)
// absence of a 'users' key
reqBody = client.WithJSONBody(t, map[string]interface{}{})
alice.MustDoFunc(t, "PUT", []string{"_matrix", "client", "v3", "rooms", roomID, "state", "m.room.power_levels"}, reqBody)
// this should now give a 403 (not a 500)
res := alice.DoFunc(t, "PUT", []string{"_matrix", "client", "v3", "rooms", roomID, "state", "m.room.power_levels"}, reqBody)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't what the sytest is doing.

      # absence of an 'events' key
      matrix_put_room_state(
         $user,
         $room_id,
         type      => "m.room.power_levels",
         state_key => "",
         content   => {
            users => {
               $user->user_id => 100,
            },
         },
      )->then( sub {
         # absence of a 'users' key
         matrix_put_room_state(
            $user,
            $room_id,
            type      => "m.room.power_levels",
            state_key => "",
            content   => {
            },
         );
      })->then( sub {
         # this should now give a 403 (not a 500)
         matrix_put_room_state(
            $user,
            $room_id,
            type      => "m.room.power_levels",
            state_key => "",
            content   => {
               users => {},
            },
         ) -> main::expect_http_403;
      })

So the order is:

  • PUT with users key with alice
  • PUT with empty content
  • PUT with users key present but empty -> 403

must.MatchResponse(t, res, match.HTTPResponse{
StatusCode: http.StatusForbidden,
})
})
})
}
193 changes: 193 additions & 0 deletions tests/csapi/room_versions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package csapi_tests

import (
"fmt"
"net/url"
"testing"

"github.com/matrix-org/gomatrixserverlib"
"github.com/tidwall/gjson"

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

func TestRoomVersions(t *testing.T) {
deployment := Deploy(t, b.BlueprintFederationTwoLocalOneRemote)
defer deployment.Destroy(t)

alice := deployment.Client(t, "hs1", "@alice:hs1")
bob := deployment.Client(t, "hs1", "@bob:hs1")
charlie := deployment.Client(t, "hs2", "@charlie:hs2")

roomVersions := gomatrixserverlib.RoomVersions()

t.Run("Parallel", func(t *testing.T) {
// iterate over all room versions
for v := range roomVersions {
roomVersion := v
// sytest: User can create and send/receive messages in a room with version $version
t.Run(fmt.Sprintf("User can create and send/receive messages in a room with version %s", roomVersion), func(t *testing.T) {
t.Parallel()
roomID := createRoomSynced(t, alice, map[string]interface{}{
"room_version": roomVersion,
})
alice.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(alice.UserID, roomID))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the whole point of the createRoomSynced function to literally do this? Why do it again?


res, _ := alice.MustSync(t, client.SyncReq{})
room := res.Get("rooms.join." + client.GjsonEscape(roomID))
ev0 := room.Get("timeline.events").Array()[0]
must.EqualStr(t, ev0.Get("type").Str, "m.room.create", "not a m.room.create event")
sendMessageSynced(t, alice, roomID)
})

userTypes := map[string]*client.CSAPI{
"local": bob,
"remote": charlie,
}
for typ, joiner := range userTypes {
typ := typ
joiner := joiner
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need comments to explain why you do this (else it'll take the last value only).


// sytest: $user_type user can join room with version $version
t.Run(fmt.Sprintf("%s user can join room with version %s", typ, roomVersion), func(t *testing.T) {
t.Parallel()
roomAlias := fmt.Sprintf("roomAlias_V%s%s", typ, roomVersion)
t.Logf("RoomAlias: %s", roomAlias)
roomID := createRoomSynced(t, alice, map[string]interface{}{
"room_version": roomVersion,
"room_alias_name": roomAlias,
"preset": "public_chat",
})
joinRoomSynced(t, joiner, roomID, fmt.Sprintf("#%s:%s", roomAlias, "hs1"))
_, nextBatch := joiner.MustSync(t, client.SyncReq{})
eventID := sendMessageSynced(t, alice, roomID)
joiner.MustSyncUntil(t, client.SyncReq{Since: nextBatch}, client.SyncTimelineHas(roomID, func(result gjson.Result) bool {
if len(result.Array()) > 1 {
t.Fatal("Expected a single timeline event")
}
must.EqualStr(t, result.Array()[0].Get("event_id").Str, eventID, "wrong event id")
return true
}))
})

// sytest: User can invite $user_type user to room with version $version
t.Run(fmt.Sprintf("User can invite %s user to room with version %s", typ, roomVersion), func(t *testing.T) {
t.Parallel()
roomID := createRoomSynced(t, alice, map[string]interface{}{
"room_version": roomVersion,
"preset": "private_chat",
})
alice.InviteRoom(t, roomID, joiner.UserID)
joiner.MustSyncUntil(t, client.SyncReq{}, client.SyncInvitedTo(joiner.UserID, roomID))
joinRoomSynced(t, joiner, roomID, "")
_, nextBatch := joiner.MustSync(t, client.SyncReq{})
eventID := sendMessageSynced(t, alice, roomID)
joiner.MustSyncUntil(t, client.SyncReq{Since: nextBatch}, client.SyncTimelineHas(roomID, func(result gjson.Result) bool {
if len(result.Array()) > 1 {
t.Fatal("Expected a single timeline event")
}
must.EqualStr(t, result.Array()[0].Get("event_id").Str, eventID, "wrong event id")
return true
}))
})

}

// sytest: Remote user can backfill in a room with version $version
t.Run(fmt.Sprintf("Remote user can backfill in a room with version %s", roomVersion), func(t *testing.T) {
t.Parallel()
roomID := createRoomSynced(t, alice, map[string]interface{}{
"room_version": roomVersion,
"invite": []string{charlie.UserID},
})
for i := 0; i < 20; i++ {
sendMessageSynced(t, alice, roomID)
}
charlie.MustSyncUntil(t, client.SyncReq{}, client.SyncInvitedTo(charlie.UserID, roomID))
joinRoomSynced(t, charlie, roomID, "")

queryParams := url.Values{}
queryParams.Set("dir", "b")
queryParams.Set("limit", "6")
res := charlie.MustDoFunc(t, "GET", []string{"_matrix", "client", "v3", "rooms", roomID, "messages"}, client.WithQueries(queryParams))
body := gjson.ParseBytes(must.ParseJSON(t, res.Body))
defer res.Body.Close()
if len(body.Get("chunk").Array()) != 6 {
t.Fatal("Expected 6 messages")
}
})

// sytest: Can reject invites over federation for rooms with version $version
t.Run(fmt.Sprintf("Can reject invites over federation for rooms with version %s", roomVersion), func(t *testing.T) {
t.Parallel()
roomID := createRoomSynced(t, alice, map[string]interface{}{
"room_version": roomVersion,
"invite": []string{charlie.UserID},
})
charlie.MustSyncUntil(t, client.SyncReq{}, client.SyncInvitedTo(charlie.UserID, roomID))
charlie.LeaveRoom(t, roomID)
})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No check for alice to see that the invite was rejected?


// sytest: Can receive redactions from regular users over federation in room version $version
t.Run(fmt.Sprintf("Can receive redactions from regular users over federation in room version %s", roomVersion), func(t *testing.T) {
t.Parallel()
roomID := createRoomSynced(t, alice, map[string]interface{}{
"room_version": roomVersion,
"invite": []string{charlie.UserID},
})
charlie.MustSyncUntil(t, client.SyncReq{}, client.SyncInvitedTo(charlie.UserID, roomID))
joinRoomSynced(t, charlie, roomID, "")
eventID := sendMessageSynced(t, charlie, roomID)
// redact the message
res := charlie.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "redact", eventID}, client.WithRawBody([]byte("{}")))
js := must.ParseJSON(t, res.Body)
defer res.Body.Close()
redactID := must.GetJSONFieldStr(t, js, "event_id")
alice.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHas(roomID, func(result gjson.Result) bool {
return redactID == result.Get("event_id").Str
}))
// query messages
queryParams := url.Values{}
queryParams.Set("dir", "b")
res = alice.MustDoFunc(t, "GET", []string{"_matrix", "client", "v3", "rooms", roomID, "messages"}, client.WithQueries(queryParams))
body := gjson.ParseBytes(must.ParseJSON(t, res.Body))
defer res.Body.Close()
events := body.Get("chunk").Array()
// first event should be the redaction
must.EqualStr(t, events[0].Get("event_id").Str, redactID, "wrong event")
must.EqualStr(t, events[0].Get("redacts").Str, eventID, "wrong event")
// second event should be the original event
must.EqualStr(t, events[1].Get("event_id").Str, eventID, "wrong event")
must.EqualStr(t, events[1].Get("unsigned.redacted_by").Str, redactID, "wrong event")
})
}
})
}

func sendMessageSynced(t *testing.T, cl *client.CSAPI, roomID string) (eventID string) {
return cl.SendEventSynced(t, roomID, b.Event{
Type: "m.room.message",
Content: map[string]interface{}{
"msgtype": "m.text",
"body": "hello world",
},
})
}

func joinRoomSynced(t *testing.T, cl *client.CSAPI, roomID, alias string) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should probably go into the Client impl to be honest. For now leave them here though.

joinRoom := roomID
if alias != "" {
joinRoom = alias
}
cl.JoinRoom(t, joinRoom, []string{})
cl.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(cl.UserID, roomID))
}

func createRoomSynced(t *testing.T, c *client.CSAPI, content map[string]interface{}) (roomID string) {
t.Helper()
roomID = c.CreateRoom(t, content)
c.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(c.UserID, roomID))
return
}
24 changes: 21 additions & 3 deletions tests/csapi/rooms_state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,11 @@ func TestRoomCreationReportsEventsToMyself(t *testing.T) {
userID := "@alice:hs1"
alice := deployment.Client(t, "hs1", userID)
roomID := alice.CreateRoom(t, struct{}{})

alice.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(userID, roomID))
t.Run("parallel", func(t *testing.T) {
// sytest: Room creation reports m.room.create to myself
t.Run("Room creation reports m.room.create to myself", func(t *testing.T) {
t.Parallel()
alice := deployment.Client(t, "hs1", userID)
alice.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHas(roomID, func(ev gjson.Result) bool {
if ev.Get("type").Str != "m.room.create" {
return false
Expand All @@ -45,7 +44,6 @@ func TestRoomCreationReportsEventsToMyself(t *testing.T) {
// sytest: Room creation reports m.room.member to myself
t.Run("Room creation reports m.room.member to myself", func(t *testing.T) {
t.Parallel()
alice := deployment.Client(t, "hs1", userID)
alice.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHas(roomID, func(ev gjson.Result) bool {
if ev.Get("type").Str != "m.room.member" {
return false
Expand All @@ -56,5 +54,25 @@ func TestRoomCreationReportsEventsToMyself(t *testing.T) {
return true
}))
})
// sytest: Setting room topic reports m.room.topic to myself
t.Run("Setting room topic reports m.room.topic to myself", func(t *testing.T) {
t.Parallel()
topic := "Testing topic for the new room"
reqBody := client.WithJSONBody(t, map[string]string{
"topic": topic,
})
alice.MustDoFunc(t, "PUT", []string{"_matrix", "client", "v3", "rooms", roomID, "state", "m.room.topic"}, reqBody)
alice.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHas(roomID, func(ev gjson.Result) bool {
if ev.Get("type").Str != "m.room.topic" {
return false
}
if !ev.Get("sender").Exists() || !ev.Get("content").Exists() {
return false
}
must.EqualStr(t, ev.Get("sender").Str, userID, "wrong sender")
must.EqualStr(t, ev.Get("content.topic").Str, topic, "wrong topic")
return true
}))
})
})
}