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