em = $this->createStub(EntityManagerInterface::class); } private function makeController(bool $exhaust = false, ?EntityManagerInterface $em = null): PushController { $storage = new InMemoryStorage(); $factory = new RateLimiterFactory([ 'id' => 'push_test', 'policy' => 'token_bucket', 'limit' => 5, 'rate' => ['interval' => '1 minute'], ], $storage); if ($exhaust) { $limiter = $factory->create('1.2.3.4'); for ($i = 0; $i < 6; $i++) { $limiter->consume(); } } $controller = new PushController($em ?? $this->em, $factory, 'fake-vapid-public-key'); $container = $this->createStub(ContainerInterface::class); $container->method('has')->willReturn(false); $controller->setContainer($container); return $controller; } private function makeRequest(string $method, string $body, string $ip = '1.2.3.4'): Request { $request = Request::create('/api/push/subscribe', $method, [], [], [], [], $body); $request->server->set('REMOTE_ADDR', $ip); return $request; } // ── subscribe ───────────────────────────────────────────────────────────── public function testSubscribeBodyTooLargeReturns413(): void { $body = json_encode([ 'endpoint' => 'https://push.example.com/sub', 'keys' => ['auth' => 'a', 'p256dh' => 'b'], 'pad' => str_repeat('x', 4100), ]); self::assertSame(413, $this->controller()->subscribe($this->makeRequest('POST', $body))->getStatusCode()); } public function testSubscribeMissingFieldsReturns422(): void { self::assertSame(422, $this->controller()->subscribe( $this->makeRequest('POST', json_encode(['endpoint' => 'https://push.example.com'])) )->getStatusCode()); } public function testSubscribeInvalidEndpointUrlReturns422(): void { $body = json_encode(['endpoint' => 'not-a-url', 'keys' => ['auth' => 'a', 'p256dh' => 'b']]); self::assertSame(422, $this->controller()->subscribe($this->makeRequest('POST', $body))->getStatusCode()); } public function testSubscribeValidRequestReturns201(): void { $em = $this->createMock(EntityManagerInterface::class); $em->method('getRepository')->willReturn( $this->createConfiguredStub(\Doctrine\ORM\EntityRepository::class, ['findOneBy' => null]) ); $em->expects(self::once())->method('persist'); $em->expects(self::once())->method('flush'); $body = json_encode([ 'endpoint' => 'https://fcm.googleapis.com/fcm/send/abc123', 'keys' => ['auth' => 'authtoken', 'p256dh' => 'p256key'], ]); self::assertSame(201, $this->makeController(em: $em)->subscribe($this->makeRequest('POST', $body))->getStatusCode()); } public function testSubscribeRateLimitExceededReturns429(): void { self::assertSame(429, $this->makeController(exhaust: true)->subscribe( $this->makeRequest('POST', json_encode([ 'endpoint' => 'https://push.example.com/sub', 'keys' => ['auth' => 'a', 'p256dh' => 'b'], ])) )->getStatusCode()); } // ── unsubscribe ─────────────────────────────────────────────────────────── public function testUnsubscribeMissingEndpointReturns422(): void { self::assertSame(422, $this->controller()->unsubscribe( $this->makeRequest('DELETE', json_encode([])) )->getStatusCode()); } public function testUnsubscribeUnknownEndpointReturns204(): void { $this->em->method('getRepository')->willReturn( $this->createConfiguredStub(\Doctrine\ORM\EntityRepository::class, ['findOneBy' => null]) ); self::assertSame(204, $this->controller()->unsubscribe( $this->makeRequest('DELETE', json_encode(['endpoint' => 'https://push.example.com/sub'])) )->getStatusCode()); } public function testUnsubscribeRateLimitExceededReturns429(): void { self::assertSame(429, $this->makeController(exhaust: true)->unsubscribe( $this->makeRequest('DELETE', json_encode(['endpoint' => 'https://push.example.com/sub'])) )->getStatusCode()); } private function controller(): PushController { return $this->makeController(); } }