previousContainer = Container::getInstance(); $this->previousFacadeApplication = Facade::getFacadeApplication(); $this->logs = []; $app = new Container(); $app->instance('config', new Repository([ 'services' => [ 'playnow' => [ 'keno_url' => 'https://draw.example.test/keno', 'proxy' => [ 'scheme' => 'http', 'host' => 'proxy.example.test', 'port' => '3128', 'username' => 'timing-test-user', 'password' => 'timing-test-secret/@', ], ], ], ])); $app->instance(Factory::class, new Factory()); $logger = Mockery::mock(); $logger->shouldReceive('info')->andReturnUsing(function (string $message, array $context = []) { $this->logs[] = ['message' => $message, 'context' => $context]; }); $logManager = Mockery::mock(); $logManager->shouldReceive('channel')->with('issue')->andReturn($logger); $app->instance('log', $logManager); Container::setInstance($app); Facade::clearResolvedInstances(); Facade::setFacadeApplication($app); } protected function tearDown(): void { Mockery::close(); Facade::clearResolvedInstances(); Facade::setFacadeApplication($this->previousFacadeApplication); Container::setInstance($this->previousContainer); parent::tearDown(); } public function test_success_logs_elapsed_time_and_cumulative_network_timings_without_proxy_credentials(): void { Http::fake(function (Request $request, array $options) { $this->assertRequestOptions($request, $options); $response = new PsrResponse(200, [], '{"drawNumber":123}'); $options['on_stats'](new TransferStats($request->toPsrRequest(), $response, 1.8, 0, [ 'namelookup_time' => 0.1, 'connect_time' => 0.4, 'appconnect_time' => 0.7, 'starttransfer_time' => 1.2, 'total_time' => 1.8, 'url' => $options['proxy'], ])); return Http::response(['drawNumber' => 123]); }); $this->assertSame(['drawNumber' => 123], $this->fetch()); $timing = $this->timingLog(); $this->assertSame(200, $timing['http_status']); // These values are cumulative from request start, not per-phase differences. $this->assertSame([ 'dns_ms' => 100.0, 'connect_ms' => 400.0, 'tls_ms' => 700.0, 'first_byte_ms' => 1200.0, 'total_ms' => 1800.0, 'curl_error_code' => 0, ], $timing['network_timings_ms']); $this->assertSafeTimingContext($timing); Http::assertSentCount(1); } public function test_connection_failure_still_logs_elapsed_time_partial_network_timings_and_error_code(): void { Http::fake(function (Request $request, array $options) { $this->assertRequestOptions($request, $options); $options['on_stats'](new TransferStats($request->toPsrRequest(), null, 25.0, 28, [ 'namelookup_time' => 0.05, 'connect_time' => 0.06, 'total_time' => 25.0, 'url' => $options['proxy'], ])); throw new ConnectException('Synthetic connection timeout', $request->toPsrRequest()); }); try { $this->fetch(); $this->fail('Expected the network error to propagate'); } catch (ConnectionException $exception) { $this->assertSame('Synthetic connection timeout', $exception->getMessage()); } $timing = $this->timingLog(); $this->assertNull($timing['http_status']); $this->assertSame([ 'dns_ms' => 50.0, 'connect_ms' => 60.0, 'total_ms' => 25000.0, 'curl_error_code' => 28, ], $timing['network_timings_ms']); $this->assertSafeTimingContext($timing); } public function test_http_failure_returns_null_and_still_logs_elapsed_time(): void { Http::fake(['*' => Http::response('Synthetic upstream failure', 503)]); $this->assertNull($this->fetch()); $timing = $this->timingLog(); $this->assertSame(503, $timing['http_status']); $this->assertSame([], $timing['network_timings_ms']); $this->assertSafeTimingContext($timing); Http::assertSentCount(1); } private function fetch() { $method = new ReflectionMethod(IssueService::class, 'fetchPlayNowKenoResult'); $method->setAccessible(true); return $method->invoke(null); } private function assertRequestOptions(Request $request, array $options): void { $this->assertSame('https://draw.example.test/keno', $request->url()); $this->assertSame(25, $options['timeout']); $this->assertSame(10, $options['connect_timeout']); $this->assertSame('http://timing-test-user:timing-test-secret%2F%40@proxy.example.test:3128', $options['proxy']); $this->assertIsCallable($options['on_stats']); } private function timingLog(): array { $timingLogs = array_values(array_filter($this->logs, fn (array $entry) => $entry['message'] === 'PlayNow请求耗时')); $this->assertCount(1, $timingLogs); return $timingLogs[0]['context']; } private function assertSafeTimingContext(array $context): void { $this->assertSame('draw.example.test', $context['target_host']); $this->assertIsFloat($context['elapsed_ms']); $this->assertGreaterThanOrEqual(0, $context['elapsed_ms']); $this->assertSame('configured', $context['proxy']); $this->assertSame('configured', $context['proxy_auth']); $serialized = json_encode($this->logs, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR); foreach (['timing-test-user', 'timing-test-secret', 'timing-test-secret%2F%40', 'proxy.example.test:3128'] as $sensitiveValue) { $this->assertStringNotContainsString($sensitiveValue, $serialized); } } }