Files
SkinbaseNova/tests/Feature/Http/SlowHttpRequestLoggingTest.php
T
klevze 8a80aae21e Ship production optimization M1-M12.5A: queues, metrics, HTTP observability, and vector search reliability.
Keep similar-ai from tripping the global circuit on a lone URL 502, clamp Qdrant search to 100, and add Server-Timing plus slow-request logging. Studio shared props, Academy S3 exists caching, heat chunking, and Redis/scheduler hygiene stay in this rollout.
2026-08-25 07:58:47 +02:00

114 lines
3.7 KiB
PHP

<?php
use App\Models\Artwork;
use App\Models\User;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
function slowHttpLogPath(): string
{
return storage_path('logs/slow-http-test.log');
}
function configureSlowHttpLog(): void
{
config([
'http_observability.slow_request.enabled' => true,
'http_observability.slow_request.threshold_ms' => 750,
'logging.channels.slow-http.driver' => 'single',
'logging.channels.slow-http.path' => slowHttpLogPath(),
'logging.channels.slow-http.level' => 'info',
]);
Log::forgetChannel('slow-http');
File::delete(slowHttpLogPath());
}
afterEach(function (): void {
File::delete(slowHttpLogPath());
});
it('does not write a slow-http record for a fast request', function () {
configureSlowHttpLog();
config(['http_observability.slow_request.test_duration_ms' => 120]);
$this->get('/leaderboard')->assertOk();
expect(File::exists(slowHttpLogPath()))->toBeFalse();
});
it('writes one structured slow-http record using the route template', function () {
configureSlowHttpLog();
config(['http_observability.slow_request.test_duration_ms' => 900]);
$response = $this->get('/leaderboard');
$response->assertOk();
expect($response->headers->get('Server-Timing'))->toContain('app;desc="Laravel"');
$raw = File::get(slowHttpLogPath());
expect(substr_count(trim($raw), "\n"))->toBe(0);
$line = trim($raw);
preg_match('/\{.*\}\s*$/', $line, $matches);
expect($matches[0] ?? null)->not->toBeNull();
$json = json_decode($matches[0], true, flags: JSON_THROW_ON_ERROR);
expect($json['method'])->toBe('GET')
->and($json['status'])->toBe(200)
->and($json['duration_ms'])->toEqual(900)
->and($json['route_uri'])->not->toContain('?')
->and($json['route_uri'])->not->toContain('leaderboard/')
->and($json)->not->toHaveKey('query')
->and($json)->not->toHaveKey('ip')
->and($json)->not->toHaveKey('email')
->and($json)->not->toHaveKey('user_id')
->and($json['authenticated'])->toBeFalse();
});
it('does not log when slow-request logging is disabled', function () {
configureSlowHttpLog();
config([
'http_observability.slow_request.enabled' => false,
'http_observability.slow_request.test_duration_ms' => 5000,
]);
$this->get('/leaderboard')->assertOk()->assertHeader('Server-Timing');
expect(File::exists(slowHttpLogPath()))->toBeFalse();
});
it('does not break a missing-file download response and still sends Server-Timing', function () {
configureSlowHttpLog();
config(['http_observability.slow_request.test_duration_ms' => 100]);
$artwork = Artwork::factory()->create([
'is_public' => true,
'is_approved' => true,
'published_at' => now()->subDay(),
'file_ext' => 'jpg',
'hash' => str_repeat('cd', 32),
]);
$this->get('/download/artwork/'.$artwork->id)
->assertNotFound()
->assertHeader('Server-Timing');
});
it('does not log query strings or user identifiers on an authenticated slow request', function () {
configureSlowHttpLog();
config(['http_observability.slow_request.test_duration_ms' => 800]);
$user = User::factory()->create([
'email' => '[email protected]',
'username' => 'slowlogsecret',
]);
$this->actingAs($user)->get('/leaderboard?period=weekly&debug=1')->assertOk();
$raw = File::get(slowHttpLogPath());
expect($raw)->not->toContain('[email protected]')
->and($raw)->not->toContain('period=weekly')
->and($raw)->not->toContain('debug=1')
->and($raw)->not->toContain('"user_id"')
->and($raw)->not->toContain($user->email);
});