assertContains( HasUlids::class, class_uses_recursive($modelClass), $modelClass.' must use HasUlids trait (addendum Q1)' ); $model = (new $modelClass()); $this->assertSame( 'string', $model->getKeyType(), $modelClass.'::getKeyType() must be "string" for ULID keys' ); // Creating-event-driven ULID generation $instance = new $modelClass(); $instance->setAttribute($instance->getKeyName(), null); $reflection = new \ReflectionClass($instance); // Simulate the creating hook exactly as Eloquent would. $method = $reflection->getMethod('newUniqueId'); $generated = $method->invoke($instance); $this->assertMatchesRegularExpression( self::CROCKFORD_ULID_PATTERN, (string) $generated, $modelClass.'::newUniqueId() must return a 26-char Crockford ULID' ); } /** @return array */ public static function modelBackedAcategoryTables(): array { return [ 'A-05 UserOrganisationTag' => [UserOrganisationTag::class], 'A-06 PersonSectionPreference' => [PersonSectionPreference::class], 'A-09 FormSubmissionSectionStatus' => [FormSubmissionSectionStatus::class], 'A-10 FormValue' => [FormValue::class], 'A-11 FormValueOption' => [FormValueOption::class], ]; } public function test_route_model_binding_resolves_a_category_models(): void { $organisation = Organisation::factory()->create(); $event = Event::factory()->for($organisation)->create(); $festivalSection = FestivalSection::factory()->for($event)->create(); $person = Person::factory()->for($event)->create(); $tag = PersonTag::factory()->for($organisation)->create(); $user = User::factory()->create(); $uotag = UserOrganisationTag::create([ 'user_id' => $user->id, 'organisation_id' => $organisation->id, 'person_tag_id' => $tag->id, 'source' => 'self_reported', 'assigned_at' => now(), ]); $preference = PersonSectionPreference::create([ 'person_id' => $person->id, 'festival_section_id' => $festivalSection->id, 'priority' => 1, ]); // Route::bind resolves via implicit model binding because // getRouteKeyName() defaults to the primary key, which is now a ULID. $this->assertEquals($uotag->id, $uotag->resolveRouteBinding($uotag->id)?->id); $this->assertEquals($preference->id, $preference->resolveRouteBinding($preference->id)?->id); } public function test_pure_pivot_tables_generate_distinct_sortable_crockford_ulids(): void { $organisation = Organisation::factory()->create(); $event = Event::factory()->for($organisation)->create(); $crowdType = CrowdType::factory()->for($organisation)->create(); $person1 = Person::factory()->for($event)->for($crowdType)->create(); $person2 = Person::factory()->for($event)->for($crowdType)->create(); // A-04 event_person_activations is seeded via DB::table() in the // dev seeder and attach-less in production; insert two rows and // assert the resulting ULIDs. $idA = (string) Str::ulid(); $idB = (string) Str::ulid(); DB::table('event_person_activations')->insert([ ['id' => $idA, 'event_id' => $event->id, 'person_id' => $person1->id], ['id' => $idB, 'event_id' => $event->id, 'person_id' => $person2->id], ]); $rows = DB::table('event_person_activations')->orderBy('id')->pluck('id'); $this->assertCount(2, $rows); $this->assertNotEquals($rows[0], $rows[1], 'Pivot ULIDs must be distinct'); foreach ($rows as $id) { $this->assertMatchesRegularExpression( self::CROCKFORD_ULID_PATTERN, (string) $id, 'Pure pivot rows must carry 26-char Crockford ULIDs' ); } $this->assertSame( [$idA, $idB], $rows->toArray(), 'ULIDs must sort lexicographically by creation order' ); } public function test_organisation_user_pivot_auto_generates_ulid_on_attach(): void { // A-01 organisation_user is driven by Eloquent belongsToMany // ->using(OrganisationUser::class). attach() must produce a ULID // via the Pivot's HasUlids trait. $organisation = Organisation::factory()->create(); $user = User::factory()->create(); $user->organisations()->attach($organisation, ['role' => 'org_admin']); $pivotId = DB::table('organisation_user') ->where('user_id', $user->id) ->where('organisation_id', $organisation->id) ->value('id'); $this->assertIsString($pivotId); $this->assertMatchesRegularExpression( self::CROCKFORD_ULID_PATTERN, (string) $pivotId ); } public function test_form_value_option_fk_chain_resolves_with_ulids(): void { // A-10/A-11 coupling: form_value_options.form_value_id FK must be // ULID-typed after the combined migration. Insert a full chain and // verify the join path works end-to-end. $organisation = Organisation::factory()->create(); $schema = FormSchema::factory()->for($organisation)->create(); $section = FormSchemaSection::factory()->for($schema, 'schema')->create(); $field = FormField::factory()->for($section, 'section')->for($schema, 'schema')->create(); $submission = FormSubmission::factory()->for($schema, 'schema')->create(); $value = FormValue::create([ 'form_submission_id' => $submission->id, 'form_field_id' => $field->id, 'value' => ['value' => 'x'], 'value_anonymised' => false, ]); $option = FormValueOption::create([ 'form_value_id' => $value->id, 'form_field_id' => $field->id, 'form_submission_id' => $submission->id, 'option_value' => 'x', ]); $this->assertMatchesRegularExpression(self::CROCKFORD_ULID_PATTERN, (string) $value->id); $this->assertMatchesRegularExpression(self::CROCKFORD_ULID_PATTERN, (string) $option->id); $this->assertSame($value->id, $option->form_value_id); } public function test_pivot_model_class_uses_has_ulids(): void { // The 4 pure-pivot tables are wired via Pivot subclasses with // HasUlids — addendum Q1 literal text says "no model required", // but Laravel's BelongsToMany::attach() cannot auto-generate the // ULID without a Pivot class. Minimal Pivot models were added. $this->assertContains( HasUlids::class, class_uses_recursive(EventPersonActivation::class), 'Pivot classes wired to pure pivots must use HasUlids' ); } }