diff --git a/resources/js/Pages/Events/Index.vue b/resources/js/Pages/Events/Index.vue index dd716d2..141af76 100644 --- a/resources/js/Pages/Events/Index.vue +++ b/resources/js/Pages/Events/Index.vue @@ -230,7 +230,7 @@

This JSON template will be used when triggering the event. Use variables like {{user_id}} for dynamic values.

- +
@@ -245,7 +245,7 @@

Optional. Define expected payload fields using Laravel validation rules. Trigger requests that don't match will be rejected with a 422.

- +
@@ -257,7 +257,7 @@ {{ form.processing ? 'Saving...' : 'Create' }} @@ -388,6 +388,8 @@ const managingEvent = ref(null) const triggeringEvent = ref(null) const payloadText = ref('') const schemaText = ref('') +const payloadJsonError = ref('') +const schemaJsonError = ref('') const triggerPayload = ref('') const triggerProcessing = ref(false) const selectedEndpoints = ref([]) @@ -433,18 +435,34 @@ const availableEndpoints = computed(() => props.endpoints || []) // Watch for payload changes watch(payloadText, (newValue) => { + if (!newValue) { + form.payload = null + payloadJsonError.value = '' + return + } + try { - form.payload = newValue ? JSON.parse(newValue) : null + form.payload = JSON.parse(newValue) + payloadJsonError.value = '' } catch (e) { - // Invalid JSON - will be handled by backend validation + // Invalid JSON - surface the error and stop the stale value from being submitted + payloadJsonError.value = 'Invalid JSON: ' + e.message } }) watch(schemaText, (newValue) => { + if (!newValue) { + form.schema = null + schemaJsonError.value = '' + return + } + try { - form.schema = newValue ? JSON.parse(newValue) : null + form.schema = JSON.parse(newValue) + schemaJsonError.value = '' } catch (e) { - // Invalid JSON - will be handled by backend validation + // Invalid JSON - surface the error and stop the stale value from being submitted + schemaJsonError.value = 'Invalid JSON: ' + e.message } }) @@ -455,9 +473,15 @@ function closeModal() { form.clearErrors() payloadText.value = '' schemaText.value = '' + payloadJsonError.value = '' + schemaJsonError.value = '' } function saveEvent() { + if (payloadJsonError.value || schemaJsonError.value) { + return + } + form.post(route('events.store'), { onSuccess: () => closeModal() }) diff --git a/tests/Feature/DashboardEventCreateTest.php b/tests/Feature/DashboardEventCreateTest.php new file mode 100644 index 0000000..e4516ca --- /dev/null +++ b/tests/Feature/DashboardEventCreateTest.php @@ -0,0 +1,48 @@ +withPersonalTeam()->create(); + + $response = $this->actingAs($user)->post(route('events.store'), [ + 'name' => 'order.created', + 'event_type' => 'order.created', + 'description' => 'Fired when an order is created', + 'payload' => ['user_id' => 1, 'nested' => ['count' => 2]], + 'schema' => ['user_id' => 'required|integer'], + ]); + + $response->assertRedirect(route('events')); + $response->assertSessionHas('success'); + + $event = Event::where('user_id', $user->id)->where('name', 'order.created')->firstOrFail(); + + $this->assertSame(['user_id' => 1, 'nested' => ['count' => 2]], $event->payload); + $this->assertSame(['user_id' => 'required|integer'], $event->schema); + } + + public function test_dashboard_create_rejects_duplicate_name_for_same_user(): void + { + $user = User::factory()->withPersonalTeam()->create(); + Event::factory()->for($user)->create(['name' => 'order.created']); + + $response = $this->actingAs($user)->post(route('events.store'), [ + 'name' => 'order.created', + 'payload' => ['user_id' => 1], + ]); + + $response->assertSessionHasErrors('name'); + $this->assertSame(1, Event::where('user_id', $user->id)->where('name', 'order.created')->count()); + } +} diff --git a/tests/e2e/specs/events.spec.ts b/tests/e2e/specs/events.spec.ts index 3cc0c4a..d6e24f1 100644 --- a/tests/e2e/specs/events.spec.ts +++ b/tests/e2e/specs/events.spec.ts @@ -44,6 +44,26 @@ test.describe('Events', () => { await expect(page.getByText(EVENT_NAME)).toBeVisible({ timeout: 10_000 }) }) + test('rejects invalid JSON payload in the create modal instead of silently discarding it', async ({ page }) => { + await page.getByRole('button', { name: /create event/i }).click() + + const createButton = page.getByRole('button', { name: /^create$/i }) + await expect(createButton).toBeEnabled() + + // Break the JSON - the Create button must disable and an error must appear, + // instead of silently keeping the last valid value and reporting success. + await page.fill('#payload', '{ "user_id": 1, ') + await expect(page.getByText(/invalid json/i)).toBeVisible() + await expect(createButton).toBeDisabled() + + // Fixing the JSON clears the error and re-enables saving. + await page.fill('#payload', JSON.stringify({ user_id: 1 })) + await expect(page.getByText(/invalid json/i)).not.toBeVisible() + await expect(createButton).toBeEnabled() + + await page.getByRole('button', { name: /cancel/i }).click() + }) + test('searches events by name', async ({ page }) => { await page.fill('input[placeholder*="Search"]', EVENT_NAME) await expect(page.getByText(EVENT_NAME)).toBeVisible({ timeout: 8_000 })