369 lines
13 KiB
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);
|
|
}
|
|
} |