Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 31 additions & 7 deletions resources/js/Pages/Events/Index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
This JSON template will be used when triggering the event. Use variables like {<!-- -->{user_id}<!-- -->} for dynamic values.
</p>
<InputError :message="form.errors.payload" class="mt-2" />
<InputError :message="payloadJsonError || form.errors.payload" class="mt-2" />
</div>

<div>
Expand All @@ -245,7 +245,7 @@
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Optional. Define expected payload fields using Laravel validation rules. Trigger requests that don't match will be rejected with a 422.
</p>
<InputError :message="form.errors.schema" class="mt-2" />
<InputError :message="schemaJsonError || form.errors.schema" class="mt-2" />
</div>
</form>
</template>
Expand All @@ -257,7 +257,7 @@

<PrimaryButton
@click="saveEvent"
:disabled="form.processing"
:disabled="form.processing || !!payloadJsonError || !!schemaJsonError"
class="ml-3"
>
{{ form.processing ? 'Saving...' : 'Create' }}
Expand Down Expand Up @@ -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([])
Expand Down Expand Up @@ -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
}
})

Expand All @@ -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()
})
Expand Down
48 changes: 48 additions & 0 deletions tests/Feature/DashboardEventCreateTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace Tests\Feature;

use App\Models\Event;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class DashboardEventCreateTest extends TestCase
{
use RefreshDatabase;

public function test_dashboard_create_persists_payload_and_schema(): void
{
$user = User::factory()->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());
}
}
20 changes: 20 additions & 0 deletions tests/e2e/specs/events.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down
Loading