- 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.
350 lines
13 KiB
PHP
350 lines
13 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Database\ValueObjects\ConstraintName;
|
|
use App\Framework\Database\ValueObjects\TableName;
|
|
use App\Framework\Database\ValueObjects\ColumnName;
|
|
|
|
describe('ConstraintName', function () {
|
|
it('creates constraint name from string', function () {
|
|
$constraint = ConstraintName::fromString('fk_users_company_id');
|
|
|
|
expect($constraint->value)->toBe('fk_users_company_id');
|
|
expect($constraint->toString())->toBe('fk_users_company_id');
|
|
});
|
|
|
|
it('creates PRIMARY KEY constraint', function () {
|
|
$primaryKey = ConstraintName::primaryKey();
|
|
|
|
expect($primaryKey->value)->toBe('PRIMARY KEY');
|
|
expect($primaryKey->isPrimaryKey())->toBeTrue();
|
|
});
|
|
|
|
it('creates UNIQUE constraint with conventional naming', function () {
|
|
$table = TableName::fromString('users');
|
|
$emailColumn = ColumnName::fromString('email');
|
|
|
|
$unique = ConstraintName::unique($table, $emailColumn);
|
|
|
|
expect($unique->value)->toBe('uq_users_email');
|
|
expect($unique->isUnique())->toBeTrue();
|
|
});
|
|
|
|
it('creates multi-column UNIQUE constraint', function () {
|
|
$table = TableName::fromString('users');
|
|
$emailColumn = ColumnName::fromString('email');
|
|
$tenantColumn = ColumnName::fromString('tenant_id');
|
|
|
|
$unique = ConstraintName::unique($table, $emailColumn, $tenantColumn);
|
|
|
|
expect($unique->value)->toBe('uq_users_email_tenant_id');
|
|
expect($unique->isUnique())->toBeTrue();
|
|
});
|
|
|
|
it('creates FOREIGN KEY constraint with conventional naming', function () {
|
|
$table = TableName::fromString('orders');
|
|
$column = ColumnName::fromString('user_id');
|
|
$refTable = TableName::fromString('users');
|
|
|
|
$fk = ConstraintName::foreignKey($table, $column, $refTable);
|
|
|
|
expect($fk->value)->toBe('fk_orders_user_id_users');
|
|
expect($fk->isForeignKey())->toBeTrue();
|
|
});
|
|
|
|
it('creates CHECK constraint with conventional naming', function () {
|
|
$table = TableName::fromString('products');
|
|
|
|
$check = ConstraintName::check($table, 'positive_price');
|
|
|
|
expect($check->value)->toBe('chk_products_positive_price');
|
|
expect($check->isCheck())->toBeTrue();
|
|
});
|
|
|
|
it('validates constraint name format', function () {
|
|
// Valid names - should not throw
|
|
ConstraintName::fromString('fk_users_company');
|
|
ConstraintName::fromString('uq_email');
|
|
ConstraintName::fromString('chk_positive_amount');
|
|
|
|
expect(true)->toBeTrue(); // Validation passed
|
|
|
|
// Invalid names - should throw
|
|
try {
|
|
ConstraintName::fromString('');
|
|
expect(false)->toBeTrue('Should have thrown for empty name');
|
|
} catch (\InvalidArgumentException $e) {
|
|
expect($e->getMessage())->toContain('cannot be empty');
|
|
}
|
|
|
|
try {
|
|
ConstraintName::fromString('123invalid');
|
|
expect(false)->toBeTrue('Should have thrown for name starting with number');
|
|
} catch (\InvalidArgumentException $e) {
|
|
expect($e->getMessage())->toContain('must start with a letter or underscore');
|
|
}
|
|
|
|
try {
|
|
ConstraintName::fromString('invalid-name');
|
|
expect(false)->toBeTrue('Should have thrown for name with hyphen');
|
|
} catch (\InvalidArgumentException $e) {
|
|
expect($e->getMessage())->toContain('can only contain letters, numbers, and underscores');
|
|
}
|
|
});
|
|
|
|
it('allows keyword constraints as special cases', function () {
|
|
// These are SQL keywords but valid constraints
|
|
ConstraintName::fromString('PRIMARY KEY');
|
|
ConstraintName::fromString('UNIQUE');
|
|
ConstraintName::fromString('CHECK');
|
|
ConstraintName::fromString('FOREIGN KEY');
|
|
|
|
expect(true)->toBeTrue(); // All keyword constraints created successfully
|
|
});
|
|
|
|
it('validates maximum length', function () {
|
|
// Valid length
|
|
$validName = 'fk_' . str_repeat('a', 61);
|
|
ConstraintName::fromString($validName); // Should not throw
|
|
|
|
// Too long
|
|
$tooLong = 'fk_' . str_repeat('a', 62);
|
|
try {
|
|
ConstraintName::fromString($tooLong);
|
|
expect(false)->toBeTrue('Should have thrown for name exceeding max length');
|
|
} catch (\InvalidArgumentException $e) {
|
|
expect($e->getMessage())->toContain('exceeds maximum length');
|
|
}
|
|
});
|
|
|
|
it('detects SQL injection attempts', function () {
|
|
// Note: SQL injection attempts with special characters are caught by format validation
|
|
try {
|
|
ConstraintName::fromString("fk'; DROP TABLE--");
|
|
expect(false)->toBeTrue('Should have thrown for SQL injection attempt');
|
|
} catch (\InvalidArgumentException $e) {
|
|
expect($e->getMessage())->toBeString();
|
|
}
|
|
|
|
try {
|
|
ConstraintName::fromString('fk/*comment*/');
|
|
expect(false)->toBeTrue('Should have thrown for SQL comment injection');
|
|
} catch (\InvalidArgumentException $e) {
|
|
expect($e->getMessage())->toBeString();
|
|
}
|
|
});
|
|
|
|
it('quotes constraint names for different platforms', function () {
|
|
$constraint = ConstraintName::fromString('fk_users_company');
|
|
|
|
expect($constraint->quoted('mysql'))->toBe('`fk_users_company`');
|
|
expect($constraint->quoted('postgresql'))->toBe('"fk_users_company"');
|
|
expect($constraint->quoted('sqlite'))->toBe('"fk_users_company"');
|
|
expect($constraint->quoted())->toBe('`fk_users_company`'); // Default MySQL
|
|
});
|
|
|
|
it('never quotes keyword constraints', function () {
|
|
$primaryKey = ConstraintName::primaryKey();
|
|
$unique = ConstraintName::fromString('UNIQUE');
|
|
$check = ConstraintName::fromString('CHECK');
|
|
|
|
expect($primaryKey->quoted('mysql'))->toBe('PRIMARY KEY');
|
|
expect($primaryKey->quoted('postgresql'))->toBe('PRIMARY KEY');
|
|
expect($unique->quoted('mysql'))->toBe('UNIQUE');
|
|
expect($check->quoted('postgresql'))->toBe('CHECK');
|
|
});
|
|
|
|
it('compares constraint names for equality', function () {
|
|
$fk1 = ConstraintName::fromString('fk_users_company');
|
|
$fk2 = ConstraintName::fromString('fk_users_company');
|
|
$fk3 = ConstraintName::fromString('FK_USERS_COMPANY'); // Different case
|
|
$fk4 = ConstraintName::fromString('fk_orders_user');
|
|
|
|
expect($fk1->equals($fk2))->toBeTrue();
|
|
expect($fk1->equals($fk3))->toBeTrue(); // Case-insensitive
|
|
expect($fk1->equals($fk4))->toBeFalse();
|
|
});
|
|
|
|
it('matches constraint name patterns', function () {
|
|
$constraint = ConstraintName::fromString('fk_users_company_id');
|
|
|
|
expect($constraint->matches('fk_*'))->toBeTrue();
|
|
expect($constraint->matches('*_company_id'))->toBeTrue();
|
|
expect($constraint->matches('fk_users_*'))->toBeTrue();
|
|
expect($constraint->matches('uq_*'))->toBeFalse();
|
|
});
|
|
|
|
it('detects reserved SQL keywords', function () {
|
|
$constraint = ConstraintName::fromString('fk_users_company');
|
|
expect($constraint->isReservedKeyword())->toBeFalse();
|
|
});
|
|
|
|
it('converts to lowercase', function () {
|
|
$constraint = ConstraintName::fromString('FK_Users_Company');
|
|
|
|
expect($constraint->toLower())->toBe('fk_users_company');
|
|
});
|
|
|
|
it('checks for constraint name prefix', function () {
|
|
$constraint = ConstraintName::fromString('fk_users_company');
|
|
|
|
expect($constraint->hasPrefix('fk_'))->toBeTrue();
|
|
expect($constraint->hasPrefix('uq_'))->toBeFalse();
|
|
});
|
|
|
|
it('checks for constraint name suffix', function () {
|
|
$constraint = ConstraintName::fromString('fk_users_company_id');
|
|
|
|
expect($constraint->hasSuffix('_id'))->toBeTrue();
|
|
expect($constraint->hasSuffix('_key'))->toBeFalse();
|
|
});
|
|
|
|
it('detects PRIMARY KEY constraints', function () {
|
|
$pk1 = ConstraintName::primaryKey();
|
|
expect($pk1->isPrimaryKey())->toBeTrue();
|
|
|
|
$pk2 = ConstraintName::fromString('PRIMARY');
|
|
expect($pk2->isPrimaryKey())->toBeTrue();
|
|
|
|
$fk = ConstraintName::fromString('fk_users_company');
|
|
expect($fk->isPrimaryKey())->toBeFalse();
|
|
});
|
|
|
|
it('detects FOREIGN KEY constraints', function () {
|
|
$fk1 = ConstraintName::fromString('fk_users_company');
|
|
expect($fk1->isForeignKey())->toBeTrue();
|
|
|
|
$fk2 = ConstraintName::fromString('FOREIGN KEY');
|
|
expect($fk2->isForeignKey())->toBeTrue();
|
|
|
|
$uq = ConstraintName::fromString('uq_email');
|
|
expect($uq->isForeignKey())->toBeFalse();
|
|
});
|
|
|
|
it('detects UNIQUE constraints', function () {
|
|
$uq1 = ConstraintName::fromString('uq_email');
|
|
expect($uq1->isUnique())->toBeTrue();
|
|
|
|
$uq2 = ConstraintName::fromString('unique_users_email');
|
|
expect($uq2->isUnique())->toBeTrue();
|
|
|
|
$uq3 = ConstraintName::fromString('UNIQUE');
|
|
expect($uq3->isUnique())->toBeTrue();
|
|
|
|
$fk = ConstraintName::fromString('fk_users_company');
|
|
expect($fk->isUnique())->toBeFalse();
|
|
});
|
|
|
|
it('detects CHECK constraints', function () {
|
|
$chk1 = ConstraintName::fromString('chk_positive_amount');
|
|
expect($chk1->isCheck())->toBeTrue();
|
|
|
|
$chk2 = ConstraintName::fromString('check_valid_status');
|
|
expect($chk2->isCheck())->toBeTrue();
|
|
|
|
$chk3 = ConstraintName::fromString('CHECK');
|
|
expect($chk3->isCheck())->toBeTrue();
|
|
|
|
$fk = ConstraintName::fromString('fk_users_company');
|
|
expect($fk->isCheck())->toBeFalse();
|
|
});
|
|
|
|
it('detects DEFAULT constraints', function () {
|
|
$df1 = ConstraintName::fromString('df_created_at');
|
|
expect($df1->isDefault())->toBeTrue();
|
|
|
|
$df2 = ConstraintName::fromString('default_status');
|
|
expect($df2->isDefault())->toBeTrue();
|
|
|
|
$df3 = ConstraintName::fromString('DEFAULT');
|
|
expect($df3->isDefault())->toBeTrue();
|
|
|
|
$fk = ConstraintName::fromString('fk_users_company');
|
|
expect($fk->isDefault())->toBeFalse();
|
|
});
|
|
|
|
it('identifies keyword constraints', function () {
|
|
$pk = ConstraintName::primaryKey();
|
|
expect($pk->isKeywordConstraint())->toBeTrue();
|
|
|
|
$unique = ConstraintName::fromString('UNIQUE');
|
|
expect($unique->isKeywordConstraint())->toBeTrue();
|
|
|
|
$check = ConstraintName::fromString('CHECK');
|
|
expect($check->isKeywordConstraint())->toBeTrue();
|
|
|
|
$custom = ConstraintName::fromString('fk_users_company');
|
|
expect($custom->isKeywordConstraint())->toBeFalse();
|
|
});
|
|
|
|
it('determines constraint type', function () {
|
|
$pk = ConstraintName::primaryKey();
|
|
expect($pk->getType())->toBe('primary_key');
|
|
|
|
$fk = ConstraintName::fromString('fk_users_company');
|
|
expect($fk->getType())->toBe('foreign_key');
|
|
|
|
$uq = ConstraintName::fromString('uq_email');
|
|
expect($uq->getType())->toBe('unique');
|
|
|
|
$chk = ConstraintName::fromString('chk_positive');
|
|
expect($chk->getType())->toBe('check');
|
|
|
|
$df = ConstraintName::fromString('df_status');
|
|
expect($df->getType())->toBe('default');
|
|
|
|
$custom = ConstraintName::fromString('my_custom_constraint');
|
|
expect($custom->getType())->toBe('custom');
|
|
});
|
|
|
|
it('converts to string via magic method', function () {
|
|
$constraint = ConstraintName::fromString('fk_users_company');
|
|
|
|
expect((string) $constraint)->toBe('fk_users_company');
|
|
});
|
|
|
|
it('is immutable', function () {
|
|
$table = TableName::fromString('users');
|
|
$column = ColumnName::fromString('email');
|
|
|
|
$unique = ConstraintName::unique($table, $column);
|
|
$value = $unique->value;
|
|
|
|
// Value cannot be changed
|
|
expect($unique->value)->toBe('uq_users_email');
|
|
expect($value)->toBe('uq_users_email');
|
|
});
|
|
|
|
it('handles edge cases correctly', function () {
|
|
// Single character (valid if letter or underscore)
|
|
$c = ConstraintName::fromString('c');
|
|
expect($c->value)->toBe('c');
|
|
|
|
$underscore = ConstraintName::fromString('_');
|
|
expect($underscore->value)->toBe('_');
|
|
|
|
// Underscore prefix
|
|
$constraint = ConstraintName::fromString('_temp_constraint');
|
|
expect($constraint->value)->toBe('_temp_constraint');
|
|
|
|
// Numbers in name
|
|
$constraint = ConstraintName::fromString('fk_table_123');
|
|
expect($constraint->value)->toBe('fk_table_123');
|
|
});
|
|
|
|
it('factory methods create valid constraint names', function () {
|
|
$table = TableName::fromString('users');
|
|
$column = ColumnName::fromString('email');
|
|
$refTable = TableName::fromString('companies');
|
|
|
|
// All factory methods should produce valid names
|
|
ConstraintName::primaryKey();
|
|
ConstraintName::unique($table, $column);
|
|
ConstraintName::foreignKey($table, $column, $refTable);
|
|
ConstraintName::check($table, 'valid_status');
|
|
|
|
expect(true)->toBeTrue(); // All factory methods succeeded
|
|
});
|
|
});
|