diff --git a/.gitignore b/.gitignore index cdb0146..54f20ab 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ vendor/ .phpunit.result.cache clover.xml composer.lock +laravel phpunit.xml psalm.xml tests.* diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e99758..f86f7d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,23 @@ All notable changes to this project will be documented in this file. ## [Unreleased] + +## [5.1.0] - 2025-03-21 +### Code Refactoring +- **Channel:** Improve error handling and structure +- **baselines:** Remove obsolete baseline error files +- **channels:** Replace method calls with Utils functions +- **collector:** Improve request header exclusion handling +- **collectors:** Remove PhpInfoCollector and update ApplicationCollector +- **collectors:** Improve exception trace filtering and naming + +### Style +- Refactor code for improved readability and simplicity + +### Tests +- Add inspections for null pointer and void function usage + + ## [5.0.0] - 2025-03-20 ### Bug Fixes @@ -1186,7 +1203,8 @@ All notable changes to this project will be documented in this file. - Merge pull request [#1](https://github.com/guanguans/laravel-exception-notify/issues/1) from guanguans/imgbot -[Unreleased]: https://github.com/guanguans/laravel-exception-notify/compare/5.0.0...HEAD +[Unreleased]: https://github.com/guanguans/laravel-exception-notify/compare/5.1.0...HEAD +[5.1.0]: https://github.com/guanguans/laravel-exception-notify/compare/5.0.0...5.1.0 [5.0.0]: https://github.com/guanguans/laravel-exception-notify/compare/5.0.0-rc1...5.0.0 [5.0.0-rc1]: https://github.com/guanguans/laravel-exception-notify/compare/5.0.0-beta2...5.0.0-rc1 [5.0.0-beta2]: https://github.com/guanguans/laravel-exception-notify/compare/5.0.0-beta1...5.0.0-beta2 diff --git a/README.md b/README.md index 3275837..20e9677 100644 --- a/README.md +++ b/README.md @@ -59,9 +59,11 @@ php artisan exception-notify:test --ansi -v ### Notification examples -| discord | lark | mail | -|:----------------------------:|:----------------------:|:----------------------:| -| ![discord](docs/discord.jpg) | ![lark](docs/lark.jpg) | ![mail](docs/mail.jpg) | +| discord | slack | telegram | +|:----------------------------:|:------------------------:|:------------------------------:| +| ![discord](docs/discord.jpg) | ![slack](docs/slack.jpg) | ![telegram](docs/telegram.jpg) | +| lark | mail | weWork | +| ![lark](docs/lark.jpg) | ![mail](docs/mail.jpg) | ![weWork](docs/weWork.jpg) | ### Skip report diff --git a/composer.json b/composer.json index b96ae68..e00224f 100644 --- a/composer.json +++ b/composer.json @@ -102,7 +102,6 @@ "guanguans/ai-commit": "dev-main", "guanguans/monorepo-builder-worker": "^1.4", "laravel/facade-documenter": "dev-main", - "maglnet/composer-require-checker": "^4.4", "mockery/mockery": "^1.6", "nette/utils": "^4.0", "orchestra/testbench": "^7.53 || ^8.0 || ^9.0 || ^10.0", @@ -119,6 +118,7 @@ "rector/type-perfect": "^2.0", "shipmonk/composer-dependency-analyser": "^1.8", "shipmonk/phpstan-baseline-per-identifier": "^2.1", + "spatie/pest-plugin-snapshots": "^1.1 || ^2.0", "spaze/phpstan-disallowed-calls": "^4.4", "symplify/phpstan-extensions": "^12.0", "symplify/phpstan-rules": "^14.4", diff --git a/docs/mail.jpg b/docs/mail.jpg index ed42501..1e21e3c 100644 Binary files a/docs/mail.jpg and b/docs/mail.jpg differ diff --git a/phpstan.neon b/phpstan.neon index 5a38216..d82f415 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -28,6 +28,8 @@ includes: parameters: paths: - src/ + scanFiles: + - vendor/composer/InstalledVersions.php excludePaths: - tests/Fixtures/ level: max diff --git a/rector.php b/rector.php index 289254d..0fb83f9 100644 --- a/rector.php +++ b/rector.php @@ -18,10 +18,8 @@ use Carbon\Carbon; use Composer\Autoload\ClassLoader; use Ergebnis\Rector\Rules\Arrays\SortAssociativeArrayByKeyRector; -use Guanguans\LaravelExceptionNotify\Channels\AbstractChannel; use Guanguans\LaravelExceptionNotify\Support\Rectors\HydratePipeFuncCallToStaticCallRector; use Guanguans\LaravelExceptionNotify\Support\Rectors\ToInternalExceptionRector; -use Guanguans\LaravelExceptionNotify\Support\Utils; use Guanguans\LaravelExceptionNotify\Template; use Illuminate\Support\Carbon as IlluminateCarbon; use Illuminate\Support\Collection; @@ -47,12 +45,8 @@ use Rector\PHPUnit\Set\PHPUnitSetList; use Rector\Renaming\Rector\FuncCall\RenameFunctionRector; use Rector\Renaming\Rector\Name\RenameClassRector; -use Rector\Transform\Rector\FuncCall\FuncCallToStaticCallRector; -use Rector\Transform\Rector\MethodCall\MethodCallToStaticCallRector; use Rector\Transform\Rector\Scalar\ScalarValueToConstFetchRector; use Rector\Transform\Rector\StaticCall\StaticCallToFuncCallRector; -use Rector\Transform\ValueObject\FuncCallToStaticCall; -use Rector\Transform\ValueObject\MethodCallToStaticCall; use Rector\Transform\ValueObject\ScalarValueToConstFetch; use Rector\Transform\ValueObject\StaticCallToFuncCall; use Rector\ValueObject\PhpVersion; @@ -153,19 +147,12 @@ 'phpstan-ignore-next-line', 'psalm-suppress', ]) - ->withConfiguredRule(MethodCallToStaticCallRector::class, [ - new MethodCallToStaticCall(AbstractChannel::class, 'applyContentToConfiguration', Utils::class, 'applyContentToConfiguration'), - new MethodCallToStaticCall(AbstractChannel::class, 'applyConfigurationToObject', Utils::class, 'applyConfigurationToObject'), - ]) ->withConfiguredRule(RenameClassRector::class, [ Carbon::class => IlluminateCarbon::class, ]) ->withConfiguredRule(StaticCallToFuncCallRector::class, [ new StaticCallToFuncCall(Str::class, 'of', 'str'), ]) - // ->withConfiguredRule(FuncCallToStaticCallRector::class, [ - // new FuncCallToStaticCall('str', Str::class, 'of'), - // ]) ->withConfiguredRule( ScalarValueToConstFetchRector::class, collect([ @@ -225,16 +212,13 @@ static function (array $carry, mixed $value, string $name) use ($class): array { 'Guanguans\Notify\Foundation\Support\rescue' => 'Guanguans\LaravelExceptionNotify\Support\rescue', 'Pest\Faker\fake' => 'fake', 'Pest\Faker\faker' => 'faker', - 'rescue' => 'Guanguans\LaravelExceptionNotify\Support\rescue', 'test' => 'it', ] + array_reduce( [ - 'make', 'env_explode', 'json_pretty_encode', - 'hydrate_pipe', - 'human_bytes', - 'human_milliseconds', + 'make', + 'rescue', ], static function (array $carry, string $func): array { /** @see https://github.com/laravel/framework/blob/11.x/src/Illuminate/Support/functions.php */ diff --git a/src/Collectors/AbstractCollector.php b/src/Collectors/AbstractCollector.php index 8f89209..24acf99 100644 --- a/src/Collectors/AbstractCollector.php +++ b/src/Collectors/AbstractCollector.php @@ -28,8 +28,9 @@ public function name(): string public static function fallbackName(): string { - return ucwords((string) str(class_basename(static::class)) - ->beforeLast(str(class_basename(self::class))->remove('Abstract')) - ->snake(' ')); + return str(class_basename(static::class)) + ->beforeLast('Collector') + ->headline() + ->toString(); } } diff --git a/src/Collectors/ApplicationCollector.php b/src/Collectors/ApplicationCollector.php index c21c196..b0275a4 100644 --- a/src/Collectors/ApplicationCollector.php +++ b/src/Collectors/ApplicationCollector.php @@ -13,9 +13,9 @@ namespace Guanguans\LaravelExceptionNotify\Collectors; +use Guanguans\LaravelExceptionNotify\Support\Utils; use Illuminate\Container\Container; use Illuminate\Support\Carbon; -use function Guanguans\LaravelExceptionNotify\Support\human_bytes; class ApplicationCollector extends AbstractCollector { @@ -35,7 +35,7 @@ public function collect(): array 'debug' => $this->container->hasDebugModeEnabled(), 'locale' => $this->container->getLocale(), 'in console' => $this->container->runningInConsole(), - 'memory' => human_bytes(memory_get_peak_usage(true)), + 'memory' => Utils::humanBytes(memory_get_peak_usage(true)), ]; } } diff --git a/src/Collectors/RequestBasicCollector.php b/src/Collectors/RequestBasicCollector.php index 495f09f..34e4ff8 100644 --- a/src/Collectors/RequestBasicCollector.php +++ b/src/Collectors/RequestBasicCollector.php @@ -13,8 +13,8 @@ namespace Guanguans\LaravelExceptionNotify\Collectors; +use Guanguans\LaravelExceptionNotify\Support\Utils; use Illuminate\Http\Request; -use function Guanguans\LaravelExceptionNotify\Support\human_milliseconds; class RequestBasicCollector extends AbstractCollector { @@ -29,7 +29,7 @@ public function collect(): array 'controller action' => $this->request->route()?->getActionName(), 'duration' => blank($startTime = \defined('LARAVEL_START') ? LARAVEL_START : $this->request->server('REQUEST_TIME_FLOAT')) ? 'Unknown' - : human_milliseconds((microtime(true) - $startTime) * 1000), + : Utils::humanMilliseconds((microtime(true) - $startTime) * 1000), ]; } } diff --git a/src/Collectors/RequestFileCollector.php b/src/Collectors/RequestFileCollector.php index 0db7048..ccdd7ea 100644 --- a/src/Collectors/RequestFileCollector.php +++ b/src/Collectors/RequestFileCollector.php @@ -13,9 +13,9 @@ namespace Guanguans\LaravelExceptionNotify\Collectors; +use Guanguans\LaravelExceptionNotify\Support\Utils; use Illuminate\Http\Request; use Illuminate\Http\UploadedFile; -use function Guanguans\LaravelExceptionNotify\Support\human_bytes; class RequestFileCollector extends AbstractCollector { @@ -33,7 +33,7 @@ public function collect(): array 'name' => $uploadedFile->getClientOriginalName(), 'type' => $uploadedFile->getMimeType(), 'error' => $uploadedFile->getError(), - 'size' => human_bytes($uploadedFile->getSize()), + 'size' => Utils::humanBytes($uploadedFile->getSize()), ]; }); diff --git a/src/ExceptionNotifyServiceProvider.php b/src/ExceptionNotifyServiceProvider.php index 4f40216..0bb482d 100644 --- a/src/ExceptionNotifyServiceProvider.php +++ b/src/ExceptionNotifyServiceProvider.php @@ -13,9 +13,13 @@ namespace Guanguans\LaravelExceptionNotify; +use Composer\InstalledVersions; use Guanguans\LaravelExceptionNotify\Commands\TestCommand; use Guanguans\LaravelExceptionNotify\Facades\ExceptionNotify; use Illuminate\Contracts\Debug\ExceptionHandler; +use Illuminate\Foundation\Console\AboutCommand; +use Illuminate\Support\Arr; +use Illuminate\Support\Facades\File; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Stringable; @@ -103,11 +107,33 @@ private function registerCommands(): self { if ($this->app->runningInConsole()) { $this->commands(TestCommand::class); + $this->addSectionToAboutCommand(); } return $this; } + private function addSectionToAboutCommand(): void + { + AboutCommand::add('Laravel Exception Notify', static function (): array { + $package = 'guanguans/laravel-exception-notify'; + + return collect( + json_decode(File::get(base_path("vendor/$package/composer.json")), true, 512, \JSON_THROW_ON_ERROR) + + Arr::get(InstalledVersions::getAllRawData(), "0.versions.$package", []) + ) + ->filter(static fn (mixed $value): bool => \is_string($value)) + ->except([ + '$schema', + 'install_path', + 'readme', + 'reference', + ]) + ->mapWithKeys(static fn (string $value, string $key): array => [str($key)->headline()->toString() => $value]) + ->all(); + }); + } + /** * @param class-string $class */ diff --git a/src/Pipes/SprintfMarkdownPipe.php b/src/Pipes/SprintfMarkdownPipe.php index a5612c8..76dfb7c 100644 --- a/src/Pipes/SprintfMarkdownPipe.php +++ b/src/Pipes/SprintfMarkdownPipe.php @@ -25,7 +25,7 @@ public function handle( Collection $collectors, \Closure $next, string $format = <<<'mark' - ```json + ``` %s ``` mark diff --git a/src/Support/Utils.php b/src/Support/Utils.php index 4138ef9..1fc8871 100644 --- a/src/Support/Utils.php +++ b/src/Support/Utils.php @@ -19,19 +19,6 @@ class Utils { - public static function applyContentToConfiguration(array $configuration, string $content): array - { - array_walk_recursive($configuration, static function (mixed &$value) use ($content): void { - \is_string($value) and $value = str_replace( - [Template::TITLE, Template::CONTENT], - [config('exception-notify.title'), $content], - $value - ); - }); - - return $configuration; - } - public static function applyConfigurationToObject(object $object, array $configuration, ?array $except = null): object { return collect($configuration) @@ -92,4 +79,45 @@ public static function applyConfigurationToObject(object $object, array $configu return $extender($object); }); } + + public static function applyContentToConfiguration(array $configuration, string $content): array + { + array_walk_recursive($configuration, static function (mixed &$value) use ($content): void { + \is_string($value) and $value = str_replace( + [Template::TITLE, Template::CONTENT], + [config('exception-notify.title'), $content], + $value + ); + }); + + return $configuration; + } + + /** + * @see https://stackoverflow.com/a/23888858/1580028 + */ + public static function humanBytes(int $bytes, int $decimals = 2): string + { + $size = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + $factor = (int) floor((\strlen((string) $bytes) - 1) / 3); + + if (0 === $factor) { + $decimals = 0; + } + + return \sprintf("%.{$decimals}f %s", $bytes / (1024 ** $factor), $size[$factor]); + } + + public static function humanMilliseconds(float $milliseconds, int $precision = 2): string + { + if (1 > $milliseconds) { + return \sprintf('%s μs', round($milliseconds * 1000, $precision)); + } + + if (1000 > $milliseconds) { + return \sprintf('%s ms', round($milliseconds, $precision)); + } + + return \sprintf('%s s', round($milliseconds / 1000, $precision)); + } } diff --git a/src/Support/helpers.php b/src/Support/helpers.php index d458bfd..1895dbf 100644 --- a/src/Support/helpers.php +++ b/src/Support/helpers.php @@ -17,6 +17,38 @@ use Illuminate\Support\Arr; use Illuminate\Support\Facades\Log; +if (!\function_exists('Guanguans\LaravelExceptionNotify\Support\env_explode')) { + /** + * @noinspection LaravelFunctionsInspection + */ + function env_explode(string $key, mixed $default = null, string $delimiter = ',', int $limit = \PHP_INT_MAX): mixed + { + $env = env($key, $default); + + if (\is_string($env)) { + return $env ? explode($delimiter, $env, $limit) : []; + } + + return $env; + } +} + +if (!\function_exists('Guanguans\LaravelExceptionNotify\Support\json_pretty_encode')) { + /** + * @param int<1, 4194304> $depth + * + * @throws \JsonException + */ + function json_pretty_encode(mixed $value, int $options = 0, int $depth = 512): string + { + return json_encode( + $value, + \JSON_PRETTY_PRINT | \JSON_UNESCAPED_UNICODE | \JSON_UNESCAPED_SLASHES | \JSON_THROW_ON_ERROR | \JSON_FORCE_OBJECT | $options, + $depth + ); + } +} + if (!\function_exists('Guanguans\LaravelExceptionNotify\Support\make')) { /** * @see https://github.com/laravel/framework/blob/12.x/src/Illuminate/Foundation/helpers.php @@ -80,67 +112,3 @@ function rescue(callable $callback, mixed $rescue = null, mixed $log = true): mi } } } - -if (!\function_exists('Guanguans\LaravelExceptionNotify\Support\env_explode')) { - /** - * @noinspection LaravelFunctionsInspection - */ - function env_explode(string $key, mixed $default = null, string $delimiter = ',', int $limit = \PHP_INT_MAX): mixed - { - $env = env($key, $default); - - if (\is_string($env)) { - return $env ? explode($delimiter, $env, $limit) : []; - } - - return $env; - } -} - -if (!\function_exists('Guanguans\LaravelExceptionNotify\Support\json_pretty_encode')) { - /** - * @param int<1, 4194304> $depth - * - * @throws \JsonException - */ - function json_pretty_encode(mixed $value, int $options = 0, int $depth = 512): string - { - return json_encode( - $value, - \JSON_PRETTY_PRINT | \JSON_UNESCAPED_UNICODE | \JSON_UNESCAPED_SLASHES | \JSON_THROW_ON_ERROR | \JSON_FORCE_OBJECT | $options, - $depth - ); - } -} - -if (!\function_exists('Guanguans\LaravelExceptionNotify\Support\human_bytes')) { - /** - * @see https://stackoverflow.com/a/23888858/1580028 - */ - function human_bytes(int $bytes, int $decimals = 2): string - { - $size = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; - $factor = (int) floor((\strlen((string) $bytes) - 1) / 3); - - if (0 === $factor) { - $decimals = 0; - } - - return \sprintf("%.{$decimals}f %s", $bytes / (1024 ** $factor), $size[$factor]); - } -} - -if (!\function_exists('Guanguans\LaravelExceptionNotify\Support\human_milliseconds')) { - function human_milliseconds(float $milliseconds, int $precision = 2): string - { - if (1 > $milliseconds) { - return \sprintf('%s μs', round($milliseconds * 1000, $precision)); - } - - if (1000 > $milliseconds) { - return \sprintf('%s ms', round($milliseconds, $precision)); - } - - return \sprintf('%s s', round($milliseconds / 1000, $precision)); - } -} diff --git a/tests/Channels/AbstractChannelTest.php b/tests/Channels/AbstractChannelTest.php index dc3f9ff..c50cbdd 100644 --- a/tests/Channels/AbstractChannelTest.php +++ b/tests/Channels/AbstractChannelTest.php @@ -23,7 +23,7 @@ it('can report', function (): void { (fn (): bool => $this->isRunningInConsole = false)->call(app()); - expect($this->app->make(ExceptionNotifyManager::class)->driver('dump')) + expect(resolve(ExceptionNotifyManager::class)->driver('dump')) ->report(new RuntimeException('testing')) ->toBeNull(); })->group(__DIR__, __FILE__); diff --git a/tests/Channels/ChannelTest.php b/tests/Channels/ChannelTest.php index 996819b..6f92955 100644 --- a/tests/Channels/ChannelTest.php +++ b/tests/Channels/ChannelTest.php @@ -27,7 +27,7 @@ it('can not report', function (): void { config()->set('exception-notify.rate_limiter.max_attempts', 0); - expect($this->app->make(ExceptionNotifyManager::class)) + expect(resolve(ExceptionNotifyManager::class)) ->report(new RuntimeException('testing')) ->toBeNull(); })->group(__DIR__, __FILE__); @@ -41,7 +41,7 @@ Log::info($reportedEvent::class); }); - expect($this->app->make(ExceptionNotifyManager::class)) + expect(resolve(ExceptionNotifyManager::class)) ->report(new RuntimeException('testing')) ->toBeNull(); })->group(__DIR__, __FILE__); @@ -57,7 +57,11 @@ it('are same fingerprints for exceptions of throw in the same position', function (): void { collect(range(1, 10)) - ->map(fn (): string => (fn () => $this->fingerprintFor(new RuntimeException(microtime())))->call(ExceptionNotify::driver('log'))) + ->map( + fn (): string => ( + fn () => $this->fingerprintFor(new RuntimeException(microtime())) + )->call(ExceptionNotify::driver('log')) + ) ->reduce(static function (?string $previousFingerprint, string $fingerprint): string { $previousFingerprint and expect($previousFingerprint)->toBe($fingerprint); diff --git a/tests/Channels/DumpChannelTest.php b/tests/Channels/DumpChannelTest.php index 0280e15..7952f24 100644 --- a/tests/Channels/DumpChannelTest.php +++ b/tests/Channels/DumpChannelTest.php @@ -22,7 +22,7 @@ use Guanguans\LaravelExceptionNotify\Exceptions\RuntimeException; it('can report', function (): void { - expect($this->app->make(ExceptionNotifyManager::class)->driver('dump')) + expect(resolve(ExceptionNotifyManager::class)->driver('dump')) ->report(new RuntimeException('testing')) ->toBeNull(); })->group(__DIR__, __FILE__); diff --git a/tests/Channels/LogChannelTest.php b/tests/Channels/LogChannelTest.php index f6fbb47..2d78af4 100644 --- a/tests/Channels/LogChannelTest.php +++ b/tests/Channels/LogChannelTest.php @@ -22,7 +22,7 @@ use Guanguans\LaravelExceptionNotify\Exceptions\RuntimeException; it('can report', function (): void { - expect($this->app->make(ExceptionNotifyManager::class)->driver('log')) + expect(resolve(ExceptionNotifyManager::class)->driver('log')) ->report(new RuntimeException('testing')) ->toBeNull(); })->group(__DIR__, __FILE__); diff --git a/tests/Channels/MailChannelTest.php b/tests/Channels/MailChannelTest.php index 81ac4bd..e072ad4 100644 --- a/tests/Channels/MailChannelTest.php +++ b/tests/Channels/MailChannelTest.php @@ -21,16 +21,10 @@ use Guanguans\LaravelExceptionNotify\ExceptionNotifyManager; use Guanguans\LaravelExceptionNotify\Exceptions\RuntimeException; use Guanguans\LaravelExceptionNotify\Mail\ReportExceptionMail; -use Guanguans\LaravelExceptionNotifyTests\Fixtures\MailableExtender; use Illuminate\Support\Facades\Mail; it('can report', function (): void { - config([ - 'exception-notify.channels.mail.render' => 'value', - 'exception-notify.channels.mail.extender' => MailableExtender::class, - ]); - - expect($this->app->make(ExceptionNotifyManager::class)->driver('mail')) + expect(resolve(ExceptionNotifyManager::class)->driver('mail')) ->report(new RuntimeException('testing')) ->toBeNull(); Mail::assertSent(ReportExceptionMail::class); diff --git a/tests/Channels/NotifyChannelTest.php b/tests/Channels/NotifyChannelTest.php index acb89a7..ca9d90d 100644 --- a/tests/Channels/NotifyChannelTest.php +++ b/tests/Channels/NotifyChannelTest.php @@ -22,7 +22,7 @@ use Guanguans\LaravelExceptionNotify\Exceptions\RuntimeException; it('can report', function (): void { - expect($this->app->make(ExceptionNotifyManager::class)->driver('bark')) + expect(resolve(ExceptionNotifyManager::class)->driver('bark')) ->report(new RuntimeException('testing')) ->toBeNull(); })->group(__DIR__, __FILE__); diff --git a/tests/Channels/StackChannelTest.php b/tests/Channels/StackChannelTest.php index 6956035..64002ed 100644 --- a/tests/Channels/StackChannelTest.php +++ b/tests/Channels/StackChannelTest.php @@ -22,13 +22,13 @@ use Guanguans\LaravelExceptionNotify\Exceptions\RuntimeException; it('can report', function (): void { - expect($this->app->make(ExceptionNotifyManager::class)->driver('stack')) + expect(resolve(ExceptionNotifyManager::class)->driver('stack')) ->report(new RuntimeException('testing')) ->toBeNull(); })->group(__DIR__, __FILE__); it('can report content', function (): void { - expect($this->app->make(ExceptionNotifyManager::class)->driver('stack')) + expect(resolve(ExceptionNotifyManager::class)->driver('stack')) ->reportContent('testing') ->toBeArray(); })->group(__DIR__, __FILE__); diff --git a/tests/Commands/TestCommandTest.php b/tests/Commands/TestCommandTest.php index ce2d4dd..0e2a36c 100644 --- a/tests/Commands/TestCommandTest.php +++ b/tests/Commands/TestCommandTest.php @@ -21,6 +21,8 @@ use Guanguans\LaravelExceptionNotify\Commands\TestCommand; use Guanguans\LaravelExceptionNotify\Exceptions\RuntimeException; use Guanguans\LaravelExceptionNotify\Facades\ExceptionNotify; +use Guanguans\Notify\Foundation\Client; +use GuzzleHttp\Psr7\Response; use Symfony\Component\Console\Command\Command; use function Pest\Laravel\artisan; @@ -41,9 +43,28 @@ })->group(__DIR__, __FILE__); it('will throws RuntimeException', function (): void { - artisan(TestCommand::class, [ - '--channel' => 'stack', - '--config' => "app.name={$this->faker()->name()}", - '--verbose' => true, - ]); + artisan(TestCommand::class); })->group(__DIR__, __FILE__)->throws(RuntimeException::class, 'Testing for exception-notify.'); + +it('will catch RuntimeException and can report it', function (): void { + try { + artisan(TestCommand::class, [ + '--channel' => $channel = 'bark', + '--config' => "app.name={$this->faker()->name()}", + '--verbose' => true, + ]); + } catch (RuntimeException $runtimeException) { + ExceptionNotify::forgetDrivers(); + + $extender = config($configurationKey = "exception-notify.channels.$channel.client.extender"); + + config()->set( + $configurationKey, + static fn (Client $client): Client => $extender($client)->mock([ + new Response(body: \sprintf('{"code":200,"message":"%s","timestamp":1708331409}', fake()->text())), + ]) + ); + + expect(ExceptionNotify::report($runtimeException))->toBeNull(); + } +})->group(__DIR__, __FILE__); diff --git a/tests/ExceptionNotifyManagerTest.php b/tests/ExceptionNotifyManagerTest.php index f28e0e3..0a85296 100644 --- a/tests/ExceptionNotifyManagerTest.php +++ b/tests/ExceptionNotifyManagerTest.php @@ -36,13 +36,13 @@ })->group(__DIR__, __FILE__); it('can report', function (): void { - expect($this->app->make(ExceptionNotifyManager::class)) + expect(resolve(ExceptionNotifyManager::class)) ->report(new RuntimeException('testing')) ->toBeNull(); })->group(__DIR__, __FILE__); it('can report content', function (): void { - expect($this->app->make(ExceptionNotifyManager::class)) + expect(resolve(ExceptionNotifyManager::class)) ->reportContent('testing') ->toBeArray(); })->group(__DIR__, __FILE__); @@ -52,14 +52,17 @@ })->group(__DIR__, __FILE__)->throws(InvalidArgumentException::class); it('can create custom driver', function (): void { - resolve(ExceptionNotifyManager::class)->extend('foo', static fn (): ChannelContract => new class implements ChannelContract { - public function report(Throwable $throwable): void {} + resolve(ExceptionNotifyManager::class)->extend( + 'foo', + static fn (): ChannelContract => new class implements ChannelContract { + public function report(Throwable $throwable): void {} - public function reportContent(string $content): mixed - { - return null; + public function reportContent(string $content): mixed + { + return null; + } } - }); + ); expect(resolve(ExceptionNotifyManager::class))->driver('foo')->toBeInstanceOf(ChannelContract::class); })->group(__DIR__, __FILE__); diff --git a/tests/ExceptionNotifyServiceProviderTest.php b/tests/ExceptionNotifyServiceProviderTest.php index 8e3261c..cdd3c25 100644 --- a/tests/ExceptionNotifyServiceProviderTest.php +++ b/tests/ExceptionNotifyServiceProviderTest.php @@ -19,6 +19,12 @@ */ use Guanguans\LaravelExceptionNotify\ExceptionNotifyServiceProvider; +use Illuminate\Contracts\Filesystem\FileNotFoundException; +use function Pest\Laravel\artisan; + +it('can add section to about command', function (): void { + artisan('about'); +})->group(__DIR__, __FILE__)->throws(FileNotFoundException::class); it('can get provides', function (): void { expect(new ExceptionNotifyServiceProvider(app())) diff --git a/tests/Faker.php b/tests/Faker.php index 4ab3757..6e56276 100644 --- a/tests/Faker.php +++ b/tests/Faker.php @@ -18,7 +18,7 @@ trait Faker { - final protected function faker(string $locale = 'en_US'): Generator + final protected function faker(string $locale = Factory::DEFAULT_LOCALE): Generator { /** * @var array $fakers diff --git a/tests/FeatureTest.php b/tests/FeatureTest.php index e1fb74e..4493af2 100644 --- a/tests/FeatureTest.php +++ b/tests/FeatureTest.php @@ -37,5 +37,5 @@ 'password' => 'password', 'file' => new UploadedFile(__FILE__, basename(__FILE__)), ]) - ->assertStatus(500); + ->assertServerError(); })->group(__DIR__, __FILE__); diff --git a/tests/Fixtures/.gitkeep b/tests/Fixtures/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/Fixtures/Illuminate b/tests/Fixtures/Illuminate deleted file mode 120000 index c937061..0000000 --- a/tests/Fixtures/Illuminate +++ /dev/null @@ -1 +0,0 @@ -/Users/yaozm/Documents/develop/laravel-exception-notify/tests/Fixtures/../../vendor/laravel/framework/src/Illuminate/ \ No newline at end of file diff --git a/tests/Fixtures/laravel b/tests/Fixtures/laravel deleted file mode 120000 index 33abf98..0000000 --- a/tests/Fixtures/laravel +++ /dev/null @@ -1 +0,0 @@ -/Users/yaozm/Documents/develop/laravel-exception-notify/tests/Fixtures/../../vendor/orchestra/testbench-core/laravel/ \ No newline at end of file diff --git a/tests/Fixtures/ln.php b/tests/Fixtures/ln.php deleted file mode 100644 index 10e0288..0000000 --- a/tests/Fixtures/ln.php +++ /dev/null @@ -1,32 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - * - * @see https://github.com/guanguans/laravel-exception-notify - */ - -use Illuminate\Console\OutputStyle; -use Symfony\Component\Console\Input\ArgvInput; -use Symfony\Component\Console\Output\ConsoleOutput; -use Symfony\Component\Process\Process; - -require __DIR__.'/../../vendor/autoload.php'; - -$process = new Process([ - 'ln', '-sf', $dir = __DIR__.'/../../vendor/orchestra/testbench-core/laravel/', basename($dir), - // 'ln', '-sf', $dir = __DIR__.'/../../vendor/laravel/framework/src/Illuminate/', basename($dir), -]); - -$outputStyle = new OutputStyle(new ArgvInput, new ConsoleOutput); - -$process->mustRun(static function ($type, $buffer) use ($outputStyle): void { - $outputStyle->write($buffer); -}); - -$outputStyle->success('ok'); diff --git a/tests/Pest.php b/tests/Pest.php index 4f3264c..98aeb9b 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -24,11 +24,16 @@ use Faker\Factory; use Guanguans\LaravelExceptionNotifyTests\TestCase; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Artisan; use Pest\Expectation; uses(TestCase::class) ->beforeAll(function (): void {}) ->beforeEach(function (): void { + links([ + __DIR__.'/../'.basename($target = __DIR__.'/../vendor/orchestra/testbench-core/laravel') => $target, + ]); + /** @var TestCase $this */ $this->defineEnvironment(app()); }) @@ -66,6 +71,19 @@ | */ +function classes(): Collection +{ + return collect(spl_autoload_functions()) + ->pipe(static fn (Collection $splAutoloadFunctions): Collection => collect( + $splAutoloadFunctions + ->firstOrFail( + static fn (mixed $loader): bool => \is_array($loader) && $loader[0] instanceof ClassLoader + )[0] + ->getClassMap() + )) + ->keys(); +} + /** * @throws ReflectionException */ @@ -91,15 +109,23 @@ function faker(string $locale = Factory::DEFAULT_LOCALE): Generator // return Factory::create($locale); // } -function classes(): Collection +/** + * @see \Illuminate\Foundation\Console\StorageLinkCommand + */ +function links(array $links, array $parameters = []): int { - return collect(spl_autoload_functions()) - ->pipe(static fn (Collection $splAutoloadFunctions): Collection => collect( - $splAutoloadFunctions - ->firstOrFail( - static fn (mixed $loader): bool => \is_array($loader) && $loader[0] instanceof ClassLoader - )[0] - ->getClassMap() - )) - ->keys(); + $originalLinks = config('filesystems.links', []); + + config()->set('filesystems.links', $links); + + $status = Artisan::call('storage:link', $parameters + [ + '--ansi' => true, + '--verbose' => true, + ]); + + config()->set('filesystems.links', $originalLinks); + + // echo Artisan::output(); + + return $status; } diff --git a/tests/Support/HeplersTest.php b/tests/Support/HeplersTest.php index f46d3c2..22cc2f3 100644 --- a/tests/Support/HeplersTest.php +++ b/tests/Support/HeplersTest.php @@ -22,24 +22,9 @@ use Guanguans\LaravelExceptionNotify\Exceptions\RuntimeException; use Pest\Expectation; use function Guanguans\LaravelExceptionNotify\Support\env_explode; -use function Guanguans\LaravelExceptionNotify\Support\human_bytes; -use function Guanguans\LaravelExceptionNotify\Support\human_milliseconds; use function Guanguans\LaravelExceptionNotify\Support\make; use function Guanguans\LaravelExceptionNotify\Support\rescue; -it('will throw `InvalidArgumentException` when abstract is empty array', function (): void { - make([]); -})->group(__DIR__, __FILE__)->throws(InvalidArgumentException::class); - -it('can catch a potential exception and return a default value', function (): void { - expect(rescue( - function (): void { - throw new RuntimeException('testing'); - }, - fn (Throwable $throwable): Throwable => $throwable - ))->toBeInstanceOf(RuntimeException::class); -})->group(__DIR__, __FILE__); - it('can explode env', function (): void { expect([ env_explode('ENV_EXPLODE_STRING'), @@ -58,30 +43,15 @@ function (): void { ); })->group(__DIR__, __FILE__); -it('can human bytes', function (): void { - expect([ - human_bytes(0), - human_bytes(10), - human_bytes(10000), - human_bytes(10000000), - ])->sequence( - static fn (Expectation $expectation): Expectation => $expectation->toBe('0 B'), - static fn (Expectation $expectation): Expectation => $expectation->toBe('10 B'), - static fn (Expectation $expectation): Expectation => $expectation->toBe('9.77 KB'), - static fn (Expectation $expectation): Expectation => $expectation->toBe('9.54 MB') - ); -})->group(__DIR__, __FILE__); +it('will throw `InvalidArgumentException` when abstract is empty array', function (): void { + make([]); +})->group(__DIR__, __FILE__)->throws(InvalidArgumentException::class); -it('can human milliseconds', function (): void { - expect([ - human_milliseconds(0), - human_milliseconds(0.5), - human_milliseconds(500), - human_milliseconds(500000), - ])->sequence( - static fn (Expectation $expectation): Expectation => $expectation->toBe('0 μs'), - static fn (Expectation $expectation): Expectation => $expectation->toBe('500 μs'), - static fn (Expectation $expectation): Expectation => $expectation->toBe('500 ms'), - static fn (Expectation $expectation): Expectation => $expectation->toBe('500 s') - ); +it('can catch a potential exception and return a default value', function (): void { + expect(rescue( + function (): void { + throw new RuntimeException('testing'); + }, + fn (Throwable $throwable): Throwable => $throwable + ))->toBeInstanceOf(RuntimeException::class); })->group(__DIR__, __FILE__); diff --git a/tests/Support/UtilsTest.php b/tests/Support/UtilsTest.php new file mode 100644 index 0000000..1d9deb3 --- /dev/null +++ b/tests/Support/UtilsTest.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + * + * @see https://github.com/guanguans/laravel-exception-notify + */ + +use Guanguans\LaravelExceptionNotify\Mail\ReportExceptionMail; +use Guanguans\LaravelExceptionNotify\Support\Utils; +use Guanguans\LaravelExceptionNotifyTests\Fixtures\MailableExtender; +use Pest\Expectation; + +it('can apply configuration to object', function (): void { + expect(Utils::applyConfigurationToObject( + new ReportExceptionMail($this->faker()->title(), $this->faker()->text()), + [ + 'render' => 'value', + 'extender' => MailableExtender::class, + ] + ))->toBeInstanceOf(ReportExceptionMail::class); +})->group(__DIR__, __FILE__); + +it('can human bytes', function (): void { + expect([ + Utils::humanBytes(0), + Utils::humanBytes(10), + Utils::humanBytes(10000), + Utils::humanBytes(10000000), + ])->sequence( + static fn (Expectation $expectation): Expectation => $expectation->toBe('0 B'), + static fn (Expectation $expectation): Expectation => $expectation->toBe('10 B'), + static fn (Expectation $expectation): Expectation => $expectation->toBe('9.77 KB'), + static fn (Expectation $expectation): Expectation => $expectation->toBe('9.54 MB') + ); +})->group(__DIR__, __FILE__); + +it('can human milliseconds', function (): void { + expect([ + Utils::humanMilliseconds(0), + Utils::humanMilliseconds(0.5), + Utils::humanMilliseconds(500), + Utils::humanMilliseconds(500000), + ])->sequence( + static fn (Expectation $expectation): Expectation => $expectation->toBe('0 μs'), + static fn (Expectation $expectation): Expectation => $expectation->toBe('500 μs'), + static fn (Expectation $expectation): Expectation => $expectation->toBe('500 ms'), + static fn (Expectation $expectation): Expectation => $expectation->toBe('500 s') + ); +})->group(__DIR__, __FILE__); diff --git a/tests/TestCase.php b/tests/TestCase.php index 49b6e8f..5f7156f 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -17,6 +17,7 @@ namespace Guanguans\LaravelExceptionNotifyTests; use Guanguans\LaravelExceptionNotify\Channels\Channel; +use Guanguans\LaravelExceptionNotify\Channels\DumpChannel; use Guanguans\LaravelExceptionNotify\Collectors\ApplicationCollector; use Guanguans\LaravelExceptionNotify\Collectors\ChoreCollector; use Guanguans\LaravelExceptionNotify\Collectors\ExceptionBasicCollector; @@ -27,6 +28,7 @@ use Guanguans\LaravelExceptionNotify\Collectors\RequestHeaderCollector; use Guanguans\LaravelExceptionNotify\Collectors\RequestPostCollector; use Guanguans\LaravelExceptionNotify\Collectors\RequestQueryCollector; +use Guanguans\LaravelExceptionNotify\Events\ReportingEvent; use Guanguans\LaravelExceptionNotify\ExceptionNotifyServiceProvider; use Guanguans\LaravelExceptionNotify\Exceptions\RuntimeException; use Guanguans\LaravelExceptionNotify\Facades\ExceptionNotify; @@ -45,6 +47,7 @@ use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; use phpmock\phpunit\PHPMock; use Symfony\Component\VarDumper\Test\VarDumperTestTrait; +use function Spatie\Snapshots\assertMatchesJsonSnapshot; class TestCase extends \Orchestra\Testbench\TestCase { @@ -109,22 +112,6 @@ protected function defineEnvironment($app): void LimitLengthPipe::with(512), ]); - config()->set('exception-notify.channels.stack.channels', [ - 'dump', - 'log', - 'mail', - 'bark', - 'chanify', - 'dingTalk', - 'discord', - 'lark', - 'ntfy', - 'pushDeer', - 'slack', - 'telegram', - 'weWork', - ]); - config([ 'exception-notify.channels.bark.authenticator.token' => fake()->uuid(), 'exception-notify.channels.chanify.authenticator.token' => fake()->uuid(), @@ -159,6 +146,28 @@ protected function defineRoutes($router): void static fn () => tap( response('proactive-report-exception'), static function (): void { + config()->set('exception-notify.channels.stack.channels', [ + 'dump', + 'log', + 'mail', + 'bark', + 'chanify', + 'dingTalk', + 'discord', + 'lark', + 'ntfy', + 'pushDeer', + 'slack', + 'telegram', + 'weWork', + ]); + + ExceptionNotify::reporting(static function (ReportingEvent $reportingEvent): void { + if ($reportingEvent->channelContract instanceof DumpChannel) { + assertMatchesJsonSnapshot($reportingEvent->content); + } + }); + ExceptionNotify::report(new RuntimeException('What happened?')); } ) diff --git a/tests/__snapshots__/FeatureTest__it_can_proactive_report_exception__1.json b/tests/__snapshots__/FeatureTest__it_can_proactive_report_exception__1.json new file mode 100644 index 0000000..840e862 --- /dev/null +++ b/tests/__snapshots__/FeatureTest__it_can_proactive_report_exception__1.json @@ -0,0 +1,69 @@ +{ + "Application": { + "time": "2025-03-22 08:20:39", + "name": "Laravel", + "version": "9.52.20", + "php version": "8.0.30", + "environment": "testing", + "debug": false, + "locale": "en", + "in console": true, + "memory": "52.50 MB" + }, + "Chore": {}, + "Exception Basic": { + "message": "What happened?", + "code": 0, + "class": "Guanguans\\LaravelExceptionNotify\\Exceptions\\RuntimeException", + "file": "\/Users\/yaozm\/Documents\/develop\/laravel-exception-notify\/tests\/TestCase.php", + "line": 171 + }, + "Exception Context": { + " 167": " assertMatchesJsonSnapshot($reportingEvent->content);\n", + " 168": " }\n", + " 169": " });\n", + " 170": "\n", + "\u27a4 171": " ExceptionNotify::report(new RuntimeException('What happened?'));\n", + " 172": " }\n", + " 173": " )\n", + " 174": " );\n", + " 175": "\n" + }, + "Exception Trace": { + "1": "#1 \/Users\/yaozm\/Documents\/develop\/laravel-exception-notify\/tests\/TestCase.php(172): tap(Object(Illuminate\\Http\\Response), Object(Closure))", + "29": "#29 \/Users\/yaozm\/Documents\/develop\/laravel-exception-notify\/tests\/FeatureTest.php(28): Orchestra\\Testbench\\TestCase->post('proactive-repor...', Array)", + "30": "#30 [internal function]: P\\Tests\\FeatureTest->{closure}()", + "32": "#32 [internal function]: P\\Tests\\FeatureTest->Pest\\Factories\\{closure}()", + "49": "#49 {main}" + }, + "Request Basic": { + "path": "proactive-report-exception", + "ip": "127.0.0.1", + "method": "POST", + "controller action": "Closure", + "duration": "15.69 ms" + }, + "Request File": { + "file": { + "name": "FeatureTest.php", + "type": "text\/x-php", + "error": 0, + "size": "1.27 KB" + } + }, + "Request Header": { + "host": "localhost", + "user-agent": "Symfony", + "accept": "text\/html,application\/xhtml+xml,application\/xml;q=0.9,*\/*;q=0.8", + "accept-language": "en-us,en;q=0.5", + "accept-charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7", + "content-type": "application\/x-www-form-urlencoded" + }, + "Request Post": { + "bar": "baz", + "password": "******" + }, + "Request Query": { + "foo": "bar" + } +}