Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 5 additions & 1 deletion sytest.ignored.list
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,8 @@ Read receipts are visible to /initialSync
Newly created users see their own presence in /initialSync (SYT-34)
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
101 changes: 101 additions & 0 deletions tests/csapi/apidoc_room_levels_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
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")

// sytest: Both GET and PUT work
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")
}
})
// 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." + client.GjsonEscape(alice.UserID)).Int()
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")
}
})
// 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))

// absence of an 'events' key
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)
reqBody = client.WithJSONBody(t, map[string]interface{}{
"users": struct{}{},
})
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,
})
})
})
}
202 changes: 202 additions & 0 deletions tests/csapi/room_versions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
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()

// Query room versions the server supports
capabilities := alice.GetCapabilities(t)
availableRoomVersions := gjson.GetBytes(capabilities, `capabilities.m\.room_versions.available`).Map()
t.Run("Parallel", func(t *testing.T) {
// iterate over all room versions
for v := range roomVersions {
roomVersion := v
// skip versions the server doesn't know about
if _, ok := availableRoomVersions[string(roomVersion)]; !ok {
t.Logf("Skipping unsupported room version %s", roomVersion)
continue
}
// 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,
})

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 {
// Ensure to use the correct value and not only the last one.
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)
alice.MustSyncUntil(t, client.SyncReq{}, client.SyncLeftFrom(charlie.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.

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
}
4 changes: 2 additions & 2 deletions tests/csapi/rooms_state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func TestRoomCreationReportsEventsToMyself(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 @@ -41,7 +41,7 @@ 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 Down