3 namespace MediaWiki\Tests\Auth
;
5 use InvalidArgumentException
;
6 use MediaWiki\Auth\Throttler
;
7 use MediaWiki\MainConfigNames
;
8 use MediaWikiIntegrationTestCase
;
9 use Psr\Log\AbstractLogger
;
10 use Psr\Log\LoggerInterface
;
11 use Psr\Log\NullLogger
;
12 use Wikimedia\ObjectCache\BagOStuff
;
13 use Wikimedia\ObjectCache\HashBagOStuff
;
14 use Wikimedia\TestingAccessWrapper
;
18 * @covers \MediaWiki\Auth\Throttler
20 class ThrottlerTest
extends MediaWikiIntegrationTestCase
{
22 public function setUp(): void
{
24 // Avoid issues where extensions attempt to interact with the DB when handling this hook then causing these
26 $this->clearHook( 'AuthenticationAttemptThrottled' );
29 public function testConstructor() {
30 $cache = new HashBagOStuff();
31 $logger = $this->getMockBuilder( AbstractLogger
::class )
32 ->onlyMethods( [ 'log' ] )
33 ->getMockForAbstractClass();
35 $throttler = new Throttler(
36 [ [ 'count' => 123, 'seconds' => 456 ] ],
37 [ 'type' => 'foo', 'cache' => $cache ]
39 $throttler->setLogger( $logger );
40 $throttlerPriv = TestingAccessWrapper
::newFromObject( $throttler );
41 $this->assertSame( [ [ 'count' => 123, 'seconds' => 456 ] ], $throttlerPriv->conditions
);
42 $this->assertSame( 'foo', $throttlerPriv->type
);
43 $this->assertSame( $cache, $throttlerPriv->cache
);
44 $this->assertSame( $logger, $throttlerPriv->logger
);
46 $throttler = new Throttler( [ [ 'count' => 123, 'seconds' => 456 ] ] );
47 $throttler->setLogger( new NullLogger() );
48 $throttlerPriv = TestingAccessWrapper
::newFromObject( $throttler );
49 $this->assertSame( [ [ 'count' => 123, 'seconds' => 456 ] ], $throttlerPriv->conditions
);
50 $this->assertSame( 'custom', $throttlerPriv->type
);
51 $this->assertInstanceOf( BagOStuff
::class, $throttlerPriv->cache
);
52 $this->assertInstanceOf( LoggerInterface
::class, $throttlerPriv->logger
);
54 $this->overrideConfigValue(
55 MainConfigNames
::PasswordAttemptThrottle
,
56 [ [ 'count' => 321, 'seconds' => 654 ] ]
58 $throttler = new Throttler();
59 $throttler->setLogger( new NullLogger() );
60 $throttlerPriv = TestingAccessWrapper
::newFromObject( $throttler );
61 $this->assertSame( [ [ 'count' => 321, 'seconds' => 654 ] ], $throttlerPriv->conditions
);
62 $this->assertSame( 'password', $throttlerPriv->type
);
63 $this->assertInstanceOf( BagOStuff
::class, $throttlerPriv->cache
);
64 $this->assertInstanceOf( LoggerInterface
::class, $throttlerPriv->logger
);
67 new Throttler( [], [ 'foo' => 1, 'bar' => 2, 'baz' => 3 ] );
68 $this->fail( 'Expected exception not thrown' );
69 } catch ( InvalidArgumentException
$ex ) {
70 $this->assertSame( 'unrecognized parameters: foo, bar, baz', $ex->getMessage() );
75 * @dataProvider provideNormalizeThrottleConditions
77 public function testNormalizeThrottleConditions( $condition, $normalized ) {
78 $throttler = new Throttler( $condition );
79 $throttler->setLogger( new NullLogger() );
80 $throttlerPriv = TestingAccessWrapper
::newFromObject( $throttler );
81 $this->assertSame( $normalized, $throttlerPriv->conditions
);
84 public static function provideNormalizeThrottleConditions() {
91 [ 'count' => 1, 'seconds' => 2 ],
92 [ [ 'count' => 1, 'seconds' => 2 ] ],
95 [ [ 'count' => 1, 'seconds' => 2 ], [ 'count' => 2, 'seconds' => 3 ] ],
96 [ [ 'count' => 1, 'seconds' => 2 ], [ 'count' => 2, 'seconds' => 3 ] ],
101 public function testNormalizeThrottleConditions2() {
102 $priv = TestingAccessWrapper
::newFromClass( Throttler
::class );
103 $this->assertSame( [], $priv->normalizeThrottleConditions( null ) );
104 $this->assertSame( [], $priv->normalizeThrottleConditions( 'bad' ) );
107 public function testIncrease() {
108 $cache = new HashBagOStuff();
109 $throttler = new Throttler( [
110 [ 'count' => 2, 'seconds' => 10, ],
111 [ 'count' => 4, 'seconds' => 15, 'allIPs' => true ],
112 ], [ 'cache' => $cache ] );
113 $throttler->setLogger( new NullLogger() );
115 $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
116 $this->assertFalse( $result, 'should not throttle' );
118 $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
119 $this->assertFalse( $result, 'should not throttle' );
121 $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
122 $this->assertSame( [ 'throttleIndex' => 0, 'count' => 2, 'wait' => 10 ], $result );
124 $result = $throttler->increase( 'OtherUser', '1.2.3.4' );
125 $this->assertFalse( $result, 'should not throttle' );
127 $result = $throttler->increase( 'SomeUser', '2.3.4.5' );
128 $this->assertFalse( $result, 'should not throttle' );
130 $result = $throttler->increase( 'SomeUser', '3.4.5.6' );
131 $this->assertFalse( $result, 'should not throttle' );
133 $result = $throttler->increase( 'SomeUser', '3.4.5.6' );
134 $this->assertSame( [ 'throttleIndex' => 1, 'count' => 4, 'wait' => 15 ], $result );
137 public function testZeroCount() {
138 $cache = new HashBagOStuff();
139 $throttler = new Throttler( [ [ 'count' => 0, 'seconds' => 10 ] ], [ 'cache' => $cache ] );
140 $throttler->setLogger( new NullLogger() );
142 $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
143 $this->assertFalse( $result, 'should not throttle, count=0 is ignored' );
145 $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
146 $this->assertFalse( $result, 'should not throttle, count=0 is ignored' );
148 $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
149 $this->assertFalse( $result, 'should not throttle, count=0 is ignored' );
152 public function testNamespacing() {
153 $cache = new HashBagOStuff();
154 $throttler1 = new Throttler( [ [ 'count' => 1, 'seconds' => 10 ] ],
155 [ 'cache' => $cache, 'type' => 'foo' ] );
156 $throttler2 = new Throttler( [ [ 'count' => 1, 'seconds' => 10 ] ],
157 [ 'cache' => $cache, 'type' => 'foo' ] );
158 $throttler3 = new Throttler( [ [ 'count' => 1, 'seconds' => 10 ] ],
159 [ 'cache' => $cache, 'type' => 'bar' ] );
160 $throttler1->setLogger( new NullLogger() );
161 $throttler2->setLogger( new NullLogger() );
162 $throttler3->setLogger( new NullLogger() );
164 $throttled = [ 'throttleIndex' => 0, 'count' => 1, 'wait' => 10 ];
166 $result = $throttler1->increase( 'SomeUser', '1.2.3.4' );
167 $this->assertFalse( $result, 'should not throttle' );
169 $result = $throttler1->increase( 'SomeUser', '1.2.3.4' );
170 $this->assertEquals( $throttled, $result, 'should throttle' );
172 $result = $throttler2->increase( 'SomeUser', '1.2.3.4' );
173 $this->assertEquals( $throttled, $result, 'should throttle, same namespace' );
175 $result = $throttler3->increase( 'SomeUser', '1.2.3.4' );
176 $this->assertFalse( $result, 'should not throttle, different namespace' );
179 public function testExpiration() {
180 $cache = $this->getMockBuilder( HashBagOStuff
::class )
181 ->onlyMethods( [ 'add', 'incrWithInit' ] )->getMock();
182 $throttler = new Throttler( [ [ 'count' => 3, 'seconds' => 10 ] ], [ 'cache' => $cache ] );
183 $throttler->setLogger( new NullLogger() );
185 $cache->expects( $this->once() )
186 ->method( 'incrWithInit' )
187 ->with( $this->anything(), 10, 1 );
188 $throttler->increase( 'SomeUser' );
193 public function testException() {
194 $throttler = new Throttler( [ [ 'count' => 3, 'seconds' => 10 ] ] );
195 $throttler->setLogger( new NullLogger() );
196 $this->expectException( InvalidArgumentException
::class );
197 $throttler->increase();
200 public function testLogAndHook() {
201 // Add a implementation of the AuthenticationAttemptThrottled hook that expects no calls.
202 $this->setTemporaryHook(
203 'AuthenticationAttemptThrottled',
205 $this->fail( 'Did not expect the AuthenticationAttemptThrottled hook to be run.' );
208 $cache = new HashBagOStuff();
209 $throttler = new Throttler( [ [ 'count' => 1, 'seconds' => 10 ] ], [ 'cache' => $cache ] );
211 // Make the logger expect no calls
212 $logger = $this->getMockBuilder( AbstractLogger
::class )
213 ->onlyMethods( [ 'log' ] )
214 ->getMockForAbstractClass();
215 $logger->expects( $this->never() )->method( 'log' );
216 $throttler->setLogger( $logger );
217 // Call the increase method and expect that the throttling did not occur.
218 $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
219 $this->assertFalse( $result, 'should not throttle' );
221 // Replace the implementation of the AuthenticationAttemptThrottled hook which one that tests that it is called
222 // with the correct data.
224 $this->setTemporaryHook(
225 'AuthenticationAttemptThrottled',
226 function ( $type, $username, $ip ) use ( &$hookCalled ) {
228 $this->assertSame( 'custom', $type );
229 $this->assertSame( 'SomeUser', $username );
230 $this->assertSame( '1.2.3.4', $ip );
233 // Create a mock logger that expects a call.
234 $logger = $this->getMockBuilder( AbstractLogger
::class )
235 ->onlyMethods( [ 'log' ] )
236 ->getMockForAbstractClass();
237 $logger->expects( $this->once() )->method( 'log' )->with( $this->anything(), $this->anything(), [
238 'throttle' => 'custom',
240 'ipKey' => '1.2.3.4',
241 'username' => 'SomeUser',
246 $throttler->setLogger( $logger );
247 // Call the increase method and expect that the throttling occurred.
248 $result = $throttler->increase( 'SomeUser', '1.2.3.4', 'foo' );
249 $this->assertSame( [ 'throttleIndex' => 0, 'count' => 1, 'wait' => 10 ], $result );
250 $this->assertTrue( $hookCalled );
253 public function testClear() {
254 $cache = new HashBagOStuff();
255 $throttler = new Throttler( [ [ 'count' => 1, 'seconds' => 10 ] ], [ 'cache' => $cache ] );
256 $throttler->setLogger( new NullLogger() );
258 $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
259 $this->assertFalse( $result, 'should not throttle' );
261 $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
262 $this->assertSame( [ 'throttleIndex' => 0, 'count' => 1, 'wait' => 10 ], $result );
264 $result = $throttler->increase( 'OtherUser', '1.2.3.4' );
265 $this->assertFalse( $result, 'should not throttle' );
267 $result = $throttler->increase( 'OtherUser', '1.2.3.4' );
268 $this->assertSame( [ 'throttleIndex' => 0, 'count' => 1, 'wait' => 10 ], $result );
270 $throttler->clear( 'SomeUser', '1.2.3.4' );
272 $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
273 $this->assertFalse( $result, 'should not throttle' );
275 $result = $throttler->increase( 'OtherUser', '1.2.3.4' );
276 $this->assertSame( [ 'throttleIndex' => 0, 'count' => 1, 'wait' => 10 ], $result );