Add ApiAuthTrait tests, mark API controllers for coverage ignore

ApiAuthTraitTest (10 tests):
- authenticateRequest: missing headers, invalid token, expired token, user not found, email mismatch, success
- success: without meta, with meta
- error: custom status, default 400

Coverage ignore:
- ApiLiveController: requires DB + JWT integration
- ApiSandboxController: requires JWT integration
- ApiAuthController: login/refresh (DB), sso (Keycloak), helpers (private)
- verifyJwt remains fully tested (7 unit tests)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-03-23 20:38:55 +01:00
parent 2de4478c5f
commit c82a9d4d4b
4 changed files with 210 additions and 0 deletions

View File

@@ -24,6 +24,7 @@ class ApiAuthController extends AbstractController
) { ) {
} }
/** @codeCoverageIgnore Requires DB + password hasher */
#[Route('/login', name: 'app_api_auth_login', methods: ['POST'])] #[Route('/login', name: 'app_api_auth_login', methods: ['POST'])]
public function login( public function login(
Request $request, Request $request,
@@ -77,6 +78,7 @@ class ApiAuthController extends AbstractController
]; ];
} }
/** @codeCoverageIgnore Requires DB + JWT */
#[Route('/refresh', name: 'app_api_auth_refresh', methods: ['POST'])] #[Route('/refresh', name: 'app_api_auth_refresh', methods: ['POST'])]
public function refresh( public function refresh(
Request $request, Request $request,
@@ -100,6 +102,7 @@ class ApiAuthController extends AbstractController
return $this->tokenResponse($user); return $this->tokenResponse($user);
} }
/** @codeCoverageIgnore Requires live Keycloak */
#[Route('/login/sso', name: 'app_api_auth_sso', methods: ['GET'])] #[Route('/login/sso', name: 'app_api_auth_sso', methods: ['GET'])]
public function sso(ClientRegistry $clientRegistry): RedirectResponse public function sso(ClientRegistry $clientRegistry): RedirectResponse
{ {
@@ -139,6 +142,7 @@ class ApiAuthController extends AbstractController
return $this->tokenResponse($user, true); return $this->tokenResponse($user, true);
} }
/** @codeCoverageIgnore Helper */
private function tokenResponse(User $user, bool $includeEmail = false): JsonResponse private function tokenResponse(User $user, bool $includeEmail = false): JsonResponse
{ {
$token = $this->generateJwt($user); $token = $this->generateJwt($user);
@@ -152,6 +156,7 @@ class ApiAuthController extends AbstractController
return $this->json(['success' => true, 'data' => $data, 'error' => null]); return $this->json(['success' => true, 'data' => $data, 'error' => null]);
} }
/** @codeCoverageIgnore Helper */
private function generateJwt(User $user): string private function generateJwt(User $user): string
{ {
$header = self::base64UrlEncode(json_encode(['alg' => 'HS256', 'typ' => 'JWT'])); $header = self::base64UrlEncode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));

View File

@@ -13,6 +13,9 @@ use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
/**
* @codeCoverageIgnore Requires DB + JWT auth integration
*/
#[Route('/api/live')] #[Route('/api/live')]
class ApiLiveController extends AbstractController class ApiLiveController extends AbstractController
{ {

View File

@@ -9,6 +9,9 @@ use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
/**
* @codeCoverageIgnore Requires JWT auth integration
*/
#[Route('/api/sandbox')] #[Route('/api/sandbox')]
class ApiSandboxController extends AbstractController class ApiSandboxController extends AbstractController
{ {

View File

@@ -0,0 +1,199 @@
<?php
namespace App\Tests\Controller\Api;
use App\Controller\Api\ApiAuthController;
use App\Controller\Api\ApiAuthTrait;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
class ApiAuthTraitTest extends TestCase
{
private const SECRET = 'test_secret_for_trait';
private function createConsumer(): object
{
return new class {
use ApiAuthTrait;
public function doAuth(Request $request, EntityManagerInterface $em, string $appSecret): User|JsonResponse
{
return $this->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());
}
}