em = $this->createStub(EntityManagerInterface::class); } private function makeController(bool $exhaust = false, ?EntityManagerInterface $em = null): FeedbackController { $storage = new InMemoryStorage(); $factory = new RateLimiterFactory([ 'id' => 'feedback_test', 'policy' => 'token_bucket', 'limit' => 10, 'rate' => ['interval' => '1 minute'], ], $storage); if ($exhaust) { // Vider les tokens avec la même clé que le controller (l'IP du request) $limiter = $factory->create('1.2.3.4'); for ($i = 0; $i < 11; $i++) { $limiter->consume(); } } $controller = new FeedbackController($em ?? $this->em, $factory); $container = $this->createStub(ContainerInterface::class); $container->method('has')->willReturn(false); $controller->setContainer($container); return $controller; } private function makeRequest(string $body, string $ip = '1.2.3.4'): Request { $request = Request::create('/api/feedback', 'POST', [], [], [], [], $body); $request->server->set('REMOTE_ADDR', $ip); return $request; } // ── Taille de payload ───────────────────────────────────────────────────── public function testBodyTooLargeReturns413(): void { $body = json_encode(['lat' => 14.65, 'lng' => -61.0, 'hasSeaweed' => true, 'pad' => str_repeat('x', 4100)]); self::assertSame(413, $this->makeController()->create($this->makeRequest($body))->getStatusCode()); } // ── Validation JSON ─────────────────────────────────────────────────────── public function testInvalidJsonReturns400(): void { self::assertSame(400, $this->makeController()->create($this->makeRequest('not-json'))->getStatusCode()); } // ── Champs obligatoires ─────────────────────────────────────────────────── public function testMissingLatReturns422(): void { self::assertSame(422, $this->makeController()->create( $this->makeRequest(json_encode(['lng' => -61.0, 'hasSeaweed' => true])) )->getStatusCode()); } public function testMissingLngReturns422(): void { self::assertSame(422, $this->makeController()->create( $this->makeRequest(json_encode(['lat' => 14.65, 'hasSeaweed' => true])) )->getStatusCode()); } public function testMissingHasSeaweedReturns422(): void { self::assertSame(422, $this->makeController()->create( $this->makeRequest(json_encode(['lat' => 14.65, 'lng' => -61.0])) )->getStatusCode()); } // ── Validation lat/lng ──────────────────────────────────────────────────── public function testInvalidLatStringReturns422(): void { self::assertSame(422, $this->makeController()->create( $this->makeRequest(json_encode(['lat' => 'abc', 'lng' => -61.0, 'hasSeaweed' => true])) )->getStatusCode()); } public function testLatAboveRangeReturns422(): void { self::assertSame(422, $this->makeController()->create( $this->makeRequest(json_encode(['lat' => 91.0, 'lng' => -61.0, 'hasSeaweed' => true])) )->getStatusCode()); } public function testLatBelowRangeReturns422(): void { self::assertSame(422, $this->makeController()->create( $this->makeRequest(json_encode(['lat' => -91.0, 'lng' => -61.0, 'hasSeaweed' => true])) )->getStatusCode()); } public function testLngAboveRangeReturns422(): void { self::assertSame(422, $this->makeController()->create( $this->makeRequest(json_encode(['lat' => 14.65, 'lng' => 181.0, 'hasSeaweed' => true])) )->getStatusCode()); } // ── Frontières exactes ──────────────────────────────────────────────────── public function testExactBoundaryLatLngIsAccepted(): void { $em = $this->createMock(EntityManagerInterface::class); $em->expects(self::once())->method('persist'); $em->expects(self::once())->method('flush'); self::assertSame(201, $this->makeController(em: $em)->create( $this->makeRequest(json_encode(['lat' => 90.0, 'lng' => -180.0, 'hasSeaweed' => false])) )->getStatusCode()); } // ── Requête valide ──────────────────────────────────────────────────────── public function testValidRequestReturns201(): void { $em = $this->createMock(EntityManagerInterface::class); $em->expects(self::once())->method('persist'); $em->expects(self::once())->method('flush'); self::assertSame(201, $this->makeController(em: $em)->create( $this->makeRequest(json_encode(['lat' => 14.65, 'lng' => -61.0, 'hasSeaweed' => true])) )->getStatusCode()); } public function testValidDensityIsAccepted(): void { $em = $this->createMock(EntityManagerInterface::class); $em->expects(self::once())->method('persist'); $em->expects(self::once())->method('flush'); self::assertSame(201, $this->makeController(em: $em)->create( $this->makeRequest(json_encode(['lat' => 14.65, 'lng' => -61.0, 'hasSeaweed' => true, 'density' => 'high'])) )->getStatusCode()); } public function testInvalidDensityIsIgnoredAndRequestSucceeds(): void { $em = $this->createMock(EntityManagerInterface::class); $em->expects(self::once())->method('persist'); $em->expects(self::once())->method('flush'); self::assertSame(201, $this->makeController(em: $em)->create( $this->makeRequest(json_encode(['lat' => 14.65, 'lng' => -61.0, 'hasSeaweed' => true, 'density' => 'extreme'])) )->getStatusCode()); } // ── Rate limiting ───────────────────────────────────────────────────────── public function testRateLimitExceededReturns429(): void { self::assertSame(429, $this->makeController(exhaust: true)->create( $this->makeRequest(json_encode(['lat' => 14.65, 'lng' => -61.0, 'hasSeaweed' => true])) )->getStatusCode()); } }