Test infrastructure now uses the same MySQL 8.0 engine as local dev
and production. SQLite is no longer used anywhere in the project.
Eliminates the SQLite "rebuild on FK add" quirk that forced session 2.5
to omit a foreign key on form_schemas.default_crowd_type_id (Task 2 of
this session restores it).
Configuration:
- phpunit.xml: DB_CONNECTION=sqlite (:memory:) replaced with mysql
pointing at crewli_test database (127.0.0.1:3306, crewli/secret)
- Makefile: new test-db-create target creates crewli_test in the
bm_mysql Docker container; make test ensures it exists before
running suite
Latent-bug surfacing — fixes that MySQL exposed:
1. form_submissions.idempotency_key was declared `ulid()` (VARCHAR 26)
while FormRequest validates `string|max:30`. SQLite ignored the cap;
MySQL truncated and rejected. Column widened to string(30) to match
validation.
2. FormFieldValidationRuleService / FormFieldConfigService /
FormFieldBindingService::snapshotShapesFor — toJsonShape iterated
collection in DB-default order (insertion-stable on SQLite, undefined
on MySQL). Schema_snapshot bytes drifted across re-emits, breaking
audit-replay. Added `->sortBy('id')` (ULID = insertion-order
semantics, deterministic) on all three.
3. FormSubmissionObserverTest::test_denormalized_indexes_exist queried
sqlite_master directly. Replaced with the cross-engine
information_schema.STATISTICS query (the real production check is
on MySQL anyway).
4. JSON column key order non-determinism: MySQL JSON columns may
round-trip associative-array keys in a different order than they
were inserted. assertSame on JSON-derived associative arrays now
uses assertEquals (structural equality) where the test was previously
over-asserting on key order:
- ConditionalLogicActivityLogPayloadTest
- ConditionalLogicBackfillTest::test_rollback_reconstructs_canonical_json
- FormFieldBindingMigrationTest::test_rollback_reconstructs_json_and_drops_table
- FormFieldOptionServiceAndScopeTest::test_replace_options_emits_activity_log_on_field_only
- FormFieldOptionsActivityLogTest::test_field_updated_payload_contains_options_diff_when_options_change
- FormFieldOptionsBackfillTest::test_forward_migration_backfills_rows_strips_translations_and_rewrites_snapshot
- FormFieldOptionsSnapshotAndStrictRequestTest::test_submission_snapshot_embeds_rich_shape_options
5. Backfill / migration tests (4 classes, 21 tests) ran migrate:rollback
then migrate inside RefreshDatabase's wrapping transaction. MySQL
DDL implicit-commits the surrounding transaction, leaving Laravel
unable to ROLLBACK TO SAVEPOINT at end-of-test (1305 SAVEPOINT
does not exist). Replaced RefreshDatabase with a per-test
migrate:fresh in setUp + RefreshDatabaseState::\$migrated = false to
force the next RefreshDatabase test to re-migrate cleanly:
- FormFieldBindingMigrationTest
- ConditionalLogicBackfillTest
- FormFieldOptionsBackfillTest
- FormFieldValidationRuleBackfillTest
All 1386 tests now pass on MySQL. Larastan baseline unchanged.
Refs: WS-6 session 2.5 deviation #1 cleanup, RFC-WS-6.md v1.1
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
168 lines
5.8 KiB
PHP
168 lines
5.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Feature\FormBuilder;
|
|
|
|
use App\Models\Event;
|
|
use App\Models\FormBuilder\FormSchema;
|
|
use App\Models\FormBuilder\FormSubmission;
|
|
use App\Models\Organisation;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Route;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* WS-4 Commit 2 coverage — addendum Q2 denormalization observer.
|
|
*
|
|
* Guarantees that every form_submissions row carries correct
|
|
* organisation_id (always) and event_id (when resolvable from schema
|
|
* owner or active route).
|
|
*/
|
|
final class FormSubmissionObserverTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
public function test_organisation_id_resolves_from_schema(): void
|
|
{
|
|
$organisation = Organisation::factory()->create();
|
|
$schema = FormSchema::factory()->for($organisation)->create();
|
|
|
|
$submission = FormSubmission::factory()->for($schema, 'schema')->create();
|
|
|
|
$this->assertSame(
|
|
$organisation->id,
|
|
$submission->organisation_id,
|
|
'Observer must denormalize organisation_id from schema parent'
|
|
);
|
|
}
|
|
|
|
public function test_event_id_resolves_from_event_owned_schema(): void
|
|
{
|
|
$organisation = Organisation::factory()->create();
|
|
$event = Event::factory()->for($organisation)->create();
|
|
$schema = FormSchema::factory()->for($organisation)->create([
|
|
'owner_type' => 'event',
|
|
'owner_id' => $event->id,
|
|
]);
|
|
|
|
$submission = FormSubmission::factory()->for($schema, 'schema')->create();
|
|
|
|
$this->assertSame(
|
|
$event->id,
|
|
$submission->event_id,
|
|
'Observer must resolve event_id from schema.owner_id when owner_type = event'
|
|
);
|
|
}
|
|
|
|
public function test_event_id_is_null_when_schema_has_no_event_owner_and_no_route(): void
|
|
{
|
|
$organisation = Organisation::factory()->create();
|
|
$schema = FormSchema::factory()->for($organisation)->create([
|
|
'owner_type' => 'organisation',
|
|
'owner_id' => $organisation->id,
|
|
]);
|
|
|
|
$submission = FormSubmission::factory()->for($schema, 'schema')->create();
|
|
|
|
$this->assertNull(
|
|
$submission->event_id,
|
|
'Non-event-owned schema outside an event route yields event_id = null'
|
|
);
|
|
$this->assertSame($organisation->id, $submission->organisation_id);
|
|
}
|
|
|
|
public function test_event_id_resolves_from_active_route_when_schema_has_no_event_owner(): void
|
|
{
|
|
$organisation = Organisation::factory()->create();
|
|
$event = Event::factory()->for($organisation)->create();
|
|
$schema = FormSchema::factory()->for($organisation)->create([
|
|
'owner_type' => 'organisation',
|
|
'owner_id' => $organisation->id,
|
|
]);
|
|
|
|
// Simulate a request bound to /events/{event}/... by firing the
|
|
// observer inside a controller invocation with the route
|
|
// parameter populated.
|
|
Route::get(
|
|
'/_test/events/{event}/submissions',
|
|
function (Event $event) use ($schema): array {
|
|
$submission = new FormSubmission;
|
|
$submission->form_schema_id = $schema->id;
|
|
$submission->status = 'draft';
|
|
$submission->is_test = false;
|
|
$submission->save();
|
|
|
|
return ['id' => $submission->id, 'event_id' => $submission->event_id];
|
|
}
|
|
);
|
|
|
|
$response = $this->getJson("/_test/events/{$event->id}/submissions");
|
|
$response->assertOk();
|
|
|
|
$this->assertSame(
|
|
$event->id,
|
|
$response->json('event_id'),
|
|
'Route {event} parameter resolves event_id when schema has no event owner'
|
|
);
|
|
}
|
|
|
|
public function test_organisation_id_is_populated_on_every_create_path(): void
|
|
{
|
|
$organisation = Organisation::factory()->create();
|
|
$schema = FormSchema::factory()->for($organisation)->create();
|
|
|
|
// Direct factory
|
|
$s1 = FormSubmission::factory()->for($schema, 'schema')->create();
|
|
// Direct new() + save()
|
|
$s2 = new FormSubmission;
|
|
$s2->form_schema_id = $schema->id;
|
|
$s2->status = 'draft';
|
|
$s2->is_test = false;
|
|
$s2->save();
|
|
// Model::create()
|
|
$s3 = FormSubmission::create([
|
|
'form_schema_id' => $schema->id,
|
|
'status' => 'draft',
|
|
'is_test' => false,
|
|
]);
|
|
|
|
foreach ([$s1, $s2, $s3] as $submission) {
|
|
$this->assertSame(
|
|
$organisation->id,
|
|
$submission->organisation_id,
|
|
'organisation_id must never be NULL — resolved on every create path'
|
|
);
|
|
}
|
|
}
|
|
|
|
public function test_denormalized_indexes_exist(): void
|
|
{
|
|
// Indexes named in the migration — addendum Q2 rapportage-hot.
|
|
// MySQL: query information_schema.STATISTICS.
|
|
$indexNames = collect(DB::select(
|
|
'SELECT INDEX_NAME AS name FROM information_schema.STATISTICS '
|
|
."WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'form_submissions'"
|
|
))->pluck('name')->toArray();
|
|
|
|
$this->assertContains('fs_org_status_idx', $indexNames);
|
|
$this->assertContains('fs_event_status_idx', $indexNames);
|
|
}
|
|
|
|
public function test_organisation_and_event_belongs_to_relationships_resolve(): void
|
|
{
|
|
$organisation = Organisation::factory()->create();
|
|
$event = Event::factory()->for($organisation)->create();
|
|
$schema = FormSchema::factory()->for($organisation)->create([
|
|
'owner_type' => 'event',
|
|
'owner_id' => $event->id,
|
|
]);
|
|
|
|
$submission = FormSubmission::factory()->for($schema, 'schema')->create();
|
|
|
|
$this->assertTrue($submission->organisation->is($organisation));
|
|
$this->assertTrue($submission->event->is($event));
|
|
}
|
|
}
|