diff --git a/tests/msc3083_test.go b/tests/msc3083_test.go index 9da71271..58a79ade 100644 --- a/tests/msc3083_test.go +++ b/tests/msc3083_test.go @@ -17,7 +17,7 @@ import ( "github.com/tidwall/gjson" ) -func failJoinRoom(t *testing.T, c *client.CSAPI, roomIDOrAlias string, serverName string) { +func failJoinRoom(t *testing.T, c *client.CSAPI, roomIDOrAlias string, serverName string, expectedErrorCode int) { t.Helper() // This is copied from Client.JoinRoom to test a join failure. @@ -30,7 +30,7 @@ func failJoinRoom(t *testing.T, c *client.CSAPI, roomIDOrAlias string, serverNam client.WithQueries(query), ) must.MatchResponse(t, res, match.HTTPResponse{ - StatusCode: 403, + StatusCode: expectedErrorCode, }) } @@ -84,7 +84,7 @@ func setupRestrictedRoom(t *testing.T, deployment *docker.Deployment) (*client.C func checkRestrictedRoom(t *testing.T, alice *client.CSAPI, bob *client.CSAPI, space string, room string) { t.Helper() - failJoinRoom(t, bob, room, "hs1") + failJoinRoom(t, bob, room, "hs1", 403) // Join the space, attempt to join the room again, which now should succeed. bob.JoinRoom(t, space, []string{"hs1"}) @@ -108,7 +108,7 @@ func checkRestrictedRoom(t *testing.T, alice *client.CSAPI, bob *client.CSAPI, s return ev.Get("content").Get("membership").Str == "leave" }) - failJoinRoom(t, bob, room, "hs1") + failJoinRoom(t, bob, room, "hs1", 403) // Invite the user and joining should work. alice.InviteRoom(t, room, bob.UserID) @@ -135,7 +135,7 @@ func checkRestrictedRoom(t *testing.T, alice *client.CSAPI, bob *client.CSAPI, s }, ) // Fails since invalid values get filtered out of allow. - failJoinRoom(t, bob, room, "hs1") + failJoinRoom(t, bob, room, "hs1", 403) alice.SendEventSynced( t, @@ -151,7 +151,7 @@ func checkRestrictedRoom(t *testing.T, alice *client.CSAPI, bob *client.CSAPI, s }, ) // Fails since a fully invalid allow key requires an invite. - failJoinRoom(t, bob, room, "hs1") + failJoinRoom(t, bob, room, "hs1", 403) } // Test joining a room with join rules restricted to membership in a space. @@ -250,7 +250,7 @@ func TestRestrictedRoomsRemoteJoinLocalUser(t *testing.T) { }) // Bob cannot join the room. - failJoinRoom(t, bob, room, "hs1") + failJoinRoom(t, bob, room, "hs1", 403) // Join the space via hs2. bob.JoinRoom(t, space, []string{"hs2"}) @@ -309,6 +309,150 @@ func TestRestrictedRoomsRemoteJoinLocalUser(t *testing.T) { bob.JoinRoom(t, room, []string{"hs1"}) } +// A server will request a failover if asked to /make_join and it does not have +// the appropriate authorisation to complete the request. +// +// Setup 3 homeservers: +// * hs1 creates the space/room. +// * hs2 joins the room +// * hs3 attempts to join via hs2 (should fail) and hs1 (should work) +func TestRestrictedRoomsRemoteJoinFailOver(t *testing.T) { + deployment := Deploy(t, b.Blueprint{ + Name: "federation_three_homeservers", + Homeservers: []b.Homeserver{ + { + Name: "hs1", + Users: []b.User{ + { + Localpart: "alice", + DisplayName: "Alice", + }, + }, + }, + { + Name: "hs2", + Users: []b.User{ + { + Localpart: "bob", + DisplayName: "Bob", + }, + }, + }, + { + Name: "hs3", + Users: []b.User{ + { + Localpart: "charlie", + DisplayName: "Charlie", + }, + }, + }, + }, + }) + defer deployment.Destroy(t) + + // Setup the user, space, and restricted room. + alice, space, room := setupRestrictedRoom(t, deployment) + + // Raise the power level so that only alice can invite. + state_key := "" + alice.SendEventSynced(t, room, b.Event{ + Type: "m.room.power_levels", + StateKey: &state_key, + Content: map[string]interface{}{ + "invite": 100, + "users": map[string]interface{}{ + alice.UserID: 100, + }, + }, + }) + + // Create a second user on a different homeserver. + bob := deployment.Client(t, "hs2", "@bob:hs2") + + // Bob joins the room and space. + bob.JoinRoom(t, space, []string{"hs1"}) + bob.JoinRoom(t, room, []string{"hs1"}) + + // Charlie should join the space (which gives access to the room). + charlie := deployment.Client(t, "hs3", "@charlie:hs3") + charlie.JoinRoom(t, space, []string{"hs1"}) + + // hs2 doesn't have anyone to invite from, so the join fails. + failJoinRoom(t, charlie, room, "hs2", 502) + + // Including hs1 (and failing over to it) allows the join to succeed. + charlie.JoinRoom(t, room, []string{"hs2", "hs1"}) + + // Double check that the join was authorised via hs1. + bob.SyncUntilTimelineHas( + t, + room, + func(ev gjson.Result) bool { + if ev.Get("type").Str != "m.room.member" || ev.Get("state_key").Str != charlie.UserID { + return false + } + must.EqualStr(t, ev.Get("content").Get("membership").Str, "join", "Charlie failed to join the room") + must.EqualStr(t, ev.Get("content").Get("join_authorised_via_users_server").Str, alice.UserID, "Join authorised via incorrect server") + + return true + }, + ) + + // Bump the power-level of bob. + alice.SendEventSynced(t, room, b.Event{ + Type: "m.room.power_levels", + StateKey: &state_key, + Content: map[string]interface{}{ + "invite": 100, + "users": map[string]interface{}{ + alice.UserID: 100, + bob.UserID: 100, + }, + }, + }) + + // Charlie leaves the room (so they can rejoin). + charlie.LeaveRoom(t, room) + + // Ensure the events have synced to hs2. + bob.SyncUntilTimelineHas( + t, + room, + func(ev gjson.Result) bool { + if ev.Get("type").Str != "m.room.member" || ev.Get("state_key").Str != charlie.UserID { + return false + } + return ev.Get("content").Get("membership").Str == "leave" + }, + ) + + // Bob leaves the space so that hs2 doesn't know if Charlie is in the space or not. + bob.LeaveRoom(t, space) + + // hs2 cannot complete the join since they do not know if Charlie meets the + // requirements (since it is no longer in the space). + failJoinRoom(t, charlie, room, "hs2", 502) + + // Including hs1 (and failing over to it) allows the join to succeed. + charlie.JoinRoom(t, room, []string{"hs2", "hs1"}) + + // Double check that the join was authorised via hs1. + bob.SyncUntilTimelineHas( + t, + room, + func(ev gjson.Result) bool { + if ev.Get("type").Str != "m.room.member" || ev.Get("state_key").Str != charlie.UserID { + return false + } + must.EqualStr(t, ev.Get("content").Get("membership").Str, "join", "Charlie failed to join the room") + must.EqualStr(t, ev.Get("content").Get("join_authorised_via_users_server").Str, alice.UserID, "Join authorised via incorrect server") + + return true + }, + ) +} + // Request the room summary and ensure the expected rooms are in the response. func requestAndAssertSummary(t *testing.T, user *client.CSAPI, space string, expected_rooms []interface{}) { t.Helper()