|null */ private static ?array $capturedLogContext = null; protected function setUp(): void { parent::setUp(); $this->seed(RoleSeeder::class); self::$capturedLogContext = null; // Spy on Log::withContext so we can assert the structured payload. Log::swap(new class extends \Illuminate\Log\LogManager { public function __construct() {} /** * @param array $context */ public function withContext(array $context = []): \Illuminate\Log\Logger { RequestIdRoundTripTest::recordContext($context); return $this->driver(); } public function driver($driver = null): \Illuminate\Log\Logger { return new \Illuminate\Log\Logger(new \Psr\Log\NullLogger); } /** * @param array $parameters */ public function __call($method, $parameters) { return null; } }); } /** * @param array $context */ public static function recordContext(array $context): void { self::$capturedLogContext = array_merge(self::$capturedLogContext ?? [], $context); } public function test_response_has_x_request_id_header_when_none_supplied(): void { $response = $this->getJson('/api/v1/'); $response->assertOk(); $requestId = $response->headers->get('X-Request-Id'); $this->assertNotNull($requestId); $this->assertMatchesRegularExpression(self::VALID_ULID_PATTERN, $requestId); } public function test_response_has_x_request_id_header_when_client_supplied_valid_ulid(): void { $supplied = (string) Str::ulid(); $response = $this->getJson('/api/v1/', ['X-Request-Id' => $supplied]); $this->assertSame($supplied, $response->headers->get('X-Request-Id')); } public function test_server_generates_when_client_supplies_invalid_ulid(): void { $response = $this->getJson('/api/v1/', ['X-Request-Id' => 'not-a-ulid-at-all']); $emitted = $response->headers->get('X-Request-Id'); $this->assertNotSame('not-a-ulid-at-all', $emitted); $this->assertMatchesRegularExpression(self::VALID_ULID_PATTERN, $emitted); } public function test_server_generates_when_client_supplies_empty_string(): void { $response = $this->getJson('/api/v1/', ['X-Request-Id' => '']); $emitted = $response->headers->get('X-Request-Id'); $this->assertNotNull($emitted); $this->assertMatchesRegularExpression(self::VALID_ULID_PATTERN, $emitted); } public function test_log_context_has_request_id(): void { $supplied = (string) Str::ulid(); $this->getJson('/api/v1/', ['X-Request-Id' => $supplied]); $this->assertSame($supplied, self::$capturedLogContext['request_id'] ?? null); } public function test_log_context_has_user_id_and_org_when_authenticated_organisation_route(): void { $org = Organisation::factory()->create(); $user = User::factory()->create(); $org->users()->attach($user, ['role' => 'org_admin']); $user->assignRole('org_admin'); Sanctum::actingAs($user); $this->getJson('/api/v1/organisations/'.$org->id.'/dashboard-stats'); $this->assertSame($org->id, self::$capturedLogContext['organisation_id'] ?? null); $this->assertSame($user->id, self::$capturedLogContext['user_id'] ?? null); } public function test_log_context_route_matches_named_route(): void { $this->getJson('/api/v1/'); // The health-check route at /api/v1/ has no name; expectation is // simply that the key is absent (filtered out for null) rather // than carrying a misleading default. $this->assertArrayNotHasKey('route', self::$capturedLogContext ?? []); } public function test_unauthenticated_request_still_gets_request_id(): void { // Hitting an authenticated route unauthenticated yields 401 — but // the request_id middleware still runs. $response = $this->getJson('/api/v1/auth/me'); $response->assertStatus(401); $this->assertNotNull($response->headers->get('X-Request-Id')); } public function test_request_id_is_valid_ulid_format(): void { $response = $this->getJson('/api/v1/'); $emitted = $response->headers->get('X-Request-Id'); $this->assertSame(26, strlen((string) $emitted)); $this->assertTrue(Str::isUlid((string) $emitted)); } }