1844965e2SAndreas Gohr<?php 2844965e2SAndreas Gohr 3844965e2SAndreas Gohrnamespace dokuwiki\plugin\captcha\test; 4844965e2SAndreas Gohr 5844965e2SAndreas Gohruse dokuwiki\plugin\captcha\IpCounter; 6844965e2SAndreas Gohruse DokuWikiTest; 7844965e2SAndreas Gohr 8844965e2SAndreas Gohr/** 9844965e2SAndreas Gohr * Tests for the IpCounter class 10844965e2SAndreas Gohr * 11844965e2SAndreas Gohr * @group plugin_captcha 12844965e2SAndreas Gohr * @group plugins 13844965e2SAndreas Gohr */ 14844965e2SAndreas Gohrclass IpCounterTest extends DokuWikiTest 15844965e2SAndreas Gohr{ 16844965e2SAndreas Gohr protected $pluginsEnabled = ['captcha']; 17844965e2SAndreas Gohr 18844965e2SAndreas Gohr /** @var IpCounter */ 19844965e2SAndreas Gohr protected $counter; 20844965e2SAndreas Gohr 21844965e2SAndreas Gohr public function setUp(): void 22844965e2SAndreas Gohr { 23844965e2SAndreas Gohr parent::setUp(); 24*194d3386SAndreas Gohr global $conf; 25*194d3386SAndreas Gohr $conf['plugin']['captcha']['logindenial'] = 5; 26*194d3386SAndreas Gohr $conf['plugin']['captcha']['logindenial_max'] = 3600; 27844965e2SAndreas Gohr $this->counter = new IpCounter(); 28844965e2SAndreas Gohr $this->counter->reset(); 29844965e2SAndreas Gohr } 30844965e2SAndreas Gohr 31844965e2SAndreas Gohr public function tearDown(): void 32844965e2SAndreas Gohr { 33844965e2SAndreas Gohr $this->counter->reset(); 34844965e2SAndreas Gohr parent::tearDown(); 35844965e2SAndreas Gohr } 36844965e2SAndreas Gohr 37844965e2SAndreas Gohr public function testInitialState() 38844965e2SAndreas Gohr { 39844965e2SAndreas Gohr $this->assertEquals(0, $this->counter->get()); 40844965e2SAndreas Gohr $this->assertEquals(0, $this->counter->getLastAttempt()); 41844965e2SAndreas Gohr } 42844965e2SAndreas Gohr 43844965e2SAndreas Gohr public function testIncrement() 44844965e2SAndreas Gohr { 45844965e2SAndreas Gohr $this->assertEquals(0, $this->counter->get()); 46844965e2SAndreas Gohr 47844965e2SAndreas Gohr $this->counter->increment(); 48844965e2SAndreas Gohr $this->assertEquals(1, $this->counter->get()); 49844965e2SAndreas Gohr 50844965e2SAndreas Gohr $this->counter->increment(); 51844965e2SAndreas Gohr $this->assertEquals(2, $this->counter->get()); 52844965e2SAndreas Gohr 53844965e2SAndreas Gohr $this->counter->increment(); 54844965e2SAndreas Gohr $this->assertEquals(3, $this->counter->get()); 55844965e2SAndreas Gohr } 56844965e2SAndreas Gohr 57844965e2SAndreas Gohr public function testReset() 58844965e2SAndreas Gohr { 59844965e2SAndreas Gohr $this->counter->increment(); 60844965e2SAndreas Gohr $this->counter->increment(); 61844965e2SAndreas Gohr $this->assertEquals(2, $this->counter->get()); 62844965e2SAndreas Gohr 63844965e2SAndreas Gohr $this->counter->reset(); 64844965e2SAndreas Gohr $this->assertEquals(0, $this->counter->get()); 65844965e2SAndreas Gohr } 66844965e2SAndreas Gohr 67844965e2SAndreas Gohr public function testGetLastAttempt() 68844965e2SAndreas Gohr { 69844965e2SAndreas Gohr $this->assertEquals(0, $this->counter->getLastAttempt()); 70844965e2SAndreas Gohr 71844965e2SAndreas Gohr $before = time(); 72844965e2SAndreas Gohr $this->counter->increment(); 73844965e2SAndreas Gohr $after = time(); 74844965e2SAndreas Gohr 75844965e2SAndreas Gohr $lastAttempt = $this->counter->getLastAttempt(); 76844965e2SAndreas Gohr $this->assertGreaterThanOrEqual($before, $lastAttempt); 77844965e2SAndreas Gohr $this->assertLessThanOrEqual($after, $lastAttempt); 78844965e2SAndreas Gohr } 79844965e2SAndreas Gohr 80844965e2SAndreas Gohr public function testCalculateTimeoutNoFailures() 81844965e2SAndreas Gohr { 82*194d3386SAndreas Gohr $this->assertEquals(0, $this->counter->calculateTimeout()); 83*194d3386SAndreas Gohr } 84*194d3386SAndreas Gohr 85*194d3386SAndreas Gohr public function testCalculateTimeoutDisabled() 86*194d3386SAndreas Gohr { 87*194d3386SAndreas Gohr global $conf; 88*194d3386SAndreas Gohr $conf['plugin']['captcha']['logindenial'] = 0; 89*194d3386SAndreas Gohr $counter = new IpCounter(); 90*194d3386SAndreas Gohr 91*194d3386SAndreas Gohr $counter->increment(); 92*194d3386SAndreas Gohr $this->assertEquals(0, $counter->calculateTimeout()); 93*194d3386SAndreas Gohr 94*194d3386SAndreas Gohr $counter->reset(); 95844965e2SAndreas Gohr } 96844965e2SAndreas Gohr 97844965e2SAndreas Gohr public function testCalculateTimeoutExponentialGrowth() 98844965e2SAndreas Gohr { 99844965e2SAndreas Gohr // First failure: base * 2^0 = 5 100844965e2SAndreas Gohr $this->counter->increment(); 101*194d3386SAndreas Gohr $this->assertEquals(5, $this->counter->calculateTimeout()); 102844965e2SAndreas Gohr 103844965e2SAndreas Gohr // Second failure: base * 2^1 = 10 104844965e2SAndreas Gohr $this->counter->increment(); 105*194d3386SAndreas Gohr $this->assertEquals(10, $this->counter->calculateTimeout()); 106844965e2SAndreas Gohr 107844965e2SAndreas Gohr // Third failure: base * 2^2 = 20 108844965e2SAndreas Gohr $this->counter->increment(); 109*194d3386SAndreas Gohr $this->assertEquals(20, $this->counter->calculateTimeout()); 110844965e2SAndreas Gohr 111844965e2SAndreas Gohr // Fourth failure: base * 2^3 = 40 112844965e2SAndreas Gohr $this->counter->increment(); 113*194d3386SAndreas Gohr $this->assertEquals(40, $this->counter->calculateTimeout()); 114844965e2SAndreas Gohr 115844965e2SAndreas Gohr // Fifth failure: base * 2^4 = 80 116844965e2SAndreas Gohr $this->counter->increment(); 117*194d3386SAndreas Gohr $this->assertEquals(80, $this->counter->calculateTimeout()); 118844965e2SAndreas Gohr } 119844965e2SAndreas Gohr 120844965e2SAndreas Gohr public function testCalculateTimeoutMaxCap() 121844965e2SAndreas Gohr { 122844965e2SAndreas Gohr // Add many failures to exceed the max 123844965e2SAndreas Gohr for ($i = 0; $i < 20; $i++) { 124844965e2SAndreas Gohr $this->counter->increment(); 125844965e2SAndreas Gohr } 126844965e2SAndreas Gohr 127*194d3386SAndreas Gohr // Should be capped at max (3600) 128*194d3386SAndreas Gohr $this->assertEquals(3600, $this->counter->calculateTimeout()); 129*194d3386SAndreas Gohr } 130*194d3386SAndreas Gohr 131*194d3386SAndreas Gohr public function testCalculateTimeoutMaxCapLower() 132*194d3386SAndreas Gohr { 133*194d3386SAndreas Gohr global $conf; 134*194d3386SAndreas Gohr $conf['plugin']['captcha']['logindenial_max'] = 100; 135*194d3386SAndreas Gohr $counter = new IpCounter(); 136*194d3386SAndreas Gohr 137*194d3386SAndreas Gohr // Add many failures to exceed the max 138*194d3386SAndreas Gohr for ($i = 0; $i < 20; $i++) { 139*194d3386SAndreas Gohr $counter->increment(); 140*194d3386SAndreas Gohr } 141*194d3386SAndreas Gohr 142*194d3386SAndreas Gohr // Should be capped at max (100) 143*194d3386SAndreas Gohr $this->assertEquals(100, $counter->calculateTimeout()); 144*194d3386SAndreas Gohr 145*194d3386SAndreas Gohr $counter->reset(); 146844965e2SAndreas Gohr } 147844965e2SAndreas Gohr 148844965e2SAndreas Gohr public function testCalculateTimeoutDifferentBase() 149844965e2SAndreas Gohr { 150*194d3386SAndreas Gohr global $conf; 151*194d3386SAndreas Gohr $conf['plugin']['captcha']['logindenial'] = 10; 152*194d3386SAndreas Gohr $counter = new IpCounter(); 153844965e2SAndreas Gohr 154*194d3386SAndreas Gohr $counter->increment(); 155*194d3386SAndreas Gohr $this->assertEquals(10, $counter->calculateTimeout()); 156844965e2SAndreas Gohr 157*194d3386SAndreas Gohr $counter->increment(); 158*194d3386SAndreas Gohr $this->assertEquals(20, $counter->calculateTimeout()); 159*194d3386SAndreas Gohr 160*194d3386SAndreas Gohr $counter->increment(); 161*194d3386SAndreas Gohr $this->assertEquals(40, $counter->calculateTimeout()); 162*194d3386SAndreas Gohr 163*194d3386SAndreas Gohr $counter->reset(); 164844965e2SAndreas Gohr } 165844965e2SAndreas Gohr 166844965e2SAndreas Gohr public function testGetRemainingTimeNoFailures() 167844965e2SAndreas Gohr { 168*194d3386SAndreas Gohr $this->assertEquals(0, $this->counter->getRemainingTime()); 169844965e2SAndreas Gohr } 170844965e2SAndreas Gohr 171844965e2SAndreas Gohr public function testGetRemainingTimeImmediatelyAfterFailure() 172844965e2SAndreas Gohr { 173844965e2SAndreas Gohr $this->counter->increment(); 174844965e2SAndreas Gohr 175*194d3386SAndreas Gohr $remaining = $this->counter->getRemainingTime(); 176844965e2SAndreas Gohr 177844965e2SAndreas Gohr // Immediately after increment, remaining should be close to full timeout (5s) 178844965e2SAndreas Gohr // Allow 1 second tolerance for test execution time 179844965e2SAndreas Gohr $this->assertGreaterThanOrEqual(4, $remaining); 180844965e2SAndreas Gohr $this->assertLessThanOrEqual(5, $remaining); 181844965e2SAndreas Gohr } 182844965e2SAndreas Gohr 183844965e2SAndreas Gohr public function testGetRemainingTimeAfterTimeoutExpires() 184844965e2SAndreas Gohr { 185844965e2SAndreas Gohr $this->counter->increment(); 186844965e2SAndreas Gohr 187844965e2SAndreas Gohr // Manipulate the file's mtime to simulate time passing 188844965e2SAndreas Gohr $store = $this->getInaccessibleProperty($this->counter, 'store'); 189844965e2SAndreas Gohr touch($store, time() - 10); // 10 seconds ago 190844965e2SAndreas Gohr 191844965e2SAndreas Gohr // Timeout is 5 seconds, 10 seconds have passed, so remaining should be 0 192*194d3386SAndreas Gohr $this->assertEquals(0, $this->counter->getRemainingTime()); 193844965e2SAndreas Gohr } 194844965e2SAndreas Gohr 195844965e2SAndreas Gohr public function testGetRemainingTimePartiallyElapsed() 196844965e2SAndreas Gohr { 197844965e2SAndreas Gohr $this->counter->increment(); 198844965e2SAndreas Gohr $this->counter->increment(); // timeout = 10 seconds 199844965e2SAndreas Gohr 200844965e2SAndreas Gohr // Manipulate the file's mtime to simulate 3 seconds passing 201844965e2SAndreas Gohr $store = $this->getInaccessibleProperty($this->counter, 'store'); 202844965e2SAndreas Gohr touch($store, time() - 3); 203844965e2SAndreas Gohr 204*194d3386SAndreas Gohr $remaining = $this->counter->getRemainingTime(); 205844965e2SAndreas Gohr 206844965e2SAndreas Gohr // 10 second timeout, 3 seconds elapsed, ~7 seconds remaining 207844965e2SAndreas Gohr $this->assertGreaterThanOrEqual(6, $remaining); 208844965e2SAndreas Gohr $this->assertLessThanOrEqual(7, $remaining); 209844965e2SAndreas Gohr } 210844965e2SAndreas Gohr} 211