From 69ad30d3bb1a8a35cf43a4a1a8c2673f26fae7fa Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 14 Aug 2025 16:10:10 +0100 Subject: [PATCH 1/4] Add tests for downgrading a v12 room --- tests/v12_test.go | 116 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/tests/v12_test.go b/tests/v12_test.go index 70c86f53..74aed70f 100644 --- a/tests/v12_test.go +++ b/tests/v12_test.go @@ -652,6 +652,122 @@ func TestMSC4289PrivilegedRoomCreators_Upgrades(t *testing.T) { } } +func TestMSC4289PrivilegedRoomCreators_Downgrades(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "alice", + }) + bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "bob", + }) + + testCases := []struct { + name string + initialCreator *client.CSAPI + initialAdditionalCreators []string + newVersion string + initialPLs map[string]any + entitiyDoingUpgrade *client.CSAPI + // assertions + wantNewUsersMap map[string]int64 + }{ + { + name: "upgrading a room from v12 to v11 keeps the old room's creator user as an admin of the new room", + initialCreator: alice, + newVersion: "11", + initialPLs: map[string]any{}, + entitiyDoingUpgrade: alice, + wantNewUsersMap: map[string]int64{ + // Max PL needed to do anything in a room with no default power levels. + alice.UserID: 50, + }, + }, + { + name: "upgrading a room from v12 to v11 sets the old room's creator to the max power level needed to carry out any action", + initialCreator: alice, + newVersion: "11", + initialPLs: map[string]any{ + "ban": 100, + }, + entitiyDoingUpgrade: alice, + wantNewUsersMap: map[string]int64{ + // Max PL needed to do anything in a room with no default power levels. + alice.UserID: 100, + }, + }, + { + name: "upgrading a room from v12 to v11 keeps the additional_creator users as admins of the new room", + initialCreator: alice, + initialAdditionalCreators: []string{ + bob.UserID, + }, + newVersion: "11", + initialPLs: map[string]any{}, + entitiyDoingUpgrade: alice, + wantNewUsersMap: map[string]int64{ + // Max PL needed to do anything in a room with no default power levels. + alice.UserID: 50, + bob.UserID: 50, + }, + }, + } + + for _, tc := range testCases { + createBody := map[string]interface{}{ + "room_version": roomVersion12, + "preset": "public_chat", + } + if tc.initialAdditionalCreators != nil { + createBody["creation_content"] = map[string]any{ + "additional_creators": tc.initialAdditionalCreators, + } + } + roomID := tc.initialCreator.MustCreateRoom(t, createBody) + alice.JoinRoom(t, roomID, []spec.ServerName{"hs1"}) + bob.JoinRoom(t, roomID, []spec.ServerName{"hs1"}) + tc.initialCreator.SendEventSynced(t, roomID, b.Event{ + Type: spec.MRoomPowerLevels, + StateKey: b.Ptr(""), + Content: tc.initialPLs, + }) + upgradeBody := map[string]any{ + "new_version": tc.newVersion, + } + res := tc.entitiyDoingUpgrade.MustDo(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "upgrade"}, client.WithJSONBody(t, upgradeBody)) + newRoomID := must.ParseJSON(t, res.Body).Get("replacement_room").Str + // New Create event assertions + createContent := tc.entitiyDoingUpgrade.MustGetStateEventContent(t, newRoomID, spec.MRoomCreate, "") + createAssertions := []match.JSON{ + match.JSONKeyEqual("room_version", tc.newVersion), + } + must.MatchGJSON( + t, createContent, createAssertions..., + ) + // New PL assertions + plContent := tc.entitiyDoingUpgrade.MustGetStateEventContent(t, newRoomID, spec.MRoomPowerLevels, "") + if tc.wantNewUsersMap != nil { + plContent.Get("users").ForEach(func(key, v gjson.Result) bool { + gotVal := v.Int() + wantVal, ok := tc.wantNewUsersMap[key.Str] + if !ok { + ct.Errorf(t, "%s: upgraded room PL content, user %s has PL %v but want it missing", tc.name, key.Str, gotVal) + return true + } + if gotVal != wantVal { + ct.Errorf(t, "%s: upgraded room PL content, user %s has PL %v want %v", tc.name, key.Str, gotVal, wantVal) + } + delete(tc.wantNewUsersMap, key.Str) + return true + }) + if len(tc.wantNewUsersMap) > 0 { + ct.Fatalf(t, "%s: upgraded room PL content missed these users %v", tc.name, tc.wantNewUsersMap) + } + } + t.Logf("OK: %v", tc.name) + } +} + // Test that the room ID is in fact the hash of the create event. func TestMSC4291RoomIDAsHashOfCreateEvent(t *testing.T) { deployment := complement.Deploy(t, 1) From 6d6d9641e03a86e04d94806719221674f699c2a2 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 14 Aug 2025 16:11:02 +0100 Subject: [PATCH 2/4] Remove initialAdditionalUsers in upgrade tests It was unused, and had a bug in it ('addition_users' should go under 'creation_content'). --- tests/v12_test.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/v12_test.go b/tests/v12_test.go index 74aed70f..25dfc507 100644 --- a/tests/v12_test.go +++ b/tests/v12_test.go @@ -495,7 +495,6 @@ func TestMSC4289PrivilegedRoomCreators_Upgrades(t *testing.T) { testCases := []struct { name string initialCreator *client.CSAPI - initialAdditionalCreators []string initialVersion string initialUserPLs map[string]int entitiyDoingUpgrade *client.CSAPI @@ -590,10 +589,6 @@ func TestMSC4289PrivilegedRoomCreators_Upgrades(t *testing.T) { "room_version": tc.initialVersion, "preset": "public_chat", } - if tc.initialAdditionalCreators != nil { - must.Equal(t, tc.initialVersion, roomVersion12, "can only set additional_creators on v12") - createBody["additional_creators"] = tc.initialAdditionalCreators - } roomID := tc.initialCreator.MustCreateRoom(t, createBody) alice.JoinRoom(t, roomID, []spec.ServerName{"hs1"}) bob.JoinRoom(t, roomID, []spec.ServerName{"hs1"}) From 9cd3a7c3166e594f9151b5a3b0dcb768b8a84cce Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 14 Aug 2025 16:11:36 +0100 Subject: [PATCH 3/4] fix typo: entitiy --- tests/v12_test.go | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/v12_test.go b/tests/v12_test.go index 25dfc507..edfc7115 100644 --- a/tests/v12_test.go +++ b/tests/v12_test.go @@ -497,7 +497,7 @@ func TestMSC4289PrivilegedRoomCreators_Upgrades(t *testing.T) { initialCreator *client.CSAPI initialVersion string initialUserPLs map[string]int - entitiyDoingUpgrade *client.CSAPI + entityDoingUpgrade *client.CSAPI newAdditionalCreators []string // assertions wantAdditionalCreators []string @@ -510,7 +510,7 @@ func TestMSC4289PrivilegedRoomCreators_Upgrades(t *testing.T) { initialUserPLs: map[string]int{ bob.UserID: 100, }, - entitiyDoingUpgrade: bob, + entityDoingUpgrade: bob, wantAdditionalCreators: []string{}, wantNewUsersMap: map[string]int64{}, }, @@ -522,7 +522,7 @@ func TestMSC4289PrivilegedRoomCreators_Upgrades(t *testing.T) { bob.UserID: 100, charlie.UserID: 100, }, - entitiyDoingUpgrade: bob, + entityDoingUpgrade: bob, wantAdditionalCreators: []string{}, wantNewUsersMap: map[string]int64{ charlie.UserID: 100, @@ -535,7 +535,7 @@ func TestMSC4289PrivilegedRoomCreators_Upgrades(t *testing.T) { initialUserPLs: map[string]int{ bob.UserID: 150, // bob has enough permission to upgrade }, - entitiyDoingUpgrade: bob, + entityDoingUpgrade: bob, newAdditionalCreators: []string{charlie.UserID}, wantAdditionalCreators: []string{charlie.UserID}, wantNewUsersMap: map[string]int64{}, @@ -548,7 +548,7 @@ func TestMSC4289PrivilegedRoomCreators_Upgrades(t *testing.T) { bob.UserID: 150, // bob has enough permission to upgrade charlie.UserID: 50, // gets removed as he will become an additional creator }, - entitiyDoingUpgrade: bob, + entityDoingUpgrade: bob, newAdditionalCreators: []string{charlie.UserID}, wantAdditionalCreators: []string{charlie.UserID}, wantNewUsersMap: map[string]int64{}, @@ -561,7 +561,7 @@ func TestMSC4289PrivilegedRoomCreators_Upgrades(t *testing.T) { alice.UserID: 100, bob.UserID: 100, }, - entitiyDoingUpgrade: alice, + entityDoingUpgrade: alice, newAdditionalCreators: []string{bob.UserID}, wantAdditionalCreators: []string{bob.UserID}, wantNewUsersMap: map[string]int64{}, // both alice and bob are removed as they are now creators. @@ -575,7 +575,7 @@ func TestMSC4289PrivilegedRoomCreators_Upgrades(t *testing.T) { bob.UserID: 100, charlie.UserID: 50, }, - entitiyDoingUpgrade: alice, + entityDoingUpgrade: alice, newAdditionalCreators: []string{bob.UserID}, wantAdditionalCreators: []string{bob.UserID}, wantNewUsersMap: map[string]int64{ @@ -606,10 +606,10 @@ func TestMSC4289PrivilegedRoomCreators_Upgrades(t *testing.T) { if tc.newAdditionalCreators != nil { upgradeBody["additional_creators"] = tc.newAdditionalCreators } - res := tc.entitiyDoingUpgrade.MustDo(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "upgrade"}, client.WithJSONBody(t, upgradeBody)) + res := tc.entityDoingUpgrade.MustDo(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "upgrade"}, client.WithJSONBody(t, upgradeBody)) newRoomID := must.ParseJSON(t, res.Body).Get("replacement_room").Str // New Create event assertions - createContent := tc.entitiyDoingUpgrade.MustGetStateEventContent(t, newRoomID, spec.MRoomCreate, "") + createContent := tc.entityDoingUpgrade.MustGetStateEventContent(t, newRoomID, spec.MRoomCreate, "") createAssertions := []match.JSON{ match.JSONKeyEqual("room_version", roomVersion12), } @@ -624,7 +624,7 @@ func TestMSC4289PrivilegedRoomCreators_Upgrades(t *testing.T) { t, createContent, createAssertions..., ) // New PL assertions - plContent := tc.entitiyDoingUpgrade.MustGetStateEventContent(t, newRoomID, spec.MRoomPowerLevels, "") + plContent := tc.entityDoingUpgrade.MustGetStateEventContent(t, newRoomID, spec.MRoomPowerLevels, "") if tc.wantNewUsersMap != nil { plContent.Get("users").ForEach(func(key, v gjson.Result) bool { gotVal := v.Int() @@ -663,7 +663,7 @@ func TestMSC4289PrivilegedRoomCreators_Downgrades(t *testing.T) { initialAdditionalCreators []string newVersion string initialPLs map[string]any - entitiyDoingUpgrade *client.CSAPI + entityDoingUpgrade *client.CSAPI // assertions wantNewUsersMap map[string]int64 }{ @@ -672,7 +672,7 @@ func TestMSC4289PrivilegedRoomCreators_Downgrades(t *testing.T) { initialCreator: alice, newVersion: "11", initialPLs: map[string]any{}, - entitiyDoingUpgrade: alice, + entityDoingUpgrade: alice, wantNewUsersMap: map[string]int64{ // Max PL needed to do anything in a room with no default power levels. alice.UserID: 50, @@ -685,7 +685,7 @@ func TestMSC4289PrivilegedRoomCreators_Downgrades(t *testing.T) { initialPLs: map[string]any{ "ban": 100, }, - entitiyDoingUpgrade: alice, + entityDoingUpgrade: alice, wantNewUsersMap: map[string]int64{ // Max PL needed to do anything in a room with no default power levels. alice.UserID: 100, @@ -699,7 +699,7 @@ func TestMSC4289PrivilegedRoomCreators_Downgrades(t *testing.T) { }, newVersion: "11", initialPLs: map[string]any{}, - entitiyDoingUpgrade: alice, + entityDoingUpgrade: alice, wantNewUsersMap: map[string]int64{ // Max PL needed to do anything in a room with no default power levels. alice.UserID: 50, @@ -729,10 +729,10 @@ func TestMSC4289PrivilegedRoomCreators_Downgrades(t *testing.T) { upgradeBody := map[string]any{ "new_version": tc.newVersion, } - res := tc.entitiyDoingUpgrade.MustDo(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "upgrade"}, client.WithJSONBody(t, upgradeBody)) + res := tc.entityDoingUpgrade.MustDo(t, "POST", []string{"_matrix", "client", "v3", "rooms", roomID, "upgrade"}, client.WithJSONBody(t, upgradeBody)) newRoomID := must.ParseJSON(t, res.Body).Get("replacement_room").Str // New Create event assertions - createContent := tc.entitiyDoingUpgrade.MustGetStateEventContent(t, newRoomID, spec.MRoomCreate, "") + createContent := tc.entityDoingUpgrade.MustGetStateEventContent(t, newRoomID, spec.MRoomCreate, "") createAssertions := []match.JSON{ match.JSONKeyEqual("room_version", tc.newVersion), } @@ -740,7 +740,7 @@ func TestMSC4289PrivilegedRoomCreators_Downgrades(t *testing.T) { t, createContent, createAssertions..., ) // New PL assertions - plContent := tc.entitiyDoingUpgrade.MustGetStateEventContent(t, newRoomID, spec.MRoomPowerLevels, "") + plContent := tc.entityDoingUpgrade.MustGetStateEventContent(t, newRoomID, spec.MRoomPowerLevels, "") if tc.wantNewUsersMap != nil { plContent.Get("users").ForEach(func(key, v gjson.Result) bool { gotVal := v.Int() From 62ba92ab71018706bd1e697f92dce34f29b5ef06 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 9 Oct 2025 18:16:23 +0100 Subject: [PATCH 4/4] Update tests with new behaviour * Creators in the new room should have the max canonicaljson int * If a non-creator in the v12 room had max canonicaljson int already, demote them by 1 --- tests/v12_test.go | 45 ++++++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/tests/v12_test.go b/tests/v12_test.go index edfc7115..ad421d1c 100644 --- a/tests/v12_test.go +++ b/tests/v12_test.go @@ -656,6 +656,11 @@ func TestMSC4289PrivilegedRoomCreators_Downgrades(t *testing.T) { bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{ LocalpartSuffix: "bob", }) + charlie := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "charlie", + }) + + max_canonicaljson_power_level := int64(math.Pow(2, 53) - 1) testCases := []struct { name string @@ -668,42 +673,52 @@ func TestMSC4289PrivilegedRoomCreators_Downgrades(t *testing.T) { wantNewUsersMap map[string]int64 }{ { - name: "upgrading a room from v12 to v11 keeps the old room's creator user as an admin of the new room", + name: "upgrading a room from v12 to v11 sets the old room's creator to the max canonicaljson power level in the new room", initialCreator: alice, newVersion: "11", initialPLs: map[string]any{}, entityDoingUpgrade: alice, wantNewUsersMap: map[string]int64{ - // Max PL needed to do anything in a room with no default power levels. - alice.UserID: 50, + // Max canonicaljson power level. + alice.UserID: max_canonicaljson_power_level, }, }, { - name: "upgrading a room from v12 to v11 sets the old room's creator to the max power level needed to carry out any action", + name: "upgrading a room from v12 to v11 keeps the additional_creator users as admins of the new room. existing users remain the same", initialCreator: alice, + initialAdditionalCreators: []string{ + bob.UserID, + }, newVersion: "11", initialPLs: map[string]any{ - "ban": 100, + "users": map[string]int64{ + charlie.UserID: 30, + }, }, entityDoingUpgrade: alice, wantNewUsersMap: map[string]int64{ - // Max PL needed to do anything in a room with no default power levels. - alice.UserID: 100, + alice.UserID: max_canonicaljson_power_level, + bob.UserID: max_canonicaljson_power_level, + charlie.UserID: 30, // charlie is not a creator }, }, { - name: "upgrading a room from v12 to v11 keeps the additional_creator users as admins of the new room", + name: "upgrading a room from v12 to v11 sets any user who wasn't a creator, but had max canonicaljson power level in the old room, to just below the max canonicaljson power level in the new room", initialCreator: alice, - initialAdditionalCreators: []string{ - bob.UserID, - }, newVersion: "11", - initialPLs: map[string]any{}, + initialPLs: map[string]any{ + "users": map[string]int64{ + bob.UserID: 30, + charlie.UserID: max_canonicaljson_power_level, + }, + }, entityDoingUpgrade: alice, wantNewUsersMap: map[string]int64{ - // Max PL needed to do anything in a room with no default power levels. - alice.UserID: 50, - bob.UserID: 50, + // Neither bob or charlie are creators. + alice.UserID: max_canonicaljson_power_level, + bob.UserID: 30, + // charlie's power level was reduced just below max canonicaljson int. + charlie.UserID: max_canonicaljson_power_level - 1, }, }, }