value)->toBe('users'); expect($tableName->toString())->toBe('users'); }); it('validates table name format', function () { // Valid names expect(fn() => TableName::fromString('users'))->not->toThrow(\InvalidArgumentException::class); expect(fn() => TableName::fromString('user_profiles'))->not->toThrow(\InvalidArgumentException::class); expect(fn() => TableName::fromString('_temp_table'))->not->toThrow(\InvalidArgumentException::class); expect(fn() => TableName::fromString('table123'))->not->toThrow(\InvalidArgumentException::class); // Invalid names expect(fn() => TableName::fromString('')) ->toThrow(\InvalidArgumentException::class, 'cannot be empty'); expect(fn() => TableName::fromString('123invalid')) ->toThrow(\InvalidArgumentException::class, 'must start with a letter or underscore'); expect(fn() => TableName::fromString('invalid-name')) ->toThrow(\InvalidArgumentException::class, 'can only contain letters, numbers, and underscores'); expect(fn() => TableName::fromString('invalid name')) ->toThrow(\InvalidArgumentException::class, 'can only contain letters, numbers, and underscores'); }); it('validates maximum length', function () { $validName = str_repeat('a', 64); expect(fn() => TableName::fromString($validName))->not->toThrow(\InvalidArgumentException::class); $tooLong = str_repeat('a', 65); expect(fn() => TableName::fromString($tooLong)) ->toThrow(\InvalidArgumentException::class, 'exceeds maximum length'); }); it('detects SQL injection attempts', function () { // Note: Most SQL injection attempts are caught by format validation first, // since they contain invalid characters (quotes, hyphens, spaces, etc.) // These all fail format validation (contain invalid characters) expect(fn() => TableName::fromString("users'; DROP TABLE users--")) ->toThrow(\InvalidArgumentException::class); expect(fn() => TableName::fromString('users UNION SELECT')) ->toThrow(\InvalidArgumentException::class); expect(fn() => TableName::fromString('users/*comment*/')) ->toThrow(\InvalidArgumentException::class); }); it('quotes table names for different platforms', function () { $tableName = TableName::fromString('users'); expect($tableName->quoted('mysql'))->toBe('`users`'); expect($tableName->quoted('postgresql'))->toBe('"users"'); expect($tableName->quoted('postgres'))->toBe('"users"'); expect($tableName->quoted('pgsql'))->toBe('"users"'); expect($tableName->quoted('sqlite'))->toBe('"users"'); expect($tableName->quoted())->toBe('`users`'); // Default MySQL expect($tableName->quoted('unknown'))->toBe('`users`'); // Fallback to MySQL }); it('compares table names for equality', function () { $table1 = TableName::fromString('users'); $table2 = TableName::fromString('users'); $table3 = TableName::fromString('USERS'); // Different case $table4 = TableName::fromString('orders'); expect($table1->equals($table2))->toBeTrue(); expect($table1->equals($table3))->toBeTrue(); // Case-insensitive expect($table1->equals($table4))->toBeFalse(); }); it('matches table name patterns', function () { $table = TableName::fromString('user_profiles'); expect($table->matches('user_*'))->toBeTrue(); expect($table->matches('*_profiles'))->toBeTrue(); expect($table->matches('user_profiles'))->toBeTrue(); expect($table->matches('order_*'))->toBeFalse(); }); it('detects reserved SQL keywords', function () { $table = TableName::fromString('users'); expect($table->isReservedKeyword())->toBeFalse(); $reservedTable = TableName::fromString('_select'); // Starts with underscore to be valid // Note: 'select' itself is reserved, but '_select' is not expect($reservedTable->isReservedKeyword())->toBeFalse(); }); it('converts to lowercase', function () { $table = TableName::fromString('UserProfiles'); expect($table->toLower())->toBe('userprofiles'); }); it('checks for table name prefix', function () { $table = TableName::fromString('wp_users'); expect($table->hasPrefix('wp_'))->toBeTrue(); expect($table->hasPrefix('drupal_'))->toBeFalse(); }); it('adds prefix to table name', function () { $table = TableName::fromString('users'); $prefixed = $table->withPrefix('wp_'); expect($prefixed->value)->toBe('wp_users'); expect($prefixed->toString())->toBe('wp_users'); // Original unchanged (immutable) expect($table->value)->toBe('users'); }); it('removes prefix from table name', function () { $table = TableName::fromString('wp_users'); $unprefixed = $table->withoutPrefix('wp_'); expect($unprefixed->value)->toBe('users'); // Removing non-existent prefix returns same table $same = $unprefixed->withoutPrefix('drupal_'); expect($same->value)->toBe('users'); }); it('converts to string via magic method', function () { $table = TableName::fromString('users'); expect((string) $table)->toBe('users'); }); it('is immutable', function () { $original = TableName::fromString('users'); $prefixed = $original->withPrefix('wp_'); // Original remains unchanged expect($original->value)->toBe('users'); // New instance created when prefix is added expect($prefixed)->not->toBe($original); expect($prefixed->value)->toBe('wp_users'); // Removing non-existent prefix returns same instance (optimization) $unprefixed = $original->withoutPrefix('wp_'); expect($unprefixed->value)->toBe('users'); // But removing actual prefix creates new instance $prefixedTable = TableName::fromString('wp_users'); $removedPrefix = $prefixedTable->withoutPrefix('wp_'); expect($removedPrefix)->not->toBe($prefixedTable); expect($removedPrefix->value)->toBe('users'); }); it('handles edge cases correctly', function () { // Single character (valid if letter or underscore) expect(fn() => TableName::fromString('a'))->not->toThrow(\InvalidArgumentException::class); expect(fn() => TableName::fromString('_'))->not->toThrow(\InvalidArgumentException::class); // Underscore-only prefix $table = TableName::fromString('_temp_users'); expect($table->value)->toBe('_temp_users'); // Numbers in name (but not at start) $table = TableName::fromString('table_123_test'); expect($table->value)->toBe('table_123_test'); }); });