Easy_solution/tests/Service/OrganizationsServiceTest.php

385 lines
14 KiB
PHP

<?php
namespace App\Tests\Service;
use App\Entity\Apps;
use App\Entity\Organizations;
use App\Entity\Roles;
use App\Entity\User;
use App\Entity\UserOrganizationApp;
use App\Entity\UsersOrganizations;
use App\Repository\UsersOrganizationsRepository;
use App\Service\AwsService;
use App\Service\LoggerService;
use App\Service\NotificationService;
use App\Service\OrganizationsService;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class OrganizationsServiceTest extends TestCase
{
private OrganizationsService $service;
// Mocks
private MockObject|AwsService $awsService;
private MockObject|EntityManagerInterface $entityManager;
private MockObject|UsersOrganizationsRepository $uoRepository;
private MockObject|NotificationService $notificationService;
private MockObject|LoggerInterface $emailNotificationLogger;
private MockObject|LoggerService $loggerService;
private string $logoDirectory = '/tmp/logos';
protected function setUp(): void
{
$this->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'],
];
}
}