diff --git a/services.yaml b/services.yaml index 1354ed9a..1944a3ea 100644 --- a/services.yaml +++ b/services.yaml @@ -63,9 +63,6 @@ services: OxidEsales\GraphQL\Base\Infrastructure\Token: class: OxidEsales\GraphQL\Base\Infrastructure\Token - OxidEsales\GraphQL\Base\Infrastructure\RefreshTokenRepository: - class: OxidEsales\GraphQL\Base\Infrastructure\RefreshTokenRepository - OxidEsales\GraphQL\Base\Infrastructure\Repository: class: OxidEsales\GraphQL\Base\Infrastructure\Repository diff --git a/src/Event/Subscriber/PasswordChangeSubscriber.php b/src/Event/Subscriber/PasswordChangeSubscriber.php index 444c49ba..3425c722 100644 --- a/src/Event/Subscriber/PasswordChangeSubscriber.php +++ b/src/Event/Subscriber/PasswordChangeSubscriber.php @@ -12,7 +12,7 @@ use OxidEsales\Eshop\Application\Model\User; use OxidEsales\EshopCommunity\Internal\Transition\ShopEvents\AfterModelUpdateEvent; use OxidEsales\EshopCommunity\Internal\Transition\ShopEvents\BeforeModelUpdateEvent; -use OxidEsales\GraphQL\Base\Infrastructure\RefreshTokenRepository; +use OxidEsales\GraphQL\Base\Infrastructure\RefreshTokenRepositoryInterface; use OxidEsales\GraphQL\Base\Infrastructure\Token; use OxidEsales\GraphQL\Base\Service\UserModelService; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -23,13 +23,13 @@ class PasswordChangeSubscriber implements EventSubscriberInterface /** * Whether the password had been changed. * - * @var string|null + * @var array */ - protected ?string $userIdWithChangedPwd = null; + protected array $userIdWithChangedPwd = []; public function __construct( private readonly UserModelService $userModelService, - private readonly RefreshTokenRepository $refreshTokenRepository, + private readonly RefreshTokenRepositoryInterface $refreshTokenRepository, private readonly Token $tokenInfrastructure ) { } @@ -44,7 +44,7 @@ public function handleBeforeUpdate(Event $event): Event /** @phpstan-ignore-next-line method.notFound */ $model = $event->getModel(); - if (!$model instanceof User || !$model->getId()) { + if (!$model instanceof User) { return $event; } @@ -53,7 +53,7 @@ public function handleBeforeUpdate(Event $event): Event return $event; } - $this->userIdWithChangedPwd = $model->getId(); + $this->userIdWithChangedPwd[$model->getId()] = true; return $event; } @@ -68,16 +68,13 @@ public function handleAfterUpdate(Event $event): Event /** @phpstan-ignore-next-line method.notFound */ $model = $event->getModel(); - if (!$model instanceof User) { - return $event; - } - - if ($model->getId() !== $this->userIdWithChangedPwd || !$this->userIdWithChangedPwd) { + if (!$model instanceof User || !isset($this->userIdWithChangedPwd[$model->getId()])) { return $event; } $this->refreshTokenRepository->invalidateUserTokens($model->getId()); $this->tokenInfrastructure->invalidateUserTokens($model->getId()); + unset($this->userIdWithChangedPwd[$model->getId()]); return $event; } diff --git a/tests/Integration/Event/PasswordChangeTest.php b/tests/Integration/Event/PasswordChangeTest.php deleted file mode 100644 index a4703399..00000000 --- a/tests/Integration/Event/PasswordChangeTest.php +++ /dev/null @@ -1,124 +0,0 @@ -create(); - $container->compile(); - $this->tokenInfrastructure = $container->get(TokenInfrastructure::class); - $this->connection = $container->get(ConnectionProviderInterface::class)->get(); - } - - #[RunInSeparateProcess] - public function testExpireTokenAfterUserPasswordChange(): void - { - $userModel = $this->getUserModel(); - $tokenModel = $this->getTokenModel(); - - $expiresAtBeforeChange = new DateTimeImmutable($tokenModel->getRawFieldData('expires_at')); - $user = new UserDataType($userModel); - - $this->assertTrue($this->tokenInfrastructure->userHasToken($user, '_changePwdUserToken')); - $this->assertFalse($expiresAtBeforeChange <= new DateTimeImmutable('now')); - - $userModel->setPassword('_newPassword'); - $userModel->save(); - - $result = $this->connection->executeQuery( - "select expires_at from `oegraphqltoken` where oxid=:tokenId", - ['tokenId' => '_changePwdUserToken'] - ); - $expiresAtAfterChange = $result->fetchOne(); - - $this->assertTrue(new DateTimeImmutable($expiresAtAfterChange) <= new DateTimeImmutable('now')); - } - - #[RunInSeparateProcess] - public function testKeepTokenAfterUserChangeEventAndNoPwdChange(): void - { - $userModel = $this->getUserModel(); - $tokenModel = $this->getTokenModel(); - - $expiresAtBeforeChange = new DateTimeImmutable($tokenModel->getRawFieldData('expires_at')); - $user = new UserDataType($userModel); - - $this->assertTrue($this->tokenInfrastructure->userHasToken($user, '_changePwdUserToken')); - $this->assertFalse($expiresAtBeforeChange <= new DateTimeImmutable('now')); - - $userModel->assign(['oxfname' => 'Test']); - $userModel->save(); - - $result = $this->connection->executeQuery( - "select expires_at from `oegraphqltoken` where oxid=:tokenId", - ['tokenId' => '_changePwdUserToken'] - ); - $expiresAtAfterChange = $result->fetchOne(); - - $this->assertFalse(new DateTimeImmutable($expiresAtAfterChange) <= new DateTimeImmutable('now')); - } - - private function getUserModel(): User - { - $userModel = oxNew(User::class); - $userModel->setId('_testUser'); - $userModel->setPassword('_testPassword'); - $userModel->assign(['oxusername' => '_testUsername']); - $userModel->save(); - - return $userModel; - } - - private function getTokenModel(): TokenModel - { - $issued = new DateTimeImmutable('now'); - $expires = new DateTimeImmutable('+8 hours'); - $tokenModel = oxNew(TokenModel::class); - $tokenModel->setId('_changePwdUserToken'); - $tokenModel->assign( - [ - 'OXID' => '_changePwdUserToken', - 'OXSHOPID' => '1', - 'OXUSERID' => '_testUser', - 'ISSUED_AT' => $issued->format('Y-m-d H:i:s'), - 'EXPIRES_AT' => $expires->format('Y-m-d H:i:s'), - 'USERAGENT' => '', - 'TOKEN' => 'very_large_string', - ] - ); - $tokenModel->save(); - $tokenModel->load('_changePwdUserToken'); - - return $tokenModel; - } -} diff --git a/tests/Integration/Event/UserDeleteTest.php b/tests/Integration/Event/UserDeleteTest.php deleted file mode 100644 index db522dbc..00000000 --- a/tests/Integration/Event/UserDeleteTest.php +++ /dev/null @@ -1,81 +0,0 @@ -create(); - $container->compile(); - $this->tokenInfrastructure = $container->get(TokenInfrastructure::class); - } - - public function testInvalidateTokenAfterDeleteUser(): void - { - $userModel = oxNew(User::class); - $userModel->setId('_testUser'); - $userModel->setPassword('_testPassword'); - $userModel->assign(['oxusername' => '_testUsername']); - $userModel->save(); - - $this->tokenInfrastructure->registerToken( - $this->getTokenMock('_deletedUser'), - new DateTimeImmutable('now'), - new DateTimeImmutable('+8 hours') - ); - - $user = new UserDataType($userModel); - $this->assertTrue($this->tokenInfrastructure->userHasToken($user, '_deletedUser')); - - $userModel->delete(self::TEST_USER_ID); - $this->assertFalse($this->tokenInfrastructure->isTokenRegistered('_deletedUser')); - } - - private function getTokenMock( - string $tokenId = self::TEST_TOKEN_ID, - string $userId = self::TEST_USER_ID - ): UnencryptedToken { - $claims = new DataSet( - [ - TokenService::CLAIM_TOKENID => $tokenId, - TokenService::CLAIM_SHOPID => 1, - TokenService::CLAIM_USERID => $userId, - ], - '' - ); - - $token = $this->getMockBuilder(UnencryptedToken::class) - ->getMock(); - $token->method('claims')->willReturn($claims); - $token->method('toString')->willReturn('here_is_the_string_token'); - - return $token; - } -} diff --git a/tests/Integration/Infrastructure/RefreshTokenRepositoryTest.php b/tests/Integration/Infrastructure/RefreshTokenRepositoryTest.php index c0792990..064d3342 100644 --- a/tests/Integration/Infrastructure/RefreshTokenRepositoryTest.php +++ b/tests/Integration/Infrastructure/RefreshTokenRepositoryTest.php @@ -10,10 +10,11 @@ namespace OxidEsales\GraphQL\Base\Tests\Integration\Infrastructure; use DateTime; +use DateTimeImmutable; use Doctrine\DBAL\Connection; use OxidEsales\EshopCommunity\Internal\Framework\Database\ConnectionProviderInterface; +use OxidEsales\GraphQL\Base\DataType\UserInterface; use OxidEsales\GraphQL\Base\Exception\InvalidRefreshToken; -use OxidEsales\GraphQL\Base\Exception\InvalidToken; use OxidEsales\GraphQL\Base\Infrastructure\RefreshTokenRepository; use OxidEsales\GraphQL\Base\Infrastructure\RefreshTokenRepositoryInterface; use OxidEsales\GraphQL\Base\Tests\Integration\TestCase; @@ -138,6 +139,41 @@ public function testGetTokenUserExplodesOnWrongToken(): void $sut->getTokenUser(uniqid()); } + public function testInvalidateRefreshTokens(): void + { + $expires = new DateTimeImmutable('+8 hours'); + $this->addToken( + oxid: 'pwd_change_token', + expires: $expires->format('Y-m-d H:i:s'), + userId: $userId = '_testUser', + token: $token = uniqid(), + ); + + $sut = $this->getSut(); + $result = $sut->invalidateUserTokens($userId); + $this->assertTrue($result !== 0); + + $this->expectException(InvalidRefreshToken::class); + $sut->getTokenUser($token); + } + + public function testInvalidateRefreshTokensWrongUserId(): void + { + $expires = new DateTimeImmutable('+8 hours'); + $this->addToken( + oxid: 'pwd_change_token', + expires: $expires->format('Y-m-d H:i:s'), + userId: '_testUser', + token: $token = uniqid(), + ); + + $sut = $this->getSut(); + $result = $sut->invalidateUserTokens('some_user_id'); + $this->assertTrue($result == 0); + + $this->assertTrue($sut->getTokenUser($token) instanceof UserInterface); + } + private function getDbConnection(): Connection { return $this->get(ConnectionProviderInterface::class)->get(); diff --git a/tests/Integration/Infrastructure/TokenTest.php b/tests/Integration/Infrastructure/TokenTest.php index c34a0404..07f0da66 100644 --- a/tests/Integration/Infrastructure/TokenTest.php +++ b/tests/Integration/Infrastructure/TokenTest.php @@ -290,6 +290,73 @@ public function testUserHasToken(): void $this->assertFalse($this->tokenInfrastructure->userHasToken($otherUser, '_second')); } + public function testInvalidateTokenAfterDeleteUser(): void + { + $userModel = oxNew(User::class); + $userModel->setId('_testUser'); + $userModel->setPassword('_testPassword'); + $userModel->assign(['oxusername' => '_testUsername']); + $userModel->save(); + + $this->tokenInfrastructure->registerToken( + $this->getTokenMock('_deletedUser'), + new DateTimeImmutable('now'), + new DateTimeImmutable('+8 hours') + ); + + $user = new UserDataType($userModel); + $this->assertTrue($this->tokenInfrastructure->userHasToken($user, '_deletedUser')); + + $userModel->delete(self::TEST_USER_ID); + $this->assertFalse($this->tokenInfrastructure->isTokenRegistered('_deletedUser')); + } + + public function testInvalidateAccessTokens(): void + { + $this->tokenInfrastructure->registerToken( + $this->getTokenMock( + $token = 'pwd_change_token', + $userId = '_testUser' + ), + new DateTimeImmutable('now'), + new DateTimeImmutable('+8 hours') + ); + + $result = $this->tokenInfrastructure->invalidateUserTokens($userId); + $this->assertTrue($result !== 0); + + $result = $this->connection->executeQuery( + "select expires_at from `oegraphqltoken` where oxid=:tokenId", + ['tokenId' => $token] + ); + $expiresAtAfterChange = $result->fetchOne(); + + $this->assertTrue(new DateTimeImmutable($expiresAtAfterChange) <= new DateTimeImmutable('now')); + } + + public function testInvalidateAccessTokensWrongUserId(): void + { + $this->tokenInfrastructure->registerToken( + $this->getTokenMock( + tokenId: $token = 'pwd_change_token', + userId: '_testUser' + ), + new DateTimeImmutable('now'), + new DateTimeImmutable('+8 hours') + ); + + $result = $this->tokenInfrastructure->invalidateUserTokens('wrong_user_id'); + $this->assertTrue($result == 0); + + $result = $this->connection->executeQuery( + "select expires_at from `oegraphqltoken` where oxid=:tokenId", + ['tokenId' => $token] + ); + $expiresAtAfterChange = $result->fetchOne(); + + $this->assertFalse(new DateTimeImmutable($expiresAtAfterChange) <= new DateTimeImmutable('now')); + } + private function getTokenMock( string $tokenId = self::TEST_TOKEN_ID, string $userId = self::TEST_USER_ID diff --git a/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php b/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php index fe934247..30ab6c04 100644 --- a/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php +++ b/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php @@ -9,10 +9,13 @@ namespace OxidEsales\GraphQL\Base\Tests\Unit\Event\Subscriber; +use OxidEsales\Eshop\Application\Model\Article; +use OxidEsales\Eshop\Application\Model\User; +use OxidEsales\Eshop\Core\Model\BaseModel; use OxidEsales\EshopCommunity\Internal\Transition\ShopEvents\AfterModelUpdateEvent; use OxidEsales\EshopCommunity\Internal\Transition\ShopEvents\BeforeModelUpdateEvent; use OxidEsales\GraphQL\Base\Event\Subscriber\PasswordChangeSubscriber; -use OxidEsales\GraphQL\Base\Infrastructure\RefreshTokenRepository; +use OxidEsales\GraphQL\Base\Infrastructure\RefreshTokenRepositoryInterface; use OxidEsales\GraphQL\Base\Infrastructure\Token; use OxidEsales\GraphQL\Base\Service\UserModelService; use OxidEsales\GraphQL\Base\Tests\Unit\BaseTestCase; @@ -46,14 +49,106 @@ public function testHandleAfterUpdateReturnsOriginalEvent(): void $this->assertSame($eventStub, $sut->handleAfterUpdate($eventStub)); } - public function getSut( + public function testSubscriberWithUserModelPwdChange(): void + { + $userModelService = $this->createPartialMock(UserModelService::class, ['isPasswordChanged']); + $userModelService->method('isPasswordChanged')->with($userId = uniqid())->willReturn(true); + + $userModelStub = $this->createStub(User::class); + $userModelStub->method('getId') + ->willReturn($userId); + + $refreshTokenRepository = $this->createMock(RefreshTokenRepositoryInterface::class); + $refreshTokenRepository->expects($this->once()) + ->method('invalidateUserTokens'); + + $tokenInfrastructure = $this->createMock(Token::class); + $tokenInfrastructure->expects($this->once()) + ->method('invalidateUserTokens'); + + $beforeUpdateStub = $this->getBeforeUpdateEvent($userModelStub); + $afterUpdateStub = $this->getAfterUpdateEvent($userModelStub); + + $sut = $this->getSut($userModelService, $refreshTokenRepository, $tokenInfrastructure); + $sut->handleBeforeUpdate($beforeUpdateStub); + $sut->handleAfterUpdate($afterUpdateStub); + } + + public function testSubscriberWithNoUserModel(): void + { + $userModelService = $this->createPartialMock(UserModelService::class, ['isPasswordChanged']); + $userModelService->expects($this->never()) + ->method('isPasswordChanged'); + + $refreshTokenRepository = $this->createMock(RefreshTokenRepositoryInterface::class); + $refreshTokenRepository->expects($this->never()) + ->method('invalidateUserTokens'); + + $tokenInfrastructure = $this->createMock(Token::class); + $tokenInfrastructure->expects($this->never()) + ->method('invalidateUserTokens'); + + $beforeUpdateStub = $this->getBeforeUpdateEvent(new Article()); + $afterUpdateStub = $this->getAfterUpdateEvent(new Article()); + + $sut = $this->getSut($userModelService, $refreshTokenRepository, $tokenInfrastructure); + $sut->handleBeforeUpdate($beforeUpdateStub); + $sut->handleAfterUpdate($afterUpdateStub); + } + + public function testSubscriberWithUserModelNoPwdChanged(): void + { + $userModelService = $this->createPartialMock(UserModelService::class, ['isPasswordChanged']); + $userModelService->method('isPasswordChanged')->with($userId = uniqid())->willReturn(false); + + $userModelStub = $this->createStub(User::class); + $userModelStub->method('getId') + ->willReturn($userId); + + $refreshTokenRepository = $this->createMock(RefreshTokenRepositoryInterface::class); + $refreshTokenRepository->expects($this->never()) + ->method('invalidateUserTokens'); + + $tokenInfrastructure = $this->createMock(Token::class); + $tokenInfrastructure->expects($this->never()) + ->method('invalidateUserTokens'); + + $beforeUpdateStub = $this->getBeforeUpdateEvent($userModelStub); + $afterUpdateStub = $this->getAfterUpdateEvent($userModelStub); + + $sut = $this->getSut($userModelService, $refreshTokenRepository, $tokenInfrastructure); + $sut->handleBeforeUpdate($beforeUpdateStub); + $sut->handleAfterUpdate($afterUpdateStub); + } + + protected function getBeforeUpdateEvent(BaseModel $model) + { + $beforeUpdateStub = $this->createStub(BeforeModelUpdateEvent::class); + $beforeUpdateStub->method('getModel') + ->willReturn($model); + + return $beforeUpdateStub; + } + + protected function getAfterUpdateEvent(BaseModel $model) + { + $afterUpdateStub = $this->createStub(AfterModelUpdateEvent::class); + $afterUpdateStub->method('getModel') + ->willReturn($model); + + return $afterUpdateStub; + } + + protected function getSut( UserModelService $userModelService = null, - RefreshTokenRepository $refreshTokenRepository = null + RefreshTokenRepositoryInterface $refreshTokenRepository = null, + Token $tokenInfrastructure = null, ): PasswordChangeSubscriber { return new PasswordChangeSubscriber( userModelService: $userModelService ?? $this->createStub(UserModelService::class), - refreshTokenRepository: $refreshTokenRepository ?? $this->createStub(RefreshTokenRepository::class), - tokenInfrastructure: $this->createStub(Token::class) + refreshTokenRepository: + $refreshTokenRepository ?? $this->createStub(RefreshTokenRepositoryInterface::class), + tokenInfrastructure: $tokenInfrastructure ?? $this->createStub(Token::class) ); } } diff --git a/tests/Unit/Service/UserModelServiceTest.php b/tests/Unit/Service/UserModelServiceTest.php index 38ef8db4..c84a3d6d 100644 --- a/tests/Unit/Service/UserModelServiceTest.php +++ b/tests/Unit/Service/UserModelServiceTest.php @@ -33,7 +33,6 @@ public function testIsPasswordChangedOnNewPassword(): void ); $this->assertTrue($sut->isPasswordChanged($userId, $newPassword)); - } public function testIsPasswordChangedOnSamePassword(): void