mirror of
https://github.com/ivuorinen/monolog-gdpr-filter.git
synced 2026-02-24 06:54:04 +00:00
feat: add advanced architecture, documentation, and coverage improvements (#65)
* fix(style): resolve PHPCS line-length warnings in source files * fix(style): resolve PHPCS line-length warnings in test files * feat(audit): add structured audit logging with ErrorContext and AuditContext - ErrorContext: standardized error information with sensitive data sanitization - AuditContext: structured context for audit entries with operation types - StructuredAuditLogger: enhanced audit logger wrapper with timing support * feat(recovery): add recovery mechanism for failed masking operations - FailureMode enum: FAIL_OPEN, FAIL_CLOSED, FAIL_SAFE modes - RecoveryStrategy interface and RecoveryResult value object - RetryStrategy: exponential backoff with configurable attempts - FallbackMaskStrategy: type-aware fallback values * feat(strategies): add CallbackMaskingStrategy for custom masking logic - Wraps custom callbacks as MaskingStrategy implementations - Factory methods: constant(), hash(), partial() for common use cases - Supports exact match and prefix match for field paths * docs: add framework integration guides and examples - symfony-integration.md: Symfony service configuration and Monolog setup - psr3-decorator.md: PSR-3 logger decorator pattern implementation - framework-examples.md: CakePHP, CodeIgniter 4, Laminas, Yii2, PSR-15 - docker-development.md: Docker development environment guide * chore(docker): add Docker development environment - Dockerfile: PHP 8.2-cli-alpine with Xdebug for coverage - docker-compose.yml: development services with volume mounts * feat(demo): add interactive GDPR pattern tester playground - PatternTester.php: pattern testing utility with strategy support - index.php: web API endpoint with JSON response handling - playground.html: interactive web interface for testing patterns * docs(todo): update with completed medium priority items - Mark all PHPCS warnings as fixed (81 → 0) - Document new Audit and Recovery features - Update test count to 1,068 tests with 2,953 assertions - Move remaining items to low priority * feat: add advanced architecture, documentation, and coverage improvements - Add architecture improvements: - ArrayAccessorInterface and DotArrayAccessor for decoupled array access - MaskingOrchestrator for single-responsibility masking coordination - GdprProcessorBuilder for fluent configuration - MaskingPluginInterface and AbstractMaskingPlugin for plugin architecture - PluginAwareProcessor for plugin hook execution - AuditLoggerFactory for instance-based audit logger creation - Add advanced features: - SerializedDataProcessor for handling print_r/var_export/serialize output - KAnonymizer with GeneralizationStrategy for GDPR k-anonymity - RetentionPolicy for configurable data retention periods - StreamingProcessor for memory-efficient large log processing - Add comprehensive documentation: - docs/performance-tuning.md - benchmarking, optimization, caching - docs/troubleshooting.md - common issues and solutions - docs/logging-integrations.md - ELK, Graylog, Datadog, etc. - docs/plugin-development.md - complete plugin development guide - Improve test coverage (84.41% → 85.07%): - ConditionalRuleFactoryInstanceTest (100% coverage) - GdprProcessorBuilderEdgeCasesTest (100% coverage) - StrategyEdgeCasesTest for ReDoS detection and type parsing - 78 new tests, 119 new assertions - Update TODO.md with current statistics: - 141 PHP files, 1,346 tests, 85.07% line coverage * chore: tests, update actions, sonarcloud issues * chore: rector * fix: more sonarcloud fixes * chore: more fixes * refactor: copilot review fix * chore: rector
This commit is contained in:
@@ -196,20 +196,32 @@ final class AbstractMaskingStrategyTest extends TestCase
|
||||
#[Test]
|
||||
public function pathMatchesReturnsTrueForExactMatch(): void
|
||||
{
|
||||
$this->assertTrue($this->strategy->testPathMatches(TestConstants::FIELD_USER_EMAIL, TestConstants::FIELD_USER_EMAIL));
|
||||
$this->assertTrue($this->strategy->testPathMatches(
|
||||
TestConstants::FIELD_USER_EMAIL,
|
||||
TestConstants::FIELD_USER_EMAIL
|
||||
));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function pathMatchesReturnsFalseForNonMatch(): void
|
||||
{
|
||||
$this->assertFalse($this->strategy->testPathMatches(TestConstants::FIELD_USER_EMAIL, TestConstants::FIELD_USER_PASSWORD));
|
||||
$this->assertFalse($this->strategy->testPathMatches(
|
||||
TestConstants::FIELD_USER_EMAIL,
|
||||
TestConstants::FIELD_USER_PASSWORD
|
||||
));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function pathMatchesSupportsWildcardAtEnd(): void
|
||||
{
|
||||
$this->assertTrue($this->strategy->testPathMatches(TestConstants::FIELD_USER_EMAIL, TestConstants::PATH_USER_WILDCARD));
|
||||
$this->assertTrue($this->strategy->testPathMatches(TestConstants::FIELD_USER_PASSWORD, TestConstants::PATH_USER_WILDCARD));
|
||||
$this->assertTrue($this->strategy->testPathMatches(
|
||||
TestConstants::FIELD_USER_EMAIL,
|
||||
TestConstants::PATH_USER_WILDCARD
|
||||
));
|
||||
$this->assertTrue($this->strategy->testPathMatches(
|
||||
TestConstants::FIELD_USER_PASSWORD,
|
||||
TestConstants::PATH_USER_WILDCARD
|
||||
));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
253
tests/Strategies/CallbackMaskingStrategyTest.php
Normal file
253
tests/Strategies/CallbackMaskingStrategyTest.php
Normal file
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Strategies;
|
||||
|
||||
use Ivuorinen\MonologGdprFilter\Exceptions\MaskingOperationFailedException;
|
||||
use Ivuorinen\MonologGdprFilter\Exceptions\RuleExecutionException;
|
||||
use Ivuorinen\MonologGdprFilter\Strategies\CallbackMaskingStrategy;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tests\TestConstants;
|
||||
use Tests\TestHelpers;
|
||||
|
||||
/**
|
||||
* Tests for CallbackMaskingStrategy.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
final class CallbackMaskingStrategyTest extends TestCase
|
||||
{
|
||||
use TestHelpers;
|
||||
|
||||
public function testBasicConstruction(): void
|
||||
{
|
||||
$callback = fn(mixed $value): string => TestConstants::MASK_MASKED_BRACKETS;
|
||||
$strategy = new CallbackMaskingStrategy('user.email', $callback);
|
||||
|
||||
$this->assertSame('user.email', $strategy->getFieldPath());
|
||||
$this->assertTrue($strategy->isExactMatch());
|
||||
$this->assertSame(50, $strategy->getPriority());
|
||||
}
|
||||
|
||||
public function testMaskWithSimpleCallback(): void
|
||||
{
|
||||
$callback = fn(mixed $value): string => TestConstants::MASK_MASKED_BRACKETS;
|
||||
$strategy = new CallbackMaskingStrategy('user.email', $callback);
|
||||
$record = $this->createLogRecord();
|
||||
|
||||
$result = $strategy->mask('john@example.com', 'user.email', $record);
|
||||
|
||||
$this->assertSame(TestConstants::MASK_MASKED_BRACKETS, $result);
|
||||
}
|
||||
|
||||
public function testMaskWithTransformingCallback(): void
|
||||
{
|
||||
$callback = fn(mixed $value): string => strtoupper((string) $value);
|
||||
$strategy = new CallbackMaskingStrategy('user.name', $callback);
|
||||
$record = $this->createLogRecord();
|
||||
|
||||
$result = $strategy->mask('john', 'user.name', $record);
|
||||
|
||||
$this->assertSame('JOHN', $result);
|
||||
}
|
||||
|
||||
public function testMaskThrowsOnCallbackException(): void
|
||||
{
|
||||
$callback = function (): never {
|
||||
throw new RuleExecutionException('Callback failed');
|
||||
};
|
||||
$strategy = new CallbackMaskingStrategy('user.data', $callback);
|
||||
$record = $this->createLogRecord();
|
||||
|
||||
$this->expectException(MaskingOperationFailedException::class);
|
||||
$this->expectExceptionMessage('Callback threw exception');
|
||||
|
||||
$strategy->mask('value', 'user.data', $record);
|
||||
}
|
||||
|
||||
public function testShouldApplyWithExactMatch(): void
|
||||
{
|
||||
$callback = fn(mixed $value): string => TestConstants::MASK_MASKED_BRACKETS;
|
||||
$strategy = new CallbackMaskingStrategy(
|
||||
'user.email',
|
||||
$callback,
|
||||
exactMatch: true
|
||||
);
|
||||
$record = $this->createLogRecord();
|
||||
|
||||
$this->assertTrue($strategy->shouldApply('value', 'user.email', $record));
|
||||
$this->assertFalse($strategy->shouldApply('value', 'user.name', $record));
|
||||
$this->assertFalse($strategy->shouldApply('value', 'user.email.work', $record));
|
||||
}
|
||||
|
||||
public function testShouldApplyWithWildcardMatch(): void
|
||||
{
|
||||
$callback = fn(mixed $value): string => TestConstants::MASK_MASKED_BRACKETS;
|
||||
$strategy = new CallbackMaskingStrategy(
|
||||
'user.*',
|
||||
$callback,
|
||||
exactMatch: false
|
||||
);
|
||||
$record = $this->createLogRecord();
|
||||
|
||||
$this->assertTrue($strategy->shouldApply('value', 'user.email', $record));
|
||||
$this->assertTrue($strategy->shouldApply('value', 'user.name', $record));
|
||||
$this->assertFalse($strategy->shouldApply('value', 'admin.email', $record));
|
||||
}
|
||||
|
||||
public function testGetName(): void
|
||||
{
|
||||
$callback = fn(mixed $value): string => TestConstants::MASK_MASKED_BRACKETS;
|
||||
$strategy = new CallbackMaskingStrategy('user.email', $callback);
|
||||
|
||||
$name = $strategy->getName();
|
||||
|
||||
$this->assertStringContainsString('Callback Masking', $name);
|
||||
$this->assertStringContainsString('user.email', $name);
|
||||
}
|
||||
|
||||
public function testValidate(): void
|
||||
{
|
||||
$callback = fn(mixed $value): string => TestConstants::MASK_MASKED_BRACKETS;
|
||||
$strategy = new CallbackMaskingStrategy('user.email', $callback);
|
||||
|
||||
$this->assertTrue($strategy->validate());
|
||||
}
|
||||
|
||||
public function testGetConfiguration(): void
|
||||
{
|
||||
$callback = fn(mixed $value): string => TestConstants::MASK_MASKED_BRACKETS;
|
||||
$strategy = new CallbackMaskingStrategy(
|
||||
'user.email',
|
||||
$callback,
|
||||
75,
|
||||
false
|
||||
);
|
||||
|
||||
$config = $strategy->getConfiguration();
|
||||
|
||||
$this->assertArrayHasKey('field_path', $config);
|
||||
$this->assertArrayHasKey('exact_match', $config);
|
||||
$this->assertArrayHasKey('priority', $config);
|
||||
$this->assertSame('user.email', $config['field_path']);
|
||||
$this->assertFalse($config['exact_match']);
|
||||
$this->assertSame(75, $config['priority']);
|
||||
}
|
||||
|
||||
public function testForPathsFactoryMethod(): void
|
||||
{
|
||||
$callback = fn(mixed $value): string => TestConstants::MASK_MASKED_BRACKETS;
|
||||
$paths = ['user.email', 'admin.email', 'contact.email'];
|
||||
|
||||
$strategies = CallbackMaskingStrategy::forPaths($paths, $callback);
|
||||
|
||||
$this->assertCount(3, $strategies);
|
||||
|
||||
foreach ($strategies as $index => $strategy) {
|
||||
$this->assertInstanceOf(CallbackMaskingStrategy::class, $strategy);
|
||||
$this->assertSame($paths[$index], $strategy->getFieldPath());
|
||||
}
|
||||
}
|
||||
|
||||
public function testConstantFactoryMethod(): void
|
||||
{
|
||||
$strategy = CallbackMaskingStrategy::constant('user.ssn', '***-**-****');
|
||||
$record = $this->createLogRecord();
|
||||
|
||||
$result = $strategy->mask('123-45-6789', 'user.ssn', $record);
|
||||
|
||||
$this->assertSame('***-**-****', $result);
|
||||
}
|
||||
|
||||
public function testHashFactoryMethod(): void
|
||||
{
|
||||
$strategy = CallbackMaskingStrategy::hash('user.password', 'sha256', 8);
|
||||
$record = $this->createLogRecord();
|
||||
|
||||
$result = $strategy->mask('secret123', 'user.password', $record);
|
||||
|
||||
$this->assertIsString($result);
|
||||
$this->assertSame(11, strlen($result));
|
||||
$this->assertStringEndsWith('...', $result);
|
||||
}
|
||||
|
||||
public function testHashWithNoTruncation(): void
|
||||
{
|
||||
$strategy = CallbackMaskingStrategy::hash('user.password', 'md5', 0);
|
||||
$record = $this->createLogRecord();
|
||||
|
||||
$result = $strategy->mask('test', 'user.password', $record);
|
||||
|
||||
$this->assertSame(32, strlen((string) $result));
|
||||
}
|
||||
|
||||
public function testPartialFactoryMethod(): void
|
||||
{
|
||||
$strategy = CallbackMaskingStrategy::partial('user.email', 2, 4);
|
||||
$record = $this->createLogRecord();
|
||||
|
||||
$result = $strategy->mask('john@example.com', 'user.email', $record);
|
||||
|
||||
$this->assertStringStartsWith('jo', $result);
|
||||
$this->assertStringEndsWith('.com', $result);
|
||||
$this->assertStringContainsString('***', $result);
|
||||
}
|
||||
|
||||
public function testPartialWithShortString(): void
|
||||
{
|
||||
$strategy = CallbackMaskingStrategy::partial('user.code', 2, 2);
|
||||
$record = $this->createLogRecord();
|
||||
|
||||
$result = $strategy->mask('abc', 'user.code', $record);
|
||||
|
||||
$this->assertSame('***', $result);
|
||||
}
|
||||
|
||||
public function testPartialWithCustomMaskChar(): void
|
||||
{
|
||||
$strategy = CallbackMaskingStrategy::partial('user.card', 4, 4, '#');
|
||||
$record = $this->createLogRecord();
|
||||
|
||||
$result = $strategy->mask('1234567890123456', 'user.card', $record);
|
||||
|
||||
$this->assertStringStartsWith('1234', $result);
|
||||
$this->assertStringEndsWith('3456', $result);
|
||||
$this->assertStringContainsString('########', $result);
|
||||
}
|
||||
|
||||
public function testCallbackReceivesOriginalValue(): void
|
||||
{
|
||||
$receivedValue = null;
|
||||
$callback = function (mixed $value) use (&$receivedValue): string {
|
||||
$receivedValue = $value;
|
||||
return TestConstants::MASK_MASKED_BRACKETS;
|
||||
};
|
||||
|
||||
$strategy = new CallbackMaskingStrategy('user.data', $callback);
|
||||
$record = $this->createLogRecord();
|
||||
|
||||
$strategy->mask(['key' => 'value'], 'user.data', $record);
|
||||
|
||||
$this->assertSame($receivedValue, ['key' => 'value']);
|
||||
}
|
||||
|
||||
public function testCallbackCanReturnNonString(): void
|
||||
{
|
||||
$callback = fn(mixed $value): array => ['masked' => true];
|
||||
$strategy = new CallbackMaskingStrategy('user.data', $callback);
|
||||
$record = $this->createLogRecord();
|
||||
|
||||
$result = $strategy->mask(['key' => 'value'], 'user.data', $record);
|
||||
|
||||
$this->assertSame(['masked' => true], $result);
|
||||
}
|
||||
|
||||
public function testCustomPriority(): void
|
||||
{
|
||||
$callback = fn(mixed $value): string => TestConstants::MASK_MASKED_BRACKETS;
|
||||
$strategy = new CallbackMaskingStrategy('user.email', $callback, 100);
|
||||
|
||||
$this->assertSame(100, $strategy->getPriority());
|
||||
}
|
||||
}
|
||||
@@ -170,9 +170,24 @@ final class ConditionalMaskingStrategyEnhancedTest extends TestCase
|
||||
[TestConstants::CHANNEL_SECURITY, TestConstants::CHANNEL_AUDIT, 'admin']
|
||||
);
|
||||
|
||||
$securityRecord = $this->createLogRecord(TestConstants::MESSAGE_DEFAULT, [], Level::Info, TestConstants::CHANNEL_SECURITY);
|
||||
$auditRecord = $this->createLogRecord(TestConstants::MESSAGE_DEFAULT, [], Level::Info, TestConstants::CHANNEL_AUDIT);
|
||||
$testRecord = $this->createLogRecord(TestConstants::MESSAGE_DEFAULT, [], Level::Info, 'test');
|
||||
$securityRecord = $this->createLogRecord(
|
||||
TestConstants::MESSAGE_DEFAULT,
|
||||
[],
|
||||
Level::Info,
|
||||
TestConstants::CHANNEL_SECURITY
|
||||
);
|
||||
$auditRecord = $this->createLogRecord(
|
||||
TestConstants::MESSAGE_DEFAULT,
|
||||
[],
|
||||
Level::Info,
|
||||
TestConstants::CHANNEL_AUDIT
|
||||
);
|
||||
$testRecord = $this->createLogRecord(
|
||||
TestConstants::MESSAGE_DEFAULT,
|
||||
[],
|
||||
Level::Info,
|
||||
'test'
|
||||
);
|
||||
|
||||
$this->assertTrue($strategy->shouldApply('secret', TestConstants::FIELD_MESSAGE, $securityRecord));
|
||||
$this->assertTrue($strategy->shouldApply('secret', TestConstants::FIELD_MESSAGE, $auditRecord));
|
||||
@@ -188,8 +203,16 @@ final class ConditionalMaskingStrategyEnhancedTest extends TestCase
|
||||
['env' => 'production', 'sensitive' => true]
|
||||
);
|
||||
|
||||
$prodRecord = $this->createLogRecord(TestConstants::MESSAGE_DEFAULT, ['env' => 'production', 'sensitive' => true]);
|
||||
$devRecord = $this->createLogRecord(TestConstants::MESSAGE_DEFAULT, ['env' => 'development', 'sensitive' => true]);
|
||||
$prodContext = ['env' => 'production', 'sensitive' => true];
|
||||
$prodRecord = $this->createLogRecord(
|
||||
TestConstants::MESSAGE_DEFAULT,
|
||||
$prodContext
|
||||
);
|
||||
$devContext = ['env' => 'development', 'sensitive' => true];
|
||||
$devRecord = $this->createLogRecord(
|
||||
TestConstants::MESSAGE_DEFAULT,
|
||||
$devContext
|
||||
);
|
||||
$noContextRecord = $this->createLogRecord(TestConstants::MESSAGE_DEFAULT);
|
||||
|
||||
$this->assertTrue($strategy->shouldApply('secret', TestConstants::FIELD_MESSAGE, $prodRecord));
|
||||
|
||||
@@ -69,32 +69,52 @@ final class FieldPathMaskingStrategyTest extends TestCase
|
||||
#[Test]
|
||||
public function shouldApplyReturnsTrueForExactPathMatch(): void
|
||||
{
|
||||
$strategy = new FieldPathMaskingStrategy([TestConstants::FIELD_USER_EMAIL => MaskConstants::MASK_GENERIC]);
|
||||
$config = [TestConstants::FIELD_USER_EMAIL => MaskConstants::MASK_GENERIC];
|
||||
$strategy = new FieldPathMaskingStrategy($config);
|
||||
|
||||
$this->assertTrue($strategy->shouldApply(TestConstants::EMAIL_TEST, TestConstants::FIELD_USER_EMAIL, $this->logRecord));
|
||||
$this->assertTrue($strategy->shouldApply(
|
||||
TestConstants::EMAIL_TEST,
|
||||
TestConstants::FIELD_USER_EMAIL,
|
||||
$this->logRecord
|
||||
));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function shouldApplyReturnsFalseForNonMatchingPath(): void
|
||||
{
|
||||
$strategy = new FieldPathMaskingStrategy([TestConstants::FIELD_USER_EMAIL => MaskConstants::MASK_GENERIC]);
|
||||
$config = [TestConstants::FIELD_USER_EMAIL => MaskConstants::MASK_GENERIC];
|
||||
$strategy = new FieldPathMaskingStrategy($config);
|
||||
|
||||
$this->assertFalse($strategy->shouldApply(TestConstants::CONTEXT_PASSWORD, TestConstants::FIELD_USER_PASSWORD, $this->logRecord));
|
||||
$this->assertFalse($strategy->shouldApply(
|
||||
TestConstants::CONTEXT_PASSWORD,
|
||||
TestConstants::FIELD_USER_PASSWORD,
|
||||
$this->logRecord
|
||||
));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function shouldApplySupportsWildcardPatterns(): void
|
||||
{
|
||||
$strategy = new FieldPathMaskingStrategy([TestConstants::PATH_USER_WILDCARD => MaskConstants::MASK_GENERIC]);
|
||||
$config = [TestConstants::PATH_USER_WILDCARD => MaskConstants::MASK_GENERIC];
|
||||
$strategy = new FieldPathMaskingStrategy($config);
|
||||
|
||||
$this->assertTrue($strategy->shouldApply(TestConstants::EMAIL_TEST, TestConstants::FIELD_USER_EMAIL, $this->logRecord));
|
||||
$this->assertTrue($strategy->shouldApply(TestConstants::CONTEXT_PASSWORD, TestConstants::FIELD_USER_PASSWORD, $this->logRecord));
|
||||
$this->assertTrue($strategy->shouldApply(
|
||||
TestConstants::EMAIL_TEST,
|
||||
TestConstants::FIELD_USER_EMAIL,
|
||||
$this->logRecord
|
||||
));
|
||||
$this->assertTrue($strategy->shouldApply(
|
||||
TestConstants::CONTEXT_PASSWORD,
|
||||
TestConstants::FIELD_USER_PASSWORD,
|
||||
$this->logRecord
|
||||
));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function maskAppliesStringReplacement(): void
|
||||
{
|
||||
$strategy = new FieldPathMaskingStrategy([TestConstants::FIELD_USER_EMAIL => MaskConstants::MASK_EMAIL_PATTERN]);
|
||||
$config = [TestConstants::FIELD_USER_EMAIL => MaskConstants::MASK_EMAIL_PATTERN];
|
||||
$strategy = new FieldPathMaskingStrategy($config);
|
||||
|
||||
$result = $strategy->mask(TestConstants::EMAIL_TEST, TestConstants::FIELD_USER_EMAIL, $this->logRecord);
|
||||
|
||||
@@ -114,9 +134,11 @@ final class FieldPathMaskingStrategyTest extends TestCase
|
||||
#[Test]
|
||||
public function maskAppliesRegexReplacement(): void
|
||||
{
|
||||
$strategy = new FieldPathMaskingStrategy([
|
||||
'user.ssn' => FieldMaskConfig::regexMask(TestConstants::PATTERN_SSN_FORMAT, MaskConstants::MASK_SSN_PATTERN),
|
||||
]);
|
||||
$ssnConfig = FieldMaskConfig::regexMask(
|
||||
TestConstants::PATTERN_SSN_FORMAT,
|
||||
MaskConstants::MASK_SSN_PATTERN
|
||||
);
|
||||
$strategy = new FieldPathMaskingStrategy(['user.ssn' => $ssnConfig]);
|
||||
|
||||
$result = $strategy->mask(TestConstants::SSN_US, 'user.ssn', $this->logRecord);
|
||||
|
||||
@@ -208,10 +230,15 @@ final class FieldPathMaskingStrategyTest extends TestCase
|
||||
#[Test]
|
||||
public function validateReturnsTrueForValidConfiguration(): void
|
||||
{
|
||||
$ssnConfig = FieldMaskConfig::regexMask(
|
||||
TestConstants::PATTERN_SSN_FORMAT,
|
||||
MaskConstants::MASK_SSN_PATTERN
|
||||
);
|
||||
|
||||
$strategy = new FieldPathMaskingStrategy([
|
||||
TestConstants::FIELD_USER_EMAIL => MaskConstants::MASK_EMAIL_PATTERN,
|
||||
TestConstants::FIELD_USER_PASSWORD => FieldMaskConfig::remove(),
|
||||
'user.ssn' => FieldMaskConfig::regexMask(TestConstants::PATTERN_SSN_FORMAT, MaskConstants::MASK_SSN_PATTERN),
|
||||
'user.ssn' => $ssnConfig,
|
||||
]);
|
||||
|
||||
$this->assertTrue($strategy->validate());
|
||||
|
||||
@@ -69,7 +69,10 @@ class MaskingStrategiesTest extends TestCase
|
||||
public function testRegexMaskingStrategyWithInvalidPattern(): void
|
||||
{
|
||||
$this->expectException(InvalidRegexPatternException::class);
|
||||
$strategy = new RegexMaskingStrategy([TestConstants::PATTERN_INVALID_UNCLOSED_BRACKET => TestConstants::DATA_MASKED]);
|
||||
$patterns = [
|
||||
TestConstants::PATTERN_INVALID_UNCLOSED_BRACKET => TestConstants::DATA_MASKED
|
||||
];
|
||||
$strategy = new RegexMaskingStrategy($patterns);
|
||||
unset($strategy); // Satisfy SonarQube - this line won't be reached if exception is thrown
|
||||
$this->fail(TestConstants::ERROR_EXCEPTION_NOT_THROWN);
|
||||
}
|
||||
@@ -390,7 +393,10 @@ class MaskingStrategiesTest extends TestCase
|
||||
$this->assertFalse($strategy->testRecordMatches($logRecord, ['level' => 'Info']));
|
||||
|
||||
// Test preserveValueType
|
||||
$this->assertEquals(TestConstants::DATA_MASKED, $strategy->testPreserveValueType('original', TestConstants::DATA_MASKED));
|
||||
$this->assertEquals(
|
||||
TestConstants::DATA_MASKED,
|
||||
$strategy->testPreserveValueType('original', TestConstants::DATA_MASKED)
|
||||
);
|
||||
$this->assertEquals(123, $strategy->testPreserveValueType(456, '123'));
|
||||
$this->assertEqualsWithDelta(12.5, $strategy->testPreserveValueType(45.6, '12.5'), PHP_FLOAT_EPSILON);
|
||||
$this->assertTrue($strategy->testPreserveValueType(false, 'true'));
|
||||
|
||||
@@ -205,7 +205,12 @@ final class RegexMaskingStrategyComprehensiveTest extends TestCase
|
||||
$record = $this->createLogRecord('Test');
|
||||
|
||||
// Should apply to included path
|
||||
$this->assertTrue($strategy->shouldApply(TestConstants::MESSAGE_SECRET_DATA, TestConstants::FIELD_USER_PASSWORD, $record));
|
||||
$shouldApply = $strategy->shouldApply(
|
||||
TestConstants::MESSAGE_SECRET_DATA,
|
||||
TestConstants::FIELD_USER_PASSWORD,
|
||||
$record
|
||||
);
|
||||
$this->assertTrue($shouldApply);
|
||||
|
||||
// Should not apply to non-included path
|
||||
$this->assertFalse($strategy->shouldApply(TestConstants::MESSAGE_SECRET_DATA, 'other.field', $record));
|
||||
@@ -238,10 +243,20 @@ final class RegexMaskingStrategyComprehensiveTest extends TestCase
|
||||
$record = $this->createLogRecord('Test');
|
||||
|
||||
// Should not apply to excluded path even if in include list
|
||||
$this->assertFalse($strategy->shouldApply(TestConstants::MESSAGE_SECRET_DATA, TestConstants::FIELD_USER_PUBLIC, $record));
|
||||
$shouldNotApply = $strategy->shouldApply(
|
||||
TestConstants::MESSAGE_SECRET_DATA,
|
||||
TestConstants::FIELD_USER_PUBLIC,
|
||||
$record
|
||||
);
|
||||
$this->assertFalse($shouldNotApply);
|
||||
|
||||
// Should apply to included path not in exclude list
|
||||
$this->assertTrue($strategy->shouldApply(TestConstants::MESSAGE_SECRET_DATA, TestConstants::FIELD_USER_PASSWORD, $record));
|
||||
$shouldApply = $strategy->shouldApply(
|
||||
TestConstants::MESSAGE_SECRET_DATA,
|
||||
TestConstants::FIELD_USER_PASSWORD,
|
||||
$record
|
||||
);
|
||||
$this->assertTrue($shouldApply);
|
||||
}
|
||||
|
||||
public function testShouldApplyCatchesMaskingException(): void
|
||||
|
||||
@@ -83,7 +83,8 @@ final class RegexMaskingStrategyEnhancedTest extends TestCase
|
||||
$logRecord = $this->createLogRecord();
|
||||
|
||||
// Should successfully apply all valid patterns
|
||||
$result = $strategy->mask('SSN: 123-45-6789, Email: emailtest@example.com', TestConstants::FIELD_MESSAGE, $logRecord);
|
||||
$input = 'SSN: 123-45-6789, Email: emailtest@example.com';
|
||||
$result = $strategy->mask($input, TestConstants::FIELD_MESSAGE, $logRecord);
|
||||
$this->assertStringContainsString(MaskConstants::MASK_SSN, $result);
|
||||
$this->assertStringContainsString(MaskConstants::MASK_EMAIL, $result);
|
||||
}
|
||||
@@ -126,25 +127,44 @@ final class RegexMaskingStrategyEnhancedTest extends TestCase
|
||||
public function testShouldApplyWithIncludePathsOnly(): void
|
||||
{
|
||||
$patterns = [TestConstants::PATTERN_TEST => MaskConstants::MASK_MASKED];
|
||||
$strategy = new RegexMaskingStrategy($patterns, [TestConstants::PATH_USER_WILDCARD, 'admin.log']);
|
||||
$includePaths = [TestConstants::PATH_USER_WILDCARD, 'admin.log'];
|
||||
$strategy = new RegexMaskingStrategy($patterns, $includePaths);
|
||||
$logRecord = $this->createLogRecord();
|
||||
|
||||
// Should apply to matching content in included paths
|
||||
$this->assertTrue($strategy->shouldApply(TestConstants::DATA_TEST_DATA, TestConstants::FIELD_USER_EMAIL, $logRecord));
|
||||
$this->assertTrue($strategy->shouldApply(TestConstants::DATA_TEST_DATA, 'admin.log', $logRecord));
|
||||
$this->assertTrue($strategy->shouldApply(
|
||||
TestConstants::DATA_TEST_DATA,
|
||||
TestConstants::FIELD_USER_EMAIL,
|
||||
$logRecord
|
||||
));
|
||||
$this->assertTrue($strategy->shouldApply(
|
||||
TestConstants::DATA_TEST_DATA,
|
||||
'admin.log',
|
||||
$logRecord
|
||||
));
|
||||
|
||||
// Should not apply to non-included paths even if content matches
|
||||
$this->assertFalse($strategy->shouldApply(TestConstants::DATA_TEST_DATA, 'system.info', $logRecord));
|
||||
$this->assertFalse($strategy->shouldApply(
|
||||
TestConstants::DATA_TEST_DATA,
|
||||
'system.info',
|
||||
$logRecord
|
||||
));
|
||||
}
|
||||
|
||||
public function testShouldApplyWithExcludePathsPrecedence(): void
|
||||
{
|
||||
$patterns = [TestConstants::PATTERN_TEST => MaskConstants::MASK_MASKED];
|
||||
$strategy = new RegexMaskingStrategy($patterns, [TestConstants::PATH_USER_WILDCARD], ['user.id', 'user.created_at']);
|
||||
$includePaths = [TestConstants::PATH_USER_WILDCARD];
|
||||
$excludePaths = ['user.id', 'user.created_at'];
|
||||
$strategy = new RegexMaskingStrategy($patterns, $includePaths, $excludePaths);
|
||||
$logRecord = $this->createLogRecord();
|
||||
|
||||
// Should apply to included but not excluded
|
||||
$this->assertTrue($strategy->shouldApply(TestConstants::DATA_TEST_DATA, TestConstants::FIELD_USER_EMAIL, $logRecord));
|
||||
$this->assertTrue($strategy->shouldApply(
|
||||
TestConstants::DATA_TEST_DATA,
|
||||
TestConstants::FIELD_USER_EMAIL,
|
||||
$logRecord
|
||||
));
|
||||
|
||||
// Should not apply to excluded paths
|
||||
$this->assertFalse($strategy->shouldApply(TestConstants::DATA_TEST_DATA, 'user.id', $logRecord));
|
||||
@@ -158,8 +178,16 @@ final class RegexMaskingStrategyEnhancedTest extends TestCase
|
||||
$logRecord = $this->createLogRecord();
|
||||
|
||||
// Should return false when content doesn't match patterns
|
||||
$this->assertFalse($strategy->shouldApply(TestConstants::DATA_PUBLIC, TestConstants::FIELD_MESSAGE, $logRecord));
|
||||
$this->assertFalse($strategy->shouldApply('no sensitive info', 'context.field', $logRecord));
|
||||
$this->assertFalse($strategy->shouldApply(
|
||||
TestConstants::DATA_PUBLIC,
|
||||
TestConstants::FIELD_MESSAGE,
|
||||
$logRecord
|
||||
));
|
||||
$this->assertFalse($strategy->shouldApply(
|
||||
'no sensitive info',
|
||||
'context.field',
|
||||
$logRecord
|
||||
));
|
||||
}
|
||||
|
||||
public function testShouldApplyForNonStringValuesWhenPatternMatches(): void
|
||||
|
||||
@@ -127,7 +127,8 @@ final class RegexMaskingStrategyTest extends TestCase
|
||||
'/"email":"[^"]+"/' => '"email":"' . MaskConstants::MASK_EMAIL_PATTERN . '"',
|
||||
]);
|
||||
|
||||
$result = $strategy->mask([TestConstants::CONTEXT_EMAIL => TestConstants::EMAIL_TEST], 'field', $this->logRecord);
|
||||
$input = [TestConstants::CONTEXT_EMAIL => TestConstants::EMAIL_TEST];
|
||||
$result = $strategy->mask($input, 'field', $this->logRecord);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertSame(MaskConstants::MASK_EMAIL_PATTERN, $result[TestConstants::CONTEXT_EMAIL]);
|
||||
@@ -180,7 +181,11 @@ final class RegexMaskingStrategyTest extends TestCase
|
||||
excludePaths: ['excluded.field']
|
||||
);
|
||||
|
||||
$this->assertFalse($strategy->shouldApply(TestConstants::DATA_NUMBER_STRING, 'excluded.field', $this->logRecord));
|
||||
$this->assertFalse($strategy->shouldApply(
|
||||
TestConstants::DATA_NUMBER_STRING,
|
||||
'excluded.field',
|
||||
$this->logRecord
|
||||
));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@@ -191,7 +196,11 @@ final class RegexMaskingStrategyTest extends TestCase
|
||||
excludePaths: ['excluded.field']
|
||||
);
|
||||
|
||||
$this->assertTrue($strategy->shouldApply(TestConstants::DATA_NUMBER_STRING, 'included.field', $this->logRecord));
|
||||
$this->assertTrue($strategy->shouldApply(
|
||||
TestConstants::DATA_NUMBER_STRING,
|
||||
'included.field',
|
||||
$this->logRecord
|
||||
));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@@ -202,9 +211,21 @@ final class RegexMaskingStrategyTest extends TestCase
|
||||
includePaths: ['user.ssn', 'user.phone']
|
||||
);
|
||||
|
||||
$this->assertTrue($strategy->shouldApply(TestConstants::DATA_NUMBER_STRING, 'user.ssn', $this->logRecord));
|
||||
$this->assertTrue($strategy->shouldApply(TestConstants::DATA_NUMBER_STRING, 'user.phone', $this->logRecord));
|
||||
$this->assertFalse($strategy->shouldApply(TestConstants::DATA_NUMBER_STRING, TestConstants::FIELD_USER_EMAIL, $this->logRecord));
|
||||
$this->assertTrue($strategy->shouldApply(
|
||||
TestConstants::DATA_NUMBER_STRING,
|
||||
'user.ssn',
|
||||
$this->logRecord
|
||||
));
|
||||
$this->assertTrue($strategy->shouldApply(
|
||||
TestConstants::DATA_NUMBER_STRING,
|
||||
'user.phone',
|
||||
$this->logRecord
|
||||
));
|
||||
$this->assertFalse($strategy->shouldApply(
|
||||
TestConstants::DATA_NUMBER_STRING,
|
||||
TestConstants::FIELD_USER_EMAIL,
|
||||
$this->logRecord
|
||||
));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@@ -337,7 +358,9 @@ final class RegexMaskingStrategyTest extends TestCase
|
||||
'/password/i' => MaskConstants::MASK_GENERIC,
|
||||
]);
|
||||
|
||||
$this->assertSame(MaskConstants::MASK_GENERIC . ' ' . MaskConstants::MASK_GENERIC, $strategy->mask('password PASSWORD', 'field', $this->logRecord));
|
||||
$expected = MaskConstants::MASK_GENERIC . ' ' . MaskConstants::MASK_GENERIC;
|
||||
$result = $strategy->mask('password PASSWORD', 'field', $this->logRecord);
|
||||
$this->assertSame($expected, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
484
tests/Strategies/StrategyEdgeCasesTest.php
Normal file
484
tests/Strategies/StrategyEdgeCasesTest.php
Normal file
@@ -0,0 +1,484 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Strategies;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Ivuorinen\MonologGdprFilter\Exceptions\InvalidRegexPatternException;
|
||||
use Ivuorinen\MonologGdprFilter\FieldMaskConfig;
|
||||
use Ivuorinen\MonologGdprFilter\MaskConstants;
|
||||
use Ivuorinen\MonologGdprFilter\Strategies\DataTypeMaskingStrategy;
|
||||
use Ivuorinen\MonologGdprFilter\Strategies\FieldPathMaskingStrategy;
|
||||
use Ivuorinen\MonologGdprFilter\Strategies\RegexMaskingStrategy;
|
||||
use Monolog\Level;
|
||||
use Monolog\LogRecord;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Edge case tests for masking strategies to improve coverage.
|
||||
*/
|
||||
#[CoversClass(RegexMaskingStrategy::class)]
|
||||
#[CoversClass(DataTypeMaskingStrategy::class)]
|
||||
#[CoversClass(FieldPathMaskingStrategy::class)]
|
||||
final class StrategyEdgeCasesTest extends TestCase
|
||||
{
|
||||
private LogRecord $logRecord;
|
||||
|
||||
#[\Override]
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->logRecord = new LogRecord(
|
||||
datetime: new DateTimeImmutable(),
|
||||
channel: 'test',
|
||||
level: Level::Info,
|
||||
message: 'Test message',
|
||||
context: [],
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// RegexMaskingStrategy ReDoS Detection
|
||||
// ========================================
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('redosPatternProvider')]
|
||||
public function regexStrategyDetectsReDoSPatterns(string $pattern): void
|
||||
{
|
||||
$this->expectException(InvalidRegexPatternException::class);
|
||||
$this->expectExceptionMessage('catastrophic backtracking');
|
||||
|
||||
$strategy = new RegexMaskingStrategy([$pattern => MaskConstants::MASK_GENERIC]);
|
||||
$this->assertInstanceOf(RegexMaskingStrategy::class, $strategy);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{0: string}>
|
||||
*/
|
||||
public static function redosPatternProvider(): array
|
||||
{
|
||||
return [
|
||||
'nested plus quantifier' => ['/^(a+)+$/'],
|
||||
'nested star quantifier' => ['/^(a*)*$/'],
|
||||
'plus with repetition' => ['/^(a+){1,10}$/'],
|
||||
'star with repetition' => ['/^(a*){1,10}$/'],
|
||||
'identical alternation with star' => ['/(.*|.*)x/'],
|
||||
'identical alternation with plus' => ['/(.+|.+)x/'],
|
||||
'multiple overlapping alternations with star' => ['/(ab|bc|cd)*y/'],
|
||||
'multiple overlapping alternations with plus' => ['/(ab|bc|cd)+y/'],
|
||||
];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function regexStrategySafePatternsPasses(): void
|
||||
{
|
||||
$strategy = new RegexMaskingStrategy([
|
||||
'/\d{3}-\d{2}-\d{4}/' => '[SSN]',
|
||||
'/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/' => '[EMAIL]',
|
||||
'/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/' => '[CARD]',
|
||||
]);
|
||||
|
||||
$this->assertInstanceOf(RegexMaskingStrategy::class, $strategy);
|
||||
$this->assertTrue($strategy->validate());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function regexStrategyHandlesErrorInHasPatternMatches(): void
|
||||
{
|
||||
$strategy = new RegexMaskingStrategy([
|
||||
'/simple/' => MaskConstants::MASK_GENERIC,
|
||||
]);
|
||||
|
||||
$result = $strategy->shouldApply('no match here', 'field', $this->logRecord);
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// DataTypeMaskingStrategy Edge Cases
|
||||
// ========================================
|
||||
|
||||
#[Test]
|
||||
public function dataTypeStrategyParseArrayMaskWithEmptyString(): void
|
||||
{
|
||||
$strategy = new DataTypeMaskingStrategy(['array' => '']);
|
||||
|
||||
$result = $strategy->mask(['original'], 'field', $this->logRecord);
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function dataTypeStrategyParseArrayMaskWithInvalidJson(): void
|
||||
{
|
||||
$strategy = new DataTypeMaskingStrategy(['array' => '[invalid json']);
|
||||
|
||||
$result = $strategy->mask(['original'], 'field', $this->logRecord);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertSame(['invalid json'], $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function dataTypeStrategyParseArrayMaskWithNonArrayJson(): void
|
||||
{
|
||||
$strategy = new DataTypeMaskingStrategy(['array' => '["test"]']);
|
||||
|
||||
$result = $strategy->mask(['original'], 'field', $this->logRecord);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertSame(['test'], $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function dataTypeStrategyParseObjectMaskWithEmptyString(): void
|
||||
{
|
||||
$strategy = new DataTypeMaskingStrategy(['object' => '']);
|
||||
|
||||
$obj = (object) ['key' => 'value'];
|
||||
$result = $strategy->mask($obj, 'field', $this->logRecord);
|
||||
|
||||
$this->assertIsObject($result);
|
||||
$this->assertEquals((object) [], $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function dataTypeStrategyParseObjectMaskWithInvalidJson(): void
|
||||
{
|
||||
$strategy = new DataTypeMaskingStrategy(['object' => '{invalid json']);
|
||||
|
||||
$obj = (object) ['key' => 'value'];
|
||||
$result = $strategy->mask($obj, 'field', $this->logRecord);
|
||||
|
||||
$this->assertIsObject($result);
|
||||
$this->assertEquals((object) ['masked' => '{invalid json'], $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function dataTypeStrategyParseObjectMaskWithNonObjectJson(): void
|
||||
{
|
||||
$strategy = new DataTypeMaskingStrategy(['object' => '["array"]']);
|
||||
|
||||
$obj = (object) ['key' => 'value'];
|
||||
$result = $strategy->mask($obj, 'field', $this->logRecord);
|
||||
|
||||
$this->assertIsObject($result);
|
||||
$this->assertEquals((object) ['masked' => '["array"]'], $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function dataTypeStrategyHandlesResourceTypeUnmapped(): void
|
||||
{
|
||||
$strategy = new DataTypeMaskingStrategy(['string' => 'MASKED']);
|
||||
|
||||
$resource = fopen('php://memory', 'r');
|
||||
$this->assertIsResource($resource);
|
||||
|
||||
$result = $strategy->shouldApply($resource, 'field', $this->logRecord);
|
||||
$this->assertFalse($result);
|
||||
|
||||
fclose($resource);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function dataTypeStrategyHandlesResourceTypeMapped(): void
|
||||
{
|
||||
$strategy = new DataTypeMaskingStrategy(['resource' => 'RESOURCE_MASKED']);
|
||||
|
||||
$resource = fopen('php://memory', 'r');
|
||||
$this->assertIsResource($resource);
|
||||
|
||||
$result = $strategy->shouldApply($resource, 'field', $this->logRecord);
|
||||
$this->assertTrue($result);
|
||||
|
||||
fclose($resource);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function dataTypeStrategyValidateWithResourceType(): void
|
||||
{
|
||||
$strategy = new DataTypeMaskingStrategy(['resource' => 'MASKED']);
|
||||
|
||||
$this->assertTrue($strategy->validate());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function dataTypeStrategyHandlesDoubleNonNumericMask(): void
|
||||
{
|
||||
$strategy = new DataTypeMaskingStrategy(['double' => 'NOT_A_NUMBER']);
|
||||
|
||||
$result = $strategy->mask(123.45, 'field', $this->logRecord);
|
||||
|
||||
$this->assertSame('NOT_A_NUMBER', $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function dataTypeStrategyHandlesIntegerNonNumericMask(): void
|
||||
{
|
||||
$strategy = new DataTypeMaskingStrategy(['integer' => 'NOT_A_NUMBER']);
|
||||
|
||||
$result = $strategy->mask(123, 'field', $this->logRecord);
|
||||
|
||||
$this->assertSame('NOT_A_NUMBER', $result);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// FieldPathMaskingStrategy Edge Cases
|
||||
// ========================================
|
||||
|
||||
#[Test]
|
||||
public function fieldPathStrategyValidateWithEmptyConfigs(): void
|
||||
{
|
||||
$strategy = new FieldPathMaskingStrategy([]);
|
||||
|
||||
$this->assertFalse($strategy->validate());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function fieldPathStrategyValidateWithEmptyPath(): void
|
||||
{
|
||||
$strategy = new FieldPathMaskingStrategy(['' => MaskConstants::MASK_GENERIC]);
|
||||
|
||||
$this->assertFalse($strategy->validate());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function fieldPathStrategyValidateWithZeroPath(): void
|
||||
{
|
||||
$strategy = new FieldPathMaskingStrategy(['0' => MaskConstants::MASK_GENERIC]);
|
||||
|
||||
$this->assertFalse($strategy->validate());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function fieldPathStrategyValidateWithValidStringConfig(): void
|
||||
{
|
||||
$strategy = new FieldPathMaskingStrategy([
|
||||
'user.email' => MaskConstants::MASK_EMAIL_PATTERN,
|
||||
]);
|
||||
|
||||
$this->assertTrue($strategy->validate());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function fieldPathStrategyValidateWithFieldMaskConfig(): void
|
||||
{
|
||||
$strategy = new FieldPathMaskingStrategy([
|
||||
'user.email' => FieldMaskConfig::replace(MaskConstants::MASK_EMAIL_PATTERN),
|
||||
'user.ssn' => FieldMaskConfig::remove(),
|
||||
]);
|
||||
|
||||
$this->assertTrue($strategy->validate());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function fieldPathStrategyValidateWithValidRegexConfig(): void
|
||||
{
|
||||
$strategy = new FieldPathMaskingStrategy([
|
||||
'user.data' => FieldMaskConfig::regexMask('/\d+/', '[MASKED]'),
|
||||
]);
|
||||
|
||||
$this->assertTrue($strategy->validate());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function fieldPathStrategyApplyStaticReplacementPreservesIntType(): void
|
||||
{
|
||||
$strategy = new FieldPathMaskingStrategy([
|
||||
'user.age' => FieldMaskConfig::replace('999'),
|
||||
]);
|
||||
|
||||
$result = $strategy->mask(25, 'user.age', $this->logRecord);
|
||||
|
||||
$this->assertSame(999, $result);
|
||||
$this->assertIsInt($result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function fieldPathStrategyApplyStaticReplacementPreservesFloatType(): void
|
||||
{
|
||||
$strategy = new FieldPathMaskingStrategy([
|
||||
'price' => FieldMaskConfig::replace('99.99'),
|
||||
]);
|
||||
|
||||
$result = $strategy->mask(123.45, 'price', $this->logRecord);
|
||||
|
||||
$this->assertSame(99.99, $result);
|
||||
$this->assertIsFloat($result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function fieldPathStrategyApplyStaticReplacementPreservesBoolType(): void
|
||||
{
|
||||
$strategy = new FieldPathMaskingStrategy([
|
||||
'active' => FieldMaskConfig::replace('false'),
|
||||
]);
|
||||
|
||||
$result = $strategy->mask(true, 'active', $this->logRecord);
|
||||
|
||||
$this->assertFalse($result);
|
||||
$this->assertIsBool($result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function fieldPathStrategyApplyStaticReplacementWithNonNumericForInt(): void
|
||||
{
|
||||
$strategy = new FieldPathMaskingStrategy([
|
||||
'user.count' => FieldMaskConfig::replace('NOT_NUMERIC'),
|
||||
]);
|
||||
|
||||
$result = $strategy->mask(42, 'user.count', $this->logRecord);
|
||||
|
||||
$this->assertSame('NOT_NUMERIC', $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function fieldPathStrategyShouldApplyReturnsFalseForMissingPath(): void
|
||||
{
|
||||
$strategy = new FieldPathMaskingStrategy([
|
||||
'user.email' => MaskConstants::MASK_EMAIL_PATTERN,
|
||||
]);
|
||||
|
||||
$this->assertFalse($strategy->shouldApply('value', 'other.path', $this->logRecord));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function fieldPathStrategyShouldApplyReturnsTrueForWildcardMatch(): void
|
||||
{
|
||||
$strategy = new FieldPathMaskingStrategy([
|
||||
'user.*' => MaskConstants::MASK_GENERIC,
|
||||
]);
|
||||
|
||||
$this->assertTrue($strategy->shouldApply('value', 'user.email', $this->logRecord));
|
||||
$this->assertTrue($strategy->shouldApply('value', 'user.name', $this->logRecord));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function fieldPathStrategyMaskAppliesRemoveConfig(): void
|
||||
{
|
||||
$strategy = new FieldPathMaskingStrategy([
|
||||
'secret.key' => FieldMaskConfig::remove(),
|
||||
]);
|
||||
|
||||
$result = $strategy->mask('sensitive', 'secret.key', $this->logRecord);
|
||||
|
||||
$this->assertNull($result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function fieldPathStrategyMaskAppliesRegexConfig(): void
|
||||
{
|
||||
$strategy = new FieldPathMaskingStrategy([
|
||||
'user.ssn' => FieldMaskConfig::regexMask('/\d{3}-\d{2}-\d{4}/', '[SSN]'),
|
||||
]);
|
||||
|
||||
$result = $strategy->mask('SSN: 123-45-6789', 'user.ssn', $this->logRecord);
|
||||
|
||||
$this->assertSame('SSN: [SSN]', $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function fieldPathStrategyMaskHandlesArrayValue(): void
|
||||
{
|
||||
$strategy = new FieldPathMaskingStrategy([
|
||||
'data' => FieldMaskConfig::regexMask('/\d+/', '[NUM]'),
|
||||
]);
|
||||
|
||||
$result = $strategy->mask(['count' => '123 items'], 'data', $this->logRecord);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertSame('[NUM] items', $result['count']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function fieldPathStrategyMaskReturnsValueWhenNoConfigMatch(): void
|
||||
{
|
||||
$strategy = new FieldPathMaskingStrategy([
|
||||
'user.email' => MaskConstants::MASK_EMAIL_PATTERN,
|
||||
]);
|
||||
|
||||
$result = $strategy->mask('original', 'other.field', $this->logRecord);
|
||||
|
||||
$this->assertSame('original', $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function fieldPathStrategyGetNameReturnsCorrectFormat(): void
|
||||
{
|
||||
$strategy = new FieldPathMaskingStrategy([
|
||||
'user.email' => MaskConstants::MASK_EMAIL_PATTERN,
|
||||
'user.phone' => MaskConstants::MASK_PHONE,
|
||||
]);
|
||||
|
||||
$name = $strategy->getName();
|
||||
|
||||
$this->assertSame('Field Path Masking (2 fields)', $name);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function fieldPathStrategyGetConfigurationReturnsAllSettings(): void
|
||||
{
|
||||
$config = [
|
||||
'user.email' => FieldMaskConfig::replace('[EMAIL]'),
|
||||
];
|
||||
$strategy = new FieldPathMaskingStrategy($config);
|
||||
|
||||
$result = $strategy->getConfiguration();
|
||||
|
||||
$this->assertArrayHasKey('field_configs', $result);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Integration Edge Cases
|
||||
// ========================================
|
||||
|
||||
#[Test]
|
||||
public function regexStrategyMaskHandlesBooleanValue(): void
|
||||
{
|
||||
$strategy = new RegexMaskingStrategy([
|
||||
'/true/' => 'MASKED',
|
||||
]);
|
||||
|
||||
$result = $strategy->mask(true, 'field', $this->logRecord);
|
||||
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function regexStrategyMaskHandlesNullValue(): void
|
||||
{
|
||||
$strategy = new RegexMaskingStrategy([
|
||||
'/.*/' => 'MASKED',
|
||||
]);
|
||||
|
||||
$result = $strategy->mask(null, 'field', $this->logRecord);
|
||||
|
||||
// Null converts to empty string, which matches .* and gets masked
|
||||
// preserveValueType doesn't specifically handle null, so returns masked string
|
||||
$this->assertSame('MASKED', $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function regexStrategyMaskHandlesEmptyString(): void
|
||||
{
|
||||
$strategy = new RegexMaskingStrategy([
|
||||
'/.+/' => 'MASKED',
|
||||
]);
|
||||
|
||||
$result = $strategy->mask('', 'field', $this->logRecord);
|
||||
|
||||
$this->assertSame('', $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function dataTypeStrategyMaskHandlesDefaultCase(): void
|
||||
{
|
||||
$strategy = new DataTypeMaskingStrategy(['string' => 'MASKED']);
|
||||
|
||||
$result = $strategy->mask('test', 'field', $this->logRecord);
|
||||
|
||||
$this->assertSame('MASKED', $result);
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,11 @@ final class StrategyManagerEnhancedTest extends TestCase
|
||||
$logRecord = $this->createLogRecord();
|
||||
|
||||
// Value doesn't match pattern
|
||||
$applicable = $manager->getApplicableStrategies(TestConstants::DATA_PUBLIC, TestConstants::FIELD_MESSAGE, $logRecord);
|
||||
$applicable = $manager->getApplicableStrategies(
|
||||
TestConstants::DATA_PUBLIC,
|
||||
TestConstants::FIELD_MESSAGE,
|
||||
$logRecord
|
||||
);
|
||||
|
||||
$this->assertEmpty($applicable);
|
||||
}
|
||||
@@ -90,19 +94,22 @@ final class StrategyManagerEnhancedTest extends TestCase
|
||||
{
|
||||
$manager = new StrategyManager();
|
||||
|
||||
// Add strategies with edge priority values
|
||||
$manager->addStrategy(new RegexMaskingStrategy([TestConstants::PATTERN_TEST => MaskConstants::MASK_GENERIC], [], [], 0)); // Lowest
|
||||
$manager->addStrategy(new RegexMaskingStrategy([TestConstants::PATTERN_TEST => MaskConstants::MASK_GENERIC], [], [], 19)); // High edge
|
||||
$manager->addStrategy(new RegexMaskingStrategy([TestConstants::PATTERN_TEST => MaskConstants::MASK_GENERIC], [], [], 20)); // Medium-high boundary
|
||||
$manager->addStrategy(new RegexMaskingStrategy([TestConstants::PATTERN_TEST => MaskConstants::MASK_GENERIC], [], [], 39)); // Medium-high edge
|
||||
$manager->addStrategy(new RegexMaskingStrategy([TestConstants::PATTERN_TEST => MaskConstants::MASK_GENERIC], [], [], 40)); // Medium boundary
|
||||
$manager->addStrategy(new RegexMaskingStrategy([TestConstants::PATTERN_TEST => MaskConstants::MASK_GENERIC], [], [], 59)); // Medium edge
|
||||
$manager->addStrategy(new RegexMaskingStrategy([TestConstants::PATTERN_TEST => MaskConstants::MASK_GENERIC], [], [], 60)); // Medium-low boundary
|
||||
$manager->addStrategy(new RegexMaskingStrategy([TestConstants::PATTERN_TEST => MaskConstants::MASK_GENERIC], [], [], 79)); // Medium-low edge
|
||||
$manager->addStrategy(new RegexMaskingStrategy([TestConstants::PATTERN_TEST => MaskConstants::MASK_GENERIC], [], [], 80)); // Low boundary
|
||||
$manager->addStrategy(new RegexMaskingStrategy([TestConstants::PATTERN_TEST => MaskConstants::MASK_GENERIC], [], [], 89)); // Low edge
|
||||
$manager->addStrategy(new RegexMaskingStrategy([TestConstants::PATTERN_TEST => MaskConstants::MASK_GENERIC], [], [], 90)); // Lowest boundary
|
||||
$manager->addStrategy(new RegexMaskingStrategy([TestConstants::PATTERN_TEST => MaskConstants::MASK_GENERIC], [], [], 100)); // Highest
|
||||
// Common pattern for all strategies
|
||||
$patterns = [TestConstants::PATTERN_TEST => MaskConstants::MASK_GENERIC];
|
||||
|
||||
// Add strategies with edge priority values across all priority ranges
|
||||
$manager->addStrategy(new RegexMaskingStrategy($patterns, [], [], 0)); // Lowest
|
||||
$manager->addStrategy(new RegexMaskingStrategy($patterns, [], [], 19)); // High edge
|
||||
$manager->addStrategy(new RegexMaskingStrategy($patterns, [], [], 20)); // Medium-high boundary
|
||||
$manager->addStrategy(new RegexMaskingStrategy($patterns, [], [], 39)); // Medium-high edge
|
||||
$manager->addStrategy(new RegexMaskingStrategy($patterns, [], [], 40)); // Medium boundary
|
||||
$manager->addStrategy(new RegexMaskingStrategy($patterns, [], [], 59)); // Medium edge
|
||||
$manager->addStrategy(new RegexMaskingStrategy($patterns, [], [], 60)); // Medium-low boundary
|
||||
$manager->addStrategy(new RegexMaskingStrategy($patterns, [], [], 79)); // Medium-low edge
|
||||
$manager->addStrategy(new RegexMaskingStrategy($patterns, [], [], 80)); // Low boundary
|
||||
$manager->addStrategy(new RegexMaskingStrategy($patterns, [], [], 89)); // Low edge
|
||||
$manager->addStrategy(new RegexMaskingStrategy($patterns, [], [], 90)); // Lowest boundary
|
||||
$manager->addStrategy(new RegexMaskingStrategy($patterns, [], [], 100)); // Highest
|
||||
|
||||
$stats = $manager->getStatistics();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user