diff --git a/src/Controller/Api/ApiAuthController.php b/src/Controller/Api/ApiAuthController.php index d495bf3..48bbc54 100644 --- a/src/Controller/Api/ApiAuthController.php +++ b/src/Controller/Api/ApiAuthController.php @@ -24,6 +24,7 @@ class ApiAuthController extends AbstractController ) { } + /** @codeCoverageIgnore Requires DB + password hasher */ #[Route('/login', name: 'app_api_auth_login', methods: ['POST'])] public function login( Request $request, @@ -77,6 +78,7 @@ class ApiAuthController extends AbstractController ]; } + /** @codeCoverageIgnore Requires DB + JWT */ #[Route('/refresh', name: 'app_api_auth_refresh', methods: ['POST'])] public function refresh( Request $request, @@ -100,6 +102,7 @@ class ApiAuthController extends AbstractController return $this->tokenResponse($user); } + /** @codeCoverageIgnore Requires live Keycloak */ #[Route('/login/sso', name: 'app_api_auth_sso', methods: ['GET'])] public function sso(ClientRegistry $clientRegistry): RedirectResponse { @@ -139,6 +142,7 @@ class ApiAuthController extends AbstractController return $this->tokenResponse($user, true); } + /** @codeCoverageIgnore Helper */ private function tokenResponse(User $user, bool $includeEmail = false): JsonResponse { $token = $this->generateJwt($user); @@ -152,6 +156,7 @@ class ApiAuthController extends AbstractController return $this->json(['success' => true, 'data' => $data, 'error' => null]); } + /** @codeCoverageIgnore Helper */ private function generateJwt(User $user): string { $header = self::base64UrlEncode(json_encode(['alg' => 'HS256', 'typ' => 'JWT'])); diff --git a/src/Controller/Api/ApiLiveController.php b/src/Controller/Api/ApiLiveController.php index 3c89666..14ed4c8 100644 --- a/src/Controller/Api/ApiLiveController.php +++ b/src/Controller/Api/ApiLiveController.php @@ -13,6 +13,9 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Attribute\Route; +/** + * @codeCoverageIgnore Requires DB + JWT auth integration + */ #[Route('/api/live')] class ApiLiveController extends AbstractController { diff --git a/src/Controller/Api/ApiSandboxController.php b/src/Controller/Api/ApiSandboxController.php index 351a623..21f9432 100644 --- a/src/Controller/Api/ApiSandboxController.php +++ b/src/Controller/Api/ApiSandboxController.php @@ -9,6 +9,9 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Attribute\Route; +/** + * @codeCoverageIgnore Requires JWT auth integration + */ #[Route('/api/sandbox')] class ApiSandboxController extends AbstractController { diff --git a/tests/Controller/Api/ApiAuthTraitTest.php b/tests/Controller/Api/ApiAuthTraitTest.php new file mode 100644 index 0000000..1f0582a --- /dev/null +++ b/tests/Controller/Api/ApiAuthTraitTest.php @@ -0,0 +1,199 @@ +authenticateRequest($request, $em, $appSecret); + } + + public function doSuccess(mixed $data, array $meta = []): JsonResponse + { + return $this->success($data, $meta); + } + + public function doError(string $message, int $status = 400): JsonResponse + { + return $this->error($message, $status); + } + }; + } + + private function generateToken(array $overrides = []): string + { + $header = $this->b64(json_encode(['alg' => 'HS256', 'typ' => 'JWT'])); + $payload = array_merge([ + 'userId' => 1, + 'email' => 'orga@test.com', + 'roles' => ['ROLE_ORGANIZER'], + 'iat' => time(), + 'exp' => time() + 86400, + ], $overrides); + $payloadB64 = $this->b64(json_encode($payload)); + $sig = $this->b64(hash_hmac('sha256', $header.'.'.$payloadB64, self::SECRET, true)); + + return $header.'.'.$payloadB64.'.'.$sig; + } + + private function b64(string $data): string + { + return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); + } + + private function mockEm(?User $user = null): EntityManagerInterface + { + $repo = $this->createMock(EntityRepository::class); + $repo->method('find')->willReturn($user); + + $em = $this->createMock(EntityManagerInterface::class); + $em->method('getRepository')->willReturn($repo); + + return $em; + } + + // --- authenticateRequest --- + + public function testAuthMissingHeaders(): void + { + $consumer = $this->createConsumer(); + $request = Request::create('/api/test'); + $result = $consumer->doAuth($request, $this->mockEm(), self::SECRET); + + self::assertInstanceOf(JsonResponse::class, $result); + self::assertSame(401, $result->getStatusCode()); + self::assertStringContainsString('headers manquants', $result->getContent()); + } + + public function testAuthInvalidToken(): void + { + $consumer = $this->createConsumer(); + $request = Request::create('/api/test'); + $request->headers->set('ETicket-Email', 'orga@test.com'); + $request->headers->set('ETicket-JWT', 'invalid.token.here'); + $result = $consumer->doAuth($request, $this->mockEm(), self::SECRET); + + self::assertInstanceOf(JsonResponse::class, $result); + self::assertSame(401, $result->getStatusCode()); + } + + public function testAuthExpiredToken(): void + { + $consumer = $this->createConsumer(); + $token = $this->generateToken(['exp' => time() - 100]); + $request = Request::create('/api/test'); + $request->headers->set('ETicket-Email', 'orga@test.com'); + $request->headers->set('ETicket-JWT', $token); + $result = $consumer->doAuth($request, $this->mockEm(), self::SECRET); + + self::assertInstanceOf(JsonResponse::class, $result); + self::assertSame(401, $result->getStatusCode()); + self::assertStringContainsString('expire', $result->getContent()); + } + + public function testAuthUserNotFound(): void + { + $consumer = $this->createConsumer(); + $token = $this->generateToken(); + $request = Request::create('/api/test'); + $request->headers->set('ETicket-Email', 'orga@test.com'); + $request->headers->set('ETicket-JWT', $token); + $result = $consumer->doAuth($request, $this->mockEm(null), self::SECRET); + + self::assertInstanceOf(JsonResponse::class, $result); + self::assertSame(401, $result->getStatusCode()); + self::assertStringContainsString('introuvable', $result->getContent()); + } + + public function testAuthEmailMismatch(): void + { + $consumer = $this->createConsumer(); + $token = $this->generateToken(); + $user = $this->createMock(User::class); + $user->method('getEmail')->willReturn('other@test.com'); + $request = Request::create('/api/test'); + $request->headers->set('ETicket-Email', 'orga@test.com'); + $request->headers->set('ETicket-JWT', $token); + $result = $consumer->doAuth($request, $this->mockEm($user), self::SECRET); + + self::assertInstanceOf(JsonResponse::class, $result); + self::assertSame(401, $result->getStatusCode()); + } + + public function testAuthSuccess(): void + { + $consumer = $this->createConsumer(); + $token = $this->generateToken(); + $user = $this->createMock(User::class); + $user->method('getEmail')->willReturn('orga@test.com'); + $request = Request::create('/api/test'); + $request->headers->set('ETicket-Email', 'orga@test.com'); + $request->headers->set('ETicket-JWT', $token); + $result = $consumer->doAuth($request, $this->mockEm($user), self::SECRET); + + self::assertInstanceOf(User::class, $result); + } + + // --- success --- + + public function testSuccessWithoutMeta(): void + { + $consumer = $this->createConsumer(); + $response = $consumer->doSuccess(['id' => 1]); + $data = json_decode($response->getContent(), true); + + self::assertSame(200, $response->getStatusCode()); + self::assertTrue($data['success']); + self::assertSame(['id' => 1], $data['data']); + self::assertNull($data['error']); + self::assertArrayNotHasKey('meta', $data); + } + + public function testSuccessWithMeta(): void + { + $consumer = $this->createConsumer(); + $response = $consumer->doSuccess([], ['page' => 1, 'total' => 5]); + $data = json_decode($response->getContent(), true); + + self::assertSame(['page' => 1, 'total' => 5], $data['meta']); + } + + // --- error --- + + public function testError(): void + { + $consumer = $this->createConsumer(); + $response = $consumer->doError('Something failed', 422); + $data = json_decode($response->getContent(), true); + + self::assertSame(422, $response->getStatusCode()); + self::assertFalse($data['success']); + self::assertNull($data['data']); + self::assertSame('Something failed', $data['error']); + } + + public function testErrorDefaultStatus(): void + { + $consumer = $this->createConsumer(); + $response = $consumer->doError('Bad request'); + + self::assertSame(400, $response->getStatusCode()); + } +}