| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102 |
- <?php
- declare(strict_types=1);
- namespace App\Tests\Integration\Admin;
- use App\Domain\Auth\Role;
- use App\Domain\Auth\TokenKind;
- use App\Domain\Ip\IpAddress;
- use App\Tests\Integration\Support\AppTestCase;
- use Doctrine\DBAL\ParameterType;
- /**
- * Covers the dashboard endpoint shape: scalar counters, by-hour
- * histogram, top reporters/categories, jobs status. Cache TTL is 30s
- * but the test issues each request with fresh state — we don't
- * exercise the cache window directly here.
- */
- final class StatsControllerTest extends AppTestCase
- {
- public function testDashboardEmptyShape(): void
- {
- $token = $this->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();
- }
- }
|