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