1*844965e2SAndreas Gohr<?php 2*844965e2SAndreas Gohr 3*844965e2SAndreas Gohrnamespace dokuwiki\plugin\captcha\test; 4*844965e2SAndreas Gohr 5*844965e2SAndreas Gohruse dokuwiki\plugin\captcha\IpCounter; 6*844965e2SAndreas Gohruse DokuWikiTest; 7*844965e2SAndreas Gohr 8*844965e2SAndreas Gohr/** 9*844965e2SAndreas Gohr * Tests for the IpCounter class 10*844965e2SAndreas Gohr * 11*844965e2SAndreas Gohr * @group plugin_captcha 12*844965e2SAndreas Gohr * @group plugins 13*844965e2SAndreas Gohr */ 14*844965e2SAndreas Gohrclass IpCounterTest extends DokuWikiTest 15*844965e2SAndreas Gohr{ 16*844965e2SAndreas Gohr protected $pluginsEnabled = ['captcha']; 17*844965e2SAndreas Gohr 18*844965e2SAndreas Gohr /** @var IpCounter */ 19*844965e2SAndreas Gohr protected $counter; 20*844965e2SAndreas Gohr 21*844965e2SAndreas Gohr public function setUp(): void 22*844965e2SAndreas Gohr { 23*844965e2SAndreas Gohr parent::setUp(); 24*844965e2SAndreas Gohr $this->counter = new IpCounter(); 25*844965e2SAndreas Gohr $this->counter->reset(); 26*844965e2SAndreas Gohr } 27*844965e2SAndreas Gohr 28*844965e2SAndreas Gohr public function tearDown(): void 29*844965e2SAndreas Gohr { 30*844965e2SAndreas Gohr $this->counter->reset(); 31*844965e2SAndreas Gohr parent::tearDown(); 32*844965e2SAndreas Gohr } 33*844965e2SAndreas Gohr 34*844965e2SAndreas Gohr public function testInitialState() 35*844965e2SAndreas Gohr { 36*844965e2SAndreas Gohr $this->assertEquals(0, $this->counter->get()); 37*844965e2SAndreas Gohr $this->assertEquals(0, $this->counter->getLastAttempt()); 38*844965e2SAndreas Gohr } 39*844965e2SAndreas Gohr 40*844965e2SAndreas Gohr public function testIncrement() 41*844965e2SAndreas Gohr { 42*844965e2SAndreas Gohr $this->assertEquals(0, $this->counter->get()); 43*844965e2SAndreas Gohr 44*844965e2SAndreas Gohr $this->counter->increment(); 45*844965e2SAndreas Gohr $this->assertEquals(1, $this->counter->get()); 46*844965e2SAndreas Gohr 47*844965e2SAndreas Gohr $this->counter->increment(); 48*844965e2SAndreas Gohr $this->assertEquals(2, $this->counter->get()); 49*844965e2SAndreas Gohr 50*844965e2SAndreas Gohr $this->counter->increment(); 51*844965e2SAndreas Gohr $this->assertEquals(3, $this->counter->get()); 52*844965e2SAndreas Gohr } 53*844965e2SAndreas Gohr 54*844965e2SAndreas Gohr public function testReset() 55*844965e2SAndreas Gohr { 56*844965e2SAndreas Gohr $this->counter->increment(); 57*844965e2SAndreas Gohr $this->counter->increment(); 58*844965e2SAndreas Gohr $this->assertEquals(2, $this->counter->get()); 59*844965e2SAndreas Gohr 60*844965e2SAndreas Gohr $this->counter->reset(); 61*844965e2SAndreas Gohr $this->assertEquals(0, $this->counter->get()); 62*844965e2SAndreas Gohr } 63*844965e2SAndreas Gohr 64*844965e2SAndreas Gohr public function testGetLastAttempt() 65*844965e2SAndreas Gohr { 66*844965e2SAndreas Gohr $this->assertEquals(0, $this->counter->getLastAttempt()); 67*844965e2SAndreas Gohr 68*844965e2SAndreas Gohr $before = time(); 69*844965e2SAndreas Gohr $this->counter->increment(); 70*844965e2SAndreas Gohr $after = time(); 71*844965e2SAndreas Gohr 72*844965e2SAndreas Gohr $lastAttempt = $this->counter->getLastAttempt(); 73*844965e2SAndreas Gohr $this->assertGreaterThanOrEqual($before, $lastAttempt); 74*844965e2SAndreas Gohr $this->assertLessThanOrEqual($after, $lastAttempt); 75*844965e2SAndreas Gohr } 76*844965e2SAndreas Gohr 77*844965e2SAndreas Gohr public function testCalculateTimeoutNoFailures() 78*844965e2SAndreas Gohr { 79*844965e2SAndreas Gohr $this->assertEquals(0, $this->counter->calculateTimeout(5, 3600)); 80*844965e2SAndreas Gohr } 81*844965e2SAndreas Gohr 82*844965e2SAndreas Gohr public function testCalculateTimeoutExponentialGrowth() 83*844965e2SAndreas Gohr { 84*844965e2SAndreas Gohr // First failure: base * 2^0 = 5 85*844965e2SAndreas Gohr $this->counter->increment(); 86*844965e2SAndreas Gohr $this->assertEquals(5, $this->counter->calculateTimeout(5, 3600)); 87*844965e2SAndreas Gohr 88*844965e2SAndreas Gohr // Second failure: base * 2^1 = 10 89*844965e2SAndreas Gohr $this->counter->increment(); 90*844965e2SAndreas Gohr $this->assertEquals(10, $this->counter->calculateTimeout(5, 3600)); 91*844965e2SAndreas Gohr 92*844965e2SAndreas Gohr // Third failure: base * 2^2 = 20 93*844965e2SAndreas Gohr $this->counter->increment(); 94*844965e2SAndreas Gohr $this->assertEquals(20, $this->counter->calculateTimeout(5, 3600)); 95*844965e2SAndreas Gohr 96*844965e2SAndreas Gohr // Fourth failure: base * 2^3 = 40 97*844965e2SAndreas Gohr $this->counter->increment(); 98*844965e2SAndreas Gohr $this->assertEquals(40, $this->counter->calculateTimeout(5, 3600)); 99*844965e2SAndreas Gohr 100*844965e2SAndreas Gohr // Fifth failure: base * 2^4 = 80 101*844965e2SAndreas Gohr $this->counter->increment(); 102*844965e2SAndreas Gohr $this->assertEquals(80, $this->counter->calculateTimeout(5, 3600)); 103*844965e2SAndreas Gohr } 104*844965e2SAndreas Gohr 105*844965e2SAndreas Gohr public function testCalculateTimeoutMaxCap() 106*844965e2SAndreas Gohr { 107*844965e2SAndreas Gohr // Add many failures to exceed the max 108*844965e2SAndreas Gohr for ($i = 0; $i < 20; $i++) { 109*844965e2SAndreas Gohr $this->counter->increment(); 110*844965e2SAndreas Gohr } 111*844965e2SAndreas Gohr 112*844965e2SAndreas Gohr // Should be capped at max 113*844965e2SAndreas Gohr $this->assertEquals(3600, $this->counter->calculateTimeout(5, 3600)); 114*844965e2SAndreas Gohr $this->assertEquals(100, $this->counter->calculateTimeout(5, 100)); 115*844965e2SAndreas Gohr } 116*844965e2SAndreas Gohr 117*844965e2SAndreas Gohr public function testCalculateTimeoutDifferentBase() 118*844965e2SAndreas Gohr { 119*844965e2SAndreas Gohr $this->counter->increment(); 120*844965e2SAndreas Gohr $this->assertEquals(10, $this->counter->calculateTimeout(10, 3600)); 121*844965e2SAndreas Gohr 122*844965e2SAndreas Gohr $this->counter->increment(); 123*844965e2SAndreas Gohr $this->assertEquals(20, $this->counter->calculateTimeout(10, 3600)); 124*844965e2SAndreas Gohr 125*844965e2SAndreas Gohr $this->counter->increment(); 126*844965e2SAndreas Gohr $this->assertEquals(40, $this->counter->calculateTimeout(10, 3600)); 127*844965e2SAndreas Gohr } 128*844965e2SAndreas Gohr 129*844965e2SAndreas Gohr public function testGetRemainingTimeNoFailures() 130*844965e2SAndreas Gohr { 131*844965e2SAndreas Gohr $this->assertEquals(0, $this->counter->getRemainingTime(5, 3600)); 132*844965e2SAndreas Gohr } 133*844965e2SAndreas Gohr 134*844965e2SAndreas Gohr public function testGetRemainingTimeImmediatelyAfterFailure() 135*844965e2SAndreas Gohr { 136*844965e2SAndreas Gohr $this->counter->increment(); 137*844965e2SAndreas Gohr 138*844965e2SAndreas Gohr $remaining = $this->counter->getRemainingTime(5, 3600); 139*844965e2SAndreas Gohr 140*844965e2SAndreas Gohr // Immediately after increment, remaining should be close to full timeout (5s) 141*844965e2SAndreas Gohr // Allow 1 second tolerance for test execution time 142*844965e2SAndreas Gohr $this->assertGreaterThanOrEqual(4, $remaining); 143*844965e2SAndreas Gohr $this->assertLessThanOrEqual(5, $remaining); 144*844965e2SAndreas Gohr } 145*844965e2SAndreas Gohr 146*844965e2SAndreas Gohr public function testGetRemainingTimeAfterTimeoutExpires() 147*844965e2SAndreas Gohr { 148*844965e2SAndreas Gohr $this->counter->increment(); 149*844965e2SAndreas Gohr 150*844965e2SAndreas Gohr // Manipulate the file's mtime to simulate time passing 151*844965e2SAndreas Gohr $store = $this->getInaccessibleProperty($this->counter, 'store'); 152*844965e2SAndreas Gohr touch($store, time() - 10); // 10 seconds ago 153*844965e2SAndreas Gohr 154*844965e2SAndreas Gohr // Timeout is 5 seconds, 10 seconds have passed, so remaining should be 0 155*844965e2SAndreas Gohr $this->assertEquals(0, $this->counter->getRemainingTime(5, 3600)); 156*844965e2SAndreas Gohr } 157*844965e2SAndreas Gohr 158*844965e2SAndreas Gohr public function testGetRemainingTimePartiallyElapsed() 159*844965e2SAndreas Gohr { 160*844965e2SAndreas Gohr $this->counter->increment(); 161*844965e2SAndreas Gohr $this->counter->increment(); // timeout = 10 seconds 162*844965e2SAndreas Gohr 163*844965e2SAndreas Gohr // Manipulate the file's mtime to simulate 3 seconds passing 164*844965e2SAndreas Gohr $store = $this->getInaccessibleProperty($this->counter, 'store'); 165*844965e2SAndreas Gohr touch($store, time() - 3); 166*844965e2SAndreas Gohr 167*844965e2SAndreas Gohr $remaining = $this->counter->getRemainingTime(5, 3600); 168*844965e2SAndreas Gohr 169*844965e2SAndreas Gohr // 10 second timeout, 3 seconds elapsed, ~7 seconds remaining 170*844965e2SAndreas Gohr $this->assertGreaterThanOrEqual(6, $remaining); 171*844965e2SAndreas Gohr $this->assertLessThanOrEqual(7, $remaining); 172*844965e2SAndreas Gohr } 173*844965e2SAndreas Gohr} 174