Files
michaelschiemer/tests/Unit/Framework/Database/Validation/DatabaseIdentifierValidatorTest.php
Michael Schiemer fc3d7e6357 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.
2025-10-25 19:18:37 +02:00

230 lines
11 KiB
PHP

<?php
declare(strict_types=1);
use App\Framework\Database\Validation\DatabaseIdentifierValidator;
describe('DatabaseIdentifierValidator', function () {
beforeEach(function () {
$this->validator = new DatabaseIdentifierValidator();
});
it('validates correct identifier format', function () {
// Valid identifiers
expect(fn() => $this->validator->validate('users', 'table'))->not->toThrow(\InvalidArgumentException::class);
expect(fn() => $this->validator->validate('user_profiles', 'table'))->not->toThrow(\InvalidArgumentException::class);
expect(fn() => $this->validator->validate('_temp_table', 'table'))->not->toThrow(\InvalidArgumentException::class);
expect(fn() => $this->validator->validate('table123', 'table'))->not->toThrow(\InvalidArgumentException::class);
});
it('throws on empty identifier', function () {
expect(fn() => $this->validator->validate('', 'table'))
->toThrow(\InvalidArgumentException::class, 'Table name cannot be empty');
expect(fn() => $this->validator->validate('', 'column'))
->toThrow(\InvalidArgumentException::class, 'Column name cannot be empty');
});
it('throws on identifier exceeding maximum length', function () {
$validLength = str_repeat('a', 64);
expect(fn() => $this->validator->validate($validLength, 'table'))->not->toThrow(\InvalidArgumentException::class);
$tooLong = str_repeat('a', 65);
expect(fn() => $this->validator->validate($tooLong, 'table'))
->toThrow(\InvalidArgumentException::class, 'exceeds maximum length');
});
it('returns maximum length constant', function () {
expect($this->validator->getMaxLength())->toBe(64);
});
it('throws on identifier not starting with letter or underscore', function () {
expect(fn() => $this->validator->validate('123invalid', 'table'))
->toThrow(\InvalidArgumentException::class, 'must start with a letter or underscore');
expect(fn() => $this->validator->validate('-invalid', 'table'))
->toThrow(\InvalidArgumentException::class, 'must start with a letter or underscore');
expect(fn() => $this->validator->validate('$invalid', 'table'))
->toThrow(\InvalidArgumentException::class, 'must start with a letter or underscore');
});
it('throws on invalid format with special characters', function () {
expect(fn() => $this->validator->validate('invalid-name', 'table'))
->toThrow(\InvalidArgumentException::class, 'can only contain letters, numbers, and underscores');
expect(fn() => $this->validator->validate('invalid name', 'table'))
->toThrow(\InvalidArgumentException::class, 'can only contain letters, numbers, and underscores');
expect(fn() => $this->validator->validate('invalid.name', 'table'))
->toThrow(\InvalidArgumentException::class, 'can only contain letters, numbers, and underscores');
expect(fn() => $this->validator->validate('invalid@name', 'table'))
->toThrow(\InvalidArgumentException::class, 'can only contain letters, numbers, and underscores');
});
it('detects SQL injection patterns', function () {
// Note: Most patterns are caught by format validation (invalid characters)
// SQL injection protection works through:
// 1. Format validation (alphanumeric + underscores only)
// 2. Additional checks for metacharacters
// SQL comments (also fails format validation due to -)
expect(fn() => $this->validator->validate('users--comment', 'table'))
->toThrow(\InvalidArgumentException::class);
// SQL comment block (also fails format validation due to /)
expect(fn() => $this->validator->validate('users/*comment*/', 'table'))
->toThrow(\InvalidArgumentException::class);
// Statement separator (also fails format validation)
expect(fn() => $this->validator->validate('users;DROP', 'table'))
->toThrow(\InvalidArgumentException::class);
// Quotes (fail format validation)
expect(fn() => $this->validator->validate("users'test", 'table'))
->toThrow(\InvalidArgumentException::class);
expect(fn() => $this->validator->validate('users"test', 'table'))
->toThrow(\InvalidArgumentException::class);
// Backslash (fails format validation)
expect(fn() => $this->validator->validate('users\\test', 'table'))
->toThrow(\InvalidArgumentException::class);
});
it('detects reserved SQL keywords', function () {
expect($this->validator->isReservedKeyword('SELECT'))->toBeTrue();
expect($this->validator->isReservedKeyword('select'))->toBeTrue(); // Case-insensitive
expect($this->validator->isReservedKeyword('INSERT'))->toBeTrue();
expect($this->validator->isReservedKeyword('UPDATE'))->toBeTrue();
expect($this->validator->isReservedKeyword('DELETE'))->toBeTrue();
expect($this->validator->isReservedKeyword('DROP'))->toBeTrue();
expect($this->validator->isReservedKeyword('CREATE'))->toBeTrue();
expect($this->validator->isReservedKeyword('ALTER'))->toBeTrue();
expect($this->validator->isReservedKeyword('TABLE'))->toBeTrue();
expect($this->validator->isReservedKeyword('INDEX'))->toBeTrue();
expect($this->validator->isReservedKeyword('FROM'))->toBeTrue();
expect($this->validator->isReservedKeyword('WHERE'))->toBeTrue();
expect($this->validator->isReservedKeyword('UNION'))->toBeTrue();
// Non-reserved words
expect($this->validator->isReservedKeyword('users'))->toBeFalse();
expect($this->validator->isReservedKeyword('email'))->toBeFalse();
expect($this->validator->isReservedKeyword('profile'))->toBeFalse();
});
it('allows special values via allowedValues parameter', function () {
// PRIMARY is a reserved keyword but still a valid identifier format
// allowedValues can be used to explicitly allow values that might be reserved
expect(fn() => $this->validator->validate('PRIMARY', 'index', ['PRIMARY']))
->not->toThrow(\InvalidArgumentException::class);
// UNIQUE can be allowed
expect(fn() => $this->validator->validate('UNIQUE', 'index', ['UNIQUE']))
->not->toThrow(\InvalidArgumentException::class);
// Note: PRIMARY and UNIQUE are valid identifier formats (they pass all validation rules)
// Reserved keyword checking is separate - use isReservedKeyword() for that
expect(fn() => $this->validator->validate('PRIMARY', 'index', []))
->not->toThrow(\InvalidArgumentException::class);
expect($this->validator->isReservedKeyword('PRIMARY'))->toBeTrue();
expect($this->validator->isReservedKeyword('UNIQUE'))->toBeTrue();
});
it('uses custom type name in error messages', function () {
try {
$this->validator->validate('', 'custom_type');
expect(false)->toBeTrue(); // Should not reach here
} catch (InvalidArgumentException $e) {
expect($e->getMessage())->toContain('Custom_type');
}
try {
$this->validator->validate('123invalid', 'my_entity');
expect(false)->toBeTrue();
} catch (InvalidArgumentException $e) {
expect($e->getMessage())->toContain('My_entity');
}
});
it('validates identifiers with underscores correctly', function () {
// Leading underscore
expect(fn() => $this->validator->validate('_temp', 'table'))->not->toThrow(\InvalidArgumentException::class);
expect(fn() => $this->validator->validate('__double', 'table'))->not->toThrow(\InvalidArgumentException::class);
// Multiple underscores
expect(fn() => $this->validator->validate('user_email_verified', 'column'))->not->toThrow(\InvalidArgumentException::class);
// Trailing underscore
expect(fn() => $this->validator->validate('temp_', 'table'))->not->toThrow(\InvalidArgumentException::class);
});
it('validates identifiers with numbers correctly', function () {
// Numbers in middle
expect(fn() => $this->validator->validate('table123test', 'table'))->not->toThrow(\InvalidArgumentException::class);
// Numbers at end
expect(fn() => $this->validator->validate('table123', 'table'))->not->toThrow(\InvalidArgumentException::class);
// Only numbers after first character
expect(fn() => $this->validator->validate('t123456789', 'table'))->not->toThrow(\InvalidArgumentException::class);
// But not at start
expect(fn() => $this->validator->validate('123table', 'table'))
->toThrow(\InvalidArgumentException::class);
});
it('is case-insensitive for reserved keywords', function () {
expect($this->validator->isReservedKeyword('select'))->toBeTrue();
expect($this->validator->isReservedKeyword('SELECT'))->toBeTrue();
expect($this->validator->isReservedKeyword('SeLeCt'))->toBeTrue();
expect($this->validator->isReservedKeyword('table'))->toBeTrue();
expect($this->validator->isReservedKeyword('TABLE'))->toBeTrue();
});
it('validates identifiers that contain SQL keywords as substrings', function () {
// Valid identifiers can contain SQL keywords as substrings
// This is fine because they're alphanumeric and will be quoted in SQL
expect(fn() => $this->validator->validate('usersunion', 'table'))->not->toThrow(\InvalidArgumentException::class);
expect(fn() => $this->validator->validate('updated_at', 'column'))->not->toThrow(\InvalidArgumentException::class);
expect(fn() => $this->validator->validate('created_at', 'column'))->not->toThrow(\InvalidArgumentException::class);
expect(fn() => $this->validator->validate('deleted_at', 'column'))->not->toThrow(\InvalidArgumentException::class);
});
it('validates edge cases', function () {
// Single character
expect(fn() => $this->validator->validate('a', 'table'))->not->toThrow(\InvalidArgumentException::class);
expect(fn() => $this->validator->validate('_', 'table'))->not->toThrow(\InvalidArgumentException::class);
// Exactly 64 characters (max length)
$maxLength = str_repeat('a', 64);
expect(fn() => $this->validator->validate($maxLength, 'table'))->not->toThrow(\InvalidArgumentException::class);
});
it('provides contextual error messages', function () {
try {
$this->validator->validate('', 'table');
} catch (InvalidArgumentException $e) {
expect($e->getMessage())->toContain('Table');
expect($e->getMessage())->toContain('cannot be empty');
}
try {
$this->validator->validate('', 'column');
} catch (InvalidArgumentException $e) {
expect($e->getMessage())->toContain('Column');
expect($e->getMessage())->toContain('cannot be empty');
}
try {
$this->validator->validate('', 'index');
} catch (InvalidArgumentException $e) {
expect($e->getMessage())->toContain('Index');
expect($e->getMessage())->toContain('cannot be empty');
}
});
});