awsService = $this->createMock(AwsService::class); $this->entityManager = $this->createMock(EntityManagerInterface::class); $this->uoRepository = $this->createMock(UsersOrganizationsRepository::class); $this->notificationService = $this->createMock(NotificationService::class); $this->emailNotificationLogger = $this->createMock(LoggerInterface::class); $this->loggerService = $this->createMock(LoggerService::class); // Set the ENV variable used in the service $_ENV['S3_PORTAL_BUCKET'] = 'test-bucket'; $this->service = new OrganizationsService( $this->logoDirectory, $this->awsService, $this->entityManager, $this->uoRepository, $this->notificationService, $this->emailNotificationLogger, $this->loggerService ); } /** * Helper to set private ID property via Reflection */ private function setEntityId(object $entity, int $id): void { $reflection = new \ReflectionClass($entity); if ($reflection->hasProperty('id')) { $property = $reflection->getProperty('id'); // $property->setAccessible(true); // PHP < 8.1 $property->setValue($entity, $id); } } // ========================================== // TEST: handleLogo // ========================================== public function testHandleLogoSuccess(): void { $org = new Organizations(); $this->setEntityId($org, 1); $org->setName('MyOrg'); $file = $this->createMock(UploadedFile::class); $file->method('guessExtension')->willReturn('png'); $this->service->handleLogo($org, $file); // Assert URL is set on entity $this->assertStringContainsString('uploads/organization_logos/MyOrg_', $org->getLogoUrl()); } public function testHandleLogoThrowsException(): void { // 1. Setup the Entity $org = new Organizations(); $this->setEntityId($org, 1); // Assuming you have a helper for reflection ID setting $org->setName('MyOrg'); // 2. Setup the File Mock $file = $this->createMock(UploadedFile::class); $file->method('guessExtension')->willReturn('png'); // --- CRITICAL PART --- // We tell the mock: "When move() is called, crash with an exception." // We use a generic Exception here because your try/catch block catches \Exception $file->method('move') ->willThrowException(new \Exception('Disk full or permission denied')); // 3. Expect the Logger Call $this->loggerService->expects($this->once()) ->method('logError') ->with( // This string MUST match the first argument in your actual code 'File upload failed', // We use a callback to validate the context array contains the right ID $this->callback(function($context) use ($org) { return $context['target_organization_id'] === $org->getId() && $context['message'] === 'Disk full or permission denied'; }) ); // 4. Expect the final exception re-thrown by your service $this->expectException(FileException::class); $this->expectExceptionMessage('File upload failed.'); // 5. Run the method $this->service->handleLogo($org, $file); } // ========================================== // TEST: appsAccess // ========================================== public function testAppsAccess(): void { $app1 = new Apps(); $this->setEntityId($app1, 10); $app2 = new Apps(); $this->setEntityId($app2, 20); $app3 = new Apps(); $this->setEntityId($app3, 30); $allApps = [$app1, $app2, $app3]; $orgApps = [$app2]; // Org only has access to App 2 $result = $this->service->appsAccess($allApps, $orgApps); $this->assertCount(3, $result); // App 1 -> False $this->assertSame($app1, $result[0]['entity']); $this->assertFalse($result[0]['hasAccess']); // App 2 -> True $this->assertSame($app2, $result[1]['entity']); $this->assertTrue($result[1]['hasAccess']); // App 3 -> False $this->assertSame($app3, $result[2]['entity']); $this->assertFalse($result[2]['hasAccess']); } // ========================================== // TEST: notifyOrganizationAdmins // ========================================== public function testNotifyOrganizationAdminsUserAccepted(): void { // 1. Setup Data $targetUser = new User(); $this->setEntityId($targetUser, 100); $adminUser = new User(); $this->setEntityId($adminUser, 999); $org = new Organizations(); $this->setEntityId($org, 50); $data = ['user' => $targetUser, 'organization' => $org]; // 2. Setup Admin Link (The user who IS admin) $adminUO = new UsersOrganizations(); $this->setEntityId($adminUO, 555); $adminUO->setUsers($adminUser); $adminUO->setOrganization($org); // 3. Setup Role Logic $adminRole = new Roles(); $this->setEntityId($adminRole, 1); $adminRole->setName('ADMIN'); // 4. Setup UOA Logic (Proof that user is Admin of an App) $uoa = new UserOrganizationApp(); $this->setEntityId($uoa, 777); $uoa->setUserOrganization($adminUO); $uoa->setRole($adminRole); $uoa->setIsActive(true); // 5. Mocks // Mock Roles Repo $rolesRepo = $this->createMock(EntityRepository::class); $rolesRepo->method('findOneBy')->with(['name' => 'ADMIN'])->willReturn($adminRole); // Mock UO Repo (Find potential admins in org) $this->uoRepository->expects($this->once()) ->method('findBy') ->with(['organization' => $org, 'isActive' => true]) ->willReturn([$adminUO]); // Mock UOA Repo (Check if they have ADMIN role) $uoaRepo = $this->createMock(EntityRepository::class); $uoaRepo->method('findOneBy')->willReturn($uoa); $this->entityManager->method('getRepository')->willReturnMap([ [Roles::class, $rolesRepo], [UserOrganizationApp::class, $uoaRepo], ]); // 6. Expectations $this->notificationService->expects($this->once()) ->method('notifyUserAcceptedInvite') ->with($adminUser, $targetUser, $org); $this->loggerService->expects($this->once()) ->method('logAdminNotified') ->with([ 'admin_user_id' => 999, 'target_user_id' => 100, 'organization_id' => 50, 'case' => 'USER_ACCEPTED' ]); // 7. Run $result = $this->service->notifyOrganizationAdmins($data, 'USER_ACCEPTED'); } /** * This test ensures that if the admin is the SAME person as the target user, * they do not get notified (Skip Self Check). */ public function testNotifyOrganizationAdminsSkipsSelf(): void { $user = new User(); $this->setEntityId($user, 100); $org = new Organizations(); $this->setEntityId($org, 50); // Admin IS the user $adminUO = new UsersOrganizations(); $adminUO->setUsers($user); $roleAdmin = new Roles(); $uoa = new UserOrganizationApp(); // active admin link // Mocks setup $rolesRepo = $this->createMock(EntityRepository::class); $rolesRepo->method('findOneBy')->willReturn($roleAdmin); $this->uoRepository->method('findBy')->willReturn([$adminUO]); $uoaRepo = $this->createMock(EntityRepository::class); $uoaRepo->method('findOneBy')->willReturn($uoa); $this->entityManager->method('getRepository')->willReturnMap([ [Roles::class, $rolesRepo], [UserOrganizationApp::class, $uoaRepo], ]); // Expectations: Notification service should NEVER be called $this->notificationService->expects($this->never())->method('notifyUserAcceptedInvite'); $this->loggerService->expects($this->never())->method('logAdminNotified'); $this->service->notifyOrganizationAdmins(['user' => $user, 'organization' => $org], 'USER_ACCEPTED'); } public function testNotifyOrganizationAdminsSkipsNonAdmins(): void { // 1. Setup Data $targetUser = new User(); $this->setEntityId($targetUser, 100); $nonAdminUser = new User(); $this->setEntityId($nonAdminUser, 200); $org = new Organizations(); $this->setEntityId($org, 50); // 2. Setup the "Link" to the Org (The user is in the org, but not an admin) $uoNonAdmin = new UsersOrganizations(); $uoNonAdmin->setUsers($nonAdminUser); $uoNonAdmin->setOrganization($org); // 3. Mock Repos $rolesRepo = $this->createMock(EntityRepository::class); // It doesn't matter what roles repo returns, the check fails later at UOA // The UO Repo finds the user as a member of the org $this->uoRepository->method('findBy')->willReturn([$uoNonAdmin]); // CRITICAL: The UOA Repo returns NULL (No Admin record found) $uoaRepo = $this->createMock(EntityRepository::class); $uoaRepo->method('findOneBy')->willReturn(null); $this->entityManager->method('getRepository')->willReturnMap([ [Roles::class, $rolesRepo], [UserOrganizationApp::class, $uoaRepo], ]); // 4. Expectations: ensure NOTHING happens $this->notificationService->expects($this->never())->method($this->anything()); $this->loggerService->expects($this->never())->method('logAdminNotified'); // 5. Run $this->service->notifyOrganizationAdmins( ['user' => $targetUser, 'organization' => $org], 'USER_ACCEPTED' ); } #[DataProvider('notificationCasesProvider')] public function testNotifyOrganizationAdminsHandlesAllCases(string $caseType, string $expectedMethod): void { // 1. Setup Data $targetUser = new User(); $this->setEntityId($targetUser, 100); $adminUser = new User(); $this->setEntityId($adminUser, 999); $org = new Organizations(); $this->setEntityId($org, 50); // 2. Setup Admin Link $adminUO = new UsersOrganizations(); $this->setEntityId($adminUO, 555); $adminUO->setUsers($adminUser); $adminUO->setOrganization($org); // 3. Setup Role & UOA $adminRole = new Roles(); $adminRole->setName('ADMIN'); $uoa = new UserOrganizationApp(); $uoa->setUserOrganization($adminUO); $uoa->setRole($adminRole); $uoa->setIsActive(true); // 4. Mocks $rolesRepo = $this->createMock(EntityRepository::class); $rolesRepo->method('findOneBy')->willReturn($adminRole); $this->uoRepository->method('findBy')->willReturn([$adminUO]); $uoaRepo = $this->createMock(EntityRepository::class); $uoaRepo->method('findOneBy')->willReturn($uoa); $this->entityManager->method('getRepository')->willReturnMap([ [Roles::class, $rolesRepo], [UserOrganizationApp::class, $uoaRepo], ]); // 5. Dynamic Expectations // We expect the *variable* method name passed by the provider $this->notificationService->expects($this->once()) ->method($expectedMethod) ->with($adminUser, $targetUser, $org); // We expect the logger to receive the specific $caseType $this->loggerService->expects($this->once()) ->method('logAdminNotified') ->with([ 'admin_user_id' => 999, 'target_user_id' => 100, 'organization_id' => 50, 'case' => $caseType // <--- Verified here ]); // 6. Run $this->service->notifyOrganizationAdmins( ['user' => $targetUser, 'organization' => $org], $caseType ); } /** * Provides the data for the test above. * Format: [ 'Case String', 'Expected Service Method Name' ] */ public static function notificationCasesProvider(): array { return [ 'Invited Case' => ['USER_INVITED', 'notifyUserInvited'], 'Deactivated Case' => ['USER_DEACTIVATED', 'notifyUserDeactivated'], 'Deleted Case' => ['USER_DELETED', 'notifyUserDeleted'], 'Activated Case' => ['USER_ACTIVATED', 'notifyUserActivated'], ]; } }