- 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.
230 lines
11 KiB
PHP
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');
|
|
}
|
|
});
|
|
});
|