createStub(TokenRepository::class); $em = $this->createStub(EntityManagerInterface::class); $service = new TokenService($repo, $em); return [$service, $repo, $em]; } private function makeServiceWithMockEm(): array { $repo = $this->createStub(TokenRepository::class); $em = $this->createMock(EntityManagerInterface::class); $service = new TokenService($repo, $em); return [$service, $repo, $em]; } private function makeImage(): Image { $user = new User(); $image = new Image(); $image->setUser($user)->setOriginalFilename('test.jpg')->setStoragePath('x'); return $image; } public function test_issue_returns_token_with_correct_type(): void { [$service, , $em] = $this->makeServiceWithMockEm(); $em->expects($this->once())->method('persist'); $token = $service->issue(TokenType::ShareApprove, $this->makeImage(), null, 'a@b.com', 7); $this->assertSame(TokenType::ShareApprove, $token->getType()); } public function test_issue_expiry_is_in_the_future(): void { [$service] = $this->makeService(); $token = $service->issue(TokenType::ShareApprove, $this->makeImage(), null, null, 7); $this->assertGreaterThan(new \DateTimeImmutable(), $token->getExpiresAt()); } public function test_issue_calls_em_persist(): void { [$service, , $em] = $this->makeServiceWithMockEm(); $em->expects($this->once())->method('persist')->with($this->isInstanceOf(Token::class)); $service->issue(TokenType::ShareApprove, $this->makeImage(), null, 'a@b.com', 7); } public function test_consume_calls_token_consume(): void { $repo = $this->createStub(TokenRepository::class); $em = $this->createMock(EntityManagerInterface::class); $service = new TokenService($repo, $em); /** @var Token&MockObject $token */ $token = $this->createMock(Token::class); $token->expects($this->once())->method('consume'); $em->expects($this->once())->method('flush'); $repo->method('findValidToken')->willReturn($token); $service->consume('some-uuid', TokenType::ShareApprove); } public function test_consume_calls_em_flush(): void { $repo = $this->createStub(TokenRepository::class); $em = $this->createMock(EntityManagerInterface::class); $service = new TokenService($repo, $em); $token = $this->createStub(Token::class); $em->expects($this->once())->method('flush'); $repo->method('findValidToken')->willReturn($token); $service->consume('some-uuid', TokenType::ShareApprove); } public function test_consume_returns_the_token(): void { $repo = $this->createStub(TokenRepository::class); $em = $this->createStub(EntityManagerInterface::class); $service = new TokenService($repo, $em); $token = $this->createStub(Token::class); $repo->method('findValidToken')->willReturn($token); $result = $service->consume('some-uuid', TokenType::ShareApprove); $this->assertSame($token, $result); } public function test_consume_throws_when_token_not_found(): void { [$service, $repo] = $this->makeService(); $repo->method('findValidToken')->willReturn(null); $this->expectException(\RuntimeException::class); $service->consume('invalid-uuid', TokenType::ShareApprove); } /** T-05: expired token — repo already excludes it, returns null → RuntimeException */ public function test_consume_throws_for_expired_token(): void { [$service, $repo] = $this->makeService(); $repo->method('findValidToken')->willReturn(null); $this->expectException(\RuntimeException::class); $service->consume('expired-uuid', TokenType::ShareApprove); } /** T-06: already-used token — repo excludes usedAt IS NOT NULL, returns null → RuntimeException */ public function test_consume_throws_for_already_used_token(): void { [$service, $repo] = $this->makeService(); $repo->method('findValidToken')->willReturn(null); $this->expectException(\RuntimeException::class); $service->consume('used-uuid', TokenType::ShareApprove); } /** T-07: type mismatch — repo WHERE clause filters type, returns null → RuntimeException */ public function test_consume_throws_for_type_mismatch(): void { [$service, $repo] = $this->makeService(); $repo->method('findValidToken')->willReturn(null); $this->expectException(\RuntimeException::class); // UUID issued as ShareDecline but consumed as ShareApprove $service->consume('some-uuid', TokenType::ShareApprove); } }