mirror of
https://github.com/ivuorinen/monolog-gdpr-filter.git
synced 2026-02-12 18:49:54 +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:
249
tests/Streaming/StreamingProcessorTest.php
Normal file
249
tests/Streaming/StreamingProcessorTest.php
Normal file
@@ -0,0 +1,249 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Streaming;
|
||||
|
||||
use Ivuorinen\MonologGdprFilter\Exceptions\StreamingOperationFailedException;
|
||||
use Ivuorinen\MonologGdprFilter\MaskConstants;
|
||||
use Ivuorinen\MonologGdprFilter\MaskingOrchestrator;
|
||||
use Ivuorinen\MonologGdprFilter\Streaming\StreamingProcessor;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tests\TestConstants;
|
||||
|
||||
#[CoversClass(StreamingProcessor::class)]
|
||||
final class StreamingProcessorTest extends TestCase
|
||||
{
|
||||
private function createOrchestrator(): MaskingOrchestrator
|
||||
{
|
||||
return new MaskingOrchestrator([TestConstants::PATTERN_TEST => MaskConstants::MASK_GENERIC]);
|
||||
}
|
||||
|
||||
public function testProcessStreamSingleRecord(): void
|
||||
{
|
||||
$processor = new StreamingProcessor($this->createOrchestrator(), 10);
|
||||
|
||||
$records = [
|
||||
['message' => 'test message', 'context' => []],
|
||||
];
|
||||
|
||||
$results = iterator_to_array($processor->processStream($records));
|
||||
|
||||
$this->assertCount(1, $results);
|
||||
$this->assertSame(MaskConstants::MASK_GENERIC . ' message', $results[0]['message']);
|
||||
}
|
||||
|
||||
public function testProcessStreamMultipleRecords(): void
|
||||
{
|
||||
$processor = new StreamingProcessor($this->createOrchestrator(), 10);
|
||||
|
||||
$records = [
|
||||
['message' => 'test one', 'context' => []],
|
||||
['message' => 'test two', 'context' => []],
|
||||
['message' => 'test three', 'context' => []],
|
||||
];
|
||||
|
||||
$results = iterator_to_array($processor->processStream($records));
|
||||
|
||||
$this->assertCount(3, $results);
|
||||
}
|
||||
|
||||
public function testProcessStreamChunking(): void
|
||||
{
|
||||
$processor = new StreamingProcessor($this->createOrchestrator(), 2);
|
||||
|
||||
$records = [
|
||||
['message' => 'test 1', 'context' => []],
|
||||
['message' => 'test 2', 'context' => []],
|
||||
['message' => 'test 3', 'context' => []],
|
||||
['message' => 'test 4', 'context' => []],
|
||||
['message' => 'test 5', 'context' => []],
|
||||
];
|
||||
|
||||
$results = iterator_to_array($processor->processStream($records));
|
||||
|
||||
$this->assertCount(5, $results);
|
||||
}
|
||||
|
||||
public function testProcessStreamWithContext(): void
|
||||
{
|
||||
$processor = new StreamingProcessor($this->createOrchestrator(), 10);
|
||||
|
||||
$records = [
|
||||
['message' => 'message', 'context' => ['key' => 'test value']],
|
||||
];
|
||||
|
||||
$results = iterator_to_array($processor->processStream($records));
|
||||
|
||||
$this->assertSame(MaskConstants::MASK_GENERIC . ' value', $results[0]['context']['key']);
|
||||
}
|
||||
|
||||
public function testProcessStreamWithGenerator(): void
|
||||
{
|
||||
$processor = new StreamingProcessor($this->createOrchestrator(), 2);
|
||||
|
||||
$generator = (function () {
|
||||
yield ['message' => 'test a', 'context' => []];
|
||||
yield ['message' => 'test b', 'context' => []];
|
||||
yield ['message' => 'test c', 'context' => []];
|
||||
})();
|
||||
|
||||
$results = iterator_to_array($processor->processStream($generator));
|
||||
|
||||
$this->assertCount(3, $results);
|
||||
}
|
||||
|
||||
public function testProcessFileWithTempFile(): void
|
||||
{
|
||||
$processor = new StreamingProcessor($this->createOrchestrator(), 2);
|
||||
|
||||
// Create temp file with test data containing 'test' to be masked
|
||||
$tempFile = tempnam(sys_get_temp_dir(), 'gdpr_test_');
|
||||
$this->assertIsString($tempFile, 'Failed to create temp file');
|
||||
file_put_contents($tempFile, "test line 1\ntest line 2\ntest line 3\n");
|
||||
|
||||
try {
|
||||
$lineParser = fn(string $line): array => ['message' => $line, 'context' => []];
|
||||
|
||||
$results = [];
|
||||
foreach ($processor->processFile($tempFile, $lineParser) as $result) {
|
||||
$results[] = $result;
|
||||
}
|
||||
|
||||
$this->assertCount(3, $results);
|
||||
$this->assertStringContainsString(MaskConstants::MASK_GENERIC, $results[0]['message']);
|
||||
} finally {
|
||||
unlink($tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
public function testProcessFileSkipsEmptyLines(): void
|
||||
{
|
||||
$processor = new StreamingProcessor($this->createOrchestrator(), 10);
|
||||
|
||||
$tempFile = tempnam(sys_get_temp_dir(), 'gdpr_test_');
|
||||
$this->assertIsString($tempFile, 'Failed to create temp file');
|
||||
file_put_contents($tempFile, "test line 1\n\n\ntest line 2\n");
|
||||
|
||||
try {
|
||||
$lineParser = fn(string $line): array => ['message' => $line, 'context' => []];
|
||||
|
||||
$results = iterator_to_array($processor->processFile($tempFile, $lineParser));
|
||||
|
||||
$this->assertCount(2, $results);
|
||||
} finally {
|
||||
unlink($tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
public function testProcessFileThrowsOnInvalidPath(): void
|
||||
{
|
||||
$processor = new StreamingProcessor($this->createOrchestrator(), 10);
|
||||
|
||||
$this->expectException(StreamingOperationFailedException::class);
|
||||
$this->expectExceptionMessage('Cannot open input file for streaming:');
|
||||
|
||||
iterator_to_array($processor->processFile('/nonexistent/path/file.log', fn(string $l): array => ['message' => $l, 'context' => []]));
|
||||
}
|
||||
|
||||
public function testProcessToFile(): void
|
||||
{
|
||||
$processor = new StreamingProcessor($this->createOrchestrator(), 10);
|
||||
|
||||
$records = [
|
||||
['message' => 'test line 1', 'context' => []],
|
||||
['message' => 'test line 2', 'context' => []],
|
||||
];
|
||||
|
||||
$outputFile = tempnam(sys_get_temp_dir(), 'gdpr_output_');
|
||||
$this->assertIsString($outputFile, 'Failed to create temp file');
|
||||
|
||||
try {
|
||||
$formatter = fn(array $record): string => $record['message'];
|
||||
$count = $processor->processToFile($records, $outputFile, $formatter);
|
||||
|
||||
$this->assertSame(2, $count);
|
||||
|
||||
$output = file_get_contents($outputFile);
|
||||
$this->assertNotFalse($output);
|
||||
$this->assertStringContainsString(MaskConstants::MASK_GENERIC, $output);
|
||||
} finally {
|
||||
unlink($outputFile);
|
||||
}
|
||||
}
|
||||
|
||||
public function testProcessToFileThrowsOnInvalidPath(): void
|
||||
{
|
||||
$processor = new StreamingProcessor($this->createOrchestrator(), 10);
|
||||
|
||||
$this->expectException(StreamingOperationFailedException::class);
|
||||
$this->expectExceptionMessage('Cannot open output file for streaming:');
|
||||
|
||||
$processor->processToFile([], '/nonexistent/path/output.log', fn(array $r): string => '');
|
||||
}
|
||||
|
||||
public function testGetStatistics(): void
|
||||
{
|
||||
$processor = new StreamingProcessor($this->createOrchestrator(), 10);
|
||||
|
||||
$records = [
|
||||
['message' => 'test masked', 'context' => []],
|
||||
['message' => 'no sensitive data', 'context' => []],
|
||||
['message' => 'another test here', 'context' => []],
|
||||
];
|
||||
|
||||
$stats = $processor->getStatistics($records);
|
||||
|
||||
$this->assertSame(3, $stats['processed']);
|
||||
$this->assertGreaterThan(0, $stats['masked']); // At least some should be masked
|
||||
}
|
||||
|
||||
public function testSetAuditLogger(): void
|
||||
{
|
||||
$logs = [];
|
||||
$auditLogger = function (string $path) use (&$logs): void {
|
||||
$logs[] = ['path' => $path];
|
||||
};
|
||||
|
||||
$processor = new StreamingProcessor($this->createOrchestrator(), 1);
|
||||
$processor->setAuditLogger($auditLogger);
|
||||
|
||||
$records = [['message' => 'test', 'context' => []]];
|
||||
iterator_to_array($processor->processStream($records));
|
||||
|
||||
$this->assertNotEmpty($logs);
|
||||
}
|
||||
|
||||
public function testGetChunkSize(): void
|
||||
{
|
||||
$processor = new StreamingProcessor($this->createOrchestrator(), 500);
|
||||
|
||||
$this->assertSame(500, $processor->getChunkSize());
|
||||
}
|
||||
|
||||
public function testDefaultChunkSize(): void
|
||||
{
|
||||
$processor = new StreamingProcessor($this->createOrchestrator());
|
||||
|
||||
$this->assertSame(1000, $processor->getChunkSize());
|
||||
}
|
||||
|
||||
public function testLargeDataSet(): void
|
||||
{
|
||||
$processor = new StreamingProcessor($this->createOrchestrator(), 100);
|
||||
|
||||
$records = [];
|
||||
for ($i = 1; $i <= 500; $i++) {
|
||||
$records[] = ['message' => "test record {$i}", 'context' => []];
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
foreach ($processor->processStream($records) as $record) {
|
||||
$count++;
|
||||
$this->assertIsArray($record);
|
||||
}
|
||||
|
||||
$this->assertSame(500, $count);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user