- 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.
268 lines
11 KiB
PHP
268 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Database\ValueObjects\IndexName;
|
|
use App\Framework\Database\ValueObjects\TableName;
|
|
use App\Framework\Database\ValueObjects\ColumnName;
|
|
|
|
describe('IndexName', function () {
|
|
it('creates index name from string', function () {
|
|
$index = IndexName::fromString('idx_users_email');
|
|
|
|
expect($index->value)->toBe('idx_users_email');
|
|
expect($index->toString())->toBe('idx_users_email');
|
|
});
|
|
|
|
it('creates PRIMARY KEY index', function () {
|
|
$primary = IndexName::primary();
|
|
|
|
expect($primary->value)->toBe('PRIMARY');
|
|
expect($primary->isPrimary())->toBeTrue();
|
|
});
|
|
|
|
it('validates index name format', function () {
|
|
// Valid names
|
|
expect(fn() => IndexName::fromString('idx_users_email'))->not->toThrow(\InvalidArgumentException::class);
|
|
expect(fn() => IndexName::fromString('unique_users_email'))->not->toThrow(\InvalidArgumentException::class);
|
|
expect(fn() => IndexName::fromString('_temp_index'))->not->toThrow(\InvalidArgumentException::class);
|
|
expect(fn() => IndexName::fromString('index123'))->not->toThrow(\InvalidArgumentException::class);
|
|
|
|
// Invalid names
|
|
expect(fn() => IndexName::fromString(''))
|
|
->toThrow(\InvalidArgumentException::class, 'cannot be empty');
|
|
|
|
expect(fn() => IndexName::fromString('123invalid'))
|
|
->toThrow(\InvalidArgumentException::class, 'must start with a letter or underscore');
|
|
|
|
expect(fn() => IndexName::fromString('invalid-name'))
|
|
->toThrow(\InvalidArgumentException::class, 'can only contain letters, numbers, and underscores');
|
|
});
|
|
|
|
it('allows PRIMARY as special case', function () {
|
|
// PRIMARY is always valid
|
|
expect(fn() => IndexName::fromString('PRIMARY'))->not->toThrow(\InvalidArgumentException::class);
|
|
expect(fn() => IndexName::fromString('primary'))->not->toThrow(\InvalidArgumentException::class);
|
|
});
|
|
|
|
it('validates maximum length', function () {
|
|
$validName = str_repeat('a', 64);
|
|
expect(fn() => IndexName::fromString($validName))->not->toThrow(\InvalidArgumentException::class);
|
|
|
|
$tooLong = str_repeat('a', 65);
|
|
expect(fn() => IndexName::fromString($tooLong))
|
|
->toThrow(\InvalidArgumentException::class, 'exceeds maximum length');
|
|
});
|
|
|
|
it('detects SQL injection attempts', function () {
|
|
// Note: SQL injection attempts are caught by format validation
|
|
// since they contain invalid characters (quotes, hyphens, spaces, etc.)
|
|
|
|
expect(fn() => IndexName::fromString("idx'; DROP TABLE--"))
|
|
->toThrow(\InvalidArgumentException::class);
|
|
|
|
expect(fn() => IndexName::fromString('idx UNION SELECT'))
|
|
->toThrow(\InvalidArgumentException::class);
|
|
});
|
|
|
|
it('quotes index names for different platforms', function () {
|
|
$index = IndexName::fromString('idx_users_email');
|
|
|
|
expect($index->quoted('mysql'))->toBe('`idx_users_email`');
|
|
expect($index->quoted('postgresql'))->toBe('"idx_users_email"');
|
|
expect($index->quoted('postgres'))->toBe('"idx_users_email"');
|
|
expect($index->quoted('pgsql'))->toBe('"idx_users_email"');
|
|
expect($index->quoted('sqlite'))->toBe('"idx_users_email"');
|
|
expect($index->quoted())->toBe('`idx_users_email`'); // Default MySQL
|
|
});
|
|
|
|
it('never quotes PRIMARY KEY', function () {
|
|
$primary = IndexName::primary();
|
|
|
|
expect($primary->quoted('mysql'))->toBe('PRIMARY KEY');
|
|
expect($primary->quoted('postgresql'))->toBe('PRIMARY KEY');
|
|
expect($primary->quoted('sqlite'))->toBe('PRIMARY KEY');
|
|
});
|
|
|
|
it('compares index names for equality', function () {
|
|
$idx1 = IndexName::fromString('idx_users_email');
|
|
$idx2 = IndexName::fromString('idx_users_email');
|
|
$idx3 = IndexName::fromString('IDX_USERS_EMAIL'); // Different case
|
|
$idx4 = IndexName::fromString('idx_orders_id');
|
|
|
|
expect($idx1->equals($idx2))->toBeTrue();
|
|
expect($idx1->equals($idx3))->toBeTrue(); // Case-insensitive
|
|
expect($idx1->equals($idx4))->toBeFalse();
|
|
});
|
|
|
|
it('matches index name patterns', function () {
|
|
$index = IndexName::fromString('idx_users_email');
|
|
|
|
expect($index->matches('idx_*'))->toBeTrue();
|
|
expect($index->matches('*_email'))->toBeTrue();
|
|
expect($index->matches('idx_users_*'))->toBeTrue();
|
|
expect($index->matches('unique_*'))->toBeFalse();
|
|
});
|
|
|
|
it('detects reserved SQL keywords', function () {
|
|
$index = IndexName::fromString('idx_users_email');
|
|
expect($index->isReservedKeyword())->toBeFalse();
|
|
});
|
|
|
|
it('converts to lowercase', function () {
|
|
$index = IndexName::fromString('IDX_Users_Email');
|
|
|
|
expect($index->toLower())->toBe('idx_users_email');
|
|
});
|
|
|
|
it('checks for index name prefix', function () {
|
|
$index = IndexName::fromString('idx_users_email');
|
|
|
|
expect($index->hasPrefix('idx_'))->toBeTrue();
|
|
expect($index->hasPrefix('unique_'))->toBeFalse();
|
|
});
|
|
|
|
it('checks for index name suffix', function () {
|
|
$index = IndexName::fromString('idx_users_email');
|
|
|
|
expect($index->hasSuffix('_email'))->toBeTrue();
|
|
expect($index->hasSuffix('_id'))->toBeFalse();
|
|
});
|
|
|
|
it('detects unique indexes', function () {
|
|
$uniqueIdx1 = IndexName::fromString('unique_users_email');
|
|
$uniqueIdx2 = IndexName::fromString('idx_users_email_unique');
|
|
$uniqueIdx3 = IndexName::fromString('idx_unique_constraint_users');
|
|
$normalIdx = IndexName::fromString('idx_users_email');
|
|
|
|
expect($uniqueIdx1->isUniqueIndex())->toBeTrue();
|
|
expect($uniqueIdx2->isUniqueIndex())->toBeTrue();
|
|
expect($uniqueIdx3->isUniqueIndex())->toBeTrue();
|
|
expect($normalIdx->isUniqueIndex())->toBeFalse();
|
|
});
|
|
|
|
it('detects full-text indexes', function () {
|
|
$fulltextIdx1 = IndexName::fromString('fulltext_posts_content');
|
|
$fulltextIdx2 = IndexName::fromString('idx_posts_content_fulltext');
|
|
$fulltextIdx3 = IndexName::fromString('idx_fulltext_search');
|
|
$normalIdx = IndexName::fromString('idx_posts_title');
|
|
|
|
expect($fulltextIdx1->isFullTextIndex())->toBeTrue();
|
|
expect($fulltextIdx2->isFullTextIndex())->toBeTrue();
|
|
expect($fulltextIdx3->isFullTextIndex())->toBeTrue();
|
|
expect($normalIdx->isFullTextIndex())->toBeFalse();
|
|
});
|
|
|
|
it('generates conventional index names for columns', function () {
|
|
$table = TableName::fromString('users');
|
|
$emailColumn = ColumnName::fromString('email');
|
|
|
|
$index = IndexName::forColumns($table, $emailColumn);
|
|
|
|
expect($index->value)->toBe('idx_users_email');
|
|
expect($index->hasPrefix('idx_'))->toBeTrue();
|
|
});
|
|
|
|
it('generates multi-column index names', function () {
|
|
$table = TableName::fromString('users');
|
|
$firstNameColumn = ColumnName::fromString('first_name');
|
|
$lastNameColumn = ColumnName::fromString('last_name');
|
|
|
|
$index = IndexName::forColumns($table, $firstNameColumn, $lastNameColumn);
|
|
|
|
expect($index->value)->toBe('idx_users_first_name_last_name');
|
|
});
|
|
|
|
it('generates unique constraint names', function () {
|
|
$table = TableName::fromString('users');
|
|
$emailColumn = ColumnName::fromString('email');
|
|
|
|
$index = IndexName::uniqueFor($table, $emailColumn);
|
|
|
|
expect($index->value)->toBe('unique_users_email');
|
|
expect($index->isUniqueIndex())->toBeTrue();
|
|
});
|
|
|
|
it('generates multi-column unique constraint names', function () {
|
|
$table = TableName::fromString('users');
|
|
$emailColumn = ColumnName::fromString('email');
|
|
$tenantIdColumn = ColumnName::fromString('tenant_id');
|
|
|
|
$index = IndexName::uniqueFor($table, $emailColumn, $tenantIdColumn);
|
|
|
|
expect($index->value)->toBe('unique_users_email_tenant_id');
|
|
expect($index->isUniqueIndex())->toBeTrue();
|
|
});
|
|
|
|
it('generates foreign key index names', function () {
|
|
$table = TableName::fromString('orders');
|
|
$userIdColumn = ColumnName::fromString('user_id');
|
|
|
|
$index = IndexName::foreignKeyFor($table, $userIdColumn);
|
|
|
|
expect($index->value)->toBe('fk_orders_user_id');
|
|
expect($index->hasPrefix('fk_'))->toBeTrue();
|
|
});
|
|
|
|
it('converts to string via magic method', function () {
|
|
$index = IndexName::fromString('idx_users_email');
|
|
|
|
expect((string) $index)->toBe('idx_users_email');
|
|
});
|
|
|
|
it('is immutable', function () {
|
|
$index = IndexName::fromString('idx_users_email');
|
|
$value = $index->value;
|
|
|
|
// Value cannot be changed
|
|
expect($index->value)->toBe('idx_users_email');
|
|
expect($value)->toBe('idx_users_email');
|
|
});
|
|
|
|
it('handles edge cases correctly', function () {
|
|
// Single character (valid if letter or underscore)
|
|
expect(fn() => IndexName::fromString('a'))->not->toThrow(\InvalidArgumentException::class);
|
|
expect(fn() => IndexName::fromString('_'))->not->toThrow(\InvalidArgumentException::class);
|
|
|
|
// Underscore prefix
|
|
$index = IndexName::fromString('_temp_index');
|
|
expect($index->value)->toBe('_temp_index');
|
|
|
|
// Numbers in name
|
|
$index = IndexName::fromString('idx_123');
|
|
expect($index->value)->toBe('idx_123');
|
|
});
|
|
|
|
it('detects PRIMARY in different cases', function () {
|
|
$primary1 = IndexName::fromString('PRIMARY');
|
|
$primary2 = IndexName::fromString('primary');
|
|
$primary3 = IndexName::fromString('Primary');
|
|
|
|
expect($primary1->isPrimary())->toBeTrue();
|
|
expect($primary2->isPrimary())->toBeTrue();
|
|
expect($primary3->isPrimary())->toBeTrue();
|
|
});
|
|
|
|
it('factory methods create valid index names', function () {
|
|
$table = TableName::fromString('users');
|
|
$emailColumn = ColumnName::fromString('email');
|
|
|
|
// All factory methods should produce valid names
|
|
expect(fn() => IndexName::forColumns($table, $emailColumn))->not->toThrow(\InvalidArgumentException::class);
|
|
expect(fn() => IndexName::uniqueFor($table, $emailColumn))->not->toThrow(\InvalidArgumentException::class);
|
|
expect(fn() => IndexName::foreignKeyFor($table, $emailColumn))->not->toThrow(\InvalidArgumentException::class);
|
|
});
|
|
|
|
it('combines with TableName and ColumnName correctly', function () {
|
|
$table = TableName::fromString('users');
|
|
$emailColumn = ColumnName::fromString('email');
|
|
$statusColumn = ColumnName::fromString('status');
|
|
|
|
$index = IndexName::forColumns($table, $emailColumn, $statusColumn);
|
|
|
|
expect($index->value)->toContain('users');
|
|
expect($index->value)->toContain('email');
|
|
expect($index->value)->toContain('status');
|
|
});
|
|
});
|