From d7e20c159013b3aa611e00872d7a45f223103351 Mon Sep 17 00:00:00 2001 From: Lance Pioch Date: Tue, 8 Sep 2026 22:11:30 -0400 Subject: [PATCH 1/3] Wire Feature tests into CI and raise the floor to PHP 8.4 and MariaDB 11.4 The tests/Feature directory was bound in Pest.php but registered in no testsuite and run by no CI job, so its webhook tests silently drifted after the webhook type/schema refactor. Register the Feature testsuite, run it in the SQLite job, and fix the five stale tests to match the current payload shape. The drift also hid a real bug: webhooks subscribed to custom event classes crashed on toArray(), which those events do not implement, so the listener now falls back to public properties. Raise the CI matrix to the 1.0 support floor: drop PHP 8.3, run the mysql legs on 8.4 and 8.5 so the minimum PHP hits a real engine, and drop mariadb:10.11. composer.json now requires ^8.4 with platform 8.4. --- .github/workflows/ci.yaml | 11 ++++++++--- app/Listeners/DispatchWebhooks.php | 3 ++- composer.json | 4 ++-- composer.lock | 6 +++--- phpunit.xml | 3 +++ tests/Feature/Webhooks/DispatchWebhooksTest.php | 2 +- tests/Feature/Webhooks/ProcessWebhooksTest.php | 13 ++++++++----- 7 files changed, 27 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index abf18652ab..db88044590 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -29,7 +29,7 @@ jobs: strategy: fail-fast: true matrix: - php: [8.3, 8.4, 8.5] + php: [8.4, 8.5] env: DB_CONNECTION: sqlite DB_DATABASE: testing.sqlite @@ -84,13 +84,18 @@ jobs: env: CREATE_SNAPSHOTS: "false" + - name: Feature tests + run: vendor/bin/pest tests/Feature --parallel --do-not-fail-on-phpunit-warning + env: + CREATE_SNAPSHOTS: "false" + mysql: name: MySQL runs-on: ubuntu-latest strategy: fail-fast: true matrix: - php: [8.5] + php: [8.4, 8.5] database: ["mysql:8.4", "mysql:9.6"] services: database: @@ -153,7 +158,7 @@ jobs: fail-fast: true matrix: php: [8.5] - database: ["mariadb:10.11", "mariadb:11.4"] + database: ["mariadb:11.4"] services: database: image: ${{ matrix.database }} diff --git a/app/Listeners/DispatchWebhooks.php b/app/Listeners/DispatchWebhooks.php index 93b138337c..9476c93cc1 100644 --- a/app/Listeners/DispatchWebhooks.php +++ b/app/Listeners/DispatchWebhooks.php @@ -86,7 +86,8 @@ protected function handleGenericClassEvent(string $eventName, array $payload): v $obj = $payload[0] ?? null; $webhookData = ['event' => $eventName, 'timestamp' => now()->toIso8601String()]; if (is_object($obj)) { - $webhookData['data'] = $obj->toArray(); + // Custom event classes rarely implement toArray, so fall back to their public properties + $webhookData['data'] = method_exists($obj, 'toArray') ? $obj->toArray() : get_object_vars($obj); } elseif (is_array($obj)) { $webhookData['data'] = $obj; } diff --git a/composer.json b/composer.json index 93d857d516..e84e59bcfe 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "The free, open-source game management panel. Supporting Minecraft, Spigot, BungeeCord, and SRCDS servers.", "license": "AGPL-3.0-only", "require": { - "php": "^8.3 || ^8.4 || ^8.5", + "php": "^8.4 || ^8.5", "ext-intl": "*", "ext-json": "*", "ext-mbstring": "*", @@ -94,7 +94,7 @@ "pestphp/pest-plugin": true }, "platform": { - "php": "8.3" + "php": "8.4" } }, "minimum-stability": "stable", diff --git a/composer.lock b/composer.lock index 6501ace737..fcd12fb4aa 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a1c2f787d61e8f1c48326ae48cdd0f1b", + "content-hash": "f208501bc0ee046c0764cd9bae981060", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -15879,7 +15879,7 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^8.3 || ^8.4 || ^8.5", + "php": "^8.4 || ^8.5", "ext-intl": "*", "ext-json": "*", "ext-mbstring": "*", @@ -15888,7 +15888,7 @@ }, "platform-dev": {}, "platform-overrides": { - "php": "8.3" + "php": "8.4" }, "plugin-api-version": "2.9.0" } diff --git a/phpunit.xml b/phpunit.xml index 1f52f89f67..8cc9d0652b 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -14,6 +14,9 @@ ./tests/Filament + + ./tests/Feature + diff --git a/tests/Feature/Webhooks/DispatchWebhooksTest.php b/tests/Feature/Webhooks/DispatchWebhooksTest.php index 4779273536..ae85ccb7e2 100644 --- a/tests/Feature/Webhooks/DispatchWebhooksTest.php +++ b/tests/Feature/Webhooks/DispatchWebhooksTest.php @@ -68,7 +68,7 @@ public function test_it_does_not_call_removed_events(): void 'events' => ['eloquent.created: '.Server::class], ]); - $webhookConfig->update(['events' => 'eloquent.deleted: '.Server::class]); + $webhookConfig->update(['events' => ['eloquent.deleted: '.Server::class]]); $this->createServer(); diff --git a/tests/Feature/Webhooks/ProcessWebhooksTest.php b/tests/Feature/Webhooks/ProcessWebhooksTest.php index 40c6a33822..836b278a8f 100644 --- a/tests/Feature/Webhooks/ProcessWebhooksTest.php +++ b/tests/Feature/Webhooks/ProcessWebhooksTest.php @@ -64,16 +64,19 @@ public function test_it_sends_a_single_webhook(): void ProcessWebhook::dispatchSync( $webhook, 'eloquent.created: '.Server::class, - $data, + [$data], ); $this->assertCount(1, cache()->get("webhooks.$eventName")); $this->assertEquals($webhook->id, cache()->get("webhooks.$eventName")->first()->id); + $expected = $data; + $expected['event'] = WebhookConfiguration::transformClassName($eventName); + Http::assertSentCount(1); - Http::assertSent(function (Request $request) use ($webhook, $data) { + Http::assertSent(function (Request $request) use ($webhook, $expected) { return $webhook->endpoint === $request->url() - && $request->data() === $data; + && $request->data() === $expected; }); } @@ -143,7 +146,7 @@ public function test_it_records_when_a_webhook_is_sent(): void $this->assertDatabaseCount(Webhook::class, 1); $webhook = Webhook::query()->first(); - $this->assertEquals($server->uuid, $webhook->payload[0]['uuid']); + $this->assertEquals($server->uuid, $webhook->payload['data']['uuid']); $this->assertDatabaseHas(Webhook::class, [ 'endpoint' => $webhookConfig->endpoint, @@ -165,8 +168,8 @@ public function test_it_records_when_a_webhook_fails(): void $server = $this->createServer(); $this->assertDatabaseCount(Webhook::class, 1); + $this->assertEquals($server->uuid, Webhook::query()->first()->payload['data']['uuid']); $this->assertDatabaseHas(Webhook::class, [ - 'payload' => json_encode([$server->toArray()]), 'endpoint' => $webhookConfig->endpoint, 'successful_at' => null, 'event' => 'eloquent.created: '.Server::class, From de3ee8cf3b0beaad2ed8e2d45fda90868be687c6 Mon Sep 17 00:00:00 2001 From: Lance Pioch Date: Tue, 8 Sep 2026 22:33:31 -0400 Subject: [PATCH 2/3] Drop PHP 8.3 from the PHPStan matrix to match the raised floor The vendor platform check fatals on 8.3 now that composer requires ^8.4, which killed the whole fail-fast lint matrix. --- .github/workflows/lint.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 039d526770..03a70057f0 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -35,7 +35,7 @@ jobs: strategy: fail-fast: true matrix: - php: [8.3, 8.4, 8.5] + php: [8.4, 8.5] steps: - name: Code Checkout uses: actions/checkout@v6 From 442ae348a39c1060c9ccff9ba5a7141b76e985b1 Mon Sep 17 00:00:00 2001 From: Lance Pioch Date: Tue, 8 Sep 2026 22:55:36 -0400 Subject: [PATCH 3/3] Use is_callable for the event toArray check method_exists is true for protected or private toArray methods, which would still throw when called from the listener. --- app/Listeners/DispatchWebhooks.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Listeners/DispatchWebhooks.php b/app/Listeners/DispatchWebhooks.php index 9476c93cc1..80c184036a 100644 --- a/app/Listeners/DispatchWebhooks.php +++ b/app/Listeners/DispatchWebhooks.php @@ -87,7 +87,7 @@ protected function handleGenericClassEvent(string $eventName, array $payload): v $webhookData = ['event' => $eventName, 'timestamp' => now()->toIso8601String()]; if (is_object($obj)) { // Custom event classes rarely implement toArray, so fall back to their public properties - $webhookData['data'] = method_exists($obj, 'toArray') ? $obj->toArray() : get_object_vars($obj); + $webhookData['data'] = is_callable([$obj, 'toArray']) ? $obj->toArray() : get_object_vars($obj); } elseif (is_array($obj)) { $webhookData['data'] = $obj; }