mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
Add integration tests (Phase 2: DatabaseExtended, ShareLinkExtended, RateLimitExtended) and controller validation tests (Phase 3: ThesisCreate, ThesisEdit, AutofocusField)
This commit is contained in:
156
tests/phpunit/RateLimitExtendedTest.php
Normal file
156
tests/phpunit/RateLimitExtendedTest.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* RateLimitExtendedTest — Integration tests for RateLimit using a temp directory.
|
||||
*/
|
||||
class RateLimitExtendedTest extends TestCase
|
||||
{
|
||||
private string $tmpDir;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->tmpDir = sys_get_temp_dir() . '/xamxam_ratelimit_test_' . uniqid();
|
||||
mkdir($this->tmpDir, 0755, true);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$files = glob($this->tmpDir . '/*.json');
|
||||
foreach ($files as $file) {
|
||||
unlink($file);
|
||||
}
|
||||
rmdir($this->tmpDir);
|
||||
}
|
||||
|
||||
private function newRateLimit(int $max = 5, int $window = 60): RateLimit
|
||||
{
|
||||
return new RateLimit($max, $window, $this->tmpDir);
|
||||
}
|
||||
|
||||
// ── checkKey: per-key limits, not global ──────────────────────────────────
|
||||
|
||||
public function testCheckKeyCountsPerKey(): void
|
||||
{
|
||||
$rl = $this->newRateLimit(2, 60);
|
||||
|
||||
$this->assertTrue($rl->checkKey('key-a'));
|
||||
$this->assertTrue($rl->checkKey('key-b'));
|
||||
$this->assertTrue($rl->checkKey('key-a')); // second hit for key-a, still allowed
|
||||
|
||||
// key-a is now at limit (2)
|
||||
$this->assertFalse($rl->checkKey('key-a'));
|
||||
|
||||
// key-b still has room
|
||||
$this->assertTrue($rl->checkKey('key-b'));
|
||||
$this->assertFalse($rl->checkKey('key-b'));
|
||||
}
|
||||
|
||||
public function testCheckKeyDoesNotAffectDefaultCheck(): void
|
||||
{
|
||||
$rl = $this->newRateLimit(3, 60);
|
||||
|
||||
$rl->checkKey('separate-key');
|
||||
$rl->checkKey('separate-key');
|
||||
$rl->checkKey('separate-key');
|
||||
$rl->checkKey('separate-key'); // exhausted for this key
|
||||
|
||||
// Default check (uses REMOTE_ADDR) should be unaffected by key-based tracking
|
||||
$ipKey = md5($_SERVER['REMOTE_ADDR'] ?? 'unknown');
|
||||
$ipFile = $this->tmpDir . '/' . $ipKey . '.json';
|
||||
$this->assertFileDoesNotExist($ipFile);
|
||||
}
|
||||
|
||||
// ── getRemaining: tied to client IP (REMOTE_ADDR) ─────────────────────────
|
||||
|
||||
public function testGetRemainingStartsAtMax(): void
|
||||
{
|
||||
$rl = $this->newRateLimit(5, 60);
|
||||
|
||||
$remaining = $rl->getRemaining();
|
||||
$this->assertSame(5, $remaining);
|
||||
}
|
||||
|
||||
public function testCheckDecrementsRemainingForSameIp(): void
|
||||
{
|
||||
$rl = $this->newRateLimit(5, 60);
|
||||
|
||||
// check() uses IP-based identifier. We need to use check() directly,
|
||||
// not checkKey(), for getRemaining() to reflect usage.
|
||||
$rl->check(); // hit 1
|
||||
$this->assertSame(4, $rl->getRemaining());
|
||||
|
||||
$rl->check(); // hit 2
|
||||
$this->assertSame(3, $rl->getRemaining());
|
||||
}
|
||||
|
||||
public function testCheckAndCheckKeyAreIndependent(): void
|
||||
{
|
||||
$rl = $this->newRateLimit(5, 60);
|
||||
|
||||
// checkKey hits don't affect IP-based remaining
|
||||
$rl->checkKey('some-key');
|
||||
$rl->checkKey('some-key');
|
||||
|
||||
$this->assertSame(5, $rl->getRemaining());
|
||||
}
|
||||
|
||||
// ── Consistent client identifier ─────────────────────────────────────────
|
||||
|
||||
public function testMultipleChecksFromSameClient(): void
|
||||
{
|
||||
$rl = $this->newRateLimit(1, 60);
|
||||
|
||||
// First check passes, second from same IP fails
|
||||
$this->assertTrue($rl->check());
|
||||
$this->assertFalse($rl->check());
|
||||
}
|
||||
|
||||
public function testGetRemainingReturnsZeroAfterExhaustion(): void
|
||||
{
|
||||
$rl = $this->newRateLimit(1, 60);
|
||||
|
||||
$rl->check();
|
||||
$this->assertSame(0, $rl->getRemaining());
|
||||
}
|
||||
|
||||
// ── reset time ────────────────────────────────────────────────────────────
|
||||
|
||||
public function testGetResetTimeReturnsZeroWhenNoData(): void
|
||||
{
|
||||
$rl = $this->newRateLimit(5, 60);
|
||||
$this->assertSame(0, $rl->getResetTime());
|
||||
}
|
||||
|
||||
public function testGetResetTimePositiveAfterHits(): void
|
||||
{
|
||||
$rl = $this->newRateLimit(5, 60);
|
||||
$rl->check(); // use IP-based check so file is written
|
||||
|
||||
$reset = $rl->getResetTime();
|
||||
$this->assertGreaterThan(0, $reset);
|
||||
$this->assertLessThanOrEqual(60, $reset);
|
||||
}
|
||||
|
||||
// ── cleanup ───────────────────────────────────────────────────────────────
|
||||
|
||||
public function testCleanupRemovesOldFiles(): void
|
||||
{
|
||||
$rl = $this->newRateLimit(5, 60);
|
||||
$rl->checkKey('cleanup-test');
|
||||
|
||||
// Touch the cache file to make it old
|
||||
$files = glob($this->tmpDir . '/*.json');
|
||||
$this->assertNotEmpty($files);
|
||||
foreach ($files as $file) {
|
||||
touch($file, time() - 90000); // 25 hours ago
|
||||
}
|
||||
|
||||
$rl->cleanup();
|
||||
|
||||
// The old file should now be gone
|
||||
$filesAfter = glob($this->tmpDir . '/*.json');
|
||||
$this->assertEmpty($filesAfter);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user