diff --git a/composer.json b/composer.json index ba5761bc..379f9231 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,8 @@ "symfony/framework-bundle": "^4.4 | ^5.0 | ^6.0", "symfony/phpunit-bridge": "^5.0 | ^6.0", "vimeo/psalm": "^4.3", - "doctrine/doctrine-bundle": "^2.0.3" + "doctrine/doctrine-bundle": "^2.0.3", + "symfony/uid": "^v5.1 | ^6.0" }, "conflict": { "doctrine/orm": "<2.7", diff --git a/src/Persistence/Repository/ResetPasswordRequestRepositoryTrait.php b/src/Persistence/Repository/ResetPasswordRequestRepositoryTrait.php index 6f321c04..905e2bb1 100644 --- a/src/Persistence/Repository/ResetPasswordRequestRepositoryTrait.php +++ b/src/Persistence/Repository/ResetPasswordRequestRepositoryTrait.php @@ -9,6 +9,9 @@ namespace SymfonyCasts\Bundle\ResetPassword\Persistence\Repository; +use Symfony\Component\Uid\AbstractUid; +use Symfony\Component\Uid\Ulid; +use Symfony\Component\Uid\Uuid; use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface; /** @@ -41,6 +44,8 @@ public function findResetPasswordRequest(string $selector): ?ResetPasswordReques public function getMostRecentNonExpiredRequestDate(object $user): ?\DateTimeInterface { + $params = $this->getQueryParams($user); + // Normally there is only 1 max request per use, but written to be flexible /** @var ResetPasswordRequestInterface $resetPasswordRequest */ $resetPasswordRequest = $this->createQueryBuilder('t') @@ -61,6 +66,8 @@ public function getMostRecentNonExpiredRequestDate(object $user): ?\DateTimeInte public function removeResetPasswordRequest(ResetPasswordRequestInterface $resetPasswordRequest): void { + $params = $this->getQueryParams($resetPasswordRequest->getUser()); + $this->createQueryBuilder('t') ->delete() ->where('t.user = :user') @@ -82,4 +89,24 @@ public function removeExpiredResetPasswordRequests(): int return $query->execute(); } + + private function getQueryParams(object $user): array + { + $paramValue = $user; + $paramType = null; + + if (method_exists($paramValue, 'getId') && class_exists(AbstractUid::class)) { + if ($paramValue->getId instanceof Uuid) { + $paramType = 'uuid'; + $paramValue = $paramValue->getId(); + } + + if ($paramValue->getId instanceof Ulid) { + $paramType = 'ulid'; + $paramValue = $paramValue->getId(); + } + } + + return ['value' => $paramValue, 'type' => $paramType]; + } } diff --git a/tests/Fixtures/Entity/ResetPasswordTestFixtureUuidRequest.php b/tests/Fixtures/Entity/ResetPasswordTestFixtureUuidRequest.php new file mode 100644 index 00000000..15b72262 --- /dev/null +++ b/tests/Fixtures/Entity/ResetPasswordTestFixtureUuidRequest.php @@ -0,0 +1,78 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace SymfonyCasts\Bundle\ResetPassword\Tests\Fixtures\Entity; + +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Uid\Uuid; +use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface; + +/** + * @author Jesse Rushlow + * @author Ryan Weaver + * + * @internal + * @ORM\Entity(repositoryClass="SymfonyCasts\Bundle\ResetPassword\Tests\Fixtures\ResetPasswordTestFixtureRequestUuidRepository") + */ +final class ResetPasswordTestFixtureUuidRequest implements ResetPasswordRequestInterface +{ + /** + * @ORM\Id() + * @ORM\Column(type="uuid") + */ + public $id; + + /** + * @ORM\Column(type="string", nullable=true) + */ + public $selector; + + /** + * @ORM\Column(type="datetime_immutable", nullable=true) + */ + public $expiresAt; + + /** + * @ORM\Column(type="datetime_immutable", nullable=true) + */ + public $requestedAt; + + /** + * @ORM\ManyToOne(targetEntity="ResetPasswordTestFixtureUser") + */ + public $user; + + public function __construct() + { + $this->id = Uuid::v4(); + } + + public function getRequestedAt(): \DateTimeInterface + { + return $this->requestedAt; + } + + public function isExpired(): bool + { + return $this->expiresAt->getTimestamp() <= time(); + } + + public function getExpiresAt(): \DateTimeInterface + { + } + + public function getHashedToken(): string + { + } + + public function getUser(): object + { + return $this->user; + } +} diff --git a/tests/Fixtures/ResetPasswordTestFixtureRequestUuidRepository.php b/tests/Fixtures/ResetPasswordTestFixtureRequestUuidRepository.php new file mode 100644 index 00000000..fce81e1c --- /dev/null +++ b/tests/Fixtures/ResetPasswordTestFixtureRequestUuidRepository.php @@ -0,0 +1,43 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace SymfonyCasts\Bundle\ResetPassword\Tests\Fixtures; + +use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\Persistence\ManagerRegistry; +use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface; +use SymfonyCasts\Bundle\ResetPassword\Persistence\Repository\ResetPasswordRequestRepositoryTrait; +use SymfonyCasts\Bundle\ResetPassword\Persistence\ResetPasswordRequestRepositoryInterface; +use SymfonyCasts\Bundle\ResetPassword\Tests\Fixtures\Entity\ResetPasswordTestFixtureUuidRequest; + +/** + * @author Jesse Rushlow + * @author Ryan Weaver + * + * @internal + */ +final class ResetPasswordTestFixtureRequestUuidRepository extends ServiceEntityRepository implements ResetPasswordRequestRepositoryInterface +{ + use ResetPasswordRequestRepositoryTrait; + + private $manager; + + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, ResetPasswordTestFixtureUuidRequest::class); + } + + public function createResetPasswordRequest( + object $user, + \DateTimeInterface $expiresAt, + string $selector, + string $hashedToken + ): ResetPasswordRequestInterface { + } +} diff --git a/tests/FunctionalTests/Persistence/ResetPasswordRequestRepositoryUuidTest.php b/tests/FunctionalTests/Persistence/ResetPasswordRequestRepositoryUuidTest.php new file mode 100644 index 00000000..0835afb2 --- /dev/null +++ b/tests/FunctionalTests/Persistence/ResetPasswordRequestRepositoryUuidTest.php @@ -0,0 +1,226 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace SymfonyCasts\Bundle\ResetPassword\Tests\FunctionalTests\Persistence; + +use Doctrine\Bundle\DoctrineBundle\Registry; +use Doctrine\ORM\Tools\SchemaTool; +use Doctrine\Persistence\ObjectManager; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Uid\Uuid; +use SymfonyCasts\Bundle\ResetPassword\Tests\Fixtures\Entity\ResetPasswordTestFixtureUuidRequest; +use SymfonyCasts\Bundle\ResetPassword\Tests\Fixtures\Entity\ResetPasswordTestFixtureUser; +use SymfonyCasts\Bundle\ResetPassword\Tests\Fixtures\ResetPasswordTestFixtureRequestUuidRepository; +use SymfonyCasts\Bundle\ResetPassword\Tests\ResetPasswordTestKernel; + +/** + * @author Jesse Rushlow + * @author Ryan Weaver + * + * @internal + */ +final class ResetPasswordRequestRepositoryUuidTest extends TestCase +{ + /** + * @var ObjectManager|object + */ + private $manager; + + /** + * @var ResetPasswordTestFixtureRequestUuidRepository + */ + private $repository; + + /** + * {@inheritdoc} + */ + protected function setUp(): void + { + $kernel = new ResetPasswordTestKernel(); + $kernel->boot(); + + $container = $kernel->getContainer(); + + /** @var Registry $registry */ + $registry = $container->get('doctrine'); + $this->manager = $registry->getManager(); + + $this->configureDatabase(); + + $this->repository = $this->manager->getRepository(ResetPasswordTestFixtureUuidRequest::class); + } + + public function testPersistResetPasswordRequestPersistsRequestObject(): void + { + $fixture = new ResetPasswordTestFixtureUuidRequest(); + + $this->repository->persistResetPasswordRequest($fixture); + + $result = $this->repository->findAll(); + + self::assertSame($fixture, $result[0]); + } + + public function testGetUserIdentifierRetrievesObjectIdFromPersistence(): void + { + $fixture = new ResetPasswordTestFixtureUuidRequest(); + + $this->manager->persist($fixture); + $this->manager->flush(); + + $result = $this->repository->getUserIdentifier($fixture); + + $result = Uuid::fromString($result); + + self::assertInstanceOf(Uuid::class, $result); +// self::assertSame('1', $result); + } + + public function testFindResetPasswordRequestReturnsObjectWithGivenSelector(): void + { + $fixture = new ResetPasswordTestFixtureUuidRequest(); + $fixture->selector = '1234'; + + $this->manager->persist($fixture); + $this->manager->flush(); + + $result = $this->repository->findResetPasswordRequest('1234'); + + self::assertSame($fixture, $result); + } + + public function testGetMostRecentNonExpiredRequestDateReturnsExpected(): void + { + $userFixture = new ResetPasswordTestFixtureUser(); + + $this->manager->persist($userFixture); + + $fixtureOld = new ResetPasswordTestFixtureUuidRequest(); + $fixtureOld->requestedAt = new \DateTimeImmutable('-5 minutes'); + $fixtureOld->user = $userFixture; + + $this->manager->persist($fixtureOld); + + $expectedTime = new \DateTimeImmutable(); + + $fixtureNewest = new ResetPasswordTestFixtureUuidRequest(); + $fixtureNewest->expiresAt = new \DateTimeImmutable('+1 hours'); + $fixtureNewest->requestedAt = $expectedTime; + $fixtureNewest->user = $userFixture; + + $this->manager->persist($fixtureNewest); + $this->manager->flush(); + + $result = $this->repository->getMostRecentNonExpiredRequestDate($userFixture); + + self::assertSame($expectedTime, $result); + } + + public function testGetMostRecentNonExpiredRequestDateReturnsNullOnExpiredRequest(): void + { + $userFixture = new ResetPasswordTestFixtureUser(); + + $this->manager->persist($userFixture); + + $expiredFixture = new ResetPasswordTestFixtureUuidRequest(); + $expiredFixture->user = $userFixture; + $expiredFixture->expiresAt = new \DateTimeImmutable('-1 hours'); + $expiredFixture->requestedAt = new \DateTimeImmutable('-2 hours'); + + $this->manager->persist($expiredFixture); + $this->manager->flush(); + + $result = $this->repository->getMostRecentNonExpiredRequestDate($userFixture); + + self::assertNull($result); + } + + public function testGetMostRecentNonExpiredRequestDateReturnsNullIfRequestNotFound(): void + { + $userFixture = new ResetPasswordTestFixtureUser(); + + $this->manager->persist($userFixture); + $this->manager->persist(new ResetPasswordTestFixtureUuidRequest()); + $this->manager->flush(); + + $result = $this->repository->getMostRecentNonExpiredRequestDate($userFixture); + + self::assertNull($result); + } + + public function testRemoveResetPasswordRequestRemovedGivenObjectFromPersistence(): void + { + $userFixture = new ResetPasswordTestFixtureUser(); + $requestFixture = new ResetPasswordTestFixtureUuidRequest(); + $requestFixture->user = $userFixture; + + $this->manager->persist($requestFixture); + $this->manager->persist($userFixture); + $this->manager->flush(); + + $this->repository->removeResetPasswordRequest($requestFixture); + + $this->assertCount(0, $this->repository->findAll()); + } + + public function testRemoveResetPasswordRequestRemovesAllRequestsForUser(): void + { + $userFixtureA = new ResetPasswordTestFixtureUser(); + $userFixtureB = new ResetPasswordTestFixtureUser(); + $requestFixtureA = new ResetPasswordTestFixtureUuidRequest(); + $requestFixtureA->user = $userFixtureA; + $requestFixtureB = new ResetPasswordTestFixtureUuidRequest(); + $requestFixtureB->user = $userFixtureA; + $requestFixtureC = new ResetPasswordTestFixtureUuidRequest(); + $requestFixtureC->user = $userFixtureB; + + $this->manager->persist($requestFixtureA); + $this->manager->persist($requestFixtureB); + $this->manager->persist($requestFixtureC); + $this->manager->persist($userFixtureA); + $this->manager->persist($userFixtureB); + $this->manager->flush(); + + $this->repository->removeResetPasswordRequest($requestFixtureB); + + $requests = $this->repository->findAll(); + + $this->assertCount(1, $requests); + $this->assertSame($requestFixtureC->id, $requests[0]->id); + } + + public function testRemovedExpiredResetPasswordRequestsOnlyRemovedExpiredRequestsFromPersistence(): void + { + $expiredFixture = new ResetPasswordTestFixtureUuidRequest(); + $expiredFixture->expiresAt = new \DateTimeImmutable('-2 weeks'); + + $this->manager->persist($expiredFixture); + + $futureFixture = new ResetPasswordTestFixtureUuidRequest(); + + $this->manager->persist($futureFixture); + $this->manager->flush(); + + $this->repository->removeExpiredResetPasswordRequests(); + + $result = $this->repository->findAll(); + + self::assertCount(1, $result); + self::assertSame($futureFixture, $result[0]); + } + + private function configureDatabase(): void + { + $metaData = $this->manager->getMetadataFactory(); + + $tool = new SchemaTool($this->manager); + $tool->dropDatabase(); + $tool->createSchema($metaData->getAllMetadata()); + } +} diff --git a/tests/ResetPasswordTestKernel.php b/tests/ResetPasswordTestKernel.php index 38fdf85c..92aa5259 100644 --- a/tests/ResetPasswordTestKernel.php +++ b/tests/ResetPasswordTestKernel.php @@ -19,6 +19,7 @@ use Symfony\Component\Routing\RouteCollection; use SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle; use SymfonyCasts\Bundle\ResetPassword\Tests\Fixtures\ResetPasswordTestFixtureRequestRepository; +use SymfonyCasts\Bundle\ResetPassword\Tests\Fixtures\ResetPasswordTestFixtureRequestUuidRepository; /** * @author Jesse Rushlow @@ -104,6 +105,12 @@ public function registerContainerConfiguration(LoaderInterface $loader): void ->setAutowired(true) ; + $container->register(ResetPasswordTestFixtureRequestUuidRepository::class) + ->setAutoconfigured(true) + ->setAutowired(true) + ; + + $container->loadFromExtension('symfonycasts_reset_password', [ 'request_password_repository' => ResetPasswordTestFixtureRequestRepository::class, ]);