mirror of
https://github.com/ivuorinen/monolog-gdpr-filter.git
synced 2026-02-10 04:48:47 +00:00
feat: performance, integrations, advanced features (#2)
* feat: performance, integrations, advanced features * chore: fix linting problems * chore: suppressions and linting * chore(lint): pre-commit linting, fixes * feat: comprehensive input validation, security hardening, and regression testing - Add extensive input validation throughout codebase with proper error handling - Implement comprehensive security hardening with ReDoS protection and bounds checking - Add 3 new regression test suites covering critical bugs, security, and validation scenarios - Enhance rate limiting with memory management and configurable cleanup intervals - Update configuration security settings and improve Laravel integration - Fix TODO.md timestamps to reflect actual development timeline - Strengthen static analysis configuration and improve code quality standards * feat: configure static analysis tools and enhance development workflow - Complete configuration of Psalm, PHPStan, and Rector for harmonious static analysis. - Fix invalid configurations and tool conflicts that prevented proper code quality analysis. - Add comprehensive safe analysis script with interactive workflow, backup/restore capabilities, and dry-run modes. Update documentation with linting policy requiring issue resolution over suppression. - Clean completed items from TODO to focus on actionable improvements. - All static analysis tools now work together seamlessly to provide code quality insights without breaking existing functionality. * fix(test): update Invalid regex pattern expectation * chore: phpstan, psalm fixes * chore: phpstan, psalm fixes, more tests * chore: tooling tweaks, cleanup * chore: tweaks to get the tests pass * fix(lint): rector config tweaks and successful run * feat: refactoring, more tests, fixes, cleanup * chore: deduplication, use constants * chore: psalm fixes * chore: ignore phpstan deliberate errors in tests * chore: improve codebase, deduplicate code * fix: lint * chore: deduplication, codebase simplification, sonarqube fixes * fix: resolve SonarQube reliability rating issues Fix useless object instantiation warnings in test files by assigning instantiated objects to variables. This resolves the SonarQube reliability rating issue (was C, now targeting A). Changes: - tests/Strategies/MaskingStrategiesTest.php: Fix 3 instances - tests/Strategies/FieldPathMaskingStrategyTest.php: Fix 1 instance The tests use expectException() to verify that constructors throw exceptions for invalid input. SonarQube flagged standalone `new` statements as useless. Fixed by assigning to variables with explicit unset() and fail() calls. All tests pass (623/623) and static analysis tools pass. * fix: resolve more SonarQube detected issues * fix: resolve psalm detected issues * fix: resolve more SonarQube detected issues * fix: resolve psalm detected issues * fix: duplications * fix: resolve SonarQube reliability rating issues * fix: resolve psalm and phpstan detected issues
This commit is contained in:
517
tests/InputValidation/ConfigValidationTest.php
Normal file
517
tests/InputValidation/ConfigValidationTest.php
Normal file
@@ -0,0 +1,517 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\InputValidation;
|
||||
|
||||
use PHPUnit\Framework\Attributes\CoversNothing;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tests\TestConstants;
|
||||
|
||||
/**
|
||||
* Tests for the ConfigValidationTest class.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[CoversNothing]
|
||||
class ConfigValidationTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Get a test configuration array that simulates the actual config without Laravel dependencies.
|
||||
*
|
||||
* @return ((bool|int|string)[]|bool|int)[]
|
||||
*
|
||||
* @psalm-return array{auto_register: bool, channels: list{'single', 'daily', 'stack'}, patterns: array<never, never>, field_paths: array<never, never>, custom_callbacks: array<never, never>, max_depth: int<1, 1000>, audit_logging: array{enabled: bool, channel: string}, performance: array{chunk_size: int<100, 10000>, garbage_collection_threshold: int<1000, 100000>}, validation: array{max_pattern_length: int<10, 1000>, max_field_path_length: int<5, 500>, allow_empty_patterns: bool, strict_regex_validation: bool}}
|
||||
*/
|
||||
private function getTestConfig(): array
|
||||
{
|
||||
return [
|
||||
'auto_register' => filter_var($_ENV['GDPR_AUTO_REGISTER'] ?? false, FILTER_VALIDATE_BOOLEAN),
|
||||
'channels' => ['single', 'daily', 'stack'],
|
||||
'patterns' => [],
|
||||
'field_paths' => [],
|
||||
'custom_callbacks' => [],
|
||||
'max_depth' => max(1, min(1000, (int) ($_ENV['GDPR_MAX_DEPTH'] ?? 100))),
|
||||
'audit_logging' => [
|
||||
'enabled' => filter_var($_ENV['GDPR_AUDIT_ENABLED'] ?? false, FILTER_VALIDATE_BOOLEAN),
|
||||
'channel' => trim($_ENV['GDPR_AUDIT_CHANNEL'] ?? 'gdpr-audit') ?: 'gdpr-audit',
|
||||
],
|
||||
'performance' => [
|
||||
'chunk_size' => max(100, min(10000, (int) ($_ENV['GDPR_CHUNK_SIZE'] ?? 1000))),
|
||||
'garbage_collection_threshold' => max(1000, min(100000, (int) ($_ENV['GDPR_GC_THRESHOLD'] ?? 10000))),
|
||||
],
|
||||
'validation' => [
|
||||
'max_pattern_length' => max(10, min(1000, (int) ($_ENV['GDPR_MAX_PATTERN_LENGTH'] ?? 500))),
|
||||
'max_field_path_length' => max(5, min(500, (int) ($_ENV['GDPR_MAX_FIELD_PATH_LENGTH'] ?? 100))),
|
||||
'allow_empty_patterns' => filter_var($_ENV['GDPR_ALLOW_EMPTY_PATTERNS'] ?? false, FILTER_VALIDATE_BOOLEAN),
|
||||
'strict_regex_validation' => filter_var($_ENV['GDPR_STRICT_REGEX_VALIDATION'] ?? true, FILTER_VALIDATE_BOOLEAN),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function configFileExists(): void
|
||||
{
|
||||
$configPath = __DIR__ . '/../../config/gdpr.php';
|
||||
$this->assertFileExists($configPath, 'GDPR configuration file should exist');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function configReturnsValidArray(): void
|
||||
{
|
||||
$config = $this->getTestConfig();
|
||||
|
||||
$this->assertIsArray($config, 'Configuration should return an array');
|
||||
$this->assertNotEmpty($config, 'Configuration should not be empty');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function configHasRequiredKeys(): void
|
||||
{
|
||||
$config = $this->getTestConfig();
|
||||
|
||||
$requiredKeys = [
|
||||
'auto_register',
|
||||
'channels',
|
||||
'patterns',
|
||||
'field_paths',
|
||||
'custom_callbacks',
|
||||
'max_depth',
|
||||
'audit_logging',
|
||||
'performance',
|
||||
'validation'
|
||||
];
|
||||
|
||||
foreach ($requiredKeys as $key) {
|
||||
$this->assertArrayHasKey($key, $config, sprintf("Configuration should have '%s' key", $key));
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function autoRegisterDefaultsToFalseForSecurity(): void
|
||||
{
|
||||
// Clear environment variable to test default
|
||||
$oldValue = $_ENV['GDPR_AUTO_REGISTER'] ?? null;
|
||||
unset($_ENV['GDPR_AUTO_REGISTER']);
|
||||
|
||||
$config = $this->getTestConfig();
|
||||
|
||||
$this->assertFalse($config['auto_register'], 'auto_register should default to false for security');
|
||||
|
||||
// Restore environment variable
|
||||
if ($oldValue !== null) {
|
||||
$_ENV['GDPR_AUTO_REGISTER'] = $oldValue;
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function autoRegisterValidatesBooleanValues(): void
|
||||
{
|
||||
$testCases = [
|
||||
'true' => true,
|
||||
'1' => true,
|
||||
'yes' => true,
|
||||
'on' => true,
|
||||
'false' => false,
|
||||
'0' => false,
|
||||
'no' => false,
|
||||
'off' => false,
|
||||
'' => false,
|
||||
'invalid' => false
|
||||
];
|
||||
|
||||
foreach ($testCases as $envValue => $expectedResult) {
|
||||
$_ENV['GDPR_AUTO_REGISTER'] = $envValue;
|
||||
|
||||
$config = $this->getTestConfig();
|
||||
|
||||
$this->assertSame(
|
||||
$expectedResult,
|
||||
$config['auto_register'],
|
||||
sprintf(TestConstants::TEMPLATE_ENV_VALUE_RESULT, $envValue) . ($expectedResult ? 'true' : 'false')
|
||||
);
|
||||
}
|
||||
|
||||
unset($_ENV['GDPR_AUTO_REGISTER']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function maxDepthHasValidBounds(): void
|
||||
{
|
||||
$testCases = [
|
||||
'-10' => 1, // Below minimum, should be clamped to 1
|
||||
'0' => 1, // Below minimum, should be clamped to 1
|
||||
'1' => 1, // Valid minimum
|
||||
'100' => 100, // Valid default
|
||||
'1000' => 1000, // Valid maximum
|
||||
'1500' => 1000, // Above maximum, should be clamped to 1000
|
||||
'invalid' => 1, // Invalid value, should be clamped to 1 (via int cast)
|
||||
'' => 1 // Empty value, should be clamped to 1
|
||||
];
|
||||
|
||||
foreach ($testCases as $envValue => $expectedResult) {
|
||||
$_ENV['GDPR_MAX_DEPTH'] = $envValue;
|
||||
|
||||
$config = $this->getTestConfig();
|
||||
|
||||
$this->assertSame(
|
||||
$expectedResult,
|
||||
$config['max_depth'],
|
||||
sprintf(TestConstants::TEMPLATE_ENV_VALUE_RESULT_FULL, $envValue, $expectedResult)
|
||||
);
|
||||
}
|
||||
|
||||
unset($_ENV['GDPR_MAX_DEPTH']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function auditLoggingEnabledValidatesBooleanValues(): void
|
||||
{
|
||||
$testCases = [
|
||||
'true' => true,
|
||||
'1' => true,
|
||||
'false' => false,
|
||||
'0' => false,
|
||||
'invalid' => false
|
||||
];
|
||||
|
||||
foreach ($testCases as $envValue => $expectedResult) {
|
||||
$_ENV['GDPR_AUDIT_ENABLED'] = $envValue;
|
||||
|
||||
$config = $this->getTestConfig();
|
||||
|
||||
$this->assertSame(
|
||||
$expectedResult,
|
||||
$config['audit_logging']['enabled'],
|
||||
sprintf(TestConstants::TEMPLATE_ENV_VALUE_RESULT, $envValue) . ($expectedResult ? 'true' : 'false')
|
||||
);
|
||||
}
|
||||
|
||||
unset($_ENV['GDPR_AUDIT_ENABLED']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function auditLoggingChannelHandlesEmptyValues(): void
|
||||
{
|
||||
$testCases = [
|
||||
'custom-channel' => 'custom-channel',
|
||||
' spaced ' => 'spaced', // Should be trimmed
|
||||
'' => 'gdpr-audit', // Empty should use default
|
||||
' ' => 'gdpr-audit' // Whitespace only should use default
|
||||
];
|
||||
|
||||
foreach ($testCases as $envValue => $expectedResult) {
|
||||
$_ENV['GDPR_AUDIT_CHANNEL'] = $envValue;
|
||||
|
||||
$config = $this->getTestConfig();
|
||||
|
||||
$this->assertSame(
|
||||
$expectedResult,
|
||||
$config['audit_logging']['channel'],
|
||||
sprintf("Environment value '%s' should result in '%s'", $envValue, $expectedResult)
|
||||
);
|
||||
}
|
||||
|
||||
unset($_ENV['GDPR_AUDIT_CHANNEL']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function performanceChunkSizeHasValidBounds(): void
|
||||
{
|
||||
$testCases = [
|
||||
'50' => 100, // Below minimum, should be clamped to 100
|
||||
'100' => 100, // Valid minimum
|
||||
'1000' => 1000, // Valid default
|
||||
'10000' => 10000, // Valid maximum
|
||||
'15000' => 10000, // Above maximum, should be clamped to 10000
|
||||
'invalid' => 100 // Invalid value, should be clamped to minimum
|
||||
];
|
||||
|
||||
foreach ($testCases as $envValue => $expectedResult) {
|
||||
$_ENV['GDPR_CHUNK_SIZE'] = $envValue;
|
||||
|
||||
$config = $this->getTestConfig();
|
||||
|
||||
$this->assertSame(
|
||||
$expectedResult,
|
||||
$config['performance']['chunk_size'],
|
||||
sprintf(TestConstants::TEMPLATE_ENV_VALUE_RESULT_FULL, $envValue, $expectedResult)
|
||||
);
|
||||
}
|
||||
|
||||
unset($_ENV['GDPR_CHUNK_SIZE']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function performanceGcThresholdHasValidBounds(): void
|
||||
{
|
||||
$testCases = [
|
||||
'500' => 1000, // Below minimum, should be clamped to 1000
|
||||
'1000' => 1000, // Valid minimum
|
||||
'10000' => 10000, // Valid default
|
||||
'100000' => 100000, // Valid maximum
|
||||
'150000' => 100000, // Above maximum, should be clamped to 100000
|
||||
'invalid' => 1000 // Invalid value, should be clamped to minimum
|
||||
];
|
||||
|
||||
foreach ($testCases as $envValue => $expectedResult) {
|
||||
$_ENV['GDPR_GC_THRESHOLD'] = $envValue;
|
||||
|
||||
$config = $this->getTestConfig();
|
||||
|
||||
$this->assertSame(
|
||||
$expectedResult,
|
||||
$config['performance']['garbage_collection_threshold'],
|
||||
sprintf(TestConstants::TEMPLATE_ENV_VALUE_RESULT_FULL, $envValue, $expectedResult)
|
||||
);
|
||||
}
|
||||
|
||||
unset($_ENV['GDPR_GC_THRESHOLD']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function validationSectionExists(): void
|
||||
{
|
||||
$config = $this->getTestConfig();
|
||||
|
||||
$this->assertArrayHasKey('validation', $config, 'Configuration should have validation section');
|
||||
$this->assertIsArray($config['validation'], 'Validation section should be an array');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function validationSectionHasRequiredKeys(): void
|
||||
{
|
||||
$config = $this->getTestConfig();
|
||||
|
||||
$validationKeys = [
|
||||
'max_pattern_length',
|
||||
'max_field_path_length',
|
||||
'allow_empty_patterns',
|
||||
'strict_regex_validation'
|
||||
];
|
||||
|
||||
foreach ($validationKeys as $key) {
|
||||
$this->assertArrayHasKey(
|
||||
$key,
|
||||
$config['validation'],
|
||||
sprintf("Validation section should have '%s' key", $key)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function validationMaxPatternLengthHasValidBounds(): void
|
||||
{
|
||||
$testCases = [
|
||||
'5' => 10, // Below minimum, should be clamped to 10
|
||||
'10' => 10, // Valid minimum
|
||||
'500' => 500, // Valid default
|
||||
'1000' => 1000, // Valid maximum
|
||||
'1500' => 1000, // Above maximum, should be clamped to 1000
|
||||
'invalid' => 10 // Invalid value, should be clamped to minimum
|
||||
];
|
||||
|
||||
foreach ($testCases as $envValue => $expectedResult) {
|
||||
$_ENV['GDPR_MAX_PATTERN_LENGTH'] = $envValue;
|
||||
|
||||
$config = $this->getTestConfig();
|
||||
|
||||
$this->assertSame(
|
||||
$expectedResult,
|
||||
$config['validation']['max_pattern_length'],
|
||||
sprintf(TestConstants::TEMPLATE_ENV_VALUE_RESULT_FULL, $envValue, $expectedResult)
|
||||
);
|
||||
}
|
||||
|
||||
unset($_ENV['GDPR_MAX_PATTERN_LENGTH']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function validationMaxFieldPathLengthHasValidBounds(): void
|
||||
{
|
||||
$testCases = [
|
||||
'3' => 5, // Below minimum, should be clamped to 5
|
||||
'5' => 5, // Valid minimum
|
||||
'100' => 100, // Valid default
|
||||
'500' => 500, // Valid maximum
|
||||
'600' => 500, // Above maximum, should be clamped to 500
|
||||
'invalid' => 5 // Invalid value, should be clamped to minimum
|
||||
];
|
||||
|
||||
foreach ($testCases as $envValue => $expectedResult) {
|
||||
$_ENV['GDPR_MAX_FIELD_PATH_LENGTH'] = $envValue;
|
||||
|
||||
$config = $this->getTestConfig();
|
||||
|
||||
$this->assertSame(
|
||||
$expectedResult,
|
||||
$config['validation']['max_field_path_length'],
|
||||
sprintf(TestConstants::TEMPLATE_ENV_VALUE_RESULT_FULL, $envValue, $expectedResult)
|
||||
);
|
||||
}
|
||||
|
||||
unset($_ENV['GDPR_MAX_FIELD_PATH_LENGTH']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function validationAllowEmptyPatternsValidatesBooleanValues(): void
|
||||
{
|
||||
$testCases = [
|
||||
'true' => true,
|
||||
'1' => true,
|
||||
'false' => false,
|
||||
'0' => false,
|
||||
'invalid' => false
|
||||
];
|
||||
|
||||
foreach ($testCases as $envValue => $expectedResult) {
|
||||
$_ENV['GDPR_ALLOW_EMPTY_PATTERNS'] = $envValue;
|
||||
|
||||
$config = $this->getTestConfig();
|
||||
|
||||
$this->assertSame(
|
||||
$expectedResult,
|
||||
$config['validation']['allow_empty_patterns'],
|
||||
sprintf(TestConstants::TEMPLATE_ENV_VALUE_RESULT, $envValue) . ($expectedResult ? 'true' : 'false')
|
||||
);
|
||||
}
|
||||
|
||||
unset($_ENV['GDPR_ALLOW_EMPTY_PATTERNS']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function validationStrictRegexValidationValidatesBooleanValues(): void
|
||||
{
|
||||
$testCases = [
|
||||
'true' => true,
|
||||
'1' => true,
|
||||
'false' => false,
|
||||
'0' => false,
|
||||
'invalid' => false
|
||||
];
|
||||
|
||||
foreach ($testCases as $envValue => $expectedResult) {
|
||||
$_ENV['GDPR_STRICT_REGEX_VALIDATION'] = $envValue;
|
||||
|
||||
$config = $this->getTestConfig();
|
||||
|
||||
$this->assertSame(
|
||||
$expectedResult,
|
||||
$config['validation']['strict_regex_validation'],
|
||||
sprintf(TestConstants::TEMPLATE_ENV_VALUE_RESULT, $envValue) . ($expectedResult ? 'true' : 'false')
|
||||
);
|
||||
}
|
||||
|
||||
unset($_ENV['GDPR_STRICT_REGEX_VALIDATION']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function configDefaultsAreSecure(): void
|
||||
{
|
||||
// Clear all environment variables to test defaults
|
||||
$envVars = [
|
||||
'GDPR_AUTO_REGISTER',
|
||||
'GDPR_AUDIT_ENABLED',
|
||||
'GDPR_ALLOW_EMPTY_PATTERNS'
|
||||
];
|
||||
|
||||
$oldValues = [];
|
||||
foreach ($envVars as $var) {
|
||||
$oldValues[$var] = $_ENV[$var] ?? null;
|
||||
unset($_ENV[$var]);
|
||||
}
|
||||
|
||||
$config = $this->getTestConfig();
|
||||
|
||||
// Security-focused defaults
|
||||
$this->assertFalse($config['auto_register'], 'auto_register should default to false');
|
||||
$this->assertFalse($config['audit_logging']['enabled'], 'audit logging should default to false');
|
||||
$this->assertFalse($config['validation']['allow_empty_patterns'], 'empty patterns should not be allowed by default');
|
||||
$this->assertTrue($config['validation']['strict_regex_validation'], 'strict regex validation should be enabled by default');
|
||||
|
||||
// Restore environment variables
|
||||
foreach ($oldValues as $var => $value) {
|
||||
if ($value !== null) {
|
||||
$_ENV[$var] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function configHandlesAllDataTypes(): void
|
||||
{
|
||||
$config = $this->getTestConfig();
|
||||
|
||||
// Test data types
|
||||
$this->assertIsBool($config['auto_register']);
|
||||
$this->assertIsArray($config['channels']);
|
||||
$this->assertIsArray($config['patterns']);
|
||||
$this->assertIsArray($config['field_paths']);
|
||||
$this->assertIsArray($config['custom_callbacks']);
|
||||
$this->assertIsInt($config['max_depth']);
|
||||
$this->assertIsArray($config['audit_logging']);
|
||||
$this->assertIsBool($config['audit_logging']['enabled']);
|
||||
$this->assertIsString($config['audit_logging']['channel']);
|
||||
$this->assertIsArray($config['performance']);
|
||||
$this->assertIsInt($config['performance']['chunk_size']);
|
||||
$this->assertIsInt($config['performance']['garbage_collection_threshold']);
|
||||
$this->assertIsArray($config['validation']);
|
||||
$this->assertIsInt($config['validation']['max_pattern_length']);
|
||||
$this->assertIsInt($config['validation']['max_field_path_length']);
|
||||
$this->assertIsBool($config['validation']['allow_empty_patterns']);
|
||||
$this->assertIsBool($config['validation']['strict_regex_validation']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function configBoundsAreReasonable(): void
|
||||
{
|
||||
$config = $this->getTestConfig();
|
||||
|
||||
// Test reasonable bounds
|
||||
$this->assertGreaterThanOrEqual(1, $config['max_depth']);
|
||||
$this->assertLessThanOrEqual(1000, $config['max_depth']);
|
||||
|
||||
$this->assertGreaterThanOrEqual(100, $config['performance']['chunk_size']);
|
||||
$this->assertLessThanOrEqual(10000, $config['performance']['chunk_size']);
|
||||
|
||||
$this->assertGreaterThanOrEqual(1000, $config['performance']['garbage_collection_threshold']);
|
||||
$this->assertLessThanOrEqual(100000, $config['performance']['garbage_collection_threshold']);
|
||||
|
||||
$this->assertGreaterThanOrEqual(10, $config['validation']['max_pattern_length']);
|
||||
$this->assertLessThanOrEqual(1000, $config['validation']['max_pattern_length']);
|
||||
|
||||
$this->assertGreaterThanOrEqual(5, $config['validation']['max_field_path_length']);
|
||||
$this->assertLessThanOrEqual(500, $config['validation']['max_field_path_length']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function configChannelsArrayIsValid(): void
|
||||
{
|
||||
$config = $this->getTestConfig();
|
||||
|
||||
$this->assertIsArray($config['channels']);
|
||||
$this->assertNotEmpty($config['channels']);
|
||||
|
||||
foreach ($config['channels'] as $channel) {
|
||||
$this->assertIsString($channel, 'Each channel should be a string');
|
||||
$this->assertNotEmpty($channel, 'Channel names should not be empty');
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function configEmptyArraysAreProperlyInitialized(): void
|
||||
{
|
||||
$config = $this->getTestConfig();
|
||||
|
||||
// These should be empty arrays by default but properly initialized
|
||||
$this->assertIsArray($config['patterns']);
|
||||
$this->assertIsArray($config['field_paths']);
|
||||
$this->assertIsArray($config['custom_callbacks']);
|
||||
|
||||
// They can be empty, that's fine
|
||||
$this->assertCount(0, $config['patterns']);
|
||||
$this->assertCount(0, $config['field_paths']);
|
||||
$this->assertCount(0, $config['custom_callbacks']);
|
||||
}
|
||||
}
|
||||
282
tests/InputValidation/FieldMaskConfigValidationTest.php
Normal file
282
tests/InputValidation/FieldMaskConfigValidationTest.php
Normal file
@@ -0,0 +1,282 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\InputValidation;
|
||||
|
||||
use Ivuorinen\MonologGdprFilter\Exceptions\InvalidConfigurationException;
|
||||
use Ivuorinen\MonologGdprFilter\Exceptions\InvalidRegexPatternException;
|
||||
use Ivuorinen\MonologGdprFilter\FieldMaskConfig;
|
||||
use Ivuorinen\MonologGdprFilter\MaskConstants as Mask;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tests\TestConstants;
|
||||
|
||||
/**
|
||||
* Tests for the FieldMaskConfig class.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[CoversClass(FieldMaskConfig::class)]
|
||||
class FieldMaskConfigValidationTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function regexMaskThrowsExceptionForEmptyPattern(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage('Regex pattern cannot be empty');
|
||||
|
||||
FieldMaskConfig::regexMask('');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function regexMaskThrowsExceptionForWhitespaceOnlyPattern(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage('Regex pattern cannot be empty');
|
||||
|
||||
FieldMaskConfig::regexMask(' ');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function regexMaskThrowsExceptionForEmptyReplacement(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage('Replacement string cannot be empty');
|
||||
|
||||
FieldMaskConfig::regexMask('/valid/', '');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function regexMaskThrowsExceptionForWhitespaceOnlyReplacement(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage('Replacement string cannot be empty');
|
||||
|
||||
FieldMaskConfig::regexMask('/valid/', ' ');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function regexMaskThrowsExceptionForInvalidRegexPattern(): void
|
||||
{
|
||||
$this->expectException(InvalidRegexPatternException::class);
|
||||
$this->expectExceptionMessage("Invalid regex pattern 'invalid_regex'");
|
||||
|
||||
FieldMaskConfig::regexMask('invalid_regex');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function regexMaskThrowsExceptionForIncompleteRegexPattern(): void
|
||||
{
|
||||
$this->expectException(InvalidRegexPatternException::class);
|
||||
$this->expectExceptionMessage("Invalid regex pattern '/unclosed'");
|
||||
|
||||
FieldMaskConfig::regexMask('/unclosed');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function regexMaskThrowsExceptionForEmptyDelimitersPattern(): void
|
||||
{
|
||||
$this->expectException(InvalidRegexPatternException::class);
|
||||
$this->expectExceptionMessage("Invalid regex pattern '//'");
|
||||
|
||||
FieldMaskConfig::regexMask('//');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function regexMaskAcceptsValidPattern(): void
|
||||
{
|
||||
$config = FieldMaskConfig::regexMask(TestConstants::PATTERN_DIGITS, Mask::MASK_NUMBER);
|
||||
|
||||
$this->assertSame(FieldMaskConfig::MASK_REGEX, $config->type);
|
||||
$this->assertSame(TestConstants::PATTERN_DIGITS . '::' . Mask::MASK_NUMBER, $config->replacement);
|
||||
$this->assertSame(TestConstants::PATTERN_DIGITS, $config->getRegexPattern());
|
||||
$this->assertSame(Mask::MASK_NUMBER, $config->getReplacement());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function regexMaskUsesDefaultReplacementWhenNotProvided(): void
|
||||
{
|
||||
$config = FieldMaskConfig::regexMask(TestConstants::PATTERN_TEST);
|
||||
|
||||
$this->assertSame(Mask::MASK_MASKED, $config->getReplacement());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function regexMaskAcceptsComplexRegexPatterns(): void
|
||||
{
|
||||
$complexPattern = '/(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/';
|
||||
$config = FieldMaskConfig::regexMask($complexPattern, Mask::MASK_IP);
|
||||
|
||||
$this->assertSame(FieldMaskConfig::MASK_REGEX, $config->type);
|
||||
$this->assertSame($complexPattern, $config->getRegexPattern());
|
||||
$this->assertSame(Mask::MASK_IP, $config->getReplacement());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function fromArrayThrowsExceptionForInvalidType(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage("Must be one of: mask_regex, remove, replace");
|
||||
|
||||
FieldMaskConfig::fromArray(['type' => 'invalid_type']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function fromArrayThrowsExceptionForEmptyReplacementWithReplaceType(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage(TestConstants::ERROR_REPLACE_TYPE_EMPTY);
|
||||
|
||||
FieldMaskConfig::fromArray([
|
||||
'type' => FieldMaskConfig::REPLACE,
|
||||
'replacement' => ''
|
||||
]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function fromArrayThrowsExceptionForNullReplacementWithReplaceType(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage(TestConstants::ERROR_REPLACE_TYPE_EMPTY);
|
||||
|
||||
FieldMaskConfig::fromArray([
|
||||
'type' => FieldMaskConfig::REPLACE,
|
||||
'replacement' => null
|
||||
]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function fromArrayThrowsExceptionForWhitespaceOnlyReplacementWithReplaceType(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage(TestConstants::ERROR_REPLACE_TYPE_EMPTY);
|
||||
|
||||
FieldMaskConfig::fromArray([
|
||||
'type' => FieldMaskConfig::REPLACE,
|
||||
'replacement' => ' '
|
||||
]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function fromArrayAcceptsValidRemoveType(): void
|
||||
{
|
||||
$config = FieldMaskConfig::fromArray(['type' => FieldMaskConfig::REMOVE]);
|
||||
|
||||
$this->assertSame(FieldMaskConfig::REMOVE, $config->type);
|
||||
$this->assertNull($config->replacement);
|
||||
$this->assertTrue($config->shouldRemove());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function fromArrayAcceptsValidReplaceType(): void
|
||||
{
|
||||
$config = FieldMaskConfig::fromArray([
|
||||
'type' => FieldMaskConfig::REPLACE,
|
||||
'replacement' => Mask::MASK_BRACKETS
|
||||
]);
|
||||
|
||||
$this->assertSame(FieldMaskConfig::REPLACE, $config->type);
|
||||
$this->assertSame(Mask::MASK_BRACKETS, $config->replacement);
|
||||
$this->assertSame(Mask::MASK_BRACKETS, $config->getReplacement());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function fromArrayAcceptsValidMaskRegexType(): void
|
||||
{
|
||||
$config = FieldMaskConfig::fromArray([
|
||||
'type' => FieldMaskConfig::MASK_REGEX,
|
||||
'replacement' => TestConstants::PATTERN_DIGITS . '::' . Mask::MASK_NUMBER
|
||||
]);
|
||||
|
||||
$this->assertSame(FieldMaskConfig::MASK_REGEX, $config->type);
|
||||
$this->assertTrue($config->hasRegexPattern());
|
||||
$this->assertSame(TestConstants::PATTERN_DIGITS, $config->getRegexPattern());
|
||||
$this->assertSame(Mask::MASK_NUMBER, $config->getReplacement());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function fromArrayUsesDefaultValuesWhenMissing(): void
|
||||
{
|
||||
$config = FieldMaskConfig::fromArray([]);
|
||||
|
||||
$this->assertSame(FieldMaskConfig::REPLACE, $config->type);
|
||||
$this->assertNull($config->replacement);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function fromArrayHandlesMissingReplacementForNonReplaceTypes(): void
|
||||
{
|
||||
$config = FieldMaskConfig::fromArray(['type' => FieldMaskConfig::REMOVE]);
|
||||
|
||||
$this->assertSame(FieldMaskConfig::REMOVE, $config->type);
|
||||
$this->assertNull($config->replacement);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function toArrayAndFromArrayRoundTripWorksCorrectly(): void
|
||||
{
|
||||
$original = FieldMaskConfig::replace('[REDACTED]');
|
||||
$array = $original->toArray();
|
||||
$restored = FieldMaskConfig::fromArray($array);
|
||||
|
||||
$this->assertSame($original->type, $restored->type);
|
||||
$this->assertSame($original->replacement, $restored->replacement);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorAcceptsValidParameters(): void
|
||||
{
|
||||
$config = new FieldMaskConfig(FieldMaskConfig::REPLACE, TestConstants::REPLACEMENT_TEST);
|
||||
|
||||
$this->assertSame(FieldMaskConfig::REPLACE, $config->type);
|
||||
$this->assertSame(TestConstants::REPLACEMENT_TEST, $config->replacement);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorAcceptsNullReplacement(): void
|
||||
{
|
||||
$config = new FieldMaskConfig(FieldMaskConfig::REMOVE, null);
|
||||
|
||||
$this->assertSame(FieldMaskConfig::REMOVE, $config->type);
|
||||
$this->assertNull($config->replacement);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function staticMethodsCreateCorrectConfigurations(): void
|
||||
{
|
||||
$removeConfig = FieldMaskConfig::remove();
|
||||
$this->assertTrue($removeConfig->shouldRemove());
|
||||
$this->assertSame(FieldMaskConfig::REMOVE, $removeConfig->type);
|
||||
|
||||
$replaceConfig = FieldMaskConfig::replace('[HIDDEN]');
|
||||
$this->assertSame(FieldMaskConfig::REPLACE, $replaceConfig->type);
|
||||
$this->assertSame('[HIDDEN]', $replaceConfig->getReplacement());
|
||||
|
||||
$regexConfig = FieldMaskConfig::regexMask('/email/', Mask::MASK_EMAIL);
|
||||
$this->assertTrue($regexConfig->hasRegexPattern());
|
||||
$this->assertSame('/email/', $regexConfig->getRegexPattern());
|
||||
$this->assertSame(Mask::MASK_EMAIL, $regexConfig->getReplacement());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getRegexPatternReturnsNullForNonRegexTypes(): void
|
||||
{
|
||||
$removeConfig = FieldMaskConfig::remove();
|
||||
$this->assertNull($removeConfig->getRegexPattern());
|
||||
|
||||
$replaceConfig = FieldMaskConfig::replace(TestConstants::REPLACEMENT_TEST);
|
||||
$this->assertNull($replaceConfig->getRegexPattern());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function hasRegexPatternReturnsFalseForNonRegexTypes(): void
|
||||
{
|
||||
$removeConfig = FieldMaskConfig::remove();
|
||||
$this->assertFalse($removeConfig->hasRegexPattern());
|
||||
|
||||
$replaceConfig = FieldMaskConfig::replace(TestConstants::REPLACEMENT_TEST);
|
||||
$this->assertFalse($replaceConfig->hasRegexPattern());
|
||||
}
|
||||
}
|
||||
433
tests/InputValidation/GdprProcessorValidationTest.php
Normal file
433
tests/InputValidation/GdprProcessorValidationTest.php
Normal file
@@ -0,0 +1,433 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\InputValidation;
|
||||
|
||||
use Ivuorinen\MonologGdprFilter\Exceptions\InvalidConfigurationException;
|
||||
use Ivuorinen\MonologGdprFilter\MaskConstants;
|
||||
use Ivuorinen\MonologGdprFilter\Exceptions\InvalidRegexPatternException;
|
||||
use Ivuorinen\MonologGdprFilter\FieldMaskConfig;
|
||||
use Ivuorinen\MonologGdprFilter\GdprProcessor;
|
||||
use Monolog\Level;
|
||||
use Monolog\LogRecord;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Ivuorinen\MonologGdprFilter\PatternValidator;
|
||||
use Tests\TestConstants;
|
||||
|
||||
/**
|
||||
* Tests for the GdprProcessor class.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[CoversClass(GdprProcessor::class)]
|
||||
class GdprProcessorValidationTest extends TestCase
|
||||
{
|
||||
#[\Override]
|
||||
protected function tearDown(): void
|
||||
{
|
||||
// Clear pattern cache between tests
|
||||
PatternValidator::clearCache();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForNonStringPatternKey(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage('Pattern must be of type string, got integer');
|
||||
|
||||
new GdprProcessor([123 => 'replacement']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForEmptyPatternKey(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage('Pattern cannot be empty');
|
||||
|
||||
new GdprProcessor(['' => 'replacement']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForWhitespaceOnlyPatternKey(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage('Pattern cannot be empty');
|
||||
|
||||
new GdprProcessor([' ' => 'replacement']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForNonStringPatternReplacement(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage('Pattern replacement must be of type string, got integer');
|
||||
|
||||
$processor = new GdprProcessor([TestConstants::PATTERN_TEST => 123]);
|
||||
$this->assertInstanceOf(GdprProcessor::class, $processor);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForInvalidRegexPattern(): void
|
||||
{
|
||||
$this->expectException(InvalidRegexPatternException::class);
|
||||
$this->expectExceptionMessage("Invalid regex pattern 'invalid_pattern'");
|
||||
|
||||
new GdprProcessor(['invalid_pattern' => 'replacement']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorAcceptsValidPatterns(): void
|
||||
{
|
||||
$processor = new GdprProcessor([
|
||||
TestConstants::PATTERN_DIGITS => MaskConstants::MASK_NUMBER,
|
||||
TestConstants::PATTERN_TEST => MaskConstants::MASK_MASKED
|
||||
]);
|
||||
|
||||
$this->assertInstanceOf(GdprProcessor::class, $processor);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForNonStringFieldPathKey(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage('Field path must be of type string, got integer');
|
||||
|
||||
new GdprProcessor([], [123 => FieldMaskConfig::remove()]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForEmptyFieldPathKey(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage('Field path cannot be empty');
|
||||
|
||||
new GdprProcessor([], ['' => FieldMaskConfig::remove()]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForWhitespaceOnlyFieldPathKey(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage('Field path cannot be empty');
|
||||
|
||||
new GdprProcessor([], [' ' => FieldMaskConfig::remove()]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForInvalidFieldPathValue(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage('Field path value must be of type FieldMaskConfig or string, got integer');
|
||||
|
||||
$processor = new GdprProcessor([], [TestConstants::FIELD_USER_EMAIL => 123]);
|
||||
$this->assertInstanceOf(GdprProcessor::class, $processor);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForEmptyStringFieldPathValue(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage("Cannot have empty string value");
|
||||
|
||||
$processor = new GdprProcessor([], [TestConstants::FIELD_USER_EMAIL => '']);
|
||||
$this->assertInstanceOf(GdprProcessor::class, $processor);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorAcceptsValidFieldPaths(): void
|
||||
{
|
||||
$processor = new GdprProcessor([], [
|
||||
TestConstants::FIELD_USER_EMAIL => FieldMaskConfig::remove(),
|
||||
TestConstants::FIELD_USER_NAME => 'masked_value',
|
||||
'payment.card' => FieldMaskConfig::replace('[CARD]')
|
||||
]);
|
||||
|
||||
$this->assertInstanceOf(GdprProcessor::class, $processor);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForNonStringCustomCallbackKey(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage('Custom callback path must be of type string, got integer');
|
||||
|
||||
new GdprProcessor([], [], [123 => fn($value) => $value]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForEmptyCustomCallbackKey(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage('Custom callback path cannot be empty');
|
||||
|
||||
new GdprProcessor([], [], ['' => fn($value) => $value]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForWhitespaceOnlyCustomCallbackKey(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage('Custom callback path cannot be empty');
|
||||
|
||||
new GdprProcessor([], [], [' ' => fn($value) => $value]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForNonCallableCustomCallback(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage("Must be callable");
|
||||
|
||||
new GdprProcessor([], [], ['user.id' => 'not_callable']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorAcceptsValidCustomCallbacks(): void
|
||||
{
|
||||
$processor = new GdprProcessor([], [], [
|
||||
'user.id' => fn($value): string => hash('sha256', (string) $value),
|
||||
TestConstants::FIELD_USER_NAME => fn($value) => strtoupper((string) $value)
|
||||
]);
|
||||
|
||||
$this->assertInstanceOf(GdprProcessor::class, $processor);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForNonCallableAuditLogger(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage('Audit logger must be of type callable or null, got string');
|
||||
|
||||
new GdprProcessor([], [], [], 'not_callable');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorAcceptsNullAuditLogger(): void
|
||||
{
|
||||
$processor = new GdprProcessor([], [], [], null);
|
||||
$this->assertInstanceOf(GdprProcessor::class, $processor);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorAcceptsCallableAuditLogger(): void
|
||||
{
|
||||
$processor = new GdprProcessor([], [], [], fn($path, $original, $masked): null => null);
|
||||
$this->assertInstanceOf(GdprProcessor::class, $processor);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForZeroMaxDepth(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage('Must be a positive integer');
|
||||
|
||||
new GdprProcessor([], [], [], null, 0);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForNegativeMaxDepth(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage('Must be a positive integer');
|
||||
|
||||
new GdprProcessor([], [], [], null, -10);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForExcessiveMaxDepth(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage('Cannot exceed 1,000 for stack safety');
|
||||
|
||||
new GdprProcessor([], [], [], null, 1001);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorAcceptsValidMaxDepth(): void
|
||||
{
|
||||
$processor1 = new GdprProcessor([], [], [], null, 1);
|
||||
$this->assertInstanceOf(GdprProcessor::class, $processor1);
|
||||
|
||||
$processor2 = new GdprProcessor([], [], [], null, 1000);
|
||||
$this->assertInstanceOf(GdprProcessor::class, $processor2);
|
||||
|
||||
$processor3 = new GdprProcessor([], [], [], null, 100);
|
||||
$this->assertInstanceOf(GdprProcessor::class, $processor3);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForNonStringDataTypeMaskKey(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage('Data type mask key must be of type string, got integer');
|
||||
|
||||
new GdprProcessor([], [], [], null, 100, [123 => MaskConstants::MASK_MASKED]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForInvalidDataTypeMaskKey(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage("Must be one of: integer, double, string, boolean, NULL, array, object, resource");
|
||||
|
||||
new GdprProcessor([], [], [], null, 100, ['invalid_type' => MaskConstants::MASK_MASKED]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForNonStringDataTypeMaskValue(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage('Data type mask value must be of type string, got integer');
|
||||
|
||||
new GdprProcessor([], [], [], null, 100, ['string' => 123]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForEmptyDataTypeMaskValue(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage("Cannot be empty");
|
||||
|
||||
new GdprProcessor([], [], [], null, 100, ['string' => '']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForWhitespaceOnlyDataTypeMaskValue(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage("Cannot be empty");
|
||||
|
||||
new GdprProcessor([], [], [], null, 100, ['string' => ' ']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorAcceptsValidDataTypeMasks(): void
|
||||
{
|
||||
$processor = new GdprProcessor([], [], [], null, 100, [
|
||||
'string' => MaskConstants::MASK_STRING,
|
||||
'integer' => MaskConstants::MASK_INT,
|
||||
'double' => MaskConstants::MASK_FLOAT,
|
||||
'boolean' => MaskConstants::MASK_BOOL,
|
||||
'NULL' => MaskConstants::MASK_NULL,
|
||||
'array' => MaskConstants::MASK_ARRAY,
|
||||
'object' => MaskConstants::MASK_OBJECT,
|
||||
'resource' => MaskConstants::MASK_RESOURCE
|
||||
]);
|
||||
|
||||
$this->assertInstanceOf(GdprProcessor::class, $processor);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForNonStringConditionalRuleKey(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage('Conditional rule name must be of type string, got integer');
|
||||
|
||||
new GdprProcessor([], [], [], null, 100, [], [123 => fn(): true => true]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForEmptyConditionalRuleKey(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage('Conditional rule name cannot be empty');
|
||||
|
||||
new GdprProcessor([], [], [], null, 100, [], ['' => fn(): true => true]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForWhitespaceOnlyConditionalRuleKey(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage('Conditional rule name cannot be empty');
|
||||
|
||||
new GdprProcessor([], [], [], null, 100, [], [' ' => fn(): true => true]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForNonCallableConditionalRule(): void
|
||||
{
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage("Must have a callable callback");
|
||||
|
||||
new GdprProcessor([], [], [], null, 100, [], ['level_rule' => 'not_callable']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorAcceptsValidConditionalRules(): void
|
||||
{
|
||||
$processor = new GdprProcessor([], [], [], null, 100, [], [
|
||||
'level_rule' => fn(LogRecord $record): bool => $record->level === Level::Error,
|
||||
'channel_rule' => fn(LogRecord $record): bool => $record->channel === 'app'
|
||||
]);
|
||||
|
||||
$this->assertInstanceOf(GdprProcessor::class, $processor);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorAcceptsEmptyArraysForOptionalParameters(): void
|
||||
{
|
||||
$processor = new GdprProcessor([]);
|
||||
$this->assertInstanceOf(GdprProcessor::class, $processor);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorAcceptsAllParametersWithValidValues(): void
|
||||
{
|
||||
$processor = new GdprProcessor(
|
||||
patterns: [TestConstants::PATTERN_DIGITS => MaskConstants::MASK_NUMBER],
|
||||
fieldPaths: [TestConstants::FIELD_USER_EMAIL => FieldMaskConfig::remove()],
|
||||
customCallbacks: ['user.id' => fn($value): string => hash('sha256', (string) $value)],
|
||||
auditLogger: fn($path, $original, $masked): null => null,
|
||||
maxDepth: 50,
|
||||
dataTypeMasks: ['string' => MaskConstants::MASK_STRING],
|
||||
conditionalRules: ['level_rule' => fn(LogRecord $record): true => true]
|
||||
);
|
||||
|
||||
$this->assertInstanceOf(GdprProcessor::class, $processor);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorValidatesMultipleInvalidParametersAndThrowsFirstError(): void
|
||||
{
|
||||
// Should throw for the first validation error (patterns)
|
||||
$this->expectException(InvalidConfigurationException::class);
|
||||
$this->expectExceptionMessage('Pattern must be of type string, got integer');
|
||||
|
||||
new GdprProcessor(
|
||||
patterns: [123 => 'replacement'], // First error
|
||||
fieldPaths: [456 => 'value'], // Second error (won't be reached)
|
||||
maxDepth: -1 // Third error (won't be reached)
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorHandlesComplexValidRegexPatterns(): void
|
||||
{
|
||||
$complexPatterns = [
|
||||
'/(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/' => MaskConstants::MASK_IP,
|
||||
TestConstants::PATTERN_EMAIL_FULL => MaskConstants::MASK_EMAIL,
|
||||
'/\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/' => MaskConstants::MASK_CARD
|
||||
];
|
||||
|
||||
$processor = new GdprProcessor($complexPatterns);
|
||||
$this->assertInstanceOf(GdprProcessor::class, $processor);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorHandlesMixedFieldPathConfigTypes(): void
|
||||
{
|
||||
$processor = new GdprProcessor([], [
|
||||
TestConstants::FIELD_USER_EMAIL => FieldMaskConfig::remove(),
|
||||
TestConstants::FIELD_USER_NAME => FieldMaskConfig::replace('[REDACTED]'),
|
||||
'user.phone' => FieldMaskConfig::regexMask('/\d/', '*'),
|
||||
'metadata.ip' => 'simple_string_replacement'
|
||||
]);
|
||||
|
||||
$this->assertInstanceOf(GdprProcessor::class, $processor);
|
||||
}
|
||||
}
|
||||
397
tests/InputValidation/RateLimiterValidationTest.php
Normal file
397
tests/InputValidation/RateLimiterValidationTest.php
Normal file
@@ -0,0 +1,397 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\InputValidation;
|
||||
|
||||
use Ivuorinen\MonologGdprFilter\Exceptions\InvalidRateLimitConfigurationException;
|
||||
use Ivuorinen\MonologGdprFilter\RateLimiter;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tests\TestConstants;
|
||||
|
||||
/**
|
||||
* Tests for the RateLimiter class.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[CoversClass(RateLimiter::class)]
|
||||
class RateLimiterValidationTest extends TestCase
|
||||
{
|
||||
#[\Override]
|
||||
protected function tearDown(): void
|
||||
{
|
||||
// Clean up static state between tests
|
||||
RateLimiter::clearAll();
|
||||
RateLimiter::setCleanupInterval(300); // Reset to default
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForZeroMaxRequests(): void
|
||||
{
|
||||
$this->expectException(InvalidRateLimitConfigurationException::class);
|
||||
$this->expectExceptionMessage('Maximum requests must be a positive integer, got: 0');
|
||||
|
||||
new RateLimiter(0, 60);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForNegativeMaxRequests(): void
|
||||
{
|
||||
$this->expectException(InvalidRateLimitConfigurationException::class);
|
||||
$this->expectExceptionMessage('Maximum requests must be a positive integer, got: -10');
|
||||
|
||||
new RateLimiter(-10, 60);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForExcessiveMaxRequests(): void
|
||||
{
|
||||
$this->expectException(InvalidRateLimitConfigurationException::class);
|
||||
$this->expectExceptionMessage('Cannot exceed 1,000,000 for memory safety');
|
||||
|
||||
new RateLimiter(1000001, 60);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForZeroWindowSeconds(): void
|
||||
{
|
||||
$this->expectException(InvalidRateLimitConfigurationException::class);
|
||||
$this->expectExceptionMessage('Time window must be a positive integer representing seconds, got: 0');
|
||||
|
||||
new RateLimiter(10, 0);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForNegativeWindowSeconds(): void
|
||||
{
|
||||
$this->expectException(InvalidRateLimitConfigurationException::class);
|
||||
$this->expectExceptionMessage('Time window must be a positive integer representing seconds, got: -30');
|
||||
|
||||
new RateLimiter(10, -30);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorThrowsExceptionForExcessiveWindowSeconds(): void
|
||||
{
|
||||
$this->expectException(InvalidRateLimitConfigurationException::class);
|
||||
$this->expectExceptionMessage('Cannot exceed 86,400 (24 hours) for practical reasons');
|
||||
|
||||
new RateLimiter(10, 86401);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorAcceptsValidParameters(): void
|
||||
{
|
||||
$rateLimiter = new RateLimiter(100, 3600);
|
||||
$this->assertInstanceOf(RateLimiter::class, $rateLimiter);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructorAcceptsBoundaryValues(): void
|
||||
{
|
||||
// Test minimum valid values
|
||||
$rateLimiter1 = new RateLimiter(1, 1);
|
||||
$this->assertInstanceOf(RateLimiter::class, $rateLimiter1);
|
||||
|
||||
// Test maximum valid values
|
||||
$rateLimiter2 = new RateLimiter(1000000, 86400);
|
||||
$this->assertInstanceOf(RateLimiter::class, $rateLimiter2);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function isAllowedThrowsExceptionForEmptyKey(): void
|
||||
{
|
||||
$rateLimiter = new RateLimiter(10, 60);
|
||||
|
||||
$this->expectException(InvalidRateLimitConfigurationException::class);
|
||||
$this->expectExceptionMessage(TestConstants::ERROR_RATE_LIMIT_KEY_EMPTY);
|
||||
|
||||
$rateLimiter->isAllowed('');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function isAllowedThrowsExceptionForWhitespaceOnlyKey(): void
|
||||
{
|
||||
$rateLimiter = new RateLimiter(10, 60);
|
||||
|
||||
$this->expectException(InvalidRateLimitConfigurationException::class);
|
||||
$this->expectExceptionMessage(TestConstants::ERROR_RATE_LIMIT_KEY_EMPTY);
|
||||
|
||||
$rateLimiter->isAllowed(' ');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function isAllowedThrowsExceptionForTooLongKey(): void
|
||||
{
|
||||
$rateLimiter = new RateLimiter(10, 60);
|
||||
$longKey = str_repeat('a', 251);
|
||||
|
||||
$this->expectException(InvalidRateLimitConfigurationException::class);
|
||||
$this->expectExceptionMessage('Rate limiting key length (251) exceeds maximum (250 characters)');
|
||||
|
||||
$rateLimiter->isAllowed($longKey);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function isAllowedThrowsExceptionForKeyWithControlCharacters(): void
|
||||
{
|
||||
$rateLimiter = new RateLimiter(10, 60);
|
||||
|
||||
$this->expectException(InvalidRateLimitConfigurationException::class);
|
||||
$this->expectExceptionMessage('Rate limiting key cannot contain control characters');
|
||||
|
||||
$rateLimiter->isAllowed("test\x00key");
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function isAllowedAcceptsValidKey(): void
|
||||
{
|
||||
$rateLimiter = new RateLimiter(10, 60);
|
||||
$result = $rateLimiter->isAllowed('valid_key_123');
|
||||
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getTimeUntilResetThrowsExceptionForInvalidKey(): void
|
||||
{
|
||||
$rateLimiter = new RateLimiter(10, 60);
|
||||
|
||||
$this->expectException(InvalidRateLimitConfigurationException::class);
|
||||
$this->expectExceptionMessage(TestConstants::ERROR_RATE_LIMIT_KEY_EMPTY);
|
||||
|
||||
$rateLimiter->getTimeUntilReset('');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getStatsThrowsExceptionForInvalidKey(): void
|
||||
{
|
||||
$rateLimiter = new RateLimiter(10, 60);
|
||||
|
||||
$this->expectException(InvalidRateLimitConfigurationException::class);
|
||||
$this->expectExceptionMessage(TestConstants::ERROR_RATE_LIMIT_KEY_EMPTY);
|
||||
|
||||
$rateLimiter->getStats('');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getRemainingRequestsThrowsExceptionForInvalidKey(): void
|
||||
{
|
||||
$rateLimiter = new RateLimiter(10, 60);
|
||||
|
||||
$this->expectException(InvalidRateLimitConfigurationException::class);
|
||||
$this->expectExceptionMessage(TestConstants::ERROR_RATE_LIMIT_KEY_EMPTY);
|
||||
|
||||
$rateLimiter->getRemainingRequests('');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function clearKeyThrowsExceptionForInvalidKey(): void
|
||||
{
|
||||
$this->expectException(InvalidRateLimitConfigurationException::class);
|
||||
$this->expectExceptionMessage(TestConstants::ERROR_RATE_LIMIT_KEY_EMPTY);
|
||||
|
||||
RateLimiter::clearKey('');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function setCleanupIntervalThrowsExceptionForZeroSeconds(): void
|
||||
{
|
||||
$this->expectException(InvalidRateLimitConfigurationException::class);
|
||||
$this->expectExceptionMessage('Cleanup interval must be a positive integer, got: 0');
|
||||
|
||||
RateLimiter::setCleanupInterval(0);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function setCleanupIntervalThrowsExceptionForNegativeSeconds(): void
|
||||
{
|
||||
$this->expectException(InvalidRateLimitConfigurationException::class);
|
||||
$this->expectExceptionMessage('Cleanup interval must be a positive integer, got: -100');
|
||||
|
||||
RateLimiter::setCleanupInterval(-100);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function setCleanupIntervalThrowsExceptionForTooSmallValue(): void
|
||||
{
|
||||
$this->expectException(InvalidRateLimitConfigurationException::class);
|
||||
$this->expectExceptionMessage(
|
||||
'Cleanup interval (30 seconds) is too short, minimum is 60 seconds'
|
||||
);
|
||||
|
||||
RateLimiter::setCleanupInterval(30);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function setCleanupIntervalThrowsExceptionForExcessiveValue(): void
|
||||
{
|
||||
$this->expectException(InvalidRateLimitConfigurationException::class);
|
||||
$this->expectExceptionMessage(
|
||||
'Cannot exceed 604,800 seconds (1 week) for practical reasons'
|
||||
);
|
||||
|
||||
RateLimiter::setCleanupInterval(604801);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function setCleanupIntervalAcceptsValidValues(): void
|
||||
{
|
||||
// Test minimum valid value
|
||||
RateLimiter::setCleanupInterval(60);
|
||||
$stats = RateLimiter::getMemoryStats();
|
||||
$this->assertSame(60, $stats['cleanup_interval']);
|
||||
|
||||
// Test maximum valid value
|
||||
RateLimiter::setCleanupInterval(604800);
|
||||
$stats = RateLimiter::getMemoryStats();
|
||||
$this->assertSame(604800, $stats['cleanup_interval']);
|
||||
|
||||
// Test middle value
|
||||
RateLimiter::setCleanupInterval(1800);
|
||||
$stats = RateLimiter::getMemoryStats();
|
||||
$this->assertSame(1800, $stats['cleanup_interval']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function keyValidationWorksConsistentlyAcrossAllMethods(): void
|
||||
{
|
||||
$rateLimiter = new RateLimiter(10, 60);
|
||||
$invalidKey = str_repeat('x', 251);
|
||||
|
||||
// Test all methods that should validate keys
|
||||
$methods = [
|
||||
'isAllowed',
|
||||
'getTimeUntilReset',
|
||||
'getStats',
|
||||
'getRemainingRequests'
|
||||
];
|
||||
|
||||
foreach ($methods as $method) {
|
||||
try {
|
||||
$rateLimiter->$method($invalidKey);
|
||||
$this->fail(sprintf(
|
||||
'Method %s should have thrown InvalidArgumentException for invalid key',
|
||||
$method
|
||||
));
|
||||
} catch (InvalidRateLimitConfigurationException $e) {
|
||||
$this->assertStringContainsString(
|
||||
'Rate limiting key length',
|
||||
$e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Test static method
|
||||
try {
|
||||
RateLimiter::clearKey($invalidKey);
|
||||
$this->fail('clearKey should have thrown InvalidArgumentException for invalid key');
|
||||
} catch (InvalidRateLimitConfigurationException $invalidArgumentException) {
|
||||
$this->assertStringContainsString(
|
||||
'Rate limiting key length',
|
||||
$invalidArgumentException->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function validKeysWorkCorrectlyAfterValidation(): void
|
||||
{
|
||||
$rateLimiter = new RateLimiter(5, 60);
|
||||
$validKey = 'user_123_action_login';
|
||||
|
||||
// Should not throw exceptions
|
||||
$this->assertTrue($rateLimiter->isAllowed($validKey));
|
||||
$this->assertIsInt($rateLimiter->getTimeUntilReset($validKey));
|
||||
$this->assertIsArray($rateLimiter->getStats($validKey));
|
||||
$this->assertIsInt($rateLimiter->getRemainingRequests($validKey));
|
||||
|
||||
// This should also not throw
|
||||
RateLimiter::clearKey($validKey);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function boundaryKeyLengthsWork(): void
|
||||
{
|
||||
$rateLimiter = new RateLimiter(10, 60);
|
||||
|
||||
// Test exactly 250 characters (should work)
|
||||
$maxValidKey = str_repeat('a', 250);
|
||||
$this->assertTrue($rateLimiter->isAllowed($maxValidKey));
|
||||
|
||||
// Test exactly 251 characters (should fail)
|
||||
$tooLongKey = str_repeat('a', 251);
|
||||
$this->expectException(InvalidRateLimitConfigurationException::class);
|
||||
$rateLimiter->isAllowed($tooLongKey);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function controlCharacterDetectionWorks(): void
|
||||
{
|
||||
$rateLimiter = new RateLimiter(10, 60);
|
||||
|
||||
$controlChars = [
|
||||
"\x00", // null
|
||||
"\x01", // start of heading
|
||||
"\x1F", // unit separator
|
||||
"\x7F", // delete
|
||||
];
|
||||
|
||||
foreach ($controlChars as $char) {
|
||||
try {
|
||||
$rateLimiter->isAllowed(sprintf('test%skey', $char));
|
||||
$this->fail("Should have thrown exception for control character: " . ord($char));
|
||||
} catch (InvalidRateLimitConfigurationException $e) {
|
||||
$this->assertStringContainsString(
|
||||
'Rate limiting key cannot contain control characters',
|
||||
$e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function validSpecialCharactersAreAllowed(): void
|
||||
{
|
||||
$rateLimiter = new RateLimiter(10, 60);
|
||||
|
||||
$validKeys = [
|
||||
'user-123',
|
||||
'action_login',
|
||||
'key.with.dots',
|
||||
'key@domain.com',
|
||||
'key+suffix',
|
||||
'key=value',
|
||||
'key:value',
|
||||
'key;semicolon',
|
||||
'key,comma',
|
||||
'key space',
|
||||
'key[bracket]',
|
||||
'key{brace}',
|
||||
'key(paren)',
|
||||
'key#hash',
|
||||
'key%percent',
|
||||
'key^caret',
|
||||
'key&ersand',
|
||||
'key*asterisk',
|
||||
'key!exclamation',
|
||||
'key?question',
|
||||
'key~tilde',
|
||||
'key`backtick',
|
||||
'key|pipe',
|
||||
'key\\backslash',
|
||||
'key/slash',
|
||||
'key"quote',
|
||||
"key'apostrophe",
|
||||
'key<less>',
|
||||
'key$dollar',
|
||||
];
|
||||
|
||||
foreach ($validKeys as $key) {
|
||||
$this->assertTrue($rateLimiter->isAllowed($key), 'Key should be valid: ' . $key);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user