diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f447303 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: CI + +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + php: [8.3, 8.2, 8.1] + laravel: [11.*, 10.*] + stability: [prefer-lowest, prefer-stable] + include: + - laravel: 11.* + testbench: 9.* + - laravel: 10.* + testbench: 8.* + exclude: + - laravel: 11.* + php: 8.1 + + name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo + coverage: xdebug + + - name: Install dependencies + run: | + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update + composer update --${{ matrix.stability }} --prefer-dist --no-interaction + + - name: Execute tests + run: | + vendor/bin/pest diff --git a/composer.json b/composer.json index ccd68d7..c798ca8 100644 --- a/composer.json +++ b/composer.json @@ -27,8 +27,7 @@ ], "require": { "php": "^8.1", - "guzzlehttp/guzzle": "^7.6", - "illuminate/http": "^10.0|^11.0", + "illuminate/http": "^10.32|^11.0", "illuminate/support": "^10.0|^11.0", "monolog/monolog": "^3.0" }, @@ -39,8 +38,9 @@ "pestphp/pest": "^2.33", "pestphp/pest-plugin-arch": "^2.0", "pestphp/pest-plugin-laravel": "^2.0", - "saloonphp/laravel-http-sender": "^2.0", - "saloonphp/laravel-plugin": "^3.0" + "saloonphp/laravel-http-sender": "^2.0|^3.0", + "saloonphp/laravel-plugin": "^3.0", + "spatie/invade": "^2.0" }, "suggest": { "saloonphp/laravel-plugin": "To support logging of Saloon events (^3.0)" @@ -75,4 +75,4 @@ }, "minimum-stability": "stable", "prefer-stable": true -} \ No newline at end of file +} diff --git a/src/Mixins/PendingRequestMixin.php b/src/Mixins/PendingRequestMixin.php index 2ecbffb..b8b5b77 100644 --- a/src/Mixins/PendingRequestMixin.php +++ b/src/Mixins/PendingRequestMixin.php @@ -33,10 +33,11 @@ public function log() if (! is_null($name)) { $logger = $logger->withName($name); } - foreach ($messageFormats as $format) { + foreach ($messageFormats as $key => $format) { // We'll use unshift instead of push, to add the middleware to the bottom of the stack, not the top $stack->unshift( - Middleware::log($logger, new MessageFormatter($format)) + Middleware::log($logger, new MessageFormatter($format)), + 'http-client-global-logger-'.$key ); } diff --git a/src/Providers/EventServiceProvider.php b/src/Providers/EventServiceProvider.php deleted file mode 100644 index f19e7e7..0000000 --- a/src/Providers/EventServiceProvider.php +++ /dev/null @@ -1,47 +0,0 @@ - [ - LogRequestSending::class, - ], - ResponseReceived::class => [ - LogResponseReceived::class, - ], - - // Saloon - SendingSaloonRequest::class => [ - LogRequestSending::class, - ], - SentSaloonRequest::class => [ - LogResponseReceived::class, - ], - ]; - - /** - * Get the events and handlers. - * - * @return array - */ - public function listens() - { - return config('http-client-global-logger.enabled') ? $this->listen : []; - } -} diff --git a/src/Providers/ServiceProvider.php b/src/Providers/ServiceProvider.php index 0134d2d..bfdf3a5 100644 --- a/src/Providers/ServiceProvider.php +++ b/src/Providers/ServiceProvider.php @@ -2,10 +2,17 @@ namespace Onlime\LaravelHttpClientGlobalLogger\Providers; +use Illuminate\Http\Client\Events\RequestSending; +use Illuminate\Http\Client\Events\ResponseReceived; use Illuminate\Http\Client\PendingRequest; +use Illuminate\Support\Facades\Event; use Illuminate\Support\ServiceProvider as BaseServiceProvider; use Monolog\Handler\StreamHandler; +use Onlime\LaravelHttpClientGlobalLogger\Listeners\LogRequestSending; +use Onlime\LaravelHttpClientGlobalLogger\Listeners\LogResponseReceived; use Onlime\LaravelHttpClientGlobalLogger\Mixins\PendingRequestMixin; +use Saloon\Laravel\Events\SendingSaloonRequest; +use Saloon\Laravel\Events\SentSaloonRequest; class ServiceProvider extends BaseServiceProvider { @@ -18,15 +25,26 @@ public function register() if (config('http-client-global-logger.enabled') && ! config('http-client-global-logger.mixin')) { - $this->app->register(EventServiceProvider::class); + $this->registerEventListeners(); } } + private function registerEventListeners(): void + { + // Laravel HTTP Client + Event::listen(RequestSending::class, LogRequestSending::class); + Event::listen(ResponseReceived::class, LogResponseReceived::class); + + // Saloon + Event::listen(SendingSaloonRequest::class, LogRequestSending::class); + Event::listen(SentSaloonRequest::class, LogResponseReceived::class); + } + public function boot() { $this->publishes([ $this->configFileLocation() => config_path('http-client-global-logger.php'), - ], 'config'); + ], 'http-client-global-logger'); $channel = config('http-client-global-logger.channel'); if (! array_key_exists($channel, config('logging.channels'))) { diff --git a/tests/EventHelperTest.php b/tests/EventHelperTest.php new file mode 100644 index 0000000..622c90e --- /dev/null +++ b/tests/EventHelperTest.php @@ -0,0 +1,113 @@ + 'Bar']); +} + +function laravelHttpRequest(): ClientRequest +{ + return new ClientRequest(psr7Request()); +} + +function laravelHttpResponse(): ClientResponse +{ + return new ClientResponse(psr7Response()); +} + +it('resolves the psr request from Laravel\'s RequestSending event', function () { + $psrRequest = EventHelper::getPsrRequest(new RequestSending(laravelHttpRequest())); + + expect($psrRequest)->toBeInstanceOf(Psr7Request::class) + ->and($psrRequest->getMethod())->toBe('GET') + ->and($psrRequest->getUri()->__toString())->toBe('http://localhost/test'); +}); + +it('resolves the psr request from Laravel\'s ResponseReceived event', function () { + $psrRequest = EventHelper::getPsrRequest(new ResponseReceived(laravelHttpRequest(), laravelHttpResponse())); + + expect($psrRequest)->toBeInstanceOf(Psr7Request::class) + ->and($psrRequest->getMethod())->toBe('GET') + ->and($psrRequest->getUri()->__toString())->toBe('http://localhost/test'); +}); + +it('resolves the psr response from Laravel\'s ResponseReceived event', function () { + $psrResponse = EventHelper::getPsrResponse(new ResponseReceived(laravelHttpRequest(), laravelHttpResponse())); + + expect($psrResponse)->toBeInstanceOf(Psr7Response::class) + ->and($psrResponse->getStatusCode())->toBe(200) + ->and($psrResponse->getHeaderLine('X-Foo'))->toBe('Bar'); +}); + +function saloonPendingRequest(): PendingRequest +{ + return new PendingRequest( + new class extends Connector + { + public function resolveBaseUrl(): string + { + return 'http://localhost'; + } + }, + new class extends Request + { + protected Method $method = Method::GET; + + public function resolveEndpoint(): string + { + return '/test'; + } + } + ); +} + +function saloonResponse(PendingRequest $pendingRequest): Response +{ + return new Response(psr7Response(), $pendingRequest, $pendingRequest->createPsrRequest()); +} + +it('resolves the psr request from Saloon\'s SendingSaloonRequest event', function () { + $psrRequest = EventHelper::getPsrRequest(new SendingSaloonRequest(saloonPendingRequest())); + + expect($psrRequest)->toBeInstanceOf(Psr7Request::class) + ->and($psrRequest->getMethod())->toBe('GET') + ->and($psrRequest->getUri()->__toString())->toBe('http://localhost/test'); +}); + +it('resolves the psr request from Saloon\'s SentSaloonRequest event', function () { + $pendingRequest = saloonPendingRequest(); + $psrRequest = EventHelper::getPsrRequest(new SentSaloonRequest($pendingRequest, saloonResponse($pendingRequest))); + + expect($psrRequest)->toBeInstanceOf(Psr7Request::class) + ->and($psrRequest->getMethod())->toBe('GET') + ->and($psrRequest->getUri()->__toString())->toBe('http://localhost/test'); +}); + +it('resolves the psr response from Saloon\'s SentSaloonRequest event', function () { + $pendingRequest = saloonPendingRequest(); + $psrResponse = EventHelper::getPsrResponse(new SentSaloonRequest($pendingRequest, saloonResponse($pendingRequest))); + + expect($psrResponse)->toBeInstanceOf(Psr7Response::class) + ->and($psrResponse->getStatusCode())->toBe(200) + ->and($psrResponse->getHeaderLine('X-Foo'))->toBe('Bar'); +}); diff --git a/tests/PendingRequestMixinTest.php b/tests/PendingRequestMixinTest.php new file mode 100644 index 0000000..4a9a404 --- /dev/null +++ b/tests/PendingRequestMixinTest.php @@ -0,0 +1,23 @@ + 'true'])->log(); + + /** @var HandlerStack $handler */ + $handler = $pendingRequest->getOptions()['handler']; + + // We need to invade the HandlerStack to access the stack property or findByName method + expect($handler)->toBeInstanceOf(HandlerStack::class) + ->and(invade($handler)->findByName('http-client-global-logger-0'))->toBeInt() + ->and(invade($handler)->findByName('http-client-global-logger-1'))->toBeInt() + ->and(fn () => invade($handler)->findByName('http-client-global-logger-2')) + ->toThrow(\InvalidArgumentException::class); +}); diff --git a/tests/ServiceProviderTest.php b/tests/ServiceProviderTest.php new file mode 100644 index 0000000..9dd4491 --- /dev/null +++ b/tests/ServiceProviderTest.php @@ -0,0 +1,79 @@ + true, + 'http-client-global-logger.mixin' => false, + ]); + + (new ServiceProvider(app()))->register(); + + $listeners = Event::getRawListeners()[$eventName] ?? []; + + expect($listeners)->not->toBeEmpty() + ->and($listeners)->toContain($listenerName); +})->with([ + [RequestSending::class, LogRequestSending::class], + [ResponseReceived::class, LogResponseReceived::class], + [SendingSaloonRequest::class, LogRequestSending::class], + [SentSaloonRequest::class, LogResponseReceived::class], +]); + +it('merges the default config', function () { + $config = config('http-client-global-logger'); + + expect($config)->toBeArray(); + + foreach ([ + 'enabled', + 'mixin', + 'channel', + 'logfile', + 'format', + 'obfuscate', + 'trim_response_body', + ] as $key) { + expect($config)->toHaveKey($key); + } +}); + +it('can publish the config file', function () { + @unlink(config_path('http-client-global-logger.php')); + + $this->artisan('vendor:publish', ['--tag' => 'http-client-global-logger']); + + $this->assertFileExists(config_path('http-client-global-logger.php')); +}); + +it('configures the log channel', function () { + $defaultChannel = config('http-client-global-logger.channel'); + + $config = config('logging.channels.'.$defaultChannel); + + expect($config)->toBeArray(); +}); + +it('can register the mixin on the PendingRequest class', function (bool $mixin) { + PendingRequest::flushMacros(); + + config([ + 'http-client-global-logger.mixin' => $mixin, + ]); + + (new ServiceProvider(app()))->boot(); + + expect(PendingRequest::hasMacro('log'))->toBe($mixin); +})->with([ + true, + false, +]);