Skip to content

Add (Must)GetStateEventContent #786

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

Merged
merged 2 commits into from
Jun 12, 2025
Merged
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
16 changes: 16 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,21 @@ func (c *CSAPI) SendRedaction(t ct.TestLike, roomID string, content map[string]i
return c.Do(t, "PUT", paths, WithJSONBody(t, content))
}

// MustGetStateEvent returns the event content for the given state event. Fails the test if the state event does not exist.
func (c *CSAPI) MustGetStateEventContent(t ct.TestLike, roomID, eventType, stateKey string) (content gjson.Result) {
t.Helper()
res := c.GetStateEventContent(t, roomID, eventType, stateKey)
mustRespond2xx(t, res)
body := ParseJSON(t, res)
return gjson.ParseBytes(body)
}

// GetStateEvent returns the event content for the given state event. Use this form to detect absence via 404.
func (c *CSAPI) GetStateEventContent(t ct.TestLike, roomID, eventType, stateKey string) *http.Response {
t.Helper()
return c.Do(t, "GET", []string{"_matrix", "client", "v3", "rooms", roomID, "state", eventType, stateKey})
}

// MustSendTyping marks this user as typing until the timeout is reached. If isTyping is false, timeout is ignored.
func (c *CSAPI) MustSendTyping(t ct.TestLike, roomID string, isTyping bool, timeoutMillis int) {
res := c.SendTyping(t, roomID, isTyping, timeoutMillis)
Expand Down Expand Up @@ -834,6 +849,7 @@ func (c *CSAPI) SendToDeviceMessages(t ct.TestLike, evType string, messages map[
}

func mustRespond2xx(t ct.TestLike, res *http.Response) {
t.Helper()
if res.StatusCode >= 200 && res.StatusCode < 300 {
return // 2xx
}
Expand Down
27 changes: 6 additions & 21 deletions tests/csapi/apidoc_room_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,8 @@ func TestRoomCreate(t *testing.T) {
"topic": "Test Room",
"preset": "public_chat",
})
res := alice.MustDo(t, "GET", []string{"_matrix", "client", "v3", "rooms", roomID, "state", "m.room.topic"})
must.MatchResponse(t, res, match.HTTPResponse{
StatusCode: 200,
JSON: []match.JSON{
match.JSONKeyEqual("topic", "Test Room"),
},
})
content := alice.MustGetStateEventContent(t, roomID, "m.room.topic", "")
must.MatchGJSON(t, content, match.JSONKeyEqual("topic", "Test Room"))
})
// sytest: POST /createRoom makes a room with a name
t.Run("POST /createRoom makes a room with a name", func(t *testing.T) {
Expand All @@ -74,13 +69,8 @@ func TestRoomCreate(t *testing.T) {
"name": "Test Room",
"preset": "public_chat",
})
res := alice.MustDo(t, "GET", []string{"_matrix", "client", "v3", "rooms", roomID, "state", "m.room.name"})
must.MatchResponse(t, res, match.HTTPResponse{
StatusCode: 200,
JSON: []match.JSON{
match.JSONKeyEqual("name", "Test Room"),
},
})
content := alice.MustGetStateEventContent(t, roomID, "m.room.name", "")
must.MatchGJSON(t, content, match.JSONKeyEqual("name", "Test Room"))
})
// sytest: POST /createRoom creates a room with the given version
t.Run("POST /createRoom creates a room with the given version", func(t *testing.T) {
Expand All @@ -89,13 +79,8 @@ func TestRoomCreate(t *testing.T) {
"room_version": "2",
"preset": "public_chat",
})
res := alice.MustDo(t, "GET", []string{"_matrix", "client", "v3", "rooms", roomID, "state", "m.room.create"})
must.MatchResponse(t, res, match.HTTPResponse{
StatusCode: 200,
JSON: []match.JSON{
match.JSONKeyEqual("room_version", "2"),
},
})
content := alice.MustGetStateEventContent(t, roomID, "m.room.create", "")
must.MatchGJSON(t, content, match.JSONKeyEqual("room_version", "2"))
})
// sytest: POST /createRoom makes a private room with invites
t.Run("POST /createRoom makes a private room with invites", func(t *testing.T) {
Expand Down
45 changes: 10 additions & 35 deletions tests/csapi/apidoc_room_members_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,14 +158,8 @@ func TestRoomMembers(t *testing.T) {
})

alice.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(bob.UserID, roomID))
res = alice.Do(t, "GET", []string{"_matrix", "client", "v3", "rooms", roomID, "state", "m.room.member", bob.UserID})

must.MatchResponse(t, res, match.HTTPResponse{
JSON: []match.JSON{
match.JSONKeyEqual("foo", "bar"),
match.JSONKeyEqual("membership", "join"),
},
})
content := alice.MustGetStateEventContent(t, roomID, "m.room.member", bob.UserID)
must.MatchGJSON(t, content, match.JSONKeyEqual("membership", "join"), match.JSONKeyEqual("foo", "bar"))
})
// sytest: POST /join/:room_alias can join a room with custom content
t.Run("POST /join/:room_alias can join a room with custom content", func(t *testing.T) {
Expand All @@ -187,14 +181,8 @@ func TestRoomMembers(t *testing.T) {
})

alice.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(bob.UserID, roomID))
res = alice.Do(t, "GET", []string{"_matrix", "client", "v3", "rooms", roomID, "state", "m.room.member", bob.UserID})

must.MatchResponse(t, res, match.HTTPResponse{
JSON: []match.JSON{
match.JSONKeyEqual("foo", "bar"),
match.JSONKeyEqual("membership", "join"),
},
})
content := alice.MustGetStateEventContent(t, roomID, "m.room.member", bob.UserID)
must.MatchGJSON(t, content, match.JSONKeyEqual("membership", "join"), match.JSONKeyEqual("foo", "bar"))
})

// sytest: POST /rooms/:room_id/ban can ban a user
Expand Down Expand Up @@ -222,12 +210,8 @@ func TestRoomMembers(t *testing.T) {
return ev.Get("content.membership").Str == "ban"
}))
// verify bob is banned
res = alice.Do(t, "GET", []string{"_matrix", "client", "v3", "rooms", roomID, "state", "m.room.member", bob.UserID})
must.MatchResponse(t, res, match.HTTPResponse{
JSON: []match.JSON{
match.JSONKeyEqual("membership", "ban"),
},
})
content := alice.MustGetStateEventContent(t, roomID, "m.room.member", bob.UserID)
must.MatchGJSON(t, content, match.JSONKeyEqual("membership", "ban"))
})

// sytest: POST /rooms/:room_id/invite can send an invite
Expand All @@ -236,12 +220,8 @@ func TestRoomMembers(t *testing.T) {
roomID := alice.MustCreateRoom(t, map[string]interface{}{})
alice.MustInviteRoom(t, roomID, bob.UserID)
alice.MustSyncUntil(t, client.SyncReq{}, client.SyncInvitedTo(bob.UserID, roomID))
res := alice.Do(t, "GET", []string{"_matrix", "client", "v3", "rooms", roomID, "state", "m.room.member", bob.UserID})
must.MatchResponse(t, res, match.HTTPResponse{
JSON: []match.JSON{
match.JSONKeyEqual("membership", "invite"),
},
})
content := alice.MustGetStateEventContent(t, roomID, "m.room.member", bob.UserID)
must.MatchGJSON(t, content, match.JSONKeyEqual("membership", "invite"))
})

// sytest: POST /rooms/:room_id/leave can leave a room
Expand All @@ -254,13 +234,8 @@ func TestRoomMembers(t *testing.T) {
alice.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(bob.UserID, roomID))
bob.MustLeaveRoom(t, roomID)
alice.MustSyncUntil(t, client.SyncReq{}, client.SyncLeftFrom(bob.UserID, roomID))

res := alice.Do(t, "GET", []string{"_matrix", "client", "v3", "rooms", roomID, "state", "m.room.member", bob.UserID})
must.MatchResponse(t, res, match.HTTPResponse{
JSON: []match.JSON{
match.JSONKeyEqual("membership", "leave"),
},
})
content := alice.MustGetStateEventContent(t, roomID, "m.room.member", bob.UserID)
must.MatchGJSON(t, content, match.JSONKeyEqual("membership", "leave"))
})
})
}
83 changes: 36 additions & 47 deletions tests/csapi/power_levels_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,52 +60,47 @@ func TestPowerLevels(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) {
// Test if the old state still exists
res := alice.MustDo(t, "GET", []string{"_matrix", "client", "v3", "rooms", roomID, "state", "m.room.power_levels"})

// note: before v10 we technically cannot assume that powerlevel integers are json numbers,
// as they can be both strings and numbers.
// However, for this test, we control the test environment,
// and we will assume the server is sane and give us powerlevels as numbers,
// and if it doesn't, that's an offense worthy of a frown.
content := alice.MustGetStateEventContent(t, roomID, "m.room.power_levels", "")
must.MatchGJSON(t, content,
match.JSONKeyTypeEqual("ban", gjson.Number),
match.JSONKeyTypeEqual("kick", gjson.Number),
match.JSONKeyTypeEqual("redact", gjson.Number),
match.JSONKeyTypeEqual("state_default", gjson.Number),
match.JSONKeyTypeEqual("events_default", gjson.Number),
match.JSONKeyTypeEqual("users_default", gjson.Number),

match.JSONMapEach("events", func(k, v gjson.Result) error {
if v.Type != gjson.Number {
return fmt.Errorf("key %s is not a number", k.Str)
} else {
return nil
}
}),

must.MatchResponse(t, res, match.HTTPResponse{
StatusCode: 200,
JSON: []match.JSON{
match.JSONKeyTypeEqual("ban", gjson.Number),
match.JSONKeyTypeEqual("kick", gjson.Number),
match.JSONKeyTypeEqual("redact", gjson.Number),
match.JSONKeyTypeEqual("state_default", gjson.Number),
match.JSONKeyTypeEqual("events_default", gjson.Number),
match.JSONKeyTypeEqual("users_default", gjson.Number),

match.JSONMapEach("events", func(k, v gjson.Result) error {
if v.Type != gjson.Number {
return fmt.Errorf("key %s is not a number", k.Str)
} else {
return nil
}
}),

match.JSONMapEach("users", func(k, v gjson.Result) error {
if v.Type != gjson.Number {
return fmt.Errorf("key %s is not a number", k.Str)
} else {
return nil
}
}),

func(body gjson.Result) error {
userDefault := int(body.Get("users_default").Num)
thisUser := int(body.Get("users." + client.GjsonEscape(alice.UserID)).Num)

if thisUser > userDefault {
return nil
} else {
return fmt.Errorf("expected room creator (%d) to have a higher-than-default powerlevel (which is %d)", thisUser, userDefault)
}
},
match.JSONMapEach("users", func(k, v gjson.Result) error {
if v.Type != gjson.Number {
return fmt.Errorf("key %s is not a number", k.Str)
} else {
return nil
}
}),

func(body gjson.Result) error {
userDefault := int(body.Get("users_default").Num)
thisUser := int(body.Get("users." + client.GjsonEscape(alice.UserID)).Num)

if thisUser > userDefault {
return nil
} else {
return fmt.Errorf("expected room creator (%d) to have a higher-than-default powerlevel (which is %d)", thisUser, userDefault)
}
},
})
)
})

// sytest: PUT /rooms/:room_id/state/m.room.power_levels can set levels
Expand Down Expand Up @@ -172,13 +167,7 @@ func TestPowerLevels(t *testing.T) {
})

// Test if the old state still exists
res = alice.MustDo(t, "GET", []string{"_matrix", "client", "v3", "rooms", roomID, "state", "m.room.power_levels"})

must.MatchResponse(t, res, match.HTTPResponse{
StatusCode: 200,
JSON: []match.JSON{
match.JSONKeyMissing("users"),
},
})
content := alice.MustGetStateEventContent(t, roomID, "m.room.power_levels", "")
must.MatchGJSON(t, content, match.JSONKeyMissing("users"))
})
}
48 changes: 8 additions & 40 deletions tests/csapi/room_leave_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,28 +128,12 @@ func TestLeftRoomFixture(t *testing.T) {
// sytest: Can get rooms/{roomId}/state for a departed room (SPEC-216)
t.Run("Can get rooms/{roomId}/state for a departed room", func(t *testing.T) {
// Bob gets the old state
resp := bob.MustDo(
t,
"GET",
[]string{"_matrix", "client", "v3", "rooms", roomID, "state", madeUpStateKey},
)
must.MatchResponse(t, resp, match.HTTPResponse{
JSON: []match.JSON{
match.JSONKeyEqual("body", beforeMadeUpState),
},
})
content := bob.MustGetStateEventContent(t, roomID, madeUpStateKey, "")
must.MatchGJSON(t, content, match.JSONKeyEqual("body", beforeMadeUpState))

// ...While Alice gets the new state
resp = alice.MustDo(
t,
"GET",
[]string{"_matrix", "client", "v3", "rooms", roomID, "state", madeUpStateKey},
)
must.MatchResponse(t, resp, match.HTTPResponse{
JSON: []match.JSON{
match.JSONKeyEqual("body", afterMadeUpState),
},
})
content = alice.MustGetStateEventContent(t, roomID, madeUpStateKey, "")
must.MatchGJSON(t, content, match.JSONKeyEqual("body", afterMadeUpState))
})

// sytest: Can get rooms/{roomId}/members for a departed room (SPEC-216)
Expand Down Expand Up @@ -205,28 +189,12 @@ func TestLeftRoomFixture(t *testing.T) {
// sytest: Can get 'm.room.name' state for a departed room (SPEC-216)
t.Run("Can get 'm.room.name' state for a departed room", func(t *testing.T) {
// Bob gets the old name
resp := bob.MustDo(
t,
"GET",
[]string{"_matrix", "client", "v3", "rooms", roomID, "state", "m.room.name"},
)
must.MatchResponse(t, resp, match.HTTPResponse{
JSON: []match.JSON{
match.JSONKeyEqual("name", beforeRoomName),
},
})
content := bob.MustGetStateEventContent(t, roomID, "m.room.name", "")
must.MatchGJSON(t, content, match.JSONKeyEqual("name", beforeRoomName))

// ...While Alice gets the new name
resp = alice.MustDo(
t,
"GET",
[]string{"_matrix", "client", "v3", "rooms", roomID, "state", "m.room.name"},
)
must.MatchResponse(t, resp, match.HTTPResponse{
JSON: []match.JSON{
match.JSONKeyEqual("name", afterRoomName),
},
})
content = alice.MustGetStateEventContent(t, roomID, "m.room.name", "")
must.MatchGJSON(t, content, match.JSONKeyEqual("name", afterRoomName))
})

// sytest: Getting messages going forward is limited for a departed room (SPEC-216)
Expand Down
9 changes: 2 additions & 7 deletions tests/csapi/rooms_invite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,12 +176,7 @@ func verifyState(t *testing.T, res gjson.Result, roomID string, cl *client.CSAPI
eventContent := event.Get("content." + field).Str
eventStateKey := event.Get("state_key").Str

res := cl.MustDo(t, "GET", []string{"_matrix", "client", "v3", "rooms", roomID, "state", eventType, eventStateKey})

must.MatchResponse(t, res, match.HTTPResponse{
JSON: []match.JSON{
match.JSONKeyEqual(field, eventContent),
},
})
content := cl.MustGetStateEventContent(t, roomID, eventType, eventStateKey)
must.MatchGJSON(t, content, match.JSONKeyEqual(field, eventContent))
}
}
15 changes: 6 additions & 9 deletions tests/federation_acl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,11 @@ func TestACLs(t *testing.T) {
must.ContainSubset(t, events, []string{sentinelEventID})

// Validate the ACL event is actually in the rooms state
res := user.Do(t, "GET", []string{"_matrix", "client", "v3", "rooms", roomID, "state", "m.room.server_acl"})
must.MatchResponse(t, res, match.HTTPResponse{
StatusCode: 200,
JSON: []match.JSON{
match.JSONKeyEqual("allow", []string{"*"}),
match.JSONKeyEqual("deny", []string{"hs2"}),
match.JSONKeyEqual("allow_ip_literals", true),
},
})
content := user.MustGetStateEventContent(t, roomID, "m.room.server_acl", "")
must.MatchGJSON(t, content,
match.JSONKeyEqual("allow", []string{"*"}),
match.JSONKeyEqual("deny", []string{"hs2"}),
match.JSONKeyEqual("allow_ip_literals", true),
)
}
}
11 changes: 3 additions & 8 deletions tests/federation_room_join_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,15 +357,10 @@ func TestBannedUserCannotSendJoin(t *testing.T) {
}

// Alice checks the room state to check that charlie isn't a member
res := alice.MustDo(
t,
"GET",
[]string{"_matrix", "client", "v3", "rooms", roomID, "state", "m.room.member", charlie},
content := alice.MustGetStateEventContent(t, roomID, "m.room.member", charlie)
must.MatchGJSON(t, content,
match.JSONKeyEqual("membership", "ban"),
)
stateResp := must.ParseJSON(t, res.Body)
res.Body.Close()
membership := must.GetJSONFieldStr(t, stateResp, "membership")
must.Equal(t, membership, "ban", "membership of charlie")
}

// This test checks that we cannot submit anything via /v1/send_join except a join.
Expand Down
Loading