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