Pārlūkot izejas kodu

add unit tests for AbstractVoter

Olivier Massot 2 gadi atpakaļ
vecāks
revīzija
8b13e204a8

+ 10 - 10
src/Security/Voter/AbstractVoter.php

@@ -152,15 +152,6 @@ abstract class AbstractVoter extends Voter
      */
     abstract protected function canDelete(object $subject): bool;
 
-    /**
-     * Is the client an authenticated user ?
-     *
-     * @return bool
-     */
-    protected function isUserLoggedIn(): bool {
-        return $this->user !== null;
-    }
-
     /**
      * Returns the current logged in user
      * @return Access
@@ -169,6 +160,15 @@ abstract class AbstractVoter extends Voter
         return $this->user;
     }
 
+    /**
+     * Is the client an authenticated user ?
+     *
+     * @return bool
+     */
+    protected function isUserLoggedIn(): bool {
+        return $this->getUser() !== null;
+    }
+
     /**
      * Is the current request a valid internal request?
      *
@@ -178,7 +178,7 @@ abstract class AbstractVoter extends Voter
      * @return bool
      */
     protected function isValidInternalRequest(): bool {
-        $clientIp = $_SERVER['REMOTE_ADDR'];
+        $clientIp = $_SERVER['REMOTE_ADDR'] ?? null;
         $internalRequestToken = $_SERVER['HTTP_INTERNAL_REQUESTS_TOKEN'] ?? '';
 
         return $this->internalRequestsService->isAllowed($clientIp, $internalRequestToken);

+ 363 - 0
tests/Unit/Security/Voter/AbstractVoterTest.php

@@ -0,0 +1,363 @@
+<?php
+
+namespace App\Tests\Unit\Security\Voter;
+
+use App\Entity\Access\Access;
+use App\Entity\Person\Person;
+use App\Security\Voter\AbstractVoter;
+use App\Service\Access\Utils;
+use App\Service\Security\InternalRequestsService;
+use App\Service\Security\SwitchUser;
+use Doctrine\ORM\EntityManagerInterface;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Symfony\Bundle\SecurityBundle\Security;
+use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+
+class TestableAbstractVoter extends AbstractVoter {
+    public const READ = 'READ';
+    public const EDIT = 'EDIT';
+    public const CREATE = 'CREATE';
+    public const DELETE = 'DELETE';
+
+    public static function setEntityClass(?string $entityClass): void {
+        static::$entityClass = $entityClass;
+    }
+
+    public static function getEntityClass(): ?string {
+        return static::$entityClass;
+    }
+
+    /**
+     * @param array<string> $allowedOperations
+     * @return void
+     */
+    public static function setAllowedOperations(array $allowedOperations): void {
+        static::$allowedOperations = $allowedOperations;
+    }
+
+    public static function getAllowedOperations(): array {
+        return static::$allowedOperations;
+    }
+
+    public function supports(string $attribute, mixed $subject): bool {
+        return parent::supports($attribute, $subject);
+    }
+    public function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool {
+        return parent::voteOnAttribute($attribute, $subject, $token);
+    }
+
+    public function canView(object $subject): bool { return true; }
+
+    public function canEdit(object $subject): bool { return true; }
+
+    public function canCreate(object $subject): bool { return true; }
+
+    public function canDelete(object $subject): bool { return true; }
+
+    public function isUserLoggedIn(): bool { return parent::isUserLoggedIn(); }
+    public function getUser(): ?Access { return parent::getUser(); }
+    public function isValidInternalRequest(): bool { return parent::isValidInternalRequest(); }
+}
+
+
+class AbstractVoterTest extends TestCase
+{
+    protected Security | MockObject $security;
+    protected Utils | MockObject $accessUtils;
+    protected EntityManagerInterface | MockObject $entityManager;
+    protected InternalRequestsService | MockObject $internalRequestsService;
+    protected SwitchUser | MockObject $switchUserService;
+
+    private ?string $initialEntityClass = null;
+    private ?array $initialAllowedOperations = null;
+    private ?int $initialSwitchHeader = null;
+    private ?string $initialRemoteAddr = null;
+    private ?string $initialInternalRequestToken = null;
+
+    public function __construct(?string $name = null, array $data = [], $dataName = '')
+    {
+        parent::__construct($name, $data, $dataName);
+
+        $this->initialEntityClass = TestableAbstractVoter::getEntityClass();
+        $this->initialAllowedOperations = TestableAbstractVoter::getAllowedOperations();
+        $this->initialSwitchHeader = $_SERVER['HTTP_X_SWITCH_USER'] ?? null;
+        $this->initialRemoteAddr = $_SERVER['REMOTE_ADDR'] ?? null;
+        $this->initialInternalRequestToken = $_SERVER['HTTP_INTERNAL_REQUESTS_TOKEN'] ?? null;
+    }
+
+    private function reinit() {
+        // Reinitialize the TestableAbstractVoter static properties
+        TestableAbstractVoter::setEntityClass($this->initialEntityClass);
+        TestableAbstractVoter::setAllowedOperations($this->initialAllowedOperations);
+
+        // Reinitialize the global variables
+        if ($this->initialSwitchHeader !== null) {
+            $_SERVER['HTTP_X_SWITCH_USER'] = $this->initialSwitchHeader;
+        } else if (array_key_exists('HTTP_X_SWITCH_USER', $_SERVER)) {
+            unset($_SERVER['HTTP_X_SWITCH_USER']);
+        }
+
+        $_SERVER['REMOTE_ADDR'] = $this->initialRemoteAddr;
+
+        if ($this->initialSwitchHeader !== null) {
+            $_SERVER['HTTP_INTERNAL_REQUESTS_TOKEN'] = $this->initialSwitchHeader;
+        } else if (array_key_exists('HTTP_INTERNAL_REQUESTS_TOKEN', $_SERVER)) {
+            unset($_SERVER['HTTP_INTERNAL_REQUESTS_TOKEN']);
+        }
+    }
+
+    public function setUp(): void {
+        $this->security = $this->getMockBuilder(Security::class)->disableOriginalConstructor()->getMock();
+        $this->accessUtils = $this->getMockBuilder(Utils::class)->disableOriginalConstructor()->getMock();
+        $this->internalRequestsService = $this->getMockBuilder(InternalRequestsService::class)->disableOriginalConstructor()->getMock();
+        $this->entityManager = $this->getMockBuilder(EntityManagerInterface::class)->disableOriginalConstructor()->getMock();
+        $this->switchUserService = $this->getMockBuilder(SwitchUser::class)->disableOriginalConstructor()->getMock();
+    }
+
+    public function tearDown(): void
+    {
+        $this->reinit();
+    }
+
+    private function makeAbstractVoterMockFor(string $methodName): MockObject | TestableAbstractVoter {
+        return $this->getMockBuilder(TestableAbstractVoter::class)
+            ->setConstructorArgs([
+                $this->security,
+                $this->accessUtils,
+                $this->internalRequestsService,
+                $this->entityManager,
+                $this->switchUserService
+            ])
+            ->setMethodsExcept([$methodName])
+            ->getMock();
+    }
+
+    public function testSupports(): void {
+        TestableAbstractVoter::setEntityClass(Access::class);
+
+        $abstractVoter = $this->makeAbstractVoterMockFor('supports');
+
+        $access = new Access();
+
+        $this->assertTrue(
+            $abstractVoter->supports(TestableAbstractVoter::READ, $access)
+        );
+        $this->assertTrue(
+            $abstractVoter->supports(TestableAbstractVoter::EDIT, $access)
+        );
+        $this->assertTrue(
+            $abstractVoter->supports(TestableAbstractVoter::CREATE, $access)
+        );
+        $this->assertTrue(
+            $abstractVoter->supports(TestableAbstractVoter::DELETE, $access)
+        );
+    }
+
+    public function testSupportsNotSupportedClass(): void {
+        TestableAbstractVoter::setEntityClass(Access::class);
+
+        $abstractVoter = $this->makeAbstractVoterMockFor('supports');
+
+        $person = new Person();
+
+        $this->assertFalse(
+            $abstractVoter->supports(TestableAbstractVoter::READ, $person)
+        );
+    }
+
+    public function testSupportsNotAllowedOperation(): void {
+        TestableAbstractVoter::setEntityClass(Access::class);
+        TestableAbstractVoter::setAllowedOperations([TestableAbstractVoter::READ]);
+
+        $abstractVoter = $this->makeAbstractVoterMockFor('supports');
+
+        $access = new Access();
+
+        $this->assertTrue(
+            $abstractVoter->supports(TestableAbstractVoter::READ, $access)
+        );
+        $this->assertFalse(
+            $abstractVoter->supports(TestableAbstractVoter::EDIT, $access)
+        );
+        $this->assertFalse(
+            $abstractVoter->supports(TestableAbstractVoter::CREATE, $access)
+        );
+        $this->assertFalse(
+            $abstractVoter->supports(TestableAbstractVoter::DELETE, $access)
+        );
+    }
+
+    public function testSupportsMissingEntityClass(): void {
+        $abstractVoter = $this->makeAbstractVoterMockFor('supports');
+
+        $this->expectException(\RuntimeException::class);
+        $this->expectExceptionMessage('Setup the self::$entityClass property, or override the supports() method');
+
+        $access = new Access();
+        $abstractVoter->supports(TestableAbstractVoter::READ, $access);
+    }
+
+    public function testVoteOnAttributeRead(): void {
+        $abstractVoter = $this->makeAbstractVoterMockFor('voteOnAttribute');
+
+        $subject = $this->getMockBuilder(Access::class)->getMock();
+        $token = $this->getMockBuilder(TokenInterface::class)->getMock();
+
+        $abstractVoter->expects(self::once())->method('canView')->with($subject)->willReturn(true);
+        $abstractVoter->expects(self::never())->method('canEdit');
+        $abstractVoter->expects(self::never())->method('canCreate');
+        $abstractVoter->expects(self::never())->method('canDelete');
+
+        $this->assertTrue(
+            $abstractVoter->voteOnAttribute(TestableAbstractVoter::READ, $subject, $token)
+        );
+    }
+
+    public function testVoteOnAttributeEdit(): void {
+        $abstractVoter = $this->makeAbstractVoterMockFor('voteOnAttribute');
+
+        $subject = $this->getMockBuilder(Access::class)->getMock();
+        $token = $this->getMockBuilder(TokenInterface::class)->getMock();
+
+        $abstractVoter->expects(self::never())->method('canView');
+        $abstractVoter->expects(self::once())->method('canEdit')->with($subject)->willReturn(true);
+        $abstractVoter->expects(self::never())->method('canCreate');
+        $abstractVoter->expects(self::never())->method('canDelete');
+
+        $this->assertTrue(
+            $abstractVoter->voteOnAttribute(TestableAbstractVoter::EDIT, $subject, $token)
+        );
+    }
+
+    public function testVoteOnAttributeCreate(): void {
+        $abstractVoter = $this->makeAbstractVoterMockFor('voteOnAttribute');
+
+        $subject = $this->getMockBuilder(Access::class)->getMock();
+        $token = $this->getMockBuilder(TokenInterface::class)->getMock();
+
+        $abstractVoter->expects(self::never())->method('canView');
+        $abstractVoter->expects(self::never())->method('canEdit');
+        $abstractVoter->expects(self::once())->method('canCreate')->with($subject)->willReturn(true);
+        $abstractVoter->expects(self::never())->method('canDelete');
+
+        $this->assertTrue(
+            $abstractVoter->voteOnAttribute(TestableAbstractVoter::CREATE, $subject, $token)
+        );
+    }
+
+    public function testVoteOnAttributeDelete(): void {
+        $abstractVoter = $this->makeAbstractVoterMockFor('voteOnAttribute');
+
+        $subject = $this->getMockBuilder(Access::class)->getMock();
+        $token = $this->getMockBuilder(TokenInterface::class)->getMock();
+
+        $abstractVoter->expects(self::never())->method('canView');
+        $abstractVoter->expects(self::never())->method('canEdit');
+        $abstractVoter->expects(self::never())->method('canCreate');
+        $abstractVoter->expects(self::once())->method('canDelete')->with($subject)->willReturn(true);
+
+        $this->assertTrue(
+            $abstractVoter->voteOnAttribute(TestableAbstractVoter::DELETE, $subject, $token)
+        );
+    }
+
+    public function testVoteOnAttributeInvalidAttribute(): void {
+        $abstractVoter = $this->makeAbstractVoterMockFor('voteOnAttribute');
+
+        $subject = $this->getMockBuilder(Access::class)->getMock();
+        $token = $this->getMockBuilder(TokenInterface::class)->getMock();
+
+        $abstractVoter->expects(self::never())->method('canView');
+        $abstractVoter->expects(self::never())->method('canEdit');
+        $abstractVoter->expects(self::never())->method('canCreate');
+        $abstractVoter->expects(self::never())->method('canDelete');
+
+        $this->assertFalse(
+            $abstractVoter->voteOnAttribute('other_attr', $subject, $token)
+        );
+    }
+
+    public function testGetUser() {
+
+        $user = $this->getMockBuilder(Access::class)->getMock();
+
+        $this->security->expects(self::once())->method('getUser')->willReturn($user);
+
+        $abstractVoter = $this->makeAbstractVoterMockFor('getUser');
+
+        $this->assertEquals(
+            $user,
+            $abstractVoter->getUser()
+        );
+    }
+
+    public function testGetUserNoUser() {
+
+        $this->security->expects(self::once())->method('getUser')->willReturn(null);
+
+        $abstractVoter = $this->makeAbstractVoterMockFor('getUser');
+
+        $this->assertNull(
+            $abstractVoter->getUser()
+        );
+    }
+
+    public function testGetUserSwitchUser() {
+
+        $originalUser = $this->getMockBuilder(Access::class)->getMock();
+        $switchUser = $this->getMockBuilder(Access::class)->getMock();
+
+        $_SERVER['HTTP_X_SWITCH_USER'] = 1;
+
+        $this->security->expects(self::once())->method('getUser')->willReturn($originalUser);
+        $this->entityManager->expects(self::once())->method('find')->with(Access::class, 1)->willReturn($switchUser);
+
+        $abstractVoter = $this->makeAbstractVoterMockFor('getUser');
+
+        $this->assertEquals(
+            $switchUser,
+            $abstractVoter->getUser()
+        );
+    }
+
+    public function testIsUserLoggedIn(): void {
+        $user = $this->getMockBuilder(Access::class)->getMock();
+
+        $abstractVoter = $this->makeAbstractVoterMockFor('isUserLoggedIn');
+
+        $abstractVoter->expects(self::once())->method('getUser')->willReturn($user);
+
+        $this->assertTrue($abstractVoter->isUserLoggedIn());
+    }
+
+    public function testIsUserLoggedInNoUser(): void {
+        $abstractVoter = $this->makeAbstractVoterMockFor('isUserLoggedIn');
+
+        $abstractVoter->expects(self::once())->method('getUser')->willReturn(null);
+
+        $this->assertFalse($abstractVoter->isUserLoggedIn());
+    }
+
+    public function testIsValidInternalRequest(): void {
+
+        $clientIp = '0.0.0.0';
+        $token = 'valid_token';
+
+        $_SERVER['REMOTE_ADDR'] = $clientIp;
+        $_SERVER['HTTP_INTERNAL_REQUESTS_TOKEN'] = $token;
+
+        $this->internalRequestsService
+            ->expects(self::once())
+            ->method('isAllowed')
+            ->with($clientIp, $token)
+            ->willReturn(true);
+
+        $abstractVoter = $this->makeAbstractVoterMockFor('isValidInternalRequest');
+
+        $this->assertTrue($abstractVoter->isValidInternalRequest());
+    }
+
+
+
+}