- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
179 lines
7.1 KiB
PHP
179 lines
7.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Database\ValueObjects\TableName;
|
|
use App\Framework\Database\ValueObjects\ColumnName;
|
|
|
|
describe('TableName', function () {
|
|
it('creates table name from string', function () {
|
|
$tableName = TableName::fromString('users');
|
|
|
|
expect($tableName->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');
|
|
});
|
|
});
|