@@ -2927,6 +2927,135 @@ public function testUpdateUserDelegatedAdminCannotAddToAdminGroup(): void {
29272927 $ this ->assertSame (Http::STATUS_OK , $ result ->getStatus ());
29282928 }
29292929
2930+ /**
2931+ * Wire up a sub-admin ('subadmin') editing an accessible account ('targetuser').
2932+ *
2933+ * @param list<string> $gids Every group of the scenario, mocked and returned by ID
2934+ * @param list<string> $subAdminGids Groups the caller is sub-admin of
2935+ * @param list<string> $memberGids Groups the account is currently a member of
2936+ * @return array{0: IUser&MockObject, 1: ISubAdmin&MockObject, 2: array<string, IGroup&MockObject>}
2937+ */
2938+ private function mockSubAdminEditing (array $ gids , array $ subAdminGids , array $ memberGids ): array {
2939+ $ currentUser = $ this ->createMock (IUser::class);
2940+ $ currentUser ->method ('getUID ' )->willReturn ('subadmin ' );
2941+ $ this ->userSession ->method ('getUser ' )->willReturn ($ currentUser );
2942+
2943+ $ targetUser = $ this ->createMock (IUser::class);
2944+ $ targetUser ->method ('getUID ' )->willReturn ('targetuser ' );
2945+ $ targetUser ->method ('getBackend ' )->willReturn ($ this ->createMock (UserInterface::class));
2946+ $ this ->userManager ->method ('get ' )->with ('targetuser ' )->willReturn ($ targetUser );
2947+
2948+ $ this ->groupManager ->method ('isAdmin ' )->willReturn (false );
2949+ $ this ->groupManager ->method ('isDelegatedAdmin ' )->willReturn (false );
2950+ $ this ->groupManager ->method ('groupExists ' )->willReturn (true );
2951+ $ this ->groupManager ->method ('getUserGroupIds ' )->willReturn ($ memberGids );
2952+
2953+ $ groups = [];
2954+ foreach ($ gids as $ gid ) {
2955+ $ group = $ this ->createMock (IGroup::class);
2956+ $ group ->method ('getGID ' )->willReturn ($ gid );
2957+ $ groups [$ gid ] = $ group ;
2958+ }
2959+ $ this ->groupManager ->method ('get ' )
2960+ ->willReturnMap (array_map (fn (string $ gid ): array => [$ gid , $ groups [$ gid ]], $ gids ));
2961+
2962+ $ subAdmin = $ this ->createMock (ISubAdmin::class);
2963+ $ subAdmin ->method ('isUserAccessible ' )->with ($ currentUser , $ targetUser )->willReturn (true );
2964+ $ subAdmin ->method ('getSubAdminsGroups ' )
2965+ ->willReturn (array_map (fn (string $ gid ): IGroup => $ groups [$ gid ], $ subAdminGids ));
2966+ $ this ->groupManager ->method ('getSubAdmin ' )->willReturn ($ subAdmin );
2967+
2968+ return [$ targetUser , $ subAdmin , $ groups ];
2969+ }
2970+
2971+ public function testUpdateUserSubAdminCanAddToOwnGroup (): void {
2972+ [$ targetUser , , $ groups ] = $ this ->mockSubAdminEditing (
2973+ gids: ['staff ' , 'marketing ' ],
2974+ subAdminGids: ['staff ' ],
2975+ memberGids: ['marketing ' ],
2976+ );
2977+
2978+ $ groups ['staff ' ]->expects ($ this ->once ())->method ('addUser ' )->with ($ targetUser );
2979+ // The membership the sub-admin cannot administer is repeated, not changed
2980+ $ groups ['marketing ' ]->expects ($ this ->never ())->method ('addUser ' );
2981+ $ groups ['marketing ' ]->expects ($ this ->never ())->method ('removeUser ' );
2982+
2983+ $ result = $ this ->api ->editUserMultiField ('targetuser ' , groups: ['marketing ' , 'staff ' ]);
2984+ $ this ->assertSame (Http::STATUS_OK , $ result ->getStatus ());
2985+ }
2986+
2987+ public function testUpdateUserSubAdminCannotAddToForeignGroup (): void {
2988+ [, , $ groups ] = $ this ->mockSubAdminEditing (
2989+ gids: ['staff ' , 'secret ' ],
2990+ subAdminGids: ['staff ' ],
2991+ memberGids: ['staff ' ],
2992+ );
2993+
2994+ $ groups ['secret ' ]->expects ($ this ->never ())->method ('addUser ' );
2995+
2996+ $ result = $ this ->api ->editUserMultiField ('targetuser ' , groups: ['staff ' , 'secret ' ]);
2997+ $ this ->assertSame (Http::STATUS_UNPROCESSABLE_ENTITY , $ result ->getStatus ());
2998+ $ this ->assertSame ('Insufficient privileges for group secret ' , $ result ->getData ()['errors ' ]['groups ' ]);
2999+ }
3000+
3001+ public function testUpdateUserSubAdminCanRemoveFromOwnGroup (): void {
3002+ [$ targetUser , , $ groups ] = $ this ->mockSubAdminEditing (
3003+ gids: ['staff ' , 'sales ' ],
3004+ subAdminGids: ['staff ' , 'sales ' ],
3005+ memberGids: ['staff ' , 'sales ' ],
3006+ );
3007+
3008+ $ groups ['staff ' ]->expects ($ this ->once ())->method ('removeUser ' )->with ($ targetUser );
3009+ $ groups ['sales ' ]->expects ($ this ->never ())->method ('removeUser ' );
3010+
3011+ $ result = $ this ->api ->editUserMultiField ('targetuser ' , groups: ['sales ' ]);
3012+ $ this ->assertSame (Http::STATUS_OK , $ result ->getStatus ());
3013+ }
3014+
3015+ public function testUpdateUserSubAdminCannotRemoveLastGroupTheyAdminister (): void {
3016+ [, , $ groups ] = $ this ->mockSubAdminEditing (
3017+ gids: ['staff ' ],
3018+ subAdminGids: ['staff ' ],
3019+ memberGids: ['staff ' ],
3020+ );
3021+
3022+ $ groups ['staff ' ]->expects ($ this ->never ())->method ('removeUser ' );
3023+
3024+ $ result = $ this ->api ->editUserMultiField ('targetuser ' , groups: []);
3025+ $ this ->assertSame (Http::STATUS_UNPROCESSABLE_ENTITY , $ result ->getStatus ());
3026+ $ this ->assertArrayHasKey ('groups ' , $ result ->getData ()['errors ' ]);
3027+ }
3028+
3029+ public function testUpdateUserSubAdminKeepsGroupsOutsideTheirScope (): void {
3030+ [, , $ groups ] = $ this ->mockSubAdminEditing (
3031+ gids: ['staff ' , 'marketing ' ],
3032+ subAdminGids: ['staff ' ],
3033+ memberGids: ['staff ' , 'marketing ' ],
3034+ );
3035+
3036+ // The sub-admin UI only offers the groups they administer, so 'marketing' is
3037+ // absent from the request without the sub-admin ever asking to remove it
3038+ $ groups ['marketing ' ]->expects ($ this ->never ())->method ('removeUser ' );
3039+ $ groups ['staff ' ]->expects ($ this ->never ())->method ('removeUser ' );
3040+
3041+ $ result = $ this ->api ->editUserMultiField ('targetuser ' , groups: ['staff ' ]);
3042+ $ this ->assertSame (Http::STATUS_OK , $ result ->getStatus ());
3043+ }
3044+
3045+ public function testUpdateUserSubAdminCannotChangeSubAdminGroups (): void {
3046+ [, $ subAdmin , ] = $ this ->mockSubAdminEditing (
3047+ gids: ['staff ' ],
3048+ subAdminGids: ['staff ' ],
3049+ memberGids: ['staff ' ],
3050+ );
3051+
3052+ $ subAdmin ->expects ($ this ->never ())->method ('createSubAdmin ' );
3053+
3054+ $ result = $ this ->api ->editUserMultiField ('targetuser ' , subadminGroups: ['staff ' ]);
3055+ $ this ->assertSame (Http::STATUS_UNPROCESSABLE_ENTITY , $ result ->getStatus ());
3056+ $ this ->assertArrayHasKey ('subadminGroups ' , $ result ->getData ()['errors ' ]);
3057+ }
3058+
29303059 public function testUpdateUserCannotCreateSubAdminOfAdminGroup (): void {
29313060 $ currentUser = $ this ->createMock (IUser::class);
29323061 $ currentUser ->method ('getUID ' )->willReturn ('admin ' );
0 commit comments