diff --git a/.gitignore b/.gitignore index 7db151f..0f9bc03 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ build +.idea/ +.phpunit.result.cache composer.lock docs vendor diff --git a/README.md b/README.md index f9e3c98..e4c5e04 100644 --- a/README.md +++ b/README.md @@ -245,7 +245,7 @@ class MyCustomMailgunWebhookJob extends ProcessMailgunWebhookJob ``` ### Handling multiple signing secrets -When needed might want to the package to handle multiple endpoints and secrets. Here's how to configurate that behaviour. +When needed might want to the package to handle multiple endpoints and secrets. Here's how to configure that behaviour. If you are using the `Route::mailgunWebhooks` macro, you can append the `configKey` as follows: @@ -259,7 +259,7 @@ Alternatively, if you are manually defining the route, you can add `configKey` l Route::post('webhooks/mailgun/{configKey}', 'BinaryCats\MailgunWebhooks\MailgunWebhooksController'); ``` -If this route parameter is present the verify middleware will look for the secret using a different config key, by appending the given the parameter value to the default config key. E.g. If Mailgun posts to `webhooks/mailgun/my-named-secret` you'd add a new config named `signing_secret_my-named-secret`. +If this route parameter is present verify middleware will look for the secret using a different config key, by appending the given the parameter value to the default config key. E.g. If Mailgun posts to `webhooks/mailgun/my-named-secret` you'd add a new config named `signing_secret_my-named-secret`. Example config might look like: diff --git a/composer.json b/composer.json index 9127107..2da92a6 100644 --- a/composer.json +++ b/composer.json @@ -18,42 +18,40 @@ } ], "require": { - "php": "^7.2|^8.0", - "illuminate/support": "~5.8.0|^6.0|^7.0|^8.0", + "php": "^7.4|^8.0", + "illuminate/support": "^8.0|^9.0", "spatie/laravel-webhook-client": "^2.0" }, "require-dev": { - "orchestra/testbench": "~3.8.0|^4.0|^5.0|^6.0", - "phpunit/phpunit": "^8.2|^9.0" + "orchestra/testbench": "^6.0", + "phpunit/phpunit": "^9.3.3" }, "autoload": { "psr-4": { - "BinaryCats\\MailgunWebhooks\\": "src" + "BinaryCats\\MailgunWebhooks\\": "src/" } }, "autoload-dev": { "psr-4": { - "BinaryCats\\MailgunWebhooks\\Tests\\": "tests" + "Tests\\": "tests/" } }, "suggest": { "binary-cats/laravel-mail-helpers": "^6.0" }, "scripts": { - "test": "vendor/bin/phpunit --color=always", - "check": [ - "php-cs-fixer fix --ansi --dry-run --diff", - "phpcs --report-width=200 --report-summary --report-full src/ tests/ --standard=PSR2 -n", - "phpmd src/,tests/ text ./phpmd.xml.dist" - ], - "fix": [ - "php-cs-fixer fix --ansi" - ] + "analyze": "./vendor/bin/phpstan analyse src --memory-limit=2G", + "coverage": "XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-html coverage -d pcov.enabled", + "test": "./vendor/bin/phpunit --color=always -vvv" }, "config": { + "optimize-autoloader": true, "sort-packages": true }, "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + }, "laravel": { "providers": [ "BinaryCats\\MailgunWebhooks\\MailgunWebhooksServiceProvider" diff --git a/config/mailgun-webhooks.php b/config/mailgun-webhooks.php index 511c268..69f8019 100644 --- a/config/mailgun-webhooks.php +++ b/config/mailgun-webhooks.php @@ -13,7 +13,10 @@ * here. The key is the name of the Mailgun event type with the `.` replaced by a `_`. * * You can find a list of Mailgun webhook types here: - * https://documentation.mailgun.com/en/latest/api-webhooks.html#webhooks. + * https://documentation.mailgun.com/en/latest/user_manual.html#events. + * + * The package will automatically convert the keys to lowercase, but you should + * be congnisant of the fact that array keys are case sensitive */ 'jobs' => [ // 'delivered' => \BinaryCats\MailgunWebhooks\Jobs\HandleDelivered::class, diff --git a/phpunit.xml b/phpunit.xml index aa3d4b0..2225e7e 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,29 +1,32 @@ - + stopOnFailure="false" + verbose="true" + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"> + + + src/ + + + + + + + tests - - - src/ - - - - - - - + diff --git a/src/Event.php b/src/Event.php index 8ff6d62..1a68dda 100644 --- a/src/Event.php +++ b/src/Event.php @@ -4,31 +4,32 @@ use BinaryCats\MailgunWebhooks\Contracts\WebhookEvent; -class Event implements WebhookEvent +final class Event implements WebhookEvent { /** * Attributes from the event. * - * @var array + * @var mixed[] */ - public $attributes = []; + public array $attributes = []; /** * Create new Event. * - * @param array $attributes + * @param mixed[] $attributes */ - public function __construct($attributes) + public function __construct(array $attributes) { $this->attributes = $attributes; } /** - * Construct the event. + * Static event constructor * - * @return Event + * @param mixed[] $data + * @return static */ - public static function constructFrom($data): self + public static function constructFrom(array $data): self { return new static($data); } diff --git a/src/Exceptions/UnexpectedValueException.php b/src/Exceptions/UnexpectedValueException.php index b240073..822554c 100644 --- a/src/Exceptions/UnexpectedValueException.php +++ b/src/Exceptions/UnexpectedValueException.php @@ -6,6 +6,10 @@ class UnexpectedValueException extends BaseUnexpectedValueException { + /** + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response + */ public function render($request) { return response(['error' => $this->getMessage()], 400); diff --git a/src/Exceptions/WebhookFailed.php b/src/Exceptions/WebhookFailed.php index b6034dc..1bd761e 100644 --- a/src/Exceptions/WebhookFailed.php +++ b/src/Exceptions/WebhookFailed.php @@ -5,28 +5,46 @@ use Exception; use Spatie\WebhookClient\Models\WebhookCall; -class WebhookFailed extends Exception +final class WebhookFailed extends Exception { + /** + * @return static + */ public static function invalidSignature(): self { return new static('The signature is invalid.'); } + /** + * @return static + */ public static function signingSecretNotSet(): self { return new static('The webhook signing secret is not set. Make sure that the `signing_secret` config key is set to the correct value.'); } + /** + * @param string $jobClass + * @param \Spatie\WebhookClient\Models\WebhookCall $webhookCall + * @return static + */ public static function jobClassDoesNotExist(string $jobClass, WebhookCall $webhookCall): self { - return new static("Could not process webhook id `{$webhookCall->id}` of type `{$webhookCall->type} because the configured jobclass `$jobClass` does not exist."); + return new static("Could not process webhook id `{$webhookCall->getKey()}` of type `{$webhookCall->getAttribute('type')} because the configured jobclass `$jobClass` does not exist."); } + /** + * @param \Spatie\WebhookClient\Models\WebhookCall $webhookCall + * @return static + */ public static function missingType(WebhookCall $webhookCall): self { - return new static("Webhook call id `{$webhookCall->id}` did not contain a type. Valid Mailgun webhook calls should always contain a type."); + return new static("Webhook call id `{$webhookCall->getKey()}` did not contain a type. Valid Mailgun webhook calls should always contain a type."); } - + /** + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response + */ public function render($request) { return response(['error' => $this->getMessage()], 400); diff --git a/src/Jobs/HandleDelivered.php b/src/Jobs/HandleDelivered.php index de1929e..2af92d1 100644 --- a/src/Jobs/HandleDelivered.php +++ b/src/Jobs/HandleDelivered.php @@ -13,14 +13,14 @@ class HandleDelivered /** * Bind the implementation. * - * @var Spatie\WebhookClient\Models\WebhookCall + * @var \Spatie\WebhookClient\Models\WebhookCall */ - protected $webhookCall; + protected WebhookCall $webhookCall; /** * Create new Job. * - * @param Spatie\WebhookClient\Models\WebhookCall $webhookCall + * @param \Spatie\WebhookClient\Models\WebhookCall $webhookCall */ public function __construct(WebhookCall $webhookCall) { diff --git a/src/MailgunSignatureValidator.php b/src/MailgunSignatureValidator.php index 6b5e976..d75353e 100644 --- a/src/MailgunSignatureValidator.php +++ b/src/MailgunSignatureValidator.php @@ -9,25 +9,11 @@ class MailgunSignatureValidator implements SignatureValidator { - /** - * Bind the implemetation. - * - * @var Illuminate\Http\Request - */ - protected $request; - - /** - * Inject the config. - * - * @var Spatie\WebhookClient\WebhookConfig - */ - protected $config; - /** * True if the signature has been valiates. * - * @param Illuminate\Http\Request $request - * @param Spatie\WebhookClient\WebhookConfig $config + * @param \Illuminate\Http\Request $request + * @param \Spatie\WebhookClient\WebhookConfig $config * * @return bool */ @@ -40,6 +26,7 @@ public function isValid(Request $request, WebhookConfig $config): bool try { Webhook::constructEvent($request->all(), $signature, $secret); } catch (Exception $exception) { + // make the app aware report($exception); return false; @@ -52,7 +39,7 @@ public function isValid(Request $request, WebhookConfig $config): bool * Validate the incoming signature' schema. * * @param \Illuminate\Http\Request $request - * @return array + * @return string[] */ protected function signature(Request $request): array { diff --git a/src/MailgunWebhooksController.php b/src/MailgunWebhooksController.php index 1db6ddc..8100e84 100644 --- a/src/MailgunWebhooksController.php +++ b/src/MailgunWebhooksController.php @@ -30,8 +30,6 @@ public function __invoke(Request $request, string $configKey = null) 'process_webhook_job' => config('mailgun-webhooks.process_webhook_job'), ]); - (new WebhookProcessor($request, $webhookConfig))->process(); - - return response()->json(['message' => 'ok']); + return (new WebhookProcessor($request, $webhookConfig))->process(); } } diff --git a/src/ProcessMailgunWebhookJob.php b/src/ProcessMailgunWebhookJob.php index bd3efb2..625a0bc 100644 --- a/src/ProcessMailgunWebhookJob.php +++ b/src/ProcessMailgunWebhookJob.php @@ -4,6 +4,7 @@ use BinaryCats\MailgunWebhooks\Exceptions\WebhookFailed; use Illuminate\Support\Arr; +use Illuminate\Support\Str; use Spatie\WebhookClient\ProcessWebhookJob; class ProcessMailgunWebhookJob extends ProcessWebhookJob @@ -24,29 +25,54 @@ public function handle() { $type = Arr::get($this->webhookCall, "payload.{$this->key}"); - if (! $type) { + if (!$type) { throw WebhookFailed::missingType($this->webhookCall); } - event("mailgun-webhooks::{$type}", $this->webhookCall); + event($this->determineEventKey($type), $this->webhookCall); $jobClass = $this->determineJobClass($type); - if ($jobClass === '') { + if ('' === $jobClass) { return; } - if (! class_exists($jobClass)) { + if (!class_exists($jobClass)) { throw WebhookFailed::jobClassDoesNotExist($jobClass, $this->webhookCall); } dispatch(new $jobClass($this->webhookCall)); } + /** + * @param string $eventType + * @return string + */ protected function determineJobClass(string $eventType): string { - $jobConfigKey = str_replace('.', '_', $eventType); + return config($this->determineJobConfigKey($eventType), ''); + } - return config("mailgun-webhooks.jobs.{$jobConfigKey}", ''); + /** + * @param string $eventType + * @return string + */ + protected function determineJobConfigKey(string $eventType): string + { + return Str::of($eventType) + ->replace('.', '_') + ->prepend('mailgun-webhooks.jobs.') + ->lower(); + } + + /** + * @param string $eventType + * @return string + */ + protected function determineEventKey(string $eventType): string + { + return Str::of($eventType) + ->prepend('mailgun-webhooks::') + ->lower(); } } diff --git a/src/Webhook.php b/src/Webhook.php index 6317221..3326949 100644 --- a/src/Webhook.php +++ b/src/Webhook.php @@ -9,19 +9,18 @@ class Webhook /** * Validate and raise an appropriate event. * - * @param $payload - * @param array $signature + * @param mixed[] $payload + * @param string[] $signature * @param string $secret - * @return BinaryCats\MailgunWebhooks\Event + * @return \BinaryCats\MailgunWebhooks\Event * @throws WebhookFailed */ public static function constructEvent(array $payload, array $signature, string $secret): Event { - // verify we are good, else throw an expection + // verify we are good, else throw an exception if (! WebhookSignature::make($signature, $secret)->verify()) { throw WebhookFailed::invalidSignature(); } - // Make an event return Event::constructFrom($payload); } diff --git a/src/WebhookSignature.php b/src/WebhookSignature.php index be0b898..60f6426 100644 --- a/src/WebhookSignature.php +++ b/src/WebhookSignature.php @@ -4,38 +4,39 @@ use Illuminate\Support\Arr; -class WebhookSignature +/** + * @property string $signature Resolves from $signatureArray + * @property string|int $timestamp Resolves from $signatureArray + * @property string $token Resolves from $signatureArray + */ +final class WebhookSignature { /** - * Signature array. - * - * @var array + * @var string[] */ - protected $signatureArray; + protected array $signatureArray; /** * Signature secret. * * @var string */ - protected $secret; + protected string $secret; /** - * Create new Signature. - * - * @param array $signatureArray - * @param string $secret + * @param string[] $signatureArray + * @param string $secret */ - public function __construct(array $signatureArray, string $secret) + public function __construct(array $signatureArray , string $secret) { $this->signatureArray = $signatureArray; $this->secret = $secret; } /** - * Statis accessor into the class constructor. + * Static accessor into the class constructor. * - * @param array $signatureArray + * @param string[] $signatureArray * @param string $secret * @return WebhookSignature static */ diff --git a/tests/DummyJob.php b/tests/DummyJob.php index 1677e4b..3506715 100644 --- a/tests/DummyJob.php +++ b/tests/DummyJob.php @@ -1,6 +1,6 @@ ['my_type' => DummyJob::class]]); + cache()->clear(); } @@ -56,6 +57,38 @@ public function it_can_handle_a_valid_request() $this->assertEquals($webhookCall->id, cache('dummyjob')->id); } + /** @test */ + public function it_can_handle_a_valid_request_even_with_wrong_case() + { + $payload = [ + 'event-data' => [ + 'event' => 'mY.tYpE', + 'key' => 'value', + ], + ]; + + Arr::set($payload, 'signature', $this->determineMailgunSignature($payload)); + + $this + ->postJson('mailgun-webhooks', $payload) + ->assertSuccessful(); + + $this->assertCount(1, WebhookCall::get()); + + $webhookCall = WebhookCall::first(); + + $this->assertNull($webhookCall->exception); + + Event::assertDispatched('mailgun-webhooks::my.type', function ($event, $eventPayload) use ($webhookCall) { + $this->assertInstanceOf(WebhookCall::class, $eventPayload); + $this->assertEquals($webhookCall->id, $eventPayload->id); + + return true; + }); + + $this->assertEquals($webhookCall->id, cache('dummyjob')->id); + } + public function in_will_ignore_empty_reququest() { $payload = []; diff --git a/tests/MailgunWebhookCallTest.php b/tests/MailgunWebhookCallTest.php index 8ebbde5..3cfcfd9 100644 --- a/tests/MailgunWebhookCallTest.php +++ b/tests/MailgunWebhookCallTest.php @@ -1,6 +1,6 @@ 'test_signing_secret']); } + /** + * @return void + */ protected function setUpDatabase() { include_once __DIR__.'/../vendor/spatie/laravel-webhook-client/database/migrations/create_webhook_calls_table.php.stub'; @@ -44,7 +50,6 @@ protected function setUpDatabase() /** * @param \Illuminate\Foundation\Application $app - * * @return array */ protected function getPackageProviders($app) @@ -54,6 +59,9 @@ protected function getPackageProviders($app) ]; } + /** + * @return void + */ protected function disableExceptionHandling() { $this->app->instance(ExceptionHandler::class, new class extends Handler @@ -73,6 +81,11 @@ public function render($request, Exception $exception) }); } + /** + * @param array $payload + * @param string|null $configKey + * @return array + */ protected function determineMailgunSignature(array $payload, string $configKey = null): array { $secret = ($configKey) ?