| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124 |
- <?php
- declare(strict_types=1);
- namespace App\Tests\Integration\Search;
- use App\Auth\UserContext;
- use App\Tests\Integration\Support\AppTestCase;
- /**
- * `/app/search` — global IP lookup behind the topnav search box.
- *
- * The controller fans out to three admin endpoints in this order:
- * 1. GET /api/v1/admin/ips
- * 2. GET /api/v1/admin/manual-blocks
- * 3. GET /api/v1/admin/allowlist
- *
- * Tests queue mock responses for that exact sequence.
- */
- final class SearchPageTest extends AppTestCase
- {
- protected function setUp(): void
- {
- $this->bootApp();
- // The first /app/* request in a process triggers session_start(),
- // which would clobber any $_SESSION values set here. Hit a public
- // route first so the session is already active by the time our
- // test request fires.
- $this->request('GET', '/healthz');
- $_SESSION['_user'] = (new UserContext(1, 'Admin', 'admin', null, UserContext::SOURCE_LOCAL))->toArray();
- $_SESSION['_last_active'] = time();
- $_SESSION['_authenticated_at'] = time();
- }
- public function testRendersEmptyFormWithoutQuery(): void
- {
- $resp = $this->request('GET', '/app/search');
- self::assertSame(200, $resp->getStatusCode());
- $body = (string) $resp->getBody();
- self::assertStringContainsString('Enter an IP address or prefix', $body);
- self::assertStringContainsString('name="q"', $body);
- }
- public function testRendersResultsForQuery(): void
- {
- // 1) IPs
- $this->enqueueApiResponse(200, [
- 'page' => 1,
- 'page_size' => 25,
- 'total' => 1,
- 'items' => [
- [
- 'ip' => '203.0.113.42',
- 'is_ipv4' => true,
- 'max_score' => 4.5,
- 'top_category' => 'brute_force',
- 'pair_count' => 1,
- 'last_report_at' => '2026-04-29T09:00:00Z',
- 'status' => 'scored',
- 'enrichment' => null,
- ],
- ],
- ]);
- // 2) Manual blocks
- $this->enqueueApiResponse(200, [
- 'items' => [
- ['id' => 1, 'kind' => 'subnet', 'cidr' => '203.0.113.0/24', 'reason' => 'edge', 'expires_at' => null, 'created_at' => '2026-04-29T10:00:00Z', 'created_by_user_id' => null],
- ['id' => 2, 'kind' => 'ip', 'ip' => '198.51.100.7', 'reason' => 'unrelated', 'expires_at' => null, 'created_at' => '2026-04-29T10:00:00Z', 'created_by_user_id' => null],
- ],
- 'total' => 2,
- ]);
- // 3) Allowlist
- $this->enqueueApiResponse(200, [
- 'items' => [
- ['id' => 9, 'kind' => 'ip', 'ip' => '203.0.113.42', 'reason' => 'office', 'created_at' => '2026-04-28T10:00:00Z', 'created_by_user_id' => null],
- ],
- 'total' => 1,
- ]);
- $resp = $this->request('GET', '/app/search?q=203.0.113');
- self::assertSame(200, $resp->getStatusCode());
- $body = (string) $resp->getBody();
- // IP result row + link to filtered IPs page.
- self::assertStringContainsString('203.0.113.42', $body);
- self::assertStringContainsString('/app/ips?q=203.0.113', $body);
- // Manual block row that matched the query.
- self::assertStringContainsString('203.0.113.0/24', $body);
- // Unrelated manual-block entry must not render.
- self::assertStringNotContainsString('198.51.100.7', $body);
- // Section links to the index pages.
- self::assertStringContainsString('href="/app/manual-blocks"', $body);
- self::assertStringContainsString('href="/app/allowlist"', $body);
- }
- public function testHandlesPartialApiFailureGracefully(): void
- {
- // IPs succeeds, manual-blocks 500s (twice — ApiClient retries on 5xx),
- // allowlist succeeds.
- $this->enqueueApiResponse(200, ['page' => 1, 'page_size' => 25, 'total' => 0, 'items' => []]);
- $this->enqueueApiResponse(500, ['error' => 'boom']);
- $this->enqueueApiResponse(500, ['error' => 'boom']);
- $this->enqueueApiResponse(200, ['items' => [], 'total' => 0]);
- $resp = $this->request('GET', '/app/search?q=10.0.0.1');
- self::assertSame(200, $resp->getStatusCode());
- $body = (string) $resp->getBody();
- self::assertStringContainsString('Failed to load manual blocks', $body);
- // The other sections still render their no-results state.
- self::assertStringContainsString('No IPs match this query.', $body);
- self::assertStringContainsString('No allowlist entries match this query.', $body);
- }
- public function testRedirectsAnonymousToLogin(): void
- {
- $_SESSION = [];
- $resp = $this->request('GET', '/app/search?q=foo');
- self::assertSame(302, $resp->getStatusCode());
- self::assertSame('/login', $resp->getHeaderLine('Location'));
- }
- }
|