| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183 |
- <?php
- namespace Tests\Unit;
- use App\Services\IssueService;
- use GuzzleHttp\Exception\ConnectException;
- use GuzzleHttp\Psr7\Response as PsrResponse;
- use GuzzleHttp\TransferStats;
- use Illuminate\Config\Repository;
- use Illuminate\Container\Container;
- use Illuminate\Http\Client\ConnectionException;
- use Illuminate\Http\Client\Factory;
- use Illuminate\Http\Client\Request;
- use Illuminate\Support\Facades\Facade;
- use Illuminate\Support\Facades\Http;
- use Mockery;
- use PHPUnit\Framework\TestCase;
- use ReflectionMethod;
- class PlayNowRequestTimingTest extends TestCase
- {
- private $previousContainer;
- private $previousFacadeApplication;
- private array $logs = [];
- protected function setUp(): void
- {
- parent::setUp();
- $this->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);
- }
- }
- }
|