- 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.
277 lines
9.6 KiB
PHP
277 lines
9.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Database\ValueObjects\SchemaName;
|
|
use App\Framework\Database\ValueObjects\TableName;
|
|
|
|
describe('SchemaName', function () {
|
|
it('creates schema name from string', function () {
|
|
$schema = SchemaName::fromString('app_v1');
|
|
|
|
expect($schema->value)->toBe('app_v1');
|
|
expect($schema->toString())->toBe('app_v1');
|
|
});
|
|
|
|
it('creates default public schema', function () {
|
|
$public = SchemaName::public();
|
|
|
|
expect($public->value)->toBe('public');
|
|
expect($public->isPublic())->toBeTrue();
|
|
});
|
|
|
|
it('creates information_schema', function () {
|
|
$infoSchema = SchemaName::informationSchema();
|
|
|
|
expect($infoSchema->value)->toBe('information_schema');
|
|
expect($infoSchema->isSystemSchema())->toBeTrue();
|
|
});
|
|
|
|
it('creates pg_catalog', function () {
|
|
$pgCatalog = SchemaName::pgCatalog();
|
|
|
|
expect($pgCatalog->value)->toBe('pg_catalog');
|
|
expect($pgCatalog->isSystemSchema())->toBeTrue();
|
|
});
|
|
|
|
it('validates schema name format', function () {
|
|
// Valid names - should not throw
|
|
SchemaName::fromString('app_v1');
|
|
SchemaName::fromString('my_schema');
|
|
SchemaName::fromString('_temp_schema');
|
|
SchemaName::fromString('schema123');
|
|
|
|
expect(true)->toBeTrue(); // Validation passed
|
|
|
|
// Invalid names - should throw
|
|
try {
|
|
SchemaName::fromString('');
|
|
expect(false)->toBeTrue('Should have thrown for empty name');
|
|
} catch (\InvalidArgumentException $e) {
|
|
expect($e->getMessage())->toContain('cannot be empty');
|
|
}
|
|
|
|
try {
|
|
SchemaName::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 {
|
|
SchemaName::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('validates maximum length', function () {
|
|
// Valid length
|
|
$validName = str_repeat('a', 64);
|
|
SchemaName::fromString($validName); // Should not throw
|
|
|
|
// Too long
|
|
$tooLong = str_repeat('a', 65);
|
|
try {
|
|
SchemaName::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 are caught by format validation
|
|
try {
|
|
SchemaName::fromString("app'; DROP SCHEMA--");
|
|
expect(false)->toBeTrue('Should have thrown for SQL injection attempt');
|
|
} catch (\InvalidArgumentException $e) {
|
|
expect($e->getMessage())->toBeString();
|
|
}
|
|
|
|
try {
|
|
SchemaName::fromString('app UNION SELECT');
|
|
expect(false)->toBeTrue('Should have thrown for SQL injection attempt');
|
|
} catch (\InvalidArgumentException $e) {
|
|
expect($e->getMessage())->toBeString();
|
|
}
|
|
});
|
|
|
|
it('quotes schema names for different platforms', function () {
|
|
$schema = SchemaName::fromString('app_v1');
|
|
|
|
expect($schema->quoted('postgresql'))->toBe('"app_v1"');
|
|
expect($schema->quoted('postgres'))->toBe('"app_v1"');
|
|
expect($schema->quoted('pgsql'))->toBe('"app_v1"');
|
|
expect($schema->quoted('sqlite'))->toBe('"app_v1"');
|
|
expect($schema->quoted('mysql'))->toBe('`app_v1`');
|
|
expect($schema->quoted())->toBe('"app_v1"'); // Default PostgreSQL
|
|
});
|
|
|
|
it('qualifies table names', function () {
|
|
$schema = SchemaName::fromString('app_v1');
|
|
$table = TableName::fromString('users');
|
|
|
|
expect($schema->qualifyTable($table, 'postgresql'))->toBe('"app_v1"."users"');
|
|
expect($schema->qualifyTable($table, 'mysql'))->toBe('`app_v1`.`users`');
|
|
});
|
|
|
|
it('compares schema names for equality', function () {
|
|
$schema1 = SchemaName::fromString('app_v1');
|
|
$schema2 = SchemaName::fromString('app_v1');
|
|
$schema3 = SchemaName::fromString('app_v2');
|
|
|
|
expect($schema1->equals($schema2))->toBeTrue();
|
|
expect($schema1->equals($schema3))->toBeFalse();
|
|
|
|
// PostgreSQL schemas are case-sensitive
|
|
$schemaLower = SchemaName::fromString('app');
|
|
$schemaUpper = SchemaName::fromString('APP');
|
|
expect($schemaLower->equals($schemaUpper))->toBeFalse();
|
|
});
|
|
|
|
it('matches schema name patterns', function () {
|
|
$schema = SchemaName::fromString('app_v1');
|
|
|
|
expect($schema->matches('app_*'))->toBeTrue();
|
|
expect($schema->matches('*_v1'))->toBeTrue();
|
|
expect($schema->matches('app_v1'))->toBeTrue();
|
|
expect($schema->matches('other_*'))->toBeFalse();
|
|
});
|
|
|
|
it('detects reserved SQL keywords', function () {
|
|
$schema = SchemaName::fromString('app_v1');
|
|
expect($schema->isReservedKeyword())->toBeFalse();
|
|
});
|
|
|
|
it('converts to lowercase', function () {
|
|
$schema = SchemaName::fromString('App_V1');
|
|
|
|
expect($schema->toLower())->toBe('app_v1');
|
|
});
|
|
|
|
it('checks for schema name prefix', function () {
|
|
$schema = SchemaName::fromString('app_v1');
|
|
|
|
expect($schema->hasPrefix('app_'))->toBeTrue();
|
|
expect($schema->hasPrefix('other_'))->toBeFalse();
|
|
});
|
|
|
|
it('checks for schema name suffix', function () {
|
|
$schema = SchemaName::fromString('app_v1');
|
|
|
|
expect($schema->hasSuffix('_v1'))->toBeTrue();
|
|
expect($schema->hasSuffix('_v2'))->toBeFalse();
|
|
});
|
|
|
|
it('detects public schema', function () {
|
|
$public = SchemaName::public();
|
|
expect($public->isPublic())->toBeTrue();
|
|
|
|
$custom = SchemaName::fromString('app_v1');
|
|
expect($custom->isPublic())->toBeFalse();
|
|
});
|
|
|
|
it('detects system schemas', function () {
|
|
$infoSchema = SchemaName::informationSchema();
|
|
expect($infoSchema->isSystemSchema())->toBeTrue();
|
|
|
|
$pgCatalog = SchemaName::pgCatalog();
|
|
expect($pgCatalog->isSystemSchema())->toBeTrue();
|
|
|
|
$pgSchema = SchemaName::fromString('pg_temp');
|
|
expect($pgSchema->isSystemSchema())->toBeTrue();
|
|
|
|
$customSchema = SchemaName::fromString('app_v1');
|
|
expect($customSchema->isSystemSchema())->toBeFalse();
|
|
});
|
|
|
|
it('detects temporary schemas', function () {
|
|
$tempSchema1 = SchemaName::fromString('temp_schema');
|
|
expect($tempSchema1->isTemporary())->toBeTrue();
|
|
|
|
$tempSchema2 = SchemaName::fromString('tmp_data');
|
|
expect($tempSchema2->isTemporary())->toBeTrue();
|
|
|
|
$tempSchema3 = SchemaName::fromString('schema_temp');
|
|
expect($tempSchema3->isTemporary())->toBeTrue();
|
|
|
|
$normalSchema = SchemaName::fromString('app_v1');
|
|
expect($normalSchema->isTemporary())->toBeFalse();
|
|
});
|
|
|
|
it('adds prefix to schema name', function () {
|
|
$schema = SchemaName::fromString('v1');
|
|
$prefixed = $schema->withPrefix('app_');
|
|
|
|
expect($prefixed->value)->toBe('app_v1');
|
|
|
|
// Original unchanged (immutable)
|
|
expect($schema->value)->toBe('v1');
|
|
});
|
|
|
|
it('removes prefix from schema name', function () {
|
|
$schema = SchemaName::fromString('app_v1');
|
|
$unprefixed = $schema->withoutPrefix('app_');
|
|
|
|
expect($unprefixed->value)->toBe('v1');
|
|
|
|
// Removing non-existent prefix returns same instance
|
|
$same = $unprefixed->withoutPrefix('other_');
|
|
expect($same->value)->toBe('v1');
|
|
});
|
|
|
|
it('converts to string via magic method', function () {
|
|
$schema = SchemaName::fromString('app_v1');
|
|
|
|
expect((string) $schema)->toBe('app_v1');
|
|
});
|
|
|
|
it('is immutable', function () {
|
|
$original = SchemaName::fromString('v1');
|
|
$prefixed = $original->withPrefix('app_');
|
|
|
|
// Original remains unchanged
|
|
expect($original->value)->toBe('v1');
|
|
|
|
// New instance created - verify by value difference
|
|
expect($prefixed->value)->toBe('app_v1');
|
|
|
|
// Removing non-existent prefix returns same instance (optimization)
|
|
$unprefixed = $original->withoutPrefix('app_');
|
|
expect($unprefixed->value)->toBe('v1');
|
|
|
|
// But removing actual prefix creates new instance
|
|
$prefixedSchema = SchemaName::fromString('app_v1');
|
|
$removedPrefix = $prefixedSchema->withoutPrefix('app_');
|
|
expect($removedPrefix->value)->toBe('v1');
|
|
expect($prefixedSchema->value)->toBe('app_v1'); // Original unchanged
|
|
});
|
|
|
|
it('handles edge cases correctly', function () {
|
|
// Single character (valid if letter or underscore)
|
|
$a = SchemaName::fromString('a');
|
|
expect($a->value)->toBe('a');
|
|
|
|
$underscore = SchemaName::fromString('_');
|
|
expect($underscore->value)->toBe('_');
|
|
|
|
// Underscore prefix
|
|
$schema = SchemaName::fromString('_temp');
|
|
expect($schema->value)->toBe('_temp');
|
|
|
|
// Numbers in name
|
|
$schema = SchemaName::fromString('app_123');
|
|
expect($schema->value)->toBe('app_123');
|
|
});
|
|
|
|
it('factory methods create valid schemas', function () {
|
|
SchemaName::public();
|
|
SchemaName::informationSchema();
|
|
SchemaName::pgCatalog();
|
|
|
|
expect(true)->toBeTrue(); // All factory methods succeeded
|
|
});
|
|
});
|