mirror of
https://github.com/ivuorinen/monolog-gdpr-filter.git
synced 2026-02-23 19:53:53 +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:
190
tests/Audit/AuditContextTest.php
Normal file
190
tests/Audit/AuditContextTest.php
Normal file
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Audit;
|
||||
|
||||
use Ivuorinen\MonologGdprFilter\Audit\AuditContext;
|
||||
use Ivuorinen\MonologGdprFilter\Audit\ErrorContext;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests for AuditContext value object.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
final class AuditContextTest extends TestCase
|
||||
{
|
||||
public function testSuccessCreation(): void
|
||||
{
|
||||
$context = AuditContext::success(
|
||||
AuditContext::OP_REGEX,
|
||||
12.5,
|
||||
['key' => 'value']
|
||||
);
|
||||
|
||||
$this->assertSame(AuditContext::OP_REGEX, $context->operationType);
|
||||
$this->assertSame(AuditContext::STATUS_SUCCESS, $context->status);
|
||||
$this->assertSame(1, $context->attemptNumber);
|
||||
$this->assertSame(12.5, $context->durationMs);
|
||||
$this->assertNull($context->error);
|
||||
$this->assertSame(['key' => 'value'], $context->metadata);
|
||||
}
|
||||
|
||||
public function testFailedCreation(): void
|
||||
{
|
||||
$error = ErrorContext::create('TestError', 'Something went wrong');
|
||||
$context = AuditContext::failed(
|
||||
AuditContext::OP_FIELD_PATH,
|
||||
$error,
|
||||
3,
|
||||
50.0,
|
||||
['retry' => true]
|
||||
);
|
||||
|
||||
$this->assertSame(AuditContext::OP_FIELD_PATH, $context->operationType);
|
||||
$this->assertSame(AuditContext::STATUS_FAILED, $context->status);
|
||||
$this->assertSame(3, $context->attemptNumber);
|
||||
$this->assertSame(50.0, $context->durationMs);
|
||||
$this->assertSame($error, $context->error);
|
||||
$this->assertArrayHasKey('retry', $context->metadata);
|
||||
}
|
||||
|
||||
public function testRecoveredCreation(): void
|
||||
{
|
||||
$context = AuditContext::recovered(
|
||||
AuditContext::OP_CALLBACK,
|
||||
2,
|
||||
25.0
|
||||
);
|
||||
|
||||
$this->assertSame(AuditContext::OP_CALLBACK, $context->operationType);
|
||||
$this->assertSame(AuditContext::STATUS_RECOVERED, $context->status);
|
||||
$this->assertSame(2, $context->attemptNumber);
|
||||
$this->assertSame(25.0, $context->durationMs);
|
||||
}
|
||||
|
||||
public function testSkippedCreation(): void
|
||||
{
|
||||
$context = AuditContext::skipped(
|
||||
AuditContext::OP_CONDITIONAL,
|
||||
'Condition not met'
|
||||
);
|
||||
|
||||
$this->assertSame(AuditContext::OP_CONDITIONAL, $context->operationType);
|
||||
$this->assertSame(AuditContext::STATUS_SKIPPED, $context->status);
|
||||
$this->assertArrayHasKey('skip_reason', $context->metadata);
|
||||
$this->assertSame('Condition not met', $context->metadata['skip_reason']);
|
||||
}
|
||||
|
||||
public function testWithCorrelationId(): void
|
||||
{
|
||||
$context = AuditContext::success(AuditContext::OP_REGEX);
|
||||
$this->assertNull($context->correlationId);
|
||||
|
||||
$withId = $context->withCorrelationId('abc123');
|
||||
|
||||
$this->assertNull($context->correlationId);
|
||||
$this->assertSame('abc123', $withId->correlationId);
|
||||
$this->assertSame($context->operationType, $withId->operationType);
|
||||
$this->assertSame($context->status, $withId->status);
|
||||
}
|
||||
|
||||
public function testWithMetadata(): void
|
||||
{
|
||||
$context = AuditContext::success(
|
||||
AuditContext::OP_REGEX,
|
||||
0.0,
|
||||
['original' => 'value']
|
||||
);
|
||||
|
||||
$withMeta = $context->withMetadata(['added' => 'new']);
|
||||
|
||||
$this->assertArrayHasKey('original', $withMeta->metadata);
|
||||
$this->assertArrayHasKey('added', $withMeta->metadata);
|
||||
$this->assertSame('value', $withMeta->metadata['original']);
|
||||
$this->assertSame('new', $withMeta->metadata['added']);
|
||||
}
|
||||
|
||||
public function testIsSuccess(): void
|
||||
{
|
||||
$success = AuditContext::success(AuditContext::OP_REGEX);
|
||||
$recovered = AuditContext::recovered(AuditContext::OP_REGEX, 2);
|
||||
$error = ErrorContext::create('Error', 'msg');
|
||||
$failed = AuditContext::failed(AuditContext::OP_REGEX, $error);
|
||||
$skipped = AuditContext::skipped(AuditContext::OP_REGEX, 'reason');
|
||||
|
||||
$this->assertTrue($success->isSuccess());
|
||||
$this->assertTrue($recovered->isSuccess());
|
||||
$this->assertFalse($failed->isSuccess());
|
||||
$this->assertFalse($skipped->isSuccess());
|
||||
}
|
||||
|
||||
public function testToArray(): void
|
||||
{
|
||||
$context = AuditContext::success(
|
||||
AuditContext::OP_REGEX,
|
||||
15.123456,
|
||||
['key' => 'value']
|
||||
);
|
||||
|
||||
$array = $context->toArray();
|
||||
|
||||
$this->assertArrayHasKey('operation_type', $array);
|
||||
$this->assertArrayHasKey('status', $array);
|
||||
$this->assertArrayHasKey('attempt_number', $array);
|
||||
$this->assertArrayHasKey('duration_ms', $array);
|
||||
$this->assertArrayHasKey('metadata', $array);
|
||||
$this->assertSame(15.123, $array['duration_ms']);
|
||||
}
|
||||
|
||||
public function testToArrayWithError(): void
|
||||
{
|
||||
$error = ErrorContext::create('TestError', 'Message');
|
||||
$context = AuditContext::failed(AuditContext::OP_REGEX, $error);
|
||||
|
||||
$array = $context->toArray();
|
||||
|
||||
$this->assertArrayHasKey('error', $array);
|
||||
$this->assertIsArray($array['error']);
|
||||
}
|
||||
|
||||
public function testToArrayWithCorrelationId(): void
|
||||
{
|
||||
$context = AuditContext::success(AuditContext::OP_REGEX)
|
||||
->withCorrelationId('test-id');
|
||||
|
||||
$array = $context->toArray();
|
||||
|
||||
$this->assertArrayHasKey('correlation_id', $array);
|
||||
$this->assertSame('test-id', $array['correlation_id']);
|
||||
}
|
||||
|
||||
public function testGenerateCorrelationId(): void
|
||||
{
|
||||
$id1 = AuditContext::generateCorrelationId();
|
||||
$id2 = AuditContext::generateCorrelationId();
|
||||
|
||||
$this->assertIsString($id1);
|
||||
$this->assertSame(16, strlen($id1));
|
||||
$this->assertNotSame($id1, $id2);
|
||||
}
|
||||
|
||||
public function testOperationTypeConstants(): void
|
||||
{
|
||||
$this->assertSame('regex', AuditContext::OP_REGEX);
|
||||
$this->assertSame('field_path', AuditContext::OP_FIELD_PATH);
|
||||
$this->assertSame('callback', AuditContext::OP_CALLBACK);
|
||||
$this->assertSame('data_type', AuditContext::OP_DATA_TYPE);
|
||||
$this->assertSame('json', AuditContext::OP_JSON);
|
||||
$this->assertSame('conditional', AuditContext::OP_CONDITIONAL);
|
||||
}
|
||||
|
||||
public function testStatusConstants(): void
|
||||
{
|
||||
$this->assertSame('success', AuditContext::STATUS_SUCCESS);
|
||||
$this->assertSame('failed', AuditContext::STATUS_FAILED);
|
||||
$this->assertSame('recovered', AuditContext::STATUS_RECOVERED);
|
||||
$this->assertSame('skipped', AuditContext::STATUS_SKIPPED);
|
||||
}
|
||||
}
|
||||
194
tests/Audit/ErrorContextTest.php
Normal file
194
tests/Audit/ErrorContextTest.php
Normal file
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Audit;
|
||||
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use Ivuorinen\MonologGdprFilter\Audit\ErrorContext;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RuntimeException;
|
||||
use Tests\TestConstants;
|
||||
|
||||
/**
|
||||
* Tests for ErrorContext value object.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
final class ErrorContextTest extends TestCase
|
||||
{
|
||||
public function testBasicConstruction(): void
|
||||
{
|
||||
$context = new ErrorContext(
|
||||
errorType: 'TestError',
|
||||
message: 'Something went wrong',
|
||||
code: 42,
|
||||
file: '/path/to/file.php',
|
||||
line: 123,
|
||||
metadata: ['key' => 'value']
|
||||
);
|
||||
|
||||
$this->assertSame('TestError', $context->errorType);
|
||||
$this->assertSame('Something went wrong', $context->message);
|
||||
$this->assertSame(42, $context->code);
|
||||
$this->assertSame('/path/to/file.php', $context->file);
|
||||
$this->assertSame(123, $context->line);
|
||||
$this->assertSame(['key' => 'value'], $context->metadata);
|
||||
}
|
||||
|
||||
public function testFromThrowable(): void
|
||||
{
|
||||
$exception = new RuntimeException('Test exception', 500);
|
||||
$context = ErrorContext::fromThrowable($exception);
|
||||
|
||||
$this->assertSame(RuntimeException::class, $context->errorType);
|
||||
$this->assertSame('Test exception', $context->message);
|
||||
$this->assertSame(500, $context->code);
|
||||
$this->assertNull($context->file);
|
||||
$this->assertNull($context->line);
|
||||
}
|
||||
|
||||
public function testFromThrowableWithSensitiveDetails(): void
|
||||
{
|
||||
$exception = new Exception('Error at /home/user/app');
|
||||
$context = ErrorContext::fromThrowable($exception, includeSensitive: true);
|
||||
|
||||
$this->assertNotNull($context->file);
|
||||
$this->assertNotNull($context->line);
|
||||
$this->assertArrayHasKey('trace', $context->metadata);
|
||||
}
|
||||
|
||||
public function testCreate(): void
|
||||
{
|
||||
$context = ErrorContext::create(
|
||||
'CustomError',
|
||||
'Error message',
|
||||
['detail' => 'info']
|
||||
);
|
||||
|
||||
$this->assertSame('CustomError', $context->errorType);
|
||||
$this->assertSame('Error message', $context->message);
|
||||
$this->assertSame(0, $context->code);
|
||||
$this->assertArrayHasKey('detail', $context->metadata);
|
||||
}
|
||||
|
||||
public function testSanitizesPasswordsInMessage(): void
|
||||
{
|
||||
$message = 'Connection failed: password=secret123';
|
||||
$context = ErrorContext::create('DbError', $message);
|
||||
|
||||
$this->assertStringNotContainsString('secret123', $context->message);
|
||||
$this->assertStringContainsString(TestConstants::MASK_REDACTED_BRACKETS, $context->message);
|
||||
}
|
||||
|
||||
public function testSanitizesApiKeysInMessage(): void
|
||||
{
|
||||
$message = 'Auth failed: api_key=sk_live_1234567890';
|
||||
$context = ErrorContext::create('ApiError', $message);
|
||||
|
||||
$this->assertStringNotContainsString('sk_live_1234567890', $context->message);
|
||||
$this->assertStringContainsString(TestConstants::MASK_REDACTED_BRACKETS, $context->message);
|
||||
}
|
||||
|
||||
public function testSanitizesTokensInMessage(): void
|
||||
{
|
||||
$message = 'Auth failed with bearer abc123def456';
|
||||
$context = ErrorContext::create('AuthError', $message);
|
||||
|
||||
$this->assertStringNotContainsString('abc123def456', $context->message);
|
||||
$this->assertStringContainsString(TestConstants::MASK_REDACTED_BRACKETS, $context->message);
|
||||
}
|
||||
|
||||
public function testSanitizesTokenValueInMessage(): void
|
||||
{
|
||||
$message = 'Invalid token=secret_value_here';
|
||||
$context = ErrorContext::create('AuthError', $message);
|
||||
|
||||
$this->assertStringNotContainsString('secret_value_here', $context->message);
|
||||
$this->assertStringContainsString(TestConstants::MASK_REDACTED_BRACKETS, $context->message);
|
||||
}
|
||||
|
||||
public function testSanitizesConnectionStrings(): void
|
||||
{
|
||||
$message = 'Failed: redis://admin:password@localhost:6379';
|
||||
$context = ErrorContext::create('ConnError', $message);
|
||||
|
||||
$this->assertStringNotContainsString('password', $context->message);
|
||||
$this->assertStringContainsString(TestConstants::MASK_REDACTED_BRACKETS, $context->message);
|
||||
}
|
||||
|
||||
public function testSanitizesUserCredentials(): void
|
||||
{
|
||||
$message = 'DB error: user=admin host=secret.internal.com';
|
||||
$context = ErrorContext::create('DbError', $message);
|
||||
|
||||
$this->assertStringNotContainsString('admin', $context->message);
|
||||
$this->assertStringNotContainsString('secret.internal.com', $context->message);
|
||||
$this->assertStringContainsString(TestConstants::MASK_REDACTED_BRACKETS, $context->message);
|
||||
}
|
||||
|
||||
public function testSanitizesFilePaths(): void
|
||||
{
|
||||
$message = 'Cannot read /var/www/secret-app/config/credentials.php';
|
||||
$context = ErrorContext::create('FileError', $message);
|
||||
|
||||
$this->assertStringNotContainsString('/var/www/secret-app', $context->message);
|
||||
$this->assertStringContainsString('[PATH_REDACTED]', $context->message);
|
||||
}
|
||||
|
||||
public function testToArray(): void
|
||||
{
|
||||
$context = new ErrorContext(
|
||||
errorType: 'TestError',
|
||||
message: 'Test message',
|
||||
code: 100,
|
||||
file: '/test/file.php',
|
||||
line: 50,
|
||||
metadata: ['key' => 'value']
|
||||
);
|
||||
|
||||
$array = $context->toArray();
|
||||
|
||||
$this->assertArrayHasKey('error_type', $array);
|
||||
$this->assertArrayHasKey('message', $array);
|
||||
$this->assertArrayHasKey('code', $array);
|
||||
$this->assertArrayHasKey('file', $array);
|
||||
$this->assertArrayHasKey('line', $array);
|
||||
$this->assertArrayHasKey('metadata', $array);
|
||||
|
||||
$this->assertSame('TestError', $array['error_type']);
|
||||
$this->assertSame('Test message', $array['message']);
|
||||
$this->assertSame(100, $array['code']);
|
||||
}
|
||||
|
||||
public function testToArrayOmitsNullValues(): void
|
||||
{
|
||||
$context = ErrorContext::create('Error', 'Message');
|
||||
|
||||
$array = $context->toArray();
|
||||
|
||||
$this->assertArrayNotHasKey('file', $array);
|
||||
$this->assertArrayNotHasKey('line', $array);
|
||||
}
|
||||
|
||||
public function testToArrayOmitsEmptyMetadata(): void
|
||||
{
|
||||
$context = ErrorContext::create('Error', 'Message', []);
|
||||
|
||||
$array = $context->toArray();
|
||||
|
||||
$this->assertArrayNotHasKey('metadata', $array);
|
||||
}
|
||||
|
||||
public function testFromThrowableWithNestedException(): void
|
||||
{
|
||||
$inner = new InvalidArgumentException('Inner error');
|
||||
$outer = new RuntimeException('Outer error', 0, $inner);
|
||||
|
||||
$context = ErrorContext::fromThrowable($outer);
|
||||
|
||||
$this->assertSame(RuntimeException::class, $context->errorType);
|
||||
$this->assertStringContainsString('Outer error', $context->message);
|
||||
}
|
||||
}
|
||||
207
tests/Audit/StructuredAuditLoggerTest.php
Normal file
207
tests/Audit/StructuredAuditLoggerTest.php
Normal file
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Audit;
|
||||
|
||||
use Ivuorinen\MonologGdprFilter\Audit\AuditContext;
|
||||
use Ivuorinen\MonologGdprFilter\Audit\ErrorContext;
|
||||
use Ivuorinen\MonologGdprFilter\Audit\StructuredAuditLogger;
|
||||
use Ivuorinen\MonologGdprFilter\RateLimitedAuditLogger;
|
||||
use Ivuorinen\MonologGdprFilter\RateLimiter;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tests\TestConstants;
|
||||
|
||||
/**
|
||||
* Tests for StructuredAuditLogger.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
final class StructuredAuditLoggerTest extends TestCase
|
||||
{
|
||||
/** @var array<array{path: string, original: mixed, masked: mixed}> */
|
||||
private array $logs;
|
||||
|
||||
#[\Override]
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->logs = [];
|
||||
RateLimiter::clearAll();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function tearDown(): void
|
||||
{
|
||||
RateLimiter::clearAll();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
private function createBaseLogger(): callable
|
||||
{
|
||||
return function (string $path, mixed $original, mixed $masked): void {
|
||||
$this->logs[] = [
|
||||
'path' => $path,
|
||||
'original' => $original,
|
||||
'masked' => $masked
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
public function testBasicLogging(): void
|
||||
{
|
||||
$logger = new StructuredAuditLogger($this->createBaseLogger());
|
||||
|
||||
$logger->log('user.email', TestConstants::EMAIL_JOHN, TestConstants::MASK_MASKED_BRACKETS);
|
||||
|
||||
$this->assertCount(1, $this->logs);
|
||||
$this->assertSame('user.email', $this->logs[0]['path']);
|
||||
$this->assertSame(TestConstants::EMAIL_JOHN, $this->logs[0]['original']);
|
||||
$this->assertSame(TestConstants::MASK_MASKED_BRACKETS, $this->logs[0]['masked']);
|
||||
}
|
||||
|
||||
public function testLogWithContext(): void
|
||||
{
|
||||
$logger = new StructuredAuditLogger($this->createBaseLogger());
|
||||
$context = AuditContext::success(AuditContext::OP_REGEX, 5.0);
|
||||
|
||||
$logger->log('user.email', TestConstants::EMAIL_JOHN, TestConstants::MASK_MASKED_BRACKETS, $context);
|
||||
|
||||
$this->assertCount(1, $this->logs);
|
||||
}
|
||||
|
||||
public function testLogSuccess(): void
|
||||
{
|
||||
$logger = new StructuredAuditLogger($this->createBaseLogger());
|
||||
|
||||
$logger->logSuccess(
|
||||
'user.ssn',
|
||||
'123-45-6789',
|
||||
'[SSN]',
|
||||
AuditContext::OP_REGEX,
|
||||
10.5
|
||||
);
|
||||
|
||||
$this->assertCount(1, $this->logs);
|
||||
$this->assertSame('user.ssn', $this->logs[0]['path']);
|
||||
}
|
||||
|
||||
public function testLogFailure(): void
|
||||
{
|
||||
$logger = new StructuredAuditLogger($this->createBaseLogger());
|
||||
$error = ErrorContext::create('RegexError', 'Pattern failed');
|
||||
|
||||
$logger->logFailure(
|
||||
'user.data',
|
||||
'sensitive value',
|
||||
AuditContext::OP_REGEX,
|
||||
$error
|
||||
);
|
||||
|
||||
$this->assertCount(1, $this->logs);
|
||||
$this->assertSame('[MASKING_FAILED]', $this->logs[0]['masked']);
|
||||
}
|
||||
|
||||
public function testLogRecovery(): void
|
||||
{
|
||||
$logger = new StructuredAuditLogger($this->createBaseLogger());
|
||||
|
||||
$logger->logRecovery(
|
||||
'user.email',
|
||||
TestConstants::EMAIL_JOHN,
|
||||
TestConstants::MASK_MASKED_BRACKETS,
|
||||
AuditContext::OP_REGEX,
|
||||
2,
|
||||
25.0
|
||||
);
|
||||
|
||||
$this->assertCount(1, $this->logs);
|
||||
}
|
||||
|
||||
public function testLogSkipped(): void
|
||||
{
|
||||
$logger = new StructuredAuditLogger($this->createBaseLogger());
|
||||
|
||||
$logger->logSkipped(
|
||||
'user.public_name',
|
||||
TestConstants::NAME_FULL,
|
||||
AuditContext::OP_CONDITIONAL,
|
||||
'Field not in mask list'
|
||||
);
|
||||
|
||||
$this->assertCount(1, $this->logs);
|
||||
$this->assertSame(TestConstants::NAME_FULL, $this->logs[0]['original']);
|
||||
$this->assertSame(TestConstants::NAME_FULL, $this->logs[0]['masked']);
|
||||
}
|
||||
|
||||
public function testWrapStaticFactory(): void
|
||||
{
|
||||
$logger = StructuredAuditLogger::wrap($this->createBaseLogger());
|
||||
|
||||
$logger->log('test.path', 'original', 'masked');
|
||||
|
||||
$this->assertCount(1, $this->logs);
|
||||
}
|
||||
|
||||
public function testWithRateLimitedLogger(): void
|
||||
{
|
||||
$rateLimited = new RateLimitedAuditLogger(
|
||||
$this->createBaseLogger(),
|
||||
100,
|
||||
60
|
||||
);
|
||||
$logger = new StructuredAuditLogger($rateLimited);
|
||||
|
||||
$logger->log('user.email', TestConstants::EMAIL_JOHN, TestConstants::MASK_MASKED_BRACKETS);
|
||||
|
||||
$this->assertCount(1, $this->logs);
|
||||
}
|
||||
|
||||
public function testTimerMethods(): void
|
||||
{
|
||||
$logger = new StructuredAuditLogger($this->createBaseLogger());
|
||||
|
||||
$start = $logger->startTimer();
|
||||
usleep(10000);
|
||||
$elapsed = $logger->elapsed($start);
|
||||
|
||||
$this->assertGreaterThan(0, $elapsed);
|
||||
$this->assertLessThan(100, $elapsed);
|
||||
}
|
||||
|
||||
public function testGetWrappedLogger(): void
|
||||
{
|
||||
$baseLogger = $this->createBaseLogger();
|
||||
$logger = new StructuredAuditLogger($baseLogger);
|
||||
|
||||
$wrapped = $logger->getWrappedLogger();
|
||||
|
||||
// Verify the wrapped logger works by calling it
|
||||
$wrapped('test.path', 'original', 'masked');
|
||||
$this->assertCount(1, $this->logs);
|
||||
}
|
||||
|
||||
public function testDisableTimestamp(): void
|
||||
{
|
||||
$logger = new StructuredAuditLogger(
|
||||
$this->createBaseLogger(),
|
||||
includeTimestamp: false
|
||||
);
|
||||
|
||||
$logger->log('test', 'original', 'masked');
|
||||
|
||||
$this->assertCount(1, $this->logs);
|
||||
}
|
||||
|
||||
public function testDisableDuration(): void
|
||||
{
|
||||
$logger = new StructuredAuditLogger(
|
||||
$this->createBaseLogger(),
|
||||
includeDuration: false
|
||||
);
|
||||
|
||||
$logger->log('test', 'original', 'masked');
|
||||
|
||||
$this->assertCount(1, $this->logs);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user