Add PHP unit tests and page tests.

Run "composer install", then "composer test".
This commit is contained in:
tim
2026-09-26 03:48:09 +02:00
parent 848c447c5b
commit ed959888b4
42 changed files with 5522 additions and 0 deletions
+116
View File
@@ -0,0 +1,116 @@
<?php
namespace Xxx\Tests\Unit;
use Xxx\Tests\Support\TestCase;
// Counting failed logins in session.lib.php
class LoginThrottleTest extends TestCase {
private string $user;
protected function setUp(): void {
parent::setUp();
// A new username and address for each test, so that the files of
// other tests (and other runs) do not count
$this->user = 'Ed-' . bin2hex(random_bytes(4));
$_SERVER['REMOTE_ADDR'] = '198.51.100.' . random_int(1, 254) . '-' . bin2hex(random_bytes(4));
}
protected function tearDown(): void {
foreach (array($this->user, 'other-' . $this->user) as $user) {
foreach (array_keys(login_limits($user)) as $key)
@unlink(login_throttle_file($key));
}
parent::tearDown();
}
private function failLogins(int $times, ?string $user = NULL): void {
for ($i = 0; $i < $times; $i++)
record_login_failure($user ?? $this->user);
}
public function testLimitsArePerUserAndAddressPerAddressAndPerUser(): void {
$address = $_SERVER['REMOTE_ADDR'];
$user = strtolower($this->user);
$this->assertSame(array("user-ip:$user|$address" => 5, "ip:$address" => 20, "user:$user" => 50),
login_limits($this->user));
}
public function testUsernameCaseDoesNotMatter(): void {
$this->assertSame(login_limits(strtoupper($this->user)), login_limits(strtolower($this->user)));
}
public function testThrottleFilesAreHashedOutsideTheWebRoot(): void {
$file = login_throttle_file('ip:192.0.2.1');
$this->assertSame(sys_get_temp_dir() . '/xxx-login-throttle/' . hash('sha256', 'ip:192.0.2.1'), $file);
$this->assertDirectoryExists(dirname($file));
$this->assertSame(0700, fileperms(dirname($file)) & 0777);
}
public function testFourFailuresDoNotBlock(): void {
$this->failLogins(4);
$this->assertSame(0, login_blocked_for($this->user));
}
public function testFiveFailuresBlockForTheWindow(): void {
$this->failLogins(5);
$wait = login_blocked_for($this->user);
$this->assertGreaterThan(LOGIN_WINDOW - 5, $wait);
$this->assertLessThanOrEqual(LOGIN_WINDOW, $wait);
}
public function testEachFailureCountsForEveryKey(): void {
$this->failLogins(2);
foreach (array_keys(login_limits($this->user)) as $key)
$this->assertCount(2, login_failures($key));
}
public function testOldFailuresAreForgotten(): void {
$keys = array_keys(login_limits($this->user));
$old = time() - LOGIN_WINDOW - 1;
file_put_contents(login_throttle_file($keys[0]), json_encode(array_fill(0, 5, $old)));
$this->assertSame(array(), login_failures($keys[0]));
$this->assertSame(0, login_blocked_for($this->user));
}
public function testTheWaitEndsAWindowAfterTheOldestFailure(): void {
$keys = array_keys(login_limits($this->user));
$oldest = time() - 600;
file_put_contents(login_throttle_file($keys[0]), json_encode(array($oldest, time(), time(), time(), time())));
$this->assertSame($oldest + LOGIN_WINDOW - time(), login_blocked_for($this->user));
}
public function testDamagedFilesCountAsNoFailures(): void {
$keys = array_keys(login_limits($this->user));
file_put_contents(login_throttle_file($keys[0]), 'not json');
$this->assertSame(array(), login_failures($keys[0]));
file_put_contents(login_throttle_file($keys[0]), json_encode(array('1', NULL, time())));
$this->assertCount(1, login_failures($keys[0]));
}
public function testTwentyFailuresFromOneAddressBlockEveryUser(): void {
$this->failLogins(4);
for ($i = 0; $i < 4; $i++)
$this->failLogins(4, "user$i-" . $this->user);
$this->assertGreaterThan(0, login_blocked_for('other-' . $this->user));
for ($i = 0; $i < 4; $i++) {
foreach (array_keys(login_limits("user$i-" . $this->user)) as $key)
@unlink(login_throttle_file($key));
}
}
public function testFiftyFailuresForAUserBlockEveryAddress(): void {
$keys = array_keys(login_limits($this->user));
file_put_contents(login_throttle_file($keys[2]), json_encode(array_fill(0, 50, time())));
$_SERVER['REMOTE_ADDR'] = '203.0.113.9-' . bin2hex(random_bytes(4));
$this->assertGreaterThan(0, login_blocked_for($this->user));
}
public function testClearingForgetsOnlyThisUserFromThisAddress(): void {
$this->failLogins(3);
clear_login_failures($this->user);
$keys = array_keys(login_limits($this->user));
$this->assertSame(array(), login_failures($keys[0]));
$this->assertCount(3, login_failures($keys[1]));
$this->assertCount(3, login_failures($keys[2]));
}
}