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