mirror of
https://github.com/ivuorinen/monolog-gdpr-filter.git
synced 2026-01-30 19:43:50 +00:00
* feat: performance, integrations, advanced features * chore: fix linting problems * chore: suppressions and linting * chore(lint): pre-commit linting, fixes * feat: comprehensive input validation, security hardening, and regression testing - Add extensive input validation throughout codebase with proper error handling - Implement comprehensive security hardening with ReDoS protection and bounds checking - Add 3 new regression test suites covering critical bugs, security, and validation scenarios - Enhance rate limiting with memory management and configurable cleanup intervals - Update configuration security settings and improve Laravel integration - Fix TODO.md timestamps to reflect actual development timeline - Strengthen static analysis configuration and improve code quality standards * feat: configure static analysis tools and enhance development workflow - Complete configuration of Psalm, PHPStan, and Rector for harmonious static analysis. - Fix invalid configurations and tool conflicts that prevented proper code quality analysis. - Add comprehensive safe analysis script with interactive workflow, backup/restore capabilities, and dry-run modes. Update documentation with linting policy requiring issue resolution over suppression. - Clean completed items from TODO to focus on actionable improvements. - All static analysis tools now work together seamlessly to provide code quality insights without breaking existing functionality. * fix(test): update Invalid regex pattern expectation * chore: phpstan, psalm fixes * chore: phpstan, psalm fixes, more tests * chore: tooling tweaks, cleanup * chore: tweaks to get the tests pass * fix(lint): rector config tweaks and successful run * feat: refactoring, more tests, fixes, cleanup * chore: deduplication, use constants * chore: psalm fixes * chore: ignore phpstan deliberate errors in tests * chore: improve codebase, deduplicate code * fix: lint * chore: deduplication, codebase simplification, sonarqube fixes * fix: resolve SonarQube reliability rating issues Fix useless object instantiation warnings in test files by assigning instantiated objects to variables. This resolves the SonarQube reliability rating issue (was C, now targeting A). Changes: - tests/Strategies/MaskingStrategiesTest.php: Fix 3 instances - tests/Strategies/FieldPathMaskingStrategyTest.php: Fix 1 instance The tests use expectException() to verify that constructors throw exceptions for invalid input. SonarQube flagged standalone `new` statements as useless. Fixed by assigning to variables with explicit unset() and fail() calls. All tests pass (623/623) and static analysis tools pass. * fix: resolve more SonarQube detected issues * fix: resolve psalm detected issues * fix: resolve more SonarQube detected issues * fix: resolve psalm detected issues * fix: duplications * fix: resolve SonarQube reliability rating issues * fix: resolve psalm and phpstan detected issues
226 lines
7.0 KiB
PHP
226 lines
7.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests;
|
|
|
|
use Ivuorinen\MonologGdprFilter\Exceptions\InvalidRateLimitConfigurationException;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Ivuorinen\MonologGdprFilter\RateLimiter;
|
|
|
|
/**
|
|
* Test rate limiting functionality.
|
|
* @api
|
|
*/
|
|
class RateLimiterTest extends TestCase
|
|
{
|
|
#[\Override]
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
// Clear rate limiter state before each test
|
|
RateLimiter::clearAll();
|
|
}
|
|
|
|
#[\Override]
|
|
protected function tearDown(): void
|
|
{
|
|
// Clean up after each test
|
|
RateLimiter::clearAll();
|
|
parent::tearDown();
|
|
}
|
|
|
|
public function testBasicRateLimiting(): void
|
|
{
|
|
$rateLimiter = new RateLimiter(3, 60); // 3 requests per 60 seconds
|
|
$key = 'test_key';
|
|
|
|
// First 3 requests should be allowed
|
|
$this->assertTrue($rateLimiter->isAllowed($key));
|
|
$this->assertTrue($rateLimiter->isAllowed($key));
|
|
$this->assertTrue($rateLimiter->isAllowed($key));
|
|
|
|
// 4th request should be denied
|
|
$this->assertFalse($rateLimiter->isAllowed($key));
|
|
$this->assertFalse($rateLimiter->isAllowed($key));
|
|
}
|
|
|
|
public function testRemainingRequests(): void
|
|
{
|
|
$rateLimiter = new RateLimiter(5, 60);
|
|
$key = 'test_key';
|
|
|
|
$this->assertSame(5, $rateLimiter->getRemainingRequests($key));
|
|
|
|
$rateLimiter->isAllowed($key); // Use 1 request
|
|
$this->assertSame(4, $rateLimiter->getRemainingRequests($key));
|
|
|
|
$rateLimiter->isAllowed($key); // Use another request
|
|
$this->assertSame(3, $rateLimiter->getRemainingRequests($key));
|
|
}
|
|
|
|
public function testSlidingWindow(): void
|
|
{
|
|
$rateLimiter = new RateLimiter(2, 2); // 2 requests per 2 seconds
|
|
$key = 'test_key';
|
|
|
|
// Use up the limit
|
|
$this->assertTrue($rateLimiter->isAllowed($key));
|
|
$this->assertTrue($rateLimiter->isAllowed($key));
|
|
$this->assertFalse($rateLimiter->isAllowed($key));
|
|
|
|
// Wait for window to slide (simulate time passage)
|
|
// In a real scenario, we'd wait, but for testing we'll manipulate the internal state
|
|
sleep(3); // Wait longer than the window
|
|
|
|
// Now requests should be allowed again
|
|
$this->assertTrue($rateLimiter->isAllowed($key));
|
|
$this->assertTrue($rateLimiter->isAllowed($key));
|
|
$this->assertFalse($rateLimiter->isAllowed($key));
|
|
}
|
|
|
|
public function testMultipleKeys(): void
|
|
{
|
|
$rateLimiter = new RateLimiter(2, 60);
|
|
|
|
// Each key should have its own limit
|
|
$this->assertTrue($rateLimiter->isAllowed('key1'));
|
|
$this->assertTrue($rateLimiter->isAllowed('key1'));
|
|
$this->assertFalse($rateLimiter->isAllowed('key1')); // key1 exhausted
|
|
|
|
// key2 should still work
|
|
$this->assertTrue($rateLimiter->isAllowed('key2'));
|
|
$this->assertTrue($rateLimiter->isAllowed('key2'));
|
|
$this->assertFalse($rateLimiter->isAllowed('key2')); // key2 exhausted
|
|
}
|
|
|
|
public function testTimeUntilReset(): void
|
|
{
|
|
$rateLimiter = new RateLimiter(1, 10); // 1 request per 10 seconds
|
|
$key = 'test_key';
|
|
|
|
// Use the single allowed request
|
|
$this->assertTrue($rateLimiter->isAllowed($key));
|
|
|
|
// Check time until reset (should be around 10 seconds, allowing for some variance)
|
|
$timeUntilReset = $rateLimiter->getTimeUntilReset($key);
|
|
$this->assertGreaterThan(8, $timeUntilReset);
|
|
$this->assertLessThanOrEqual(10, $timeUntilReset);
|
|
}
|
|
|
|
public function testGetStats(): void
|
|
{
|
|
$rateLimiter = new RateLimiter(5, 60);
|
|
$key = 'test_key';
|
|
|
|
// Initial stats
|
|
$stats = $rateLimiter->getStats($key);
|
|
$this->assertEquals(0, $stats['current_requests']);
|
|
$this->assertEquals(5, $stats['remaining_requests']);
|
|
$this->assertEquals(0, $stats['time_until_reset']);
|
|
|
|
// After using some requests
|
|
$rateLimiter->isAllowed($key);
|
|
$rateLimiter->isAllowed($key);
|
|
|
|
$stats = $rateLimiter->getStats($key);
|
|
$this->assertEquals(2, $stats['current_requests']);
|
|
$this->assertEquals(3, $stats['remaining_requests']);
|
|
$this->assertGreaterThan(0, $stats['time_until_reset']);
|
|
}
|
|
|
|
public function testClearAll(): void
|
|
{
|
|
$rateLimiter = new RateLimiter(1, 60);
|
|
$key = 'test_key';
|
|
|
|
// Use up the limit
|
|
$this->assertTrue($rateLimiter->isAllowed($key));
|
|
$this->assertFalse($rateLimiter->isAllowed($key));
|
|
|
|
// Clear all data
|
|
RateLimiter::clearAll();
|
|
|
|
// Should be able to make requests again
|
|
$this->assertTrue($rateLimiter->isAllowed($key));
|
|
}
|
|
|
|
public function testZeroLimit(): void
|
|
{
|
|
// Test that zero max requests throws an exception due to validation
|
|
$this->expectException(InvalidRateLimitConfigurationException::class);
|
|
$this->expectExceptionMessage('Maximum requests must be a positive integer, got: 0');
|
|
|
|
new RateLimiter(0, 60);
|
|
}
|
|
|
|
public function testHighVolumeRequests(): void
|
|
{
|
|
$rateLimiter = new RateLimiter(10, 60);
|
|
$key = 'high_volume_key';
|
|
|
|
$allowedCount = 0;
|
|
$deniedCount = 0;
|
|
|
|
// Make 20 requests
|
|
for ($i = 0; $i < 20; $i++) {
|
|
if ($rateLimiter->isAllowed($key)) {
|
|
$allowedCount++;
|
|
} else {
|
|
$deniedCount++;
|
|
}
|
|
}
|
|
|
|
$this->assertSame(10, $allowedCount);
|
|
$this->assertSame(10, $deniedCount);
|
|
}
|
|
|
|
public function testConcurrentKeyAccess(): void
|
|
{
|
|
$rateLimiter = new RateLimiter(3, 60);
|
|
|
|
// Test multiple keys being used simultaneously
|
|
$keys = ['key1', 'key2', 'key3', 'key4', 'key5'];
|
|
|
|
foreach ($keys as $key) {
|
|
// Each key should allow 3 requests
|
|
$this->assertTrue($rateLimiter->isAllowed($key));
|
|
$this->assertTrue($rateLimiter->isAllowed($key));
|
|
$this->assertTrue($rateLimiter->isAllowed($key));
|
|
$this->assertFalse($rateLimiter->isAllowed($key));
|
|
}
|
|
|
|
// Verify stats for each key
|
|
foreach ($keys as $key) {
|
|
$stats = $rateLimiter->getStats($key);
|
|
$this->assertEquals(3, $stats['current_requests']);
|
|
$this->assertEquals(0, $stats['remaining_requests']);
|
|
}
|
|
}
|
|
|
|
public function testEdgeCaseEmptyKey(): void
|
|
{
|
|
$rateLimiter = new RateLimiter(2, 60);
|
|
|
|
// Empty string key should throw validation exception
|
|
$this->expectException(InvalidRateLimitConfigurationException::class);
|
|
$this->expectExceptionMessage(TestConstants::ERROR_RATE_LIMIT_KEY_EMPTY);
|
|
|
|
$rateLimiter->isAllowed('');
|
|
}
|
|
|
|
public function testVeryShortWindow(): void
|
|
{
|
|
$rateLimiter = new RateLimiter(1, 1); // 1 request per 1 second
|
|
$key = 'short_window';
|
|
|
|
$this->assertTrue($rateLimiter->isAllowed($key));
|
|
$this->assertFalse($rateLimiter->isAllowed($key));
|
|
|
|
// Wait for the window to expire
|
|
sleep(2);
|
|
|
|
$this->assertTrue($rateLimiter->isAllowed($key));
|
|
}
|
|
}
|