Easy_solution/tests/Service/UserServiceTest.php

369 lines
13 KiB
PHP

<?php
namespace App\Tests\Service;
use App\Entity\Organizations;
use App\Entity\Roles;
use App\Entity\User;
use App\Entity\UserOrganizatonApp;
use App\Entity\UsersOrganizations;
use App\Service\ActionService;
use App\Service\AwsService;
use App\Service\EmailService;
use App\Service\LoggerService;
use App\Service\OrganizationsService;
use App\Service\UserService;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityNotFoundException;
use Doctrine\ORM\EntityRepository;
use League\Bundle\OAuth2ServerBundle\Model\AccessToken;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class UserServiceTest extends TestCase
{
private UserService $userService;
// Mocks
private MockObject|EntityManagerInterface $entityManager;
private MockObject|Security $security;
private MockObject|AwsService $awsService;
private MockObject|LoggerService $loggerService;
private MockObject|ActionService $actionService;
private MockObject|EmailService $emailService;
private MockObject|OrganizationsService $organizationsService;
protected function setUp(): void
{
$this->entityManager = $this->createMock(EntityManagerInterface::class);
$this->security = $this->createMock(Security::class);
$this->awsService = $this->createMock(AwsService::class);
$this->actionService = $this->createMock(ActionService::class);
$this->emailService = $this->createMock(EmailService::class);
$this->organizationsService = $this->createMock(OrganizationsService::class);
// HANDLING READONLY LOGGER SERVICE
// PHPUnit 10+ generally handles readonly classes fine.
// If your LoggerService is 'final readonly', you cannot mock it easily.
// Assuming it is just 'readonly class LoggerService':
$this->loggerService = $this->createMock(LoggerService::class);
$this->userService = new UserService(
$this->entityManager,
$this->security,
$this->awsService,
$this->loggerService,
$this->actionService,
$this->emailService,
$this->organizationsService
);
}
public function testGenerateRandomPassword(): void
{
$password = $this->userService->generateRandomPassword();
$this->assertEquals(50, strlen($password));
$this->assertMatchesRegularExpression('/[a-zA-Z0-9!@#$%^&*()_+]+/', $password);
}
public function testIsUserConnectedReturnsTrueIfTokenValid(): void
{
$userIdentifier = 'test@example.com';
// Mock the Repository for AccessToken
$repo = $this->createMock(EntityRepository::class);
// Mock a token that expires in the future
$token = $this->createMock(AccessToken::class);
$token->method('getExpiry')->willReturn(new \DateTimeImmutable('+1 hour'));
$repo->expects($this->once())
->method('findBy')
->with(['userIdentifier' => $userIdentifier, 'revoked' => false])
->willReturn([$token]);
$this->entityManager->expects($this->once())
->method('getRepository')
->with(AccessToken::class)
->willReturn($repo);
$result = $this->userService->isUserConnected($userIdentifier);
$this->assertTrue($result);
}
public function testIsUserConnectedReturnsFalseIfTokenExpired(): void
{
$userIdentifier = 'test@example.com';
$repo = $this->createMock(EntityRepository::class);
$token = $this->createMock(AccessToken::class);
$token->method('getExpiry')->willReturn(new \DateTimeImmutable('-1 hour'));
$repo->method('findBy')->willReturn([$token]);
$this->entityManager->method('getRepository')->willReturn($repo);
$result = $this->userService->isUserConnected($userIdentifier);
$this->assertFalse($result);
}
public function testGetUserByIdentifierFound(): void
{
$identifier = 'user@test.com';
$user = new User();
$user->setEmail($identifier);
$repo = $this->createMock(EntityRepository::class);
$repo->expects($this->once())
->method('findOneBy')
->with(['email' => $identifier])
->willReturn($user);
$this->entityManager->method('getRepository')->with(User::class)->willReturn($repo);
$result = $this->userService->getUserByIdentifier($identifier);
$this->assertSame($user, $result);
}
public function testGetUserByIdentifierNotFound(): void
{
$identifier = 'unknown@test.com';
$repo = $this->createMock(EntityRepository::class);
$repo->method('findOneBy')->willReturn(null);
$this->entityManager->method('getRepository')->with(User::class)->willReturn($repo);
// Expect Logger to be called
$this->loggerService->expects($this->once())
->method('logEntityNotFound')
->with('User', ['user_identifier' => $identifier], null);
$this->expectException(EntityNotFoundException::class);
$this->expectExceptionMessage(UserService::NOT_FOUND);
$this->userService->getUserByIdentifier($identifier);
}
public function testHasAccessToReturnsTrueForSuperAdmin(): void
{
$this->security->method('isGranted')->with('ROLE_SUPER_ADMIN')->willReturn(true);
$user = new User(); // Dummy user
$this->assertTrue($this->userService->hasAccessTo($user));
}
public function testHasAccessToReturnsTrueForSelf(): void
{
$this->security->method('isGranted')->willReturn(false);
$currentUser = new User();
$currentUser->setEmail('me@test.com');
$targetUser = new User();
$targetUser->setEmail('me@test.com');
$this->security->method('getUser')->willReturn($currentUser);
// skipSelfCheck = false (default)
$this->assertTrue($this->userService->hasAccessTo($targetUser));
}
public function testHandleProfilePictureUploadsAndLogs(): void
{
$user = new User();
$user->setName('John');
$user->setSurname('Doe');
// Mock UploadedFile
$file = $this->createMock(UploadedFile::class);
$file->method('guessExtension')->willReturn('jpg');
// Expect AWS Call
$this->awsService->expects($this->once())
->method('PutDocObj')
->with(
$this->anything(), // ENV variable usually
$file,
$this->stringContains('JohnDoe_'),
'jpg',
'profile/'
);
// Expect Logger Call
$this->loggerService->expects($this->once())
->method('logAWSAction');
// Set fake ENV for test context if needed, or ignore the argument in mock
$_ENV['S3_PORTAL_BUCKET'] = 'test-bucket';
$this->userService->handleProfilePicture($user, $file);
$this->assertStringContainsString('profile/JohnDoe_', $user->getPictureUrl());
}
public function testSyncUserRolesAddsRole(): void
{
$user = new User();
$user->setRoles(['ROLE_USER']);
$this->loggerService->expects($this->once())->method('logRoleAssignment');
$this->userService->syncUserRoles($user, 'ADMIN', true);
$this->assertContains('ROLE_ADMIN', $user->getRoles());
}
public function testSyncUserRolesRemovesRole(): void
{
$user = new User();
$user->setRoles(['ROLE_USER', 'ROLE_ADMIN']);
// Mock repositories to ensure no other org gives this role
$repoUO = $this->createMock(EntityRepository::class);
$repoUO->method('findBy')->willReturn([]); // No active org links
$this->entityManager->method('getRepository')
->willReturnMap([
[UsersOrganizations::class, $repoUO]
]);
$this->userService->syncUserRoles($user, 'ADMIN', false);
$this->assertNotContains('ROLE_ADMIN', $user->getRoles());
}
public function testIsPasswordStrong(): void
{
$this->assertTrue($this->userService->isPasswordStrong('StrongP@ss1')); // Chars + Digits + Special + Length
$this->assertFalse($this->userService->isPasswordStrong('weak')); // Too short
$this->assertFalse($this->userService->isPasswordStrong('123456789')); // No letters
}
public function testCreateNewUserSuccess(): void
{
$newUser = new User();
$newUser->setName('jane');
$newUser->setSurname('doe');
$newUser->setEmail('jane@doe.com');
$actingUser = new User();
$this->setEntityId($actingUser, 99); // Give acting user an ID
$actingUser->setEmail('admin@test.com');
// When persist is called, we force an ID onto $newUser to simulate DB insertion
$this->entityManager->expects($this->exactly(2))
->method('persist')
->with($newUser)
->willReturnCallback(function ($entity) {
$this->setEntityId($entity, 123); // Simulate DB assigning ID 123
});
$this->entityManager->expects($this->exactly(2))->method('flush');
// Now expects ID 123
$this->loggerService->expects($this->once())
->method('logUserCreated')
->with(123, 99);
$this->emailService->expects($this->once())->method('sendPasswordSetupEmail');
$this->actionService->expects($this->once())->method('createAction');
$this->userService->createNewUser($newUser, $actingUser, null);
// Assertions
$this->assertEquals('Jane', $newUser->getName());
$this->assertEquals(123, $newUser->getId()); // Verify ID was "generated"
}
public function testLinkUserToOrganization(): void
{
$user = new User();
$this->setEntityId($user, 10); // Pre-set ID for existing user
$org = new Organizations();
$this->setEntityId($org, 50); // Pre-set ID for org
$actingUser = new User();
$this->setEntityId($actingUser, 99);
// Capture the UsersOrganizations entity when it is persisted to give it an ID
$this->entityManager->expects($this->exactly(2))
->method('persist')
->willReturnCallback(function ($entity) use ($user) {
if ($entity instanceof UsersOrganizations) {
// This is the UO entity link (Call 1)
$this->setEntityId($entity, 555);
} elseif ($entity instanceof User && $entity === $user) {
// This is the User entity inside generatePasswordToken (Call 2)
// The ID is already set, so we do nothing here.
}
});
$this->entityManager->expects($this->exactly(2))->method('flush');
// Now the logger will receive valid Integers instead of null
$this->loggerService->expects($this->once())
->method('logUserOrganizationLinkCreated')
->with(10, 50, 99, 555);
$this->emailService->expects($this->once())->method('sendPasswordSetupEmail');
$this->organizationsService->expects($this->once())->method('notifyOrganizationAdmins');
$result = $this->userService->linkUserToOrganization($user, $org, $actingUser);
$this->assertInstanceOf(UsersOrganizations::class, $result);
$this->assertEquals(555, $result->getId());
}
public function testIsAdminOfOrganizationReturnsTrue(): void
{
$org = new Organizations();
$currentUser = new User();
$currentUser->setEmail('admin@test.com');
// Mock Security User
$this->security->method('getUser')->willReturn($currentUser);
$this->security->method('isGranted')->with('ROLE_ADMIN')->willReturn(true);
// 1. getUserByIdentifier (internal call) mocks
$userRepo = $this->createMock(EntityRepository::class);
$userRepo->method('findOneBy')->with(['email' => 'admin@test.com'])->willReturn($currentUser);
// 2. UsersOrganizations mock
$uoRepo = $this->createMock(EntityRepository::class);
$uo = new UsersOrganizations();
$uoRepo->method('findOneBy')->willReturn($uo);
// 3. Roles mock
$rolesRepo = $this->createMock(EntityRepository::class);
$adminRole = new Roles();
$adminRole->setName('ADMIN');
$rolesRepo->method('findOneBy')->with(['name' => 'ADMIN'])->willReturn($adminRole);
// 4. UserOrganizatonApp mock (The link checking if they are admin active)
$uoaRepo = $this->createMock(EntityRepository::class);
$uoa = new UserOrganizatonApp();
$uoaRepo->method('findOneBy')->willReturn($uoa); // Returns an object, so true
// Configure EntityManager to return these repos based on class
$this->entityManager->method('getRepository')->willReturnMap([
[User::class, $userRepo],
[UsersOrganizations::class, $uoRepo],
[Roles::class, $rolesRepo],
[UserOrganizatonApp::class, $uoaRepo],
]);
$result = $this->userService->isAdminOfOrganization($org);
$this->assertTrue($result);
}
private function setEntityId(object $entity, int $id): void
{
$reflection = new \ReflectionClass($entity);
$property = $reflection->getProperty('id');
// $property->setAccessible(true); // Required for PHP < 8.1
$property->setValue($entity, $id);
}
}