feat(Production): Complete production deployment infrastructure

- 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.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -0,0 +1,276 @@
<?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
});
});