not->toBeFalse(); expect($bootstrapContent)->toContain("'selfserve_wash_session_tasks'"); expect($bootstrapContent)->toContain("'dynamic_images_vehicle_type'"); expect($bootstrapContent)->toContain('ALTER TABLE selfserve_wash_session_tasks ADD COLUMN dynamic_images_vehicle_type INT NULL AFTER buttons'); }); it('widens self-serve task descriptions for generated workbook instructions', function (): void { $bootstrapContent = file_get_contents(app_path('classes/selfserve_schema_bootstrap.php')); expect($bootstrapContent)->not->toBeFalse(); expect($bootstrapContent)->toContain('description TEXT NULL'); expect($bootstrapContent)->toContain('ensureColumnDataType('); expect($bootstrapContent)->toContain("['text', 'mediumtext', 'longtext']"); expect($bootstrapContent)->toContain('ALTER TABLE department_selfserve_tasks MODIFY COLUMN description TEXT NULL AFTER task'); expect($bootstrapContent)->toContain('ALTER TABLE selfserve_wash_session_tasks MODIFY COLUMN description TEXT NULL AFTER task_text'); }); it('alters legacy bounded description columns to text', function (): void { $hadDb = array_key_exists('db', $GLOBALS); $previousDb = $GLOBALS['db'] ?? null; try { foreach (['varchar', 'tinytext'] as $legacyType) { $fakeDb = new class ($legacyType) { public array $queries = []; public function __construct(private readonly string $dataType) { } public function escape_string(string $value): string { return addslashes($value); } public function getDatabase(): string { return 'test_db'; } public function query(string $sql): object|bool { $this->queries[] = $sql; if (str_contains($sql, 'information_schema.COLUMNS')) { return new class ($this->dataType) { public function __construct(private readonly string $dataType) { } public function fetch_assoc(): array { return ['DATA_TYPE' => $this->dataType]; } }; } return true; } }; $GLOBALS['db'] = $fakeDb; \classes\selfserve_schema_bootstrap::ensureColumnDataType( 'selfserve_wash_session_tasks', 'description', ['text', 'mediumtext', 'longtext'], 'ALTER TABLE selfserve_wash_session_tasks MODIFY COLUMN description TEXT NULL AFTER task_text' ); expect($fakeDb->queries)->toContain('ALTER TABLE selfserve_wash_session_tasks MODIFY COLUMN description TEXT NULL AFTER task_text'); } $textDb = new class { public array $queries = []; public function escape_string(string $value): string { return addslashes($value); } public function getDatabase(): string { return 'test_db'; } public function query(string $sql): object|bool { $this->queries[] = $sql; if (str_contains($sql, 'information_schema.COLUMNS')) { return new class { public function fetch_assoc(): array { return ['DATA_TYPE' => 'text']; } }; } return true; } }; $GLOBALS['db'] = $textDb; \classes\selfserve_schema_bootstrap::ensureColumnDataType( 'selfserve_wash_session_tasks', 'description', ['text', 'mediumtext', 'longtext'], 'ALTER TABLE selfserve_wash_session_tasks MODIFY COLUMN description TEXT NULL AFTER task_text' ); expect(implode("\n", $textDb->queries))->not->toContain('ALTER TABLE selfserve_wash_session_tasks MODIFY COLUMN description TEXT NULL AFTER task_text'); } finally { if ($hadDb) { $GLOBALS['db'] = $previousDb; } else { unset($GLOBALS['db']); } } }); it('adds wash_started_at column for legacy selfserve wash session schemas', function (): void { $bootstrapContent = file_get_contents(app_path('classes/selfserve_schema_bootstrap.php')); expect($bootstrapContent)->not->toBeFalse(); expect($bootstrapContent)->toContain("'selfserve_wash_sessions'"); expect($bootstrapContent)->toContain("'wash_started_at'"); expect($bootstrapContent)->toContain('ALTER TABLE selfserve_wash_sessions ADD COLUMN wash_started_at DATETIME NULL AFTER machine_start_triggered_at'); }); it('adds reporting indexes for completed self-serve wash session counts', function (): void { $bootstrapContent = file_get_contents(app_path('classes/selfserve_schema_bootstrap.php')); $apiSchemaContent = file_get_contents(app_path('tests/Support/Api/ApiSchemaBootstrap.php')); expect($bootstrapContent)->not->toBeFalse() ->and($apiSchemaContent)->not->toBeFalse(); expect($bootstrapContent)->toContain('idx_selfserve_wash_sessions_department_completed') ->and($bootstrapContent)->toContain('ALTER TABLE selfserve_wash_sessions ADD INDEX idx_selfserve_wash_sessions_department_completed (department_id, completed_at)') ->and($bootstrapContent)->toContain('idx_selfserve_wash_sessions_order') ->and($bootstrapContent)->toContain('ALTER TABLE selfserve_wash_sessions ADD INDEX idx_selfserve_wash_sessions_order (order_id)') ->and($bootstrapContent)->toContain('public static function ensureIndex'); expect($apiSchemaContent)->toContain('idx_selfserve_wash_sessions_department_completed') ->and($apiSchemaContent)->toContain('idx_selfserve_wash_sessions_order'); }); it('adds lane-level self-serve enablement for existing department lanes', function (): void { $bootstrapContent = file_get_contents(app_path('classes/selfserve_schema_bootstrap.php')); expect($bootstrapContent)->not->toBeFalse(); expect($bootstrapContent)->toContain("'department_lanes'"); expect($bootstrapContent)->toContain("'selfserve_enabled'"); expect($bootstrapContent)->toContain('ALTER TABLE department_lanes ADD COLUMN selfserve_enabled TINYINT(1) NOT NULL DEFAULT 1 AFTER machine_type_id'); }); it('creates canvas-only self-serve studio layout storage', function (): void { $bootstrapContent = file_get_contents(app_path('classes/selfserve_schema_bootstrap.php')); expect($bootstrapContent)->not->toBeFalse(); expect($bootstrapContent)->toContain('CREATE TABLE IF NOT EXISTS department_selfserve_studio_layouts'); expect($bootstrapContent)->toContain('layout_json JSON NOT NULL'); expect($bootstrapContent)->toContain('idx_department_selfserve_studio_layouts_department_user'); }); it('uses mysql-safe identifiers for self-serve studio virtual hardware storage', function (): void { $bootstrapContent = file_get_contents(app_path('classes/selfserve_schema_bootstrap.php')); expect($bootstrapContent)->not->toBeFalse(); expect($bootstrapContent)->toContain('CREATE TABLE IF NOT EXISTS department_selfserve_studio_virtual_hardware'); expect($bootstrapContent)->toContain('UNIQUE KEY uniq_selfserve_vhw_department'); expect($bootstrapContent)->toContain('INDEX idx_selfserve_vhw_dept_updated'); expect($bootstrapContent)->not->toContain('idx_department_selfserve_studio_virtual_hardware_department_updated'); preg_match_all('/\b(?:UNIQUE\s+KEY|INDEX)\s+([a-zA-Z0-9_]+)/', $bootstrapContent, $matches); foreach ($matches[1] as $identifier) { expect(strlen($identifier))->toBeLessThanOrEqual(64); } }); it('creates self-serve studio path confirmation storage', function (): void { $bootstrapContent = file_get_contents(app_path('classes/selfserve_schema_bootstrap.php')); expect($bootstrapContent)->not->toBeFalse(); expect($bootstrapContent)->toContain('CREATE TABLE IF NOT EXISTS department_selfserve_path_confirmations'); expect($bootstrapContent)->toContain('path_signature VARCHAR(128) NOT NULL'); expect($bootstrapContent)->toContain('result_signature VARCHAR(128) NOT NULL'); expect($bootstrapContent)->toContain('idx_selfserve_path_conf_signature'); });