allows('adminsAreStudioAdmins')->andReturn($studio); $access->allows('adminsAreInstructors')->andReturn($instructor); return new RoleManager($access); } public function testRegisterAddsInitHookAndCapFilter(): void { Functions\expect('add_action') ->once() ->with('init', \Mockery::any()); Functions\expect('add_filter') ->once() ->with('user_has_cap', \Mockery::any(), 10, 1); (new RoleManager())->register(); } public function testGrantsStudioCapsToAdministrators(): void { $result = $this->roleManager()->grantStudioCapsToAdministrators(['manage_options' => true]); foreach (RoleManager::STUDIO_ADMIN_CAPS as $cap) { self::assertTrue($result[$cap], "administrator should be granted {$cap}"); } } public function testGrantsInstructorCapsToAdministrators(): void { $result = $this->roleManager()->grantStudioCapsToAdministrators(['manage_options' => true]); // A single-instructor studio owner runs as an administrator and also // teaches, so they get the instructor caps — notably manage_availability, // which is not part of the studio-admin set. self::assertTrue($result[RoleManager::CAP_MANAGE_AVAILABILITY], 'administrator should be able to manage availability'); foreach (RoleManager::INSTRUCTOR_CAPS as $cap) { self::assertTrue($result[$cap], "administrator should be granted {$cap}"); } } public function testStudioGrantDisabledWithholdsStudioCapsFromAdministrators(): void { $result = $this->roleManager(studio: false)->grantStudioCapsToAdministrators(['manage_options' => true]); // manage_instructors is studio-only, so it disappears when the grant is off. self::assertArrayNotHasKey(RoleManager::CAP_MANAGE_INSTRUCTORS, $result); // Instructor caps remain because that grant is still on. self::assertTrue($result[RoleManager::CAP_MANAGE_AVAILABILITY]); } public function testInstructorGrantDisabledWithholdsInstructorCapsFromAdministrators(): void { $result = $this->roleManager(instructor: false)->grantStudioCapsToAdministrators(['manage_options' => true]); // manage_availability is instructor-only, so it disappears when the grant is off. self::assertArrayNotHasKey(RoleManager::CAP_MANAGE_AVAILABILITY, $result); // Studio caps remain because that grant is still on. self::assertTrue($result[RoleManager::CAP_MANAGE_INSTRUCTORS]); } public function testDoesNotGrantCapsToNonAdministrators(): void { $result = $this->roleManager()->grantStudioCapsToAdministrators(['read' => true]); self::assertArrayNotHasKey(RoleManager::CAP_MANAGE_OFFERINGS, $result); self::assertSame(['read' => true], $result); } public function testCreateRolesSkipsExistingRoles(): void { Functions\when('get_role')->alias(static fn() => new \stdClass()); Functions\expect('add_role')->never(); (new RoleManager())->createRoles(); } public function testCreateRolesAddsInstructorRoleWithCorrectCaps(): void { Functions\when('get_role')->alias(static function (string $role): ?object { return $role === RoleManager::INSTRUCTOR ? null : new \stdClass(); }); Functions\expect('add_role') ->once() ->with( RoleManager::INSTRUCTOR, \Mockery::any(), \Mockery::on(static function (array $caps): bool { return ($caps['read'] ?? false) === true && ($caps[RoleManager::CAP_MANAGE_AVAILABILITY] ?? false) === true && ($caps[RoleManager::CAP_VIEW_LESSONS] ?? false) === true; }) ); (new RoleManager())->createRoles(); } public function testCreateRolesAddsStudioAdminRoleWithCorrectCaps(): void { Functions\when('get_role')->alias(static function (string $role): ?object { return $role === RoleManager::STUDIO_ADMIN ? null : new \stdClass(); }); Functions\expect('add_role') ->once() ->with( RoleManager::STUDIO_ADMIN, \Mockery::any(), \Mockery::on(static function (array $caps): bool { return ($caps['read'] ?? false) === true && ($caps[RoleManager::CAP_MANAGE_INSTRUCTORS] ?? false) === true && ($caps[RoleManager::CAP_MANAGE_OFFERINGS] ?? false) === true && ($caps[RoleManager::CAP_MANAGE_POLICIES] ?? false) === true && ($caps[RoleManager::CAP_MANAGE_BILLING] ?? false) === true && ($caps[RoleManager::CAP_VIEW_ALL_PAYMENTS] ?? false) === true; }) ); (new RoleManager())->createRoles(); } public function testCreateRolesAddsStudentRoleWithCorrectCaps(): void { Functions\when('get_role')->alias(static function (string $role): ?object { return $role === RoleManager::STUDENT ? null : new \stdClass(); }); Functions\expect('add_role') ->once() ->with( RoleManager::STUDENT, \Mockery::any(), \Mockery::on(static function (array $caps): bool { return ($caps['read'] ?? false) === true && ($caps[RoleManager::CAP_BOOK_LESSON] ?? false) === true && ($caps[RoleManager::CAP_VIEW_LESSONS] ?? false) === true; }) ); (new RoleManager())->createRoles(); } /** * The predicate every staff-side "register this person" path shares. It is * deliberately the role and not `book_lesson`, so the two accounts that have * that capability withheld — a guardian's child and an unapproved signup — are * still people the studio can act for. */ public function testIsStudentAcceptsAnyHolderOfTheStudentRole(): void { Functions\when('get_userdata')->justReturn($this->userWithRoles([RoleManager::STUDENT])); self::assertTrue(RoleManager::isStudent(5)); } public function testIsStudentRejectsSomeoneWhoIsNotAStudent(): void { Functions\when('get_userdata')->justReturn($this->userWithRoles([RoleManager::INSTRUCTOR])); self::assertFalse(RoleManager::isStudent(5)); } public function testIsStudentRejectsAnAccountThatNoLongerExists(): void { Functions\when('get_userdata')->justReturn(false); self::assertFalse(RoleManager::isStudent(5)); } public function testIsStudentRejectsNoOneChosenWithoutLookingAnyoneUp(): void { Functions\expect('get_userdata')->never(); self::assertFalse(RoleManager::isStudent(0)); } /** @param list $roles */ private function userWithRoles(array $roles): \WP_User { $user = \Mockery::mock(\WP_User::class); $user->ID = 5; $user->roles = $roles; return $user; } }