mirror of
https://github.com/ivuorinen/monolog-gdpr-filter.git
synced 2026-02-22 02:53:18 +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:
427
tests/Builder/GdprProcessorBuilderEdgeCasesTest.php
Normal file
427
tests/Builder/GdprProcessorBuilderEdgeCasesTest.php
Normal file
@@ -0,0 +1,427 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Builder;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Ivuorinen\MonologGdprFilter\ArrayAccessor\ArrayAccessorFactory;
|
||||
use Ivuorinen\MonologGdprFilter\Builder\GdprProcessorBuilder;
|
||||
use Ivuorinen\MonologGdprFilter\Builder\PluginAwareProcessor;
|
||||
use Ivuorinen\MonologGdprFilter\FieldMaskConfig;
|
||||
use Ivuorinen\MonologGdprFilter\GdprProcessor;
|
||||
use Ivuorinen\MonologGdprFilter\MaskConstants;
|
||||
use Ivuorinen\MonologGdprFilter\Plugins\AbstractMaskingPlugin;
|
||||
use Monolog\Level;
|
||||
use Monolog\LogRecord;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tests\TestConstants;
|
||||
|
||||
/**
|
||||
* Edge case tests for GdprProcessorBuilder.
|
||||
*/
|
||||
#[CoversClass(GdprProcessorBuilder::class)]
|
||||
#[CoversClass(PluginAwareProcessor::class)]
|
||||
final class GdprProcessorBuilderEdgeCasesTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $context
|
||||
*/
|
||||
private function createLogRecord(string $message = 'Test', array $context = []): LogRecord
|
||||
{
|
||||
return new LogRecord(
|
||||
datetime: new DateTimeImmutable(),
|
||||
channel: 'test',
|
||||
level: Level::Info,
|
||||
message: $message,
|
||||
context: $context,
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function setPatternsReplacesExisting(): void
|
||||
{
|
||||
$builder = GdprProcessorBuilder::create()
|
||||
->addPattern('/old/', 'OLD_MASK')
|
||||
->setPatterns(['/new/' => 'NEW_MASK']);
|
||||
|
||||
$patterns = $builder->getPatterns();
|
||||
|
||||
$this->assertArrayNotHasKey('/old/', $patterns);
|
||||
$this->assertArrayHasKey('/new/', $patterns);
|
||||
$this->assertSame('NEW_MASK', $patterns['/new/']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function setFieldPathsReplacesExisting(): void
|
||||
{
|
||||
$builder = GdprProcessorBuilder::create()
|
||||
->addFieldPath('old.path', '[OLD]')
|
||||
->setFieldPaths(['new.path' => '[NEW]']);
|
||||
|
||||
$paths = $builder->getFieldPaths();
|
||||
|
||||
$this->assertArrayNotHasKey('old.path', $paths);
|
||||
$this->assertArrayHasKey('new.path', $paths);
|
||||
$this->assertSame('[NEW]', $paths['new.path']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function addCallbacksAddsMultipleCallbacks(): void
|
||||
{
|
||||
$callback1 = fn(mixed $value): string => 'CALLBACK1';
|
||||
$callback2 = fn(mixed $value): string => 'CALLBACK2';
|
||||
|
||||
$builder = GdprProcessorBuilder::create()
|
||||
->addCallbacks([
|
||||
'path.one' => $callback1,
|
||||
'path.two' => $callback2,
|
||||
]);
|
||||
|
||||
$processor = $builder->build();
|
||||
|
||||
$record = $this->createLogRecord('Test', [
|
||||
'path' => [
|
||||
'one' => 'value1',
|
||||
'two' => 'value2',
|
||||
],
|
||||
]);
|
||||
|
||||
$processed = $processor($record);
|
||||
|
||||
$this->assertSame('CALLBACK1', $processed->context['path']['one']);
|
||||
$this->assertSame('CALLBACK2', $processed->context['path']['two']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function addDataTypeMasksAddsMultipleMasks(): void
|
||||
{
|
||||
$builder = GdprProcessorBuilder::create()
|
||||
->addDataTypeMasks([
|
||||
'integer' => '999',
|
||||
'boolean' => 'false',
|
||||
]);
|
||||
|
||||
$processor = $builder->build();
|
||||
|
||||
$record = $this->createLogRecord('Test', [
|
||||
'count' => 42,
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
$processed = $processor($record);
|
||||
|
||||
$this->assertSame(999, $processed->context['count']);
|
||||
$this->assertFalse($processed->context['active']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function addConditionalRulesAddsMultipleRules(): void
|
||||
{
|
||||
$rule1Called = false;
|
||||
$rule2Called = false;
|
||||
|
||||
$builder = GdprProcessorBuilder::create()
|
||||
->addPattern('/sensitive/', TestConstants::MASK_MASKED_BRACKETS)
|
||||
->addConditionalRules([
|
||||
'rule1' => function (LogRecord $record) use (&$rule1Called): bool {
|
||||
$rule1Called = true;
|
||||
return $record->channel === 'test';
|
||||
},
|
||||
'rule2' => function () use (&$rule2Called): bool {
|
||||
$rule2Called = true;
|
||||
return true;
|
||||
},
|
||||
]);
|
||||
|
||||
$processor = $builder->build();
|
||||
|
||||
$record = $this->createLogRecord('Contains sensitive data');
|
||||
$processed = $processor($record);
|
||||
|
||||
$this->assertTrue($rule1Called);
|
||||
$this->assertTrue($rule2Called);
|
||||
$this->assertStringContainsString(TestConstants::MASK_MASKED_BRACKETS, $processed->message);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getPluginsReturnsRegisteredPlugins(): void
|
||||
{
|
||||
$plugin1 = new class extends AbstractMaskingPlugin {
|
||||
public function getName(): string
|
||||
{
|
||||
return 'plugin-1';
|
||||
}
|
||||
};
|
||||
|
||||
$plugin2 = new class extends AbstractMaskingPlugin {
|
||||
public function getName(): string
|
||||
{
|
||||
return 'plugin-2';
|
||||
}
|
||||
};
|
||||
|
||||
$builder = GdprProcessorBuilder::create()
|
||||
->addPlugin($plugin1)
|
||||
->addPlugin($plugin2);
|
||||
|
||||
$plugins = $builder->getPlugins();
|
||||
|
||||
$this->assertCount(2, $plugins);
|
||||
$this->assertSame('plugin-1', $plugins[0]->getName());
|
||||
$this->assertSame('plugin-2', $plugins[1]->getName());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function addPluginsAddsMultiplePlugins(): void
|
||||
{
|
||||
$plugin1 = new class extends AbstractMaskingPlugin {
|
||||
public function getName(): string
|
||||
{
|
||||
return 'plugin-1';
|
||||
}
|
||||
};
|
||||
|
||||
$plugin2 = new class extends AbstractMaskingPlugin {
|
||||
public function getName(): string
|
||||
{
|
||||
return 'plugin-2';
|
||||
}
|
||||
};
|
||||
|
||||
$builder = GdprProcessorBuilder::create()
|
||||
->addPlugins([$plugin1, $plugin2]);
|
||||
|
||||
$plugins = $builder->getPlugins();
|
||||
|
||||
$this->assertCount(2, $plugins);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function buildWithPluginsReturnsGdprProcessorWhenNoPlugins(): void
|
||||
{
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->withDefaultPatterns()
|
||||
->buildWithPlugins();
|
||||
|
||||
// buildWithPlugins returns GdprProcessor when no plugins are registered
|
||||
// We can't use assertNotInstanceOf due to PHPStan's static analysis
|
||||
// Instead we verify the actual return type
|
||||
$this->assertSame(GdprProcessor::class, $processor::class);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function buildWithPluginsReturnsPluginAwareProcessorWithPlugins(): void
|
||||
{
|
||||
$plugin = new class extends AbstractMaskingPlugin {
|
||||
public function getName(): string
|
||||
{
|
||||
return 'test-plugin';
|
||||
}
|
||||
};
|
||||
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addPlugin($plugin)
|
||||
->buildWithPlugins();
|
||||
|
||||
$this->assertInstanceOf(PluginAwareProcessor::class, $processor);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function buildWithPluginsSortsPluginsByPriority(): void
|
||||
{
|
||||
$lowPriority = new class (200) extends AbstractMaskingPlugin {
|
||||
public function getName(): string
|
||||
{
|
||||
return 'low-priority';
|
||||
}
|
||||
};
|
||||
|
||||
$highPriority = new class (10) extends AbstractMaskingPlugin {
|
||||
public function getName(): string
|
||||
{
|
||||
return 'high-priority';
|
||||
}
|
||||
};
|
||||
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addPlugin($lowPriority)
|
||||
->addPlugin($highPriority)
|
||||
->buildWithPlugins();
|
||||
|
||||
$this->assertInstanceOf(PluginAwareProcessor::class, $processor);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function pluginContributesPatterns(): void
|
||||
{
|
||||
$plugin = new class extends AbstractMaskingPlugin {
|
||||
public function getName(): string
|
||||
{
|
||||
return 'pattern-plugin';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getPatterns(): array
|
||||
{
|
||||
return ['/PLUGIN-\d+/' => '[PLUGIN-ID]'];
|
||||
}
|
||||
};
|
||||
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addPlugin($plugin)
|
||||
->buildWithPlugins();
|
||||
|
||||
$record = $this->createLogRecord('Reference: PLUGIN-12345');
|
||||
$processed = $processor($record);
|
||||
|
||||
$this->assertSame('Reference: [PLUGIN-ID]', $processed->message);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function pluginContributesFieldPaths(): void
|
||||
{
|
||||
$plugin = new class extends AbstractMaskingPlugin {
|
||||
public function getName(): string
|
||||
{
|
||||
return 'field-plugin';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getFieldPaths(): array
|
||||
{
|
||||
return ['secret.key' => FieldMaskConfig::replace('[REDACTED]')];
|
||||
}
|
||||
};
|
||||
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addPlugin($plugin)
|
||||
->buildWithPlugins();
|
||||
|
||||
$record = $this->createLogRecord('Test', ['secret' => ['key' => 'sensitive-value']]);
|
||||
$processed = $processor($record);
|
||||
|
||||
$this->assertSame('[REDACTED]', $processed->context['secret']['key']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function withArrayAccessorFactoryConfiguresProcessor(): void
|
||||
{
|
||||
$factory = ArrayAccessorFactory::default();
|
||||
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->withArrayAccessorFactory($factory)
|
||||
->addFieldPath('user.email', MaskConstants::MASK_EMAIL)
|
||||
->build();
|
||||
|
||||
$record = $this->createLogRecord('Test', [
|
||||
'user' => ['email' => 'test@example.com'],
|
||||
]);
|
||||
|
||||
$processed = $processor($record);
|
||||
|
||||
$this->assertSame(MaskConstants::MASK_EMAIL, $processed->context['user']['email']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function withMaxDepthLimitsRecursion(): void
|
||||
{
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->withMaxDepth(2)
|
||||
->addPattern('/secret/', TestConstants::MASK_MASKED_BRACKETS)
|
||||
->build();
|
||||
|
||||
$record = $this->createLogRecord('Test', [
|
||||
'level1' => [
|
||||
'level2' => [
|
||||
'level3' => 'secret data',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$processed = $processor($record);
|
||||
|
||||
// Processor should handle the record without throwing
|
||||
$this->assertIsArray($processed->context);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function withAuditLoggerConfiguresLogging(): void
|
||||
{
|
||||
$auditLogs = [];
|
||||
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addFieldPath('password', MaskConstants::MASK_REDACTED)
|
||||
->withAuditLogger(function ($path, $original, $masked) use (&$auditLogs): void {
|
||||
$auditLogs[] = ['path' => $path, 'original' => $original, 'masked' => $masked];
|
||||
})
|
||||
->build();
|
||||
|
||||
$record = $this->createLogRecord('Test', ['password' => 'secret123']);
|
||||
$processor($record);
|
||||
|
||||
$this->assertNotEmpty($auditLogs);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function addConditionalRuleConfiguresProcessor(): void
|
||||
{
|
||||
$ruleExecuted = false;
|
||||
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addPattern('/data/', TestConstants::MASK_MASKED_BRACKETS)
|
||||
->addConditionalRule('track-execution', function () use (&$ruleExecuted): bool {
|
||||
$ruleExecuted = true;
|
||||
return true;
|
||||
})
|
||||
->build();
|
||||
|
||||
$record = $this->createLogRecord('Contains data value');
|
||||
$processor($record);
|
||||
|
||||
// Verify the conditional rule was executed
|
||||
$this->assertTrue($ruleExecuted);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function addFieldPathsAddsMultiplePaths(): void
|
||||
{
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addFieldPaths([
|
||||
'user.email' => MaskConstants::MASK_EMAIL,
|
||||
'user.phone' => MaskConstants::MASK_PHONE,
|
||||
])
|
||||
->build();
|
||||
|
||||
$record = $this->createLogRecord('Test', [
|
||||
'user' => [
|
||||
'email' => 'test@example.com',
|
||||
'phone' => '555-1234',
|
||||
],
|
||||
]);
|
||||
|
||||
$processed = $processor($record);
|
||||
|
||||
$this->assertSame(MaskConstants::MASK_EMAIL, $processed->context['user']['email']);
|
||||
$this->assertSame(MaskConstants::MASK_PHONE, $processed->context['user']['phone']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function addPatternsAddsMultiplePatterns(): void
|
||||
{
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addPatterns([
|
||||
'/\d{3}-\d{2}-\d{4}/' => '[SSN]',
|
||||
'/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/' => '[EMAIL]',
|
||||
])
|
||||
->build();
|
||||
|
||||
$record = $this->createLogRecord('SSN: 123-45-6789, Email: user@example.com');
|
||||
$processed = $processor($record);
|
||||
|
||||
$this->assertStringContainsString('[SSN]', $processed->message);
|
||||
$this->assertStringContainsString('[EMAIL]', $processed->message);
|
||||
}
|
||||
}
|
||||
365
tests/Builder/GdprProcessorBuilderTest.php
Normal file
365
tests/Builder/GdprProcessorBuilderTest.php
Normal file
@@ -0,0 +1,365 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Builder;
|
||||
|
||||
use Ivuorinen\MonologGdprFilter\ArrayAccessor\ArrayAccessorFactory;
|
||||
use Ivuorinen\MonologGdprFilter\Builder\GdprProcessorBuilder;
|
||||
use Ivuorinen\MonologGdprFilter\Builder\PluginAwareProcessor;
|
||||
use Ivuorinen\MonologGdprFilter\DefaultPatterns;
|
||||
use Ivuorinen\MonologGdprFilter\FieldMaskConfig;
|
||||
use Ivuorinen\MonologGdprFilter\GdprProcessor;
|
||||
use Ivuorinen\MonologGdprFilter\MaskConstants;
|
||||
use Ivuorinen\MonologGdprFilter\Plugins\AbstractMaskingPlugin;
|
||||
use Monolog\Level;
|
||||
use Monolog\LogRecord;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tests\TestConstants;
|
||||
|
||||
#[CoversClass(GdprProcessorBuilder::class)]
|
||||
final class GdprProcessorBuilderTest extends TestCase
|
||||
{
|
||||
public function testCreateReturnsBuilder(): void
|
||||
{
|
||||
$builder = GdprProcessorBuilder::create();
|
||||
|
||||
$this->assertInstanceOf(GdprProcessorBuilder::class, $builder);
|
||||
}
|
||||
|
||||
public function testBuildReturnsGdprProcessor(): void
|
||||
{
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addPattern(TestConstants::PATTERN_TEST, MaskConstants::MASK_GENERIC)
|
||||
->build();
|
||||
|
||||
$this->assertInstanceOf(GdprProcessor::class, $processor);
|
||||
}
|
||||
|
||||
public function testWithDefaultPatterns(): void
|
||||
{
|
||||
$builder = GdprProcessorBuilder::create()->withDefaultPatterns();
|
||||
$patterns = $builder->getPatterns();
|
||||
|
||||
$this->assertNotEmpty($patterns);
|
||||
$this->assertSame(DefaultPatterns::get(), $patterns);
|
||||
}
|
||||
|
||||
public function testAddPattern(): void
|
||||
{
|
||||
$builder = GdprProcessorBuilder::create()
|
||||
->addPattern(TestConstants::PATTERN_DIGITS, TestConstants::MASK_DIGITS_BRACKETS);
|
||||
|
||||
$this->assertArrayHasKey(TestConstants::PATTERN_DIGITS, $builder->getPatterns());
|
||||
}
|
||||
|
||||
public function testAddPatterns(): void
|
||||
{
|
||||
$patterns = [
|
||||
TestConstants::PATTERN_DIGITS => TestConstants::MASK_DIGITS_BRACKETS,
|
||||
TestConstants::PATTERN_TEST => '[TEST]',
|
||||
];
|
||||
|
||||
$builder = GdprProcessorBuilder::create()->addPatterns($patterns);
|
||||
|
||||
$this->assertSame($patterns, $builder->getPatterns());
|
||||
}
|
||||
|
||||
public function testSetPatternsReplacesExisting(): void
|
||||
{
|
||||
$builder = GdprProcessorBuilder::create()
|
||||
->addPattern(TestConstants::PATTERN_DIGITS, TestConstants::MASK_DIGITS_BRACKETS)
|
||||
->setPatterns([TestConstants::PATTERN_TEST => '[TEST]']);
|
||||
|
||||
$patterns = $builder->getPatterns();
|
||||
|
||||
$this->assertCount(1, $patterns);
|
||||
$this->assertArrayHasKey(TestConstants::PATTERN_TEST, $patterns);
|
||||
$this->assertArrayNotHasKey(TestConstants::PATTERN_DIGITS, $patterns);
|
||||
}
|
||||
|
||||
public function testAddFieldPath(): void
|
||||
{
|
||||
$builder = GdprProcessorBuilder::create()
|
||||
->addFieldPath(TestConstants::CONTEXT_EMAIL, FieldMaskConfig::replace('[EMAIL]'));
|
||||
|
||||
$this->assertArrayHasKey(TestConstants::CONTEXT_EMAIL, $builder->getFieldPaths());
|
||||
}
|
||||
|
||||
public function testAddFieldPaths(): void
|
||||
{
|
||||
$fieldPaths = [
|
||||
TestConstants::CONTEXT_EMAIL => FieldMaskConfig::replace('[EMAIL]'),
|
||||
TestConstants::CONTEXT_PASSWORD => FieldMaskConfig::remove(),
|
||||
];
|
||||
|
||||
$builder = GdprProcessorBuilder::create()->addFieldPaths($fieldPaths);
|
||||
|
||||
$this->assertCount(2, $builder->getFieldPaths());
|
||||
}
|
||||
|
||||
public function testAddCallback(): void
|
||||
{
|
||||
$callback = fn(mixed $val): string => strtoupper((string) $val);
|
||||
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addCallback('name', $callback)
|
||||
->build();
|
||||
|
||||
$record = new LogRecord(
|
||||
datetime: new \DateTimeImmutable(),
|
||||
channel: TestConstants::CHANNEL_TEST,
|
||||
level: Level::Info,
|
||||
message: TestConstants::MESSAGE_DEFAULT,
|
||||
context: ['name' => 'john']
|
||||
);
|
||||
|
||||
$result = $processor($record);
|
||||
|
||||
$this->assertSame('JOHN', $result->context['name']);
|
||||
}
|
||||
|
||||
public function testWithAuditLogger(): void
|
||||
{
|
||||
$logs = [];
|
||||
$auditLogger = function (string $path, mixed $original, mixed $masked) use (&$logs): void {
|
||||
$logs[] = ['path' => $path, 'original' => $original, TestConstants::DATA_MASKED => $masked];
|
||||
};
|
||||
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addFieldPath('field', FieldMaskConfig::replace('[MASKED]'))
|
||||
->withAuditLogger($auditLogger)
|
||||
->build();
|
||||
|
||||
$record = new LogRecord(
|
||||
datetime: new \DateTimeImmutable(),
|
||||
channel: TestConstants::CHANNEL_TEST,
|
||||
level: Level::Info,
|
||||
message: TestConstants::MESSAGE_DEFAULT,
|
||||
context: ['field' => 'value']
|
||||
);
|
||||
|
||||
$processor($record);
|
||||
|
||||
$this->assertCount(1, $logs);
|
||||
}
|
||||
|
||||
public function testWithMaxDepth(): void
|
||||
{
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addPattern(TestConstants::PATTERN_TEST, MaskConstants::MASK_GENERIC)
|
||||
->withMaxDepth(50)
|
||||
->build();
|
||||
|
||||
// The processor should still work
|
||||
$result = $processor->regExpMessage(TestConstants::MESSAGE_TEST_LOWERCASE);
|
||||
|
||||
$this->assertSame(MaskConstants::MASK_GENERIC . ' message', $result);
|
||||
}
|
||||
|
||||
public function testAddDataTypeMask(): void
|
||||
{
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addDataTypeMask('integer', TestConstants::MASK_INT_BRACKETS)
|
||||
->build();
|
||||
|
||||
$record = new LogRecord(
|
||||
datetime: new \DateTimeImmutable(),
|
||||
channel: TestConstants::CHANNEL_TEST,
|
||||
level: Level::Info,
|
||||
message: TestConstants::MESSAGE_DEFAULT,
|
||||
context: ['count' => 42]
|
||||
);
|
||||
|
||||
$result = $processor($record);
|
||||
|
||||
$this->assertSame(TestConstants::MASK_INT_BRACKETS, $result->context['count']);
|
||||
}
|
||||
|
||||
public function testAddConditionalRule(): void
|
||||
{
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addPattern(TestConstants::PATTERN_TEST, MaskConstants::MASK_GENERIC)
|
||||
->addConditionalRule('skip_debug', fn(LogRecord $r): bool => $r->level !== Level::Debug)
|
||||
->build();
|
||||
|
||||
$debugRecord = new LogRecord(
|
||||
datetime: new \DateTimeImmutable(),
|
||||
channel: TestConstants::CHANNEL_TEST,
|
||||
level: Level::Debug,
|
||||
message: TestConstants::MESSAGE_TEST_LOWERCASE,
|
||||
context: []
|
||||
);
|
||||
|
||||
$infoRecord = new LogRecord(
|
||||
datetime: new \DateTimeImmutable(),
|
||||
channel: TestConstants::CHANNEL_TEST,
|
||||
level: Level::Info,
|
||||
message: TestConstants::MESSAGE_TEST_LOWERCASE,
|
||||
context: []
|
||||
);
|
||||
|
||||
// Debug should not be masked
|
||||
$this->assertSame(TestConstants::MESSAGE_TEST_LOWERCASE, $processor($debugRecord)->message);
|
||||
|
||||
// Info should be masked
|
||||
$this->assertSame(MaskConstants::MASK_GENERIC . ' message', $processor($infoRecord)->message);
|
||||
}
|
||||
|
||||
public function testWithArrayAccessorFactory(): void
|
||||
{
|
||||
$factory = ArrayAccessorFactory::default();
|
||||
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addPattern(TestConstants::PATTERN_TEST, MaskConstants::MASK_GENERIC)
|
||||
->withArrayAccessorFactory($factory)
|
||||
->build();
|
||||
|
||||
$this->assertInstanceOf(GdprProcessor::class, $processor);
|
||||
}
|
||||
|
||||
public function testAddPlugin(): void
|
||||
{
|
||||
$plugin = new class extends AbstractMaskingPlugin {
|
||||
public function getName(): string
|
||||
{
|
||||
return 'test-plugin';
|
||||
}
|
||||
|
||||
public function getPatterns(): array
|
||||
{
|
||||
return ['/secret/' => '[SECRET]'];
|
||||
}
|
||||
};
|
||||
|
||||
$builder = GdprProcessorBuilder::create()->addPlugin($plugin);
|
||||
|
||||
$this->assertCount(1, $builder->getPlugins());
|
||||
}
|
||||
|
||||
public function testAddPlugins(): void
|
||||
{
|
||||
$plugin1 = new class extends AbstractMaskingPlugin {
|
||||
public function getName(): string
|
||||
{
|
||||
return 'plugin1';
|
||||
}
|
||||
};
|
||||
|
||||
$plugin2 = new class extends AbstractMaskingPlugin {
|
||||
public function getName(): string
|
||||
{
|
||||
return 'plugin2';
|
||||
}
|
||||
};
|
||||
|
||||
$builder = GdprProcessorBuilder::create()->addPlugins([$plugin1, $plugin2]);
|
||||
|
||||
$this->assertCount(2, $builder->getPlugins());
|
||||
}
|
||||
|
||||
public function testBuildWithPluginsReturnsGdprProcessorWhenNoPlugins(): void
|
||||
{
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addPattern(TestConstants::PATTERN_TEST, MaskConstants::MASK_GENERIC)
|
||||
->buildWithPlugins();
|
||||
|
||||
$this->assertInstanceOf(GdprProcessor::class, $processor);
|
||||
// When no plugins, it returns GdprProcessor directly, not PluginAwareProcessor
|
||||
$this->assertSame(GdprProcessor::class, $processor::class);
|
||||
}
|
||||
|
||||
public function testBuildWithPluginsReturnsPluginAwareProcessor(): void
|
||||
{
|
||||
$plugin = new class extends AbstractMaskingPlugin {
|
||||
public function getName(): string
|
||||
{
|
||||
return 'test-plugin';
|
||||
}
|
||||
};
|
||||
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addPattern(TestConstants::PATTERN_TEST, MaskConstants::MASK_GENERIC)
|
||||
->addPlugin($plugin)
|
||||
->buildWithPlugins();
|
||||
|
||||
$this->assertInstanceOf(PluginAwareProcessor::class, $processor);
|
||||
}
|
||||
|
||||
public function testPluginPatternsAreApplied(): void
|
||||
{
|
||||
$plugin = new class extends AbstractMaskingPlugin {
|
||||
public function getName(): string
|
||||
{
|
||||
return 'secret-plugin';
|
||||
}
|
||||
|
||||
public function getPatterns(): array
|
||||
{
|
||||
return ['/secret/' => '[SECRET]'];
|
||||
}
|
||||
};
|
||||
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addPlugin($plugin)
|
||||
->build();
|
||||
|
||||
$record = new LogRecord(
|
||||
datetime: new \DateTimeImmutable(),
|
||||
channel: TestConstants::CHANNEL_TEST,
|
||||
level: Level::Info,
|
||||
message: 'This is secret data',
|
||||
context: []
|
||||
);
|
||||
|
||||
$result = $processor($record);
|
||||
|
||||
$this->assertSame('This is [SECRET] data', $result->message);
|
||||
}
|
||||
|
||||
public function testPluginFieldPathsAreApplied(): void
|
||||
{
|
||||
$plugin = new class extends AbstractMaskingPlugin {
|
||||
public function getName(): string
|
||||
{
|
||||
return 'field-plugin';
|
||||
}
|
||||
|
||||
public function getFieldPaths(): array
|
||||
{
|
||||
return ['api_key' => FieldMaskConfig::replace('[API_KEY]')];
|
||||
}
|
||||
};
|
||||
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addPlugin($plugin)
|
||||
->build();
|
||||
|
||||
$record = new LogRecord(
|
||||
datetime: new \DateTimeImmutable(),
|
||||
channel: TestConstants::CHANNEL_TEST,
|
||||
level: Level::Info,
|
||||
message: TestConstants::MESSAGE_DEFAULT,
|
||||
context: ['api_key' => 'abc123']
|
||||
);
|
||||
|
||||
$result = $processor($record);
|
||||
|
||||
$this->assertSame('[API_KEY]', $result->context['api_key']);
|
||||
}
|
||||
|
||||
public function testFluentChaining(): void
|
||||
{
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->withDefaultPatterns()
|
||||
->addPattern('/custom/', '[CUSTOM]')
|
||||
->addFieldPath('secret', FieldMaskConfig::remove())
|
||||
->addCallback('name', fn(mixed $v): string => strtoupper((string) $v))
|
||||
->withMaxDepth(50)
|
||||
->addDataTypeMask('integer', TestConstants::MASK_INT_BRACKETS)
|
||||
->build();
|
||||
|
||||
$this->assertInstanceOf(GdprProcessor::class, $processor);
|
||||
}
|
||||
}
|
||||
370
tests/Builder/PluginAwareProcessorTest.php
Normal file
370
tests/Builder/PluginAwareProcessorTest.php
Normal file
@@ -0,0 +1,370 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Builder;
|
||||
|
||||
use Ivuorinen\MonologGdprFilter\Builder\GdprProcessorBuilder;
|
||||
use Ivuorinen\MonologGdprFilter\Builder\PluginAwareProcessor;
|
||||
use Ivuorinen\MonologGdprFilter\GdprProcessor;
|
||||
use Ivuorinen\MonologGdprFilter\MaskConstants;
|
||||
use Ivuorinen\MonologGdprFilter\Plugins\AbstractMaskingPlugin;
|
||||
use Monolog\Level;
|
||||
use Monolog\LogRecord;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tests\TestConstants;
|
||||
|
||||
#[CoversClass(PluginAwareProcessor::class)]
|
||||
final class PluginAwareProcessorTest extends TestCase
|
||||
{
|
||||
public function testInvokeAppliesPreProcessing(): void
|
||||
{
|
||||
$plugin = new class extends AbstractMaskingPlugin {
|
||||
public function getName(): string
|
||||
{
|
||||
return 'uppercase-plugin';
|
||||
}
|
||||
|
||||
public function preProcessMessage(string $message): string
|
||||
{
|
||||
return strtoupper($message);
|
||||
}
|
||||
};
|
||||
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addPattern('/TEST/', '[MASKED]')
|
||||
->addPlugin($plugin)
|
||||
->buildWithPlugins();
|
||||
|
||||
$record = new LogRecord(
|
||||
datetime: new \DateTimeImmutable(),
|
||||
channel: TestConstants::CHANNEL_TEST,
|
||||
level: Level::Info,
|
||||
message: 'This is a test message',
|
||||
context: []
|
||||
);
|
||||
|
||||
$result = $processor($record);
|
||||
|
||||
// Message should be uppercased, then 'TEST' should be masked
|
||||
$this->assertStringContainsString('[MASKED]', $result->message);
|
||||
}
|
||||
|
||||
public function testInvokeAppliesPostProcessing(): void
|
||||
{
|
||||
$plugin = new class extends AbstractMaskingPlugin {
|
||||
public function getName(): string
|
||||
{
|
||||
return 'suffix-plugin';
|
||||
}
|
||||
|
||||
public function postProcessMessage(string $message): string
|
||||
{
|
||||
return $message . ' [processed]';
|
||||
}
|
||||
};
|
||||
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addPattern(TestConstants::PATTERN_TEST, MaskConstants::MASK_GENERIC)
|
||||
->addPlugin($plugin)
|
||||
->buildWithPlugins();
|
||||
|
||||
$record = new LogRecord(
|
||||
datetime: new \DateTimeImmutable(),
|
||||
channel: TestConstants::CHANNEL_TEST,
|
||||
level: Level::Info,
|
||||
message: 'test',
|
||||
context: []
|
||||
);
|
||||
|
||||
$result = $processor($record);
|
||||
|
||||
$this->assertStringEndsWith('[processed]', $result->message);
|
||||
}
|
||||
|
||||
public function testInvokeAppliesPreProcessContext(): void
|
||||
{
|
||||
$plugin = new class extends AbstractMaskingPlugin {
|
||||
public function getName(): string
|
||||
{
|
||||
return 'add-field-plugin';
|
||||
}
|
||||
|
||||
public function preProcessContext(array $context): array
|
||||
{
|
||||
$context['added_by_plugin'] = 'true';
|
||||
return $context;
|
||||
}
|
||||
};
|
||||
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addPlugin($plugin)
|
||||
->buildWithPlugins();
|
||||
|
||||
$record = new LogRecord(
|
||||
datetime: new \DateTimeImmutable(),
|
||||
channel: TestConstants::CHANNEL_TEST,
|
||||
level: Level::Info,
|
||||
message: TestConstants::MESSAGE_DEFAULT,
|
||||
context: ['original' => 'data']
|
||||
);
|
||||
|
||||
$result = $processor($record);
|
||||
|
||||
$this->assertArrayHasKey('added_by_plugin', $result->context);
|
||||
}
|
||||
|
||||
public function testInvokeAppliesPostProcessContext(): void
|
||||
{
|
||||
$plugin = new class extends AbstractMaskingPlugin {
|
||||
public function getName(): string
|
||||
{
|
||||
return 'remove-field-plugin';
|
||||
}
|
||||
|
||||
public function postProcessContext(array $context): array
|
||||
{
|
||||
unset($context['to_remove']);
|
||||
return $context;
|
||||
}
|
||||
};
|
||||
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addPlugin($plugin)
|
||||
->buildWithPlugins();
|
||||
|
||||
$record = new LogRecord(
|
||||
datetime: new \DateTimeImmutable(),
|
||||
channel: TestConstants::CHANNEL_TEST,
|
||||
level: Level::Info,
|
||||
message: TestConstants::MESSAGE_DEFAULT,
|
||||
context: ['to_remove' => 'value', 'keep' => 'data']
|
||||
);
|
||||
|
||||
$result = $processor($record);
|
||||
|
||||
$this->assertArrayNotHasKey('to_remove', $result->context);
|
||||
$this->assertArrayHasKey('keep', $result->context);
|
||||
}
|
||||
|
||||
public function testPostProcessingRunsInReverseOrder(): void
|
||||
{
|
||||
// Test that post-processing happens by verifying the message is modified
|
||||
$plugin1 = new class extends AbstractMaskingPlugin {
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(10);
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'plugin1';
|
||||
}
|
||||
|
||||
public function postProcessMessage(string $message): string
|
||||
{
|
||||
return $message . '-plugin1';
|
||||
}
|
||||
};
|
||||
|
||||
$plugin2 = new class extends AbstractMaskingPlugin {
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(20);
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'plugin2';
|
||||
}
|
||||
|
||||
public function postProcessMessage(string $message): string
|
||||
{
|
||||
return $message . '-plugin2';
|
||||
}
|
||||
};
|
||||
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addPlugin($plugin1)
|
||||
->addPlugin($plugin2)
|
||||
->buildWithPlugins();
|
||||
|
||||
$record = new LogRecord(
|
||||
datetime: new \DateTimeImmutable(),
|
||||
channel: TestConstants::CHANNEL_TEST,
|
||||
level: Level::Info,
|
||||
message: 'base',
|
||||
context: []
|
||||
);
|
||||
|
||||
$result = $processor($record);
|
||||
|
||||
// Post-processing runs in reverse priority order (higher priority last)
|
||||
// plugin2 (priority 20) runs first in post-processing, then plugin1 (priority 10)
|
||||
$this->assertSame('base-plugin2-plugin1', $result->message);
|
||||
}
|
||||
|
||||
public function testGetProcessor(): void
|
||||
{
|
||||
$plugin = new class extends AbstractMaskingPlugin {
|
||||
public function getName(): string
|
||||
{
|
||||
return 'test-plugin';
|
||||
}
|
||||
};
|
||||
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addPattern(TestConstants::PATTERN_TEST, MaskConstants::MASK_GENERIC)
|
||||
->addPlugin($plugin)
|
||||
->buildWithPlugins();
|
||||
|
||||
$this->assertInstanceOf(PluginAwareProcessor::class, $processor);
|
||||
$this->assertInstanceOf(GdprProcessor::class, $processor->getProcessor());
|
||||
}
|
||||
|
||||
public function testGetPlugins(): void
|
||||
{
|
||||
$plugin1 = new class extends AbstractMaskingPlugin {
|
||||
public function getName(): string
|
||||
{
|
||||
return 'plugin1';
|
||||
}
|
||||
};
|
||||
|
||||
$plugin2 = new class extends AbstractMaskingPlugin {
|
||||
public function getName(): string
|
||||
{
|
||||
return 'plugin2';
|
||||
}
|
||||
};
|
||||
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addPlugin($plugin1)
|
||||
->addPlugin($plugin2)
|
||||
->buildWithPlugins();
|
||||
|
||||
$this->assertInstanceOf(PluginAwareProcessor::class, $processor);
|
||||
$this->assertCount(2, $processor->getPlugins());
|
||||
}
|
||||
|
||||
public function testRegExpMessageDelegates(): void
|
||||
{
|
||||
$plugin = new class extends AbstractMaskingPlugin {
|
||||
public function getName(): string
|
||||
{
|
||||
return 'test-plugin';
|
||||
}
|
||||
};
|
||||
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addPattern(TestConstants::PATTERN_TEST, MaskConstants::MASK_GENERIC)
|
||||
->addPlugin($plugin)
|
||||
->buildWithPlugins();
|
||||
|
||||
$this->assertInstanceOf(PluginAwareProcessor::class, $processor);
|
||||
$this->assertSame(MaskConstants::MASK_GENERIC . ' message', $processor->regExpMessage('test message'));
|
||||
}
|
||||
|
||||
public function testRecursiveMaskDelegates(): void
|
||||
{
|
||||
$plugin = new class extends AbstractMaskingPlugin {
|
||||
public function getName(): string
|
||||
{
|
||||
return 'test-plugin';
|
||||
}
|
||||
};
|
||||
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addPattern(TestConstants::PATTERN_TEST, MaskConstants::MASK_GENERIC)
|
||||
->addPlugin($plugin)
|
||||
->buildWithPlugins();
|
||||
|
||||
$this->assertInstanceOf(PluginAwareProcessor::class, $processor);
|
||||
|
||||
$result = $processor->recursiveMask(['key' => 'test value']);
|
||||
|
||||
$this->assertSame(MaskConstants::MASK_GENERIC . ' value', $result['key']);
|
||||
}
|
||||
|
||||
public function testSetAuditLoggerDelegates(): void
|
||||
{
|
||||
$logs = [];
|
||||
$auditLogger = function (string $path) use (&$logs): void {
|
||||
$logs[] = ['path' => $path];
|
||||
};
|
||||
|
||||
$plugin = new class extends AbstractMaskingPlugin {
|
||||
public function getName(): string
|
||||
{
|
||||
return 'test-plugin';
|
||||
}
|
||||
};
|
||||
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addPlugin($plugin)
|
||||
->buildWithPlugins();
|
||||
|
||||
$this->assertInstanceOf(PluginAwareProcessor::class, $processor);
|
||||
$processor->setAuditLogger($auditLogger);
|
||||
|
||||
// Audit logger should be set on underlying processor
|
||||
$this->assertTrue(true); // No exception means it worked
|
||||
}
|
||||
|
||||
public function testMultiplePluginsProcessInPriorityOrder(): void
|
||||
{
|
||||
// Test that pre-processing runs in priority order (lower number first)
|
||||
$plugin1 = new class extends AbstractMaskingPlugin {
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(20); // Lower priority (runs second)
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'plugin1';
|
||||
}
|
||||
|
||||
public function preProcessMessage(string $message): string
|
||||
{
|
||||
return $message . '-plugin1';
|
||||
}
|
||||
};
|
||||
|
||||
$plugin2 = new class extends AbstractMaskingPlugin {
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(10); // Higher priority (runs first)
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'plugin2';
|
||||
}
|
||||
|
||||
public function preProcessMessage(string $message): string
|
||||
{
|
||||
return $message . '-plugin2';
|
||||
}
|
||||
};
|
||||
|
||||
$processor = GdprProcessorBuilder::create()
|
||||
->addPlugin($plugin1)
|
||||
->addPlugin($plugin2)
|
||||
->buildWithPlugins();
|
||||
|
||||
$record = new LogRecord(
|
||||
datetime: new \DateTimeImmutable(),
|
||||
channel: TestConstants::CHANNEL_TEST,
|
||||
level: Level::Info,
|
||||
message: 'base',
|
||||
context: []
|
||||
);
|
||||
|
||||
$result = $processor($record);
|
||||
|
||||
// plugin2 (priority 10) runs first in pre-processing, then plugin1 (priority 20)
|
||||
$this->assertSame('base-plugin2-plugin1', $result->message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user