'KO-Expert:in'], Role::nominatedFrom($roles), ); } public function testCustomerExpertCanBeApprovedForStaff(): void { $roles = Role::approve([Role::EMPLOYEE, Role::pending(Role::CUSTOMER_EXPERT)], Role::CUSTOMER_EXPERT); self::assertSame([Role::EMPLOYEE, Role::CUSTOMER_EXPERT], $roles); } public function testApprovedRoleSurvivesTheNextSyncAndIsNotMarkedAgain(): void { $roles = Role::sync([Role::TEAMER, Role::HOUSE_MANAGER], [Role::HOUSE_MANAGER, Role::TEAMER]); self::assertSame([Role::TEAMER, Role::HOUSE_MANAGER], $roles); } public function testRoleTheCrmNoLongerClaimsIsRevoked(): void { // Both halves go: BusPro is the source of truth for the granted role as much as for // the nomination. $roles = Role::sync([Role::TEAMER, Role::GROUPS_ADMIN, Role::pending(Role::HOUSE_MANAGER)], [Role::TEAMER]); self::assertSame([Role::TEAMER], $roles); } public function testRevokedRoleIsNotImmediatelyNominatedAgain(): void { self::assertSame([Role::CUSTOMER], Role::sync([Role::ADMIN], [])); } public function testAccountWithoutAnEffectiveRoleFallsBackToCustomer(): void { // The nomination stays visible — it is what an approver acts on — but grants nothing, // so the account is a customer in the meantime. self::assertSame( [Role::pending(Role::HOUSE_MANAGER), Role::CUSTOMER], Role::sync([], [Role::HOUSE_MANAGER]), ); self::assertSame([Role::CUSTOMER], Role::sync([], [])); } public function testCustomerIsAFallbackAndNotABaseline(): void { self::assertSame([Role::TEAMER], Role::sync([Role::CUSTOMER], [Role::TEAMER])); } public function testUnknownClaimsAndTheImplicitRoleUserAreIgnored(): void { self::assertSame( [Role::TEAMER], Role::sync([Role::USER, Role::TEAMER], [Role::TEAMER, 'ROLE_SOMETHING_ELSE']), ); } public function testApprovalTurnsTheNominationIntoTheRole(): void { $roles = Role::approve([Role::pending(Role::HOUSE_MANAGER), Role::CUSTOMER], Role::HOUSE_MANAGER); // The customer fallback goes with it: the account now holds an effective role. self::assertSame([Role::HOUSE_MANAGER], $roles); } public function testApprovingARoleWithoutANominationIsRefused(): void { $this->expectException(\InvalidArgumentException::class); Role::approve([Role::TEAMER], Role::ADMIN); } public function testEffectiveRolesExcludeNominationsAndTheImplicitRoleUser(): void { $roles = [Role::USER, Role::TEAMER, Role::pending(Role::HOUSE_MANAGER)]; self::assertSame([Role::TEAMER], Role::effectiveOnly($roles)); self::assertSame([Role::pending(Role::HOUSE_MANAGER)], Role::pendingOnly($roles)); self::assertSame([Role::HOUSE_MANAGER => 'Hausleitung'], Role::nominatedFrom($roles)); } public function testEmployeeIsGrantedOutrightAndDisplacesTheCustomerFallback(): void { // The claim does not come from the CRM, but it travels the same path as one. self::assertSame([Role::EMPLOYEE], Role::sync([], [Role::EMPLOYEE])); } public function testEmployeeIsRevokedOnceItIsNoLongerClaimed(): void { // Somebody whose address left the staff domain: no claim, so the role goes, and with no // effective role left the fallback returns. self::assertSame([Role::CUSTOMER], Role::sync([Role::EMPLOYEE], [])); } public function testEmployeeDoesNotShortCircuitTheNominationOfAnAdministrativeRole(): void { self::assertSame( [Role::EMPLOYEE, Role::pending(Role::ADMIN)], Role::sync([], [Role::EMPLOYEE, Role::ADMIN]), ); } /** * @return iterable */ public static function employeeOnlyRoles(): iterable { foreach (Role::EMPLOYEE_ONLY as $role) { yield $role => [$role]; } } #[DataProvider('employeeOnlyRoles')] public function testEmployeeOnlyClaimWithoutEmployeeIsNotNominated(string $role): void { self::assertSame([Role::TEAMER], Role::sync([], [Role::TEAMER, $role])); } #[DataProvider('employeeOnlyRoles')] public function testEmployeeOnlyRoleIsRevokedWithoutEmployee(string $role): void { // The CRM still claims it, but the account is no longer staff: the claim counts as not // made, and a role approved back when it was staff goes with it. self::assertSame( [Role::TEAMER], Role::sync([Role::TEAMER, Role::EMPLOYEE, $role], [Role::TEAMER, $role]), ); } #[DataProvider('employeeOnlyRoles')] public function testEmployeeOnlyRoleSurvivesTheNextSyncForStaff(string $role): void { self::assertSame( [Role::EMPLOYEE, $role], Role::sync([Role::EMPLOYEE, $role], [Role::EMPLOYEE, $role]), ); } public function testHouseManagerDoesNotNeedEmployee(): void { // A Hausleitung signs in with the hotel's own address. self::assertSame([Role::pending(Role::HOUSE_MANAGER), Role::CUSTOMER], Role::sync([], [Role::HOUSE_MANAGER])); self::assertSame([Role::HOUSE_MANAGER], Role::sync([Role::HOUSE_MANAGER], [Role::HOUSE_MANAGER])); } public function testStaleEmployeeOnlyNominationWithoutEmployeeCannotBeApproved(): void { // A marker from before the rule, still stored until the account's next login. $roles = [Role::TEAMER, Role::pending(Role::ADMIN)]; self::assertSame([], Role::nominatedFrom($roles)); $this->expectException(\InvalidArgumentException::class); Role::approve($roles, Role::ADMIN); } public function testEmployeeAndTeamerCoexist(): void { self::assertSame( [Role::TEAMER, Role::EMPLOYEE], Role::sync([], [Role::TEAMER, Role::EMPLOYEE]), ); } public function testEveryRoleAndNominationHasALabel(): void { $labels = Role::labels(); foreach (Role::ALL as $role) { self::assertArrayHasKey($role, $labels); } foreach (Role::ADMINISTRATIVE as $role) { self::assertArrayHasKey(Role::pending($role), $labels); } } }