From e8796d443e7c0578189aade57a77d5f3ca78b443 Mon Sep 17 00:00:00 2001 From: jwed Date: Sun, 24 May 2026 19:34:42 -0400 Subject: [PATCH] fix(webhooks): test endpoint formats payload per platform; add missing column MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * The Test URL button was POSTing a generic {event, timestamp, data} envelope to every endpoint. Google Chat / Slack / Discord / Teams reject anything that isn't their specific shape — so a successful Laravel request still got a 400 back from the platform, making the test look broken. The real webhook events already handle this via WebhookCheckService::formatPayloadForPlatform; that helper is now exposed as a public static (buildPlatformPayload) and the test endpoint uses the same code path, so the test exercises the same format real events will. * unifi_device_states was missing a consecutive_count column the WebhookCheckService inserts on every snapshot capture. The scheduler was throwing "Unknown column 'consecutive_count'" once a minute. Added an idempotent migration. v1.6.1. Co-Authored-By: Claude Opus 4.7 (1M context) --- composer.json | 2 +- ...nsecutive_count_to_unifi_device_states.php | 26 +++++++++++++++++++ src/Http/Controllers/WebhookController.php | 10 +++++-- src/Services/WebhookCheckService.php | 17 +++++++++--- 4 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 database/migrations/2026_05_24_000003_add_consecutive_count_to_unifi_device_states.php diff --git a/composer.json b/composer.json index 9454267..048a573 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "dashboard/unifi", "description": "UniFi network management, WiFi stats, and captive portal authentication for the Dashboard platform", - "version": "1.6.0", + "version": "1.6.1", "type": "library", "license": "MIT", "autoload": { diff --git a/database/migrations/2026_05_24_000003_add_consecutive_count_to_unifi_device_states.php b/database/migrations/2026_05_24_000003_add_consecutive_count_to_unifi_device_states.php new file mode 100644 index 0000000..2ea029c --- /dev/null +++ b/database/migrations/2026_05_24_000003_add_consecutive_count_to_unifi_device_states.php @@ -0,0 +1,26 @@ +unsignedSmallInteger('consecutive_count')->default(0)->after('in_alert'); + } + }); + } + + public function down(): void + { + Schema::table('unifi_device_states', function (Blueprint $table) { + if (Schema::hasColumn('unifi_device_states', 'consecutive_count')) { + $table->dropColumn('consecutive_count'); + } + }); + } +}; diff --git a/src/Http/Controllers/WebhookController.php b/src/Http/Controllers/WebhookController.php index 92847e7..32a5c72 100644 --- a/src/Http/Controllers/WebhookController.php +++ b/src/Http/Controllers/WebhookController.php @@ -87,11 +87,17 @@ class WebhookController extends Controller private function fireTest(string $url, ?string $secret) { - $payload = [ + $message = '✅ Test webhook from ' . config('app.name') . ' — endpoint is reachable.'; + $genericPayload = [ 'event' => 'test', 'timestamp' => now()->toIso8601String(), - 'data' => ['message' => 'This is a test webhook from ' . config('app.name')], + 'message' => $message, + 'data' => ['message' => $message], ]; + // Shape the payload to match the target platform (Google Chat, + // Slack, Discord, Teams) so the test exercises the same code + // path real events use. + $payload = \Dashboard\Unifi\Services\WebhookCheckService::buildPlatformPayload($url, $message, $genericPayload); $headers = ['Content-Type' => 'application/json']; if ($secret) { diff --git a/src/Services/WebhookCheckService.php b/src/Services/WebhookCheckService.php index 7093bc3..ef23178 100644 --- a/src/Services/WebhookCheckService.php +++ b/src/Services/WebhookCheckService.php @@ -581,10 +581,19 @@ class WebhookCheckService private function formatPayloadForPlatform(string $url, string $message, array $fullPayload): array { - if (str_contains($url, 'chat.googleapis.com')) return ['text' => $message]; - if (str_contains($url, 'hooks.slack.com')) return ['text' => $message]; - if (str_contains($url, 'discord.com/api/webhooks')) return ['content' => $message]; - if (str_contains($url, 'webhook.office.com') || str_contains($url, 'workflows.office.com')) return ['text' => $message]; + return self::buildPlatformPayload($url, $message, $fullPayload); + } + + /** + * Public/static helper so the test-webhook endpoint produces the + * same per-platform payload shape that real events do. + */ + public static function buildPlatformPayload(string $url, string $message, array $fullPayload): array + { + if (str_contains($url, 'chat.googleapis.com')) return ['text' => $message]; + if (str_contains($url, 'hooks.slack.com')) return ['text' => $message]; + if (str_contains($url, 'discord.com/api/webhooks')) return ['content' => $message]; + if (str_contains($url, 'webhook.office.com') || str_contains($url, 'workflows.office.com')) return ['text' => $message]; return $fullPayload; }