createToken(TokenKind::Admin, role: Role::Viewer); $response = $this->request('GET', '/api/v1/admin/stats/dashboard', [ 'Authorization' => 'Bearer ' . $token, ]); self::assertSame(200, $response->getStatusCode()); $body = $this->decode($response); foreach (['active_blocks', 'manual_blocks_count', 'allowlist_count', 'reports_24h'] as $k) { self::assertArrayHasKey($k, $body); self::assertSame(0, $body[$k]); } self::assertSame([], $body['reports_24h_by_hour']); self::assertSame([], $body['top_reporters_24h']); self::assertSame([], $body['top_categories_24h']); self::assertSame('moderate', $body['reference_policy']); } public function testDashboardCountsFromSeededReports(): void { // Use a fresh class-level container/session per test (setUp does this). $token = $this->createToken(TokenKind::Admin, role: Role::Viewer); $reporterId = $this->createReporter('rep-stats'); for ($i = 0; $i < 3; ++$i) { $this->seedReport(sprintf('203.0.113.%d', 10 + $i), 'brute_force', $reporterId); } $this->seedReport('203.0.113.20', 'spam', $reporterId); $body = $this->decode($this->request('GET', '/api/v1/admin/stats/dashboard', [ 'Authorization' => 'Bearer ' . $token, ])); self::assertSame(4, $body['reports_24h']); self::assertNotEmpty($body['reports_24h_by_hour']); self::assertSame('rep-stats', $body['top_reporters_24h'][0]['name']); self::assertSame(4, $body['top_reporters_24h'][0]['count']); $catSlugs = array_map(static fn (array $r): string => $r['slug'], $body['top_categories_24h']); self::assertContains('brute_force', $catSlugs); self::assertContains('spam', $catSlugs); } public function testManualBlocksAndAllowlistCounters(): void { $token = $this->createToken(TokenKind::Admin, role: Role::Viewer); $this->db->insert('manual_blocks', [ 'kind' => 'ip', 'ip_bin' => IpAddress::fromString('203.0.113.5')->binary(), 'reason' => 'x', ], ['ip_bin' => ParameterType::LARGE_OBJECT]); $this->db->insert('allowlist', [ 'kind' => 'ip', 'ip_bin' => IpAddress::fromString('203.0.113.6')->binary(), 'reason' => 'y', ], ['ip_bin' => ParameterType::LARGE_OBJECT]); $body = $this->decode($this->request('GET', '/api/v1/admin/stats/dashboard', [ 'Authorization' => 'Bearer ' . $token, ])); self::assertSame(1, $body['manual_blocks_count']); self::assertSame(1, $body['allowlist_count']); } private function seedReport(string $ip, string $categorySlug, int $reporterId): void { $catId = (int) $this->db->fetchOne('SELECT id FROM categories WHERE slug = :s', ['s' => $categorySlug]); $ipObj = IpAddress::fromString($ip); $stmt = $this->db->prepare( 'INSERT INTO reports (ip_bin, ip_text, category_id, reporter_id, weight_at_report, received_at, metadata_json) ' . 'VALUES (:b, :t, :c, :r, :w, :now, NULL)' ); $stmt->bindValue('b', $ipObj->binary(), ParameterType::LARGE_OBJECT); $stmt->bindValue('t', $ipObj->text()); $stmt->bindValue('c', $catId, ParameterType::INTEGER); $stmt->bindValue('r', $reporterId, ParameterType::INTEGER); $stmt->bindValue('w', '1.00'); $stmt->bindValue('now', (new \DateTimeImmutable('now'))->format('Y-m-d H:i:s')); $stmt->executeStatement(); } }