xref: /plugin/captcha/_test/IpCounterTest.php (revision 844965e23400145eb4b17590f5016e166b5de96d)
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