newModel($modelClass); $this->assertModelFieldsExist($model); $record = $this->fakeModelRecord($model); $id = $this->primaryKeyValue($record, $model->primaryKey); $this->assertNotNull($id); $fetched = $model->find($id); $this->assertNotNull($fetched); $this->assertEquals($id, $this->recordValue($fetched, $model->primaryKey)); } protected function assertModelCanUpdate(string $modelClass): void { $model = $this->newModel($modelClass); $this->assertModelFieldsExist($model); $record = $this->fakeModelRecord($model); $id = $this->primaryKeyValue($record, $model->primaryKey); $updateData = $this->updateData($model, $record); $this->assertNotFalse($model->update($id, $updateData)); } protected function assertModelCanDelete(string $modelClass): void { $model = $this->newModel($modelClass); $this->assertModelFieldsExist($model); $record = $this->fakeModelRecord($model); $id = $this->primaryKeyValue($record, $model->primaryKey); $model->delete($id); $this->assertNull($model->find($id)); } protected function primaryKeyValue(array|object $record, string $primaryKey): int|string { return $this->recordValue($record, $primaryKey); } protected function validSchoolYear(): string { $row = Database::connect('tests') ->table('school_years') ->select('name') ->orderBy('name', 'DESC') ->get() ->getRowArray(); if ($row === null) { $year = $this->fallbackSchoolYear(); $db = Database::connect('tests'); $fields = $db->getFieldNames('school_years'); $data = [ 'name' => $year, 'status' => 'active', ]; foreach (['activated_at', 'created_at', 'updated_at'] as $field) { if (in_array($field, $fields, true)) { $data[$field] = date('Y-m-d H:i:s'); } } $db->table('school_years')->insert($data); return $year; } return $row['name']; } protected function fakeModelRecord(Model $model): array|object { return fake($model, $this->fabricatorOverrides($model)); } protected function assertModelFieldsExist(Model $model): void { $db = Database::connect('tests'); $this->assertTrue( $db->tableExists($model->table, false), sprintf('%s maps to table "%s", but that table is not present in the migrated test schema.', $model::class, $model->table) ); $databaseFields = $db->getFieldNames($model->table); foreach ($model->allowedFields as $field) { $this->assertContains( $field, $databaseFields, sprintf( '%s allows field "%s", but table "%s" does not contain it.', $model::class, $field, $model->table ) ); } } protected function fabricatorOverrides(Model $model): array { $overrides = []; if (in_array('school_year', $model->allowedFields, true)) { $overrides['school_year'] = $this->validSchoolYear(); } foreach ($model->validationRules as $field => $rules) { if (! in_array($field, $model->allowedFields, true)) { continue; } $rules = is_array($rules) ? implode('|', $rules) : (string) $rules; if (preg_match('/(?:^|\|)in_list\[([^\]]+)\]/', $rules, $matches) === 1) { $overrides[$field] = explode(',', $matches[1])[0]; } elseif (str_contains($rules, 'valid_email')) { $overrides[$field] = uniqid('model-test-', true) . '@example.test'; } elseif (str_contains($rules, 'decimal')) { $overrides[$field] = '1.00'; } elseif (str_contains($rules, 'integer')) { $overrides[$field] = 1; } elseif (str_contains($rules, 'valid_date[Y-m-d H:i:s]')) { $overrides[$field] = date('Y-m-d H:i:s'); } elseif (str_contains($rules, 'valid_date')) { $overrides[$field] = date('Y-m-d'); } } foreach ($model->allowedFields as $field) { if (array_key_exists($field, $overrides)) { continue; } if ($field === 'email') { $overrides[$field] = uniqid('model-test-', true) . '@example.test'; } elseif ($field === 'status') { $overrides[$field] = 'Active'; } elseif (str_ends_with($field, '_id')) { $overrides[$field] = 1; } elseif (str_starts_with($field, 'is_') || str_starts_with($field, 'has_')) { $overrides[$field] = 0; } } return $overrides; } protected function recordValue(array|object $record, string $field): int|string|null { if (is_array($record)) { return $record[$field] ?? null; } return $record->{$field} ?? null; } protected function updateData(Model $model, array|object $record): array { foreach ($model->allowedFields as $field) { if ($field === $model->primaryKey) { continue; } $rules = $model->validationRules[$field] ?? ''; $rules = is_array($rules) ? implode('|', $rules) : (string) $rules; if (str_contains($rules, 'is_unique')) { continue; } $value = $this->recordValue($record, $field); if ($value !== null) { return [$field => $value]; } } if ($model->useTimestamps && $model->updatedField !== '') { return [$model->updatedField => date('Y-m-d H:i:s')]; } throw new RuntimeException(sprintf('No updateable data is available for %s.', $model::class)); } private function newModel(string $modelClass): Model { $this->assertTrue(class_exists($modelClass), sprintf('Model %s is not part of the application.', $modelClass)); $model = new $modelClass(); if (! $model instanceof Model) { throw new RuntimeException(sprintf('%s is not a CodeIgniter model.', $modelClass)); } return $model; } private function fallbackSchoolYear(): string { $year = (int) date('Y'); return ($year - 1) . '-' . $year; } }