waitFor('203.0.113.7') === 0); for ($i = 1; $i < LoginThrottle::MAX_FAILURES; $i++) { $throttle->fail('203.0.113.7'); $now += 10; } check('four failures still let it try', $throttle->waitFor('203.0.113.7') === 0, $throttle->waitFor('203.0.113.7')); $wait = $throttle->fail('203.0.113.7'); check('the fifth locks it for the whole window', $wait === $window, $wait); check('and asking says so', $throttle->waitFor('203.0.113.7') === $window); check('another address is not touched', $throttle->waitFor('198.51.100.1') === 0); $now += $window - 1; check('one second before the end it still waits', $throttle->waitFor('203.0.113.7') === 1, $throttle->waitFor('203.0.113.7')); $now += 1; check('15 minutes after the last failure it may try again', $throttle->waitFor('203.0.113.7') === 0); $throttle->fail('203.0.113.7'); check('and the old failures no longer count', $throttle->waitFor('203.0.113.7') === 0); // Failures spread wider than the window never add up to five. $now += $window; for ($i = 0; $i < 10; $i++) { $throttle->fail('192.0.2.9'); $now += (int) ($window / 4) + 1; } check('failures spread over more than 15 minutes never lock', $throttle->waitFor('192.0.2.9') === 0); check( 'the rule as a function: five within the window wait for the rest of it', LoginThrottle::wait([100, 200, 300, 400, 500], 600) === 500 + $window - 600, ); check('four do not', LoginThrottle::wait([100, 200, 300, 400], 600) === 0); check( 'nor five of which the first is a whole window before the last', LoginThrottle::wait([500 - $window, 200, 300, 400, 500], 600) === 0, ); $files = glob("{$dir}/sign-ins/*.json") ?: []; check('one file per address that failed, named by its hash', count($files) === 2 &&!str_contains(implode('', $files), '203.0.113.7'), $files); check('which the web server may not read', (fileperms($files[0]) & 0o007) === 0); touch($files[0], $now - 2 * $window - 1); check('pruning drops an address whose failures are all old', $throttle->prune() >= 1 && !is_file($files[0])); // --- Quota ----------------------------------------------------------------------------------------- $data = "{$dir}/data"; mkdir("{$data}/projects", 0775, true); $users = new UserStore("{$data}/users.json"); $users->put('ann', 'password1'); $folder = $users->folderOf('ann', true); mkdir("{$data}/sounds/{$folder}", 0775, true); file_put_contents("{$data}/sounds/{$folder}/" . str_repeat('a', 64) . '.wav', str_repeat("\0", 3000)); mkdir("{$data}/videos/{$folder}", 0775, true); file_put_contents("{$data}/videos/{$folder}/" . str_repeat('b', 64) . '.mp4', str_repeat("\0", 2000)); $doc = (new Store("{$data}/projects", 'ann'))->create('Mine'); $projectBytes = filesize("{$data}/projects/{$doc['id']}.json"); mkdir("{$data}/snapshots/{$doc['id']}", 0775, true); file_put_contents("{$data}/snapshots/{$doc['id']}/s1.json", str_repeat('x', 500)); // Somebody else's things do not count, nor do the files that belong to nobody. $theirs = (new Store("{$data}/projects", 'bob'))->create('Theirs'); file_put_contents("{$data}/sounds/" . str_repeat('c', 64) . '.wav', str_repeat("\0", 9000)); $quota = new Quota($data, 10_000); $used = $quota->usage('ann', $folder); check('usage adds the uploads, the projects and their snapshots', $used === 3000 + 2000 + $projectBytes + 500, [$used, $projectBytes]); check('what fits is let through', $quota->refusal('ann', $folder, 10_000 - $used) === null); $refusal = $quota->refusal('ann', $folder, 10_000 - $used + 1); check('one byte more is refused, saying how full the account is', is_string($refusal) && str_contains($refusal, 'Not enough space'), $refusal); check('a write that makes nothing larger always fits', (new Quota($data, 1))->refusal('ann', $folder, 0) === null); check( 'an account without uploads counts its projects only', $quota->usage('bob', '') === filesize("{$data}/projects/{$theirs['id']}.json"), $quota->usage('bob', ''), ); check('the limit is 1 GB unless a test says otherwise', Quota::BYTES === 1024 * 1024 * 1024); exec('rm -rf ' . escapeshellarg($dir)); echo $failed === 0 ? "ALL PASSED – {$passed} checks\n" : "{$failed} FAILED, {$passed} passed\n"; exit($failed === 0 ? 0 : 1);