- 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.
215 lines
8.8 KiB
PHP
215 lines
8.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Database\ValueObjects\ColumnName;
|
|
use App\Framework\Database\ValueObjects\TableName;
|
|
|
|
describe('ColumnName', function () {
|
|
it('creates column name from string', function () {
|
|
$column = ColumnName::fromString('email');
|
|
|
|
expect($column->value)->toBe('email');
|
|
expect($column->toString())->toBe('email');
|
|
});
|
|
|
|
it('validates column name format', function () {
|
|
// Valid names
|
|
expect(fn() => ColumnName::fromString('email'))->not->toThrow(\InvalidArgumentException::class);
|
|
expect(fn() => ColumnName::fromString('user_email'))->not->toThrow(\InvalidArgumentException::class);
|
|
expect(fn() => ColumnName::fromString('_temp_field'))->not->toThrow(\InvalidArgumentException::class);
|
|
expect(fn() => ColumnName::fromString('column123'))->not->toThrow(\InvalidArgumentException::class);
|
|
|
|
// Invalid names
|
|
expect(fn() => ColumnName::fromString(''))
|
|
->toThrow(\InvalidArgumentException::class, 'cannot be empty');
|
|
|
|
expect(fn() => ColumnName::fromString('123invalid'))
|
|
->toThrow(\InvalidArgumentException::class, 'must start with a letter or underscore');
|
|
|
|
expect(fn() => ColumnName::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() => ColumnName::fromString($validName))->not->toThrow(\InvalidArgumentException::class);
|
|
|
|
$tooLong = str_repeat('a', 65);
|
|
expect(fn() => ColumnName::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() => ColumnName::fromString("email'; DROP TABLE--"))
|
|
->toThrow(\InvalidArgumentException::class);
|
|
|
|
expect(fn() => ColumnName::fromString('email UNION SELECT'))
|
|
->toThrow(\InvalidArgumentException::class);
|
|
});
|
|
|
|
it('quotes column names for different platforms', function () {
|
|
$column = ColumnName::fromString('email');
|
|
|
|
expect($column->quoted('mysql'))->toBe('`email`');
|
|
expect($column->quoted('postgresql'))->toBe('"email"');
|
|
expect($column->quoted('postgres'))->toBe('"email"');
|
|
expect($column->quoted('pgsql'))->toBe('"email"');
|
|
expect($column->quoted('sqlite'))->toBe('"email"');
|
|
expect($column->quoted())->toBe('`email`'); // Default MySQL
|
|
});
|
|
|
|
it('creates qualified column name', function () {
|
|
$table = TableName::fromString('users');
|
|
$column = ColumnName::fromString('email');
|
|
|
|
expect($column->qualified($table, 'mysql'))->toBe('`users`.`email`');
|
|
expect($column->qualified($table, 'postgresql'))->toBe('"users"."email"');
|
|
expect($column->qualified($table, 'sqlite'))->toBe('"users"."email"');
|
|
});
|
|
|
|
it('compares column names for equality', function () {
|
|
$col1 = ColumnName::fromString('email');
|
|
$col2 = ColumnName::fromString('email');
|
|
$col3 = ColumnName::fromString('EMAIL'); // Different case
|
|
$col4 = ColumnName::fromString('username');
|
|
|
|
expect($col1->equals($col2))->toBeTrue();
|
|
expect($col1->equals($col3))->toBeTrue(); // Case-insensitive
|
|
expect($col1->equals($col4))->toBeFalse();
|
|
});
|
|
|
|
it('matches column name patterns', function () {
|
|
$column = ColumnName::fromString('user_email');
|
|
|
|
expect($column->matches('user_*'))->toBeTrue();
|
|
expect($column->matches('*_email'))->toBeTrue();
|
|
expect($column->matches('user_email'))->toBeTrue();
|
|
expect($column->matches('order_*'))->toBeFalse();
|
|
});
|
|
|
|
it('detects reserved SQL keywords', function () {
|
|
$column = ColumnName::fromString('email');
|
|
expect($column->isReservedKeyword())->toBeFalse();
|
|
});
|
|
|
|
it('converts to lowercase', function () {
|
|
$column = ColumnName::fromString('UserEmail');
|
|
|
|
expect($column->toLower())->toBe('useremail');
|
|
});
|
|
|
|
it('checks for column name suffix', function () {
|
|
$column = ColumnName::fromString('user_id');
|
|
|
|
expect($column->hasSuffix('_id'))->toBeTrue();
|
|
expect($column->hasSuffix('_at'))->toBeFalse();
|
|
});
|
|
|
|
it('detects foreign key columns', function () {
|
|
$userId = ColumnName::fromString('user_id');
|
|
$orderId = ColumnName::fromString('order_id');
|
|
$email = ColumnName::fromString('email');
|
|
$id = ColumnName::fromString('id'); // Primary key, not foreign key
|
|
|
|
expect($userId->isForeignKey())->toBeTrue();
|
|
expect($orderId->isForeignKey())->toBeTrue();
|
|
expect($email->isForeignKey())->toBeFalse();
|
|
expect($id->isForeignKey())->toBeFalse(); // Special case: 'id' itself is not FK
|
|
});
|
|
|
|
it('detects timestamp columns', function () {
|
|
$createdAt = ColumnName::fromString('created_at');
|
|
$updatedAt = ColumnName::fromString('updated_at');
|
|
$deletedAt = ColumnName::fromString('deleted_at');
|
|
$publishedAt = ColumnName::fromString('published_at');
|
|
$email = ColumnName::fromString('email');
|
|
|
|
expect($createdAt->isTimestamp())->toBeTrue();
|
|
expect($updatedAt->isTimestamp())->toBeTrue();
|
|
expect($deletedAt->isTimestamp())->toBeTrue();
|
|
expect($publishedAt->isTimestamp())->toBeTrue(); // Any *_at
|
|
expect($email->isTimestamp())->toBeFalse();
|
|
});
|
|
|
|
it('detects standard timestamp column names', function () {
|
|
// Standard Laravel/framework timestamp columns
|
|
$createdAt = ColumnName::fromString('created_at');
|
|
$updatedAt = ColumnName::fromString('updated_at');
|
|
$deletedAt = ColumnName::fromString('deleted_at');
|
|
|
|
expect($createdAt->isTimestamp())->toBeTrue();
|
|
expect($updatedAt->isTimestamp())->toBeTrue();
|
|
expect($deletedAt->isTimestamp())->toBeTrue();
|
|
});
|
|
|
|
it('converts to string via magic method', function () {
|
|
$column = ColumnName::fromString('email');
|
|
|
|
expect((string) $column)->toBe('email');
|
|
});
|
|
|
|
it('is immutable', function () {
|
|
$column = ColumnName::fromString('email');
|
|
$value = $column->value;
|
|
|
|
// Value cannot be changed
|
|
expect($column->value)->toBe('email');
|
|
expect($value)->toBe('email');
|
|
});
|
|
|
|
it('handles edge cases correctly', function () {
|
|
// Single character (valid if letter or underscore)
|
|
expect(fn() => ColumnName::fromString('a'))->not->toThrow(\InvalidArgumentException::class);
|
|
expect(fn() => ColumnName::fromString('_'))->not->toThrow(\InvalidArgumentException::class);
|
|
|
|
// Underscore prefix
|
|
$column = ColumnName::fromString('_temp_field');
|
|
expect($column->value)->toBe('_temp_field');
|
|
|
|
// Numbers in name
|
|
$column = ColumnName::fromString('field_123');
|
|
expect($column->value)->toBe('field_123');
|
|
});
|
|
|
|
it('combines with TableName correctly', function () {
|
|
$table = TableName::fromString('users');
|
|
$column = ColumnName::fromString('email');
|
|
|
|
$qualified = $column->qualified($table);
|
|
|
|
expect($qualified)->toContain('users');
|
|
expect($qualified)->toContain('email');
|
|
expect($qualified)->toContain('.');
|
|
});
|
|
|
|
it('detects various foreign key naming patterns', function () {
|
|
// Standard pattern: table_id
|
|
expect(ColumnName::fromString('user_id')->isForeignKey())->toBeTrue();
|
|
expect(ColumnName::fromString('order_id')->isForeignKey())->toBeTrue();
|
|
expect(ColumnName::fromString('product_id')->isForeignKey())->toBeTrue();
|
|
|
|
// Non-FK patterns
|
|
expect(ColumnName::fromString('id')->isForeignKey())->toBeFalse();
|
|
expect(ColumnName::fromString('email')->isForeignKey())->toBeFalse();
|
|
expect(ColumnName::fromString('created_at')->isForeignKey())->toBeFalse();
|
|
});
|
|
|
|
it('detects various timestamp naming patterns', function () {
|
|
// Suffixed with _at
|
|
expect(ColumnName::fromString('created_at')->isTimestamp())->toBeTrue();
|
|
expect(ColumnName::fromString('updated_at')->isTimestamp())->toBeTrue();
|
|
expect(ColumnName::fromString('deleted_at')->isTimestamp())->toBeTrue();
|
|
expect(ColumnName::fromString('published_at')->isTimestamp())->toBeTrue();
|
|
expect(ColumnName::fromString('verified_at')->isTimestamp())->toBeTrue();
|
|
|
|
// Non-timestamp patterns
|
|
expect(ColumnName::fromString('email')->isTimestamp())->toBeFalse();
|
|
expect(ColumnName::fromString('user_id')->isTimestamp())->toBeFalse();
|
|
expect(ColumnName::fromString('status')->isTimestamp())->toBeFalse();
|
|
});
|
|
});
|