MediaWiki  1.34.0
CaptchaPreAuthenticationProviderTest.php
Go to the documentation of this file.
1 <?php
2 
5 use Wikimedia\TestingAccessWrapper;
6 
11 class CaptchaPreAuthenticationProviderTest extends MediaWikiTestCase {
12  public function setUp() {
13  parent::setUp();
14  $this->setMwGlobals( [
15  'wgCaptchaClass' => SimpleCaptcha::class,
16  'wgCaptchaBadLoginAttempts' => 1,
17  'wgCaptchaBadLoginPerUserAttempts' => 1,
18  'wgCaptchaStorageClass' => CaptchaHashStore::class,
19  'wgMainCacheType' => __METHOD__,
20  ] );
22  CaptchaStore::get()->clearAll();
23  $services = \MediaWiki\MediaWikiServices::getInstance();
24  if ( method_exists( $services, 'getLocalClusterObjectCache' ) ) {
25  $this->setService( 'LocalClusterObjectCache', new HashBagOStuff() );
26  } else {
27  ObjectCache::$instances[__METHOD__] = new HashBagOStuff();
28  }
29  }
30 
31  public function tearDown() {
32  parent::tearDown();
33  // make sure $wgCaptcha resets between tests
34  TestingAccessWrapper::newFromClass( ConfirmEditHooks::class )->instanceCreated = false;
35  }
36 
41  $action, $username, $triggers, $needsCaptcha, $preTestCallback = null
42  ) {
43  $this->setTriggers( $triggers );
44  if ( $preTestCallback ) {
45  $fn = array_shift( $preTestCallback );
46  call_user_func_array( [ $this, $fn ], $preTestCallback );
47  }
48 
50  $request = RequestContext::getMain()->getRequest();
51  $request->setCookie( 'UserName', $username );
52 
53  $provider = new CaptchaPreAuthenticationProvider();
54  $provider->setManager( AuthManager::singleton() );
55  $reqs = $provider->getAuthenticationRequests( $action, [ 'username' => $username ] );
56  if ( $needsCaptcha ) {
57  $this->assertCount( 1, $reqs );
58  $this->assertInstanceOf( CaptchaAuthenticationRequest::class, $reqs[0] );
59  } else {
60  $this->assertEmpty( $reqs );
61  }
62  }
63 
65  return [
66  [ AuthManager::ACTION_LOGIN, null, [], false ],
67  [ AuthManager::ACTION_LOGIN, null, [ 'badlogin' ], false ],
68  [ AuthManager::ACTION_LOGIN, null, [ 'badlogin' ], true, [ 'blockLogin', 'Foo' ] ],
69  [ AuthManager::ACTION_LOGIN, null, [ 'badloginperuser' ], false, [ 'blockLogin', 'Foo' ] ],
70  [ AuthManager::ACTION_LOGIN, 'Foo', [ 'badloginperuser' ], false, [ 'blockLogin', 'Bar' ] ],
71  [ AuthManager::ACTION_LOGIN, 'Foo', [ 'badloginperuser' ], true, [ 'blockLogin', 'Foo' ] ],
72  [ AuthManager::ACTION_LOGIN, null, [ 'badloginperuser' ], true, [ 'flagSession' ] ],
73  [ AuthManager::ACTION_CREATE, null, [], false ],
74  [ AuthManager::ACTION_CREATE, null, [ 'createaccount' ], true ],
75  [ AuthManager::ACTION_CREATE, 'UTSysop', [ 'createaccount' ], false ],
76  [ AuthManager::ACTION_LINK, null, [], false ],
77  [ AuthManager::ACTION_CHANGE, null, [], false ],
78  [ AuthManager::ACTION_REMOVE, null, [], false ],
79  ];
80  }
81 
83  $this->setTriggers( [ 'createaccount' ] );
84  $captcha = new SimpleCaptcha();
85  $provider = new CaptchaPreAuthenticationProvider();
86  $provider->setManager( AuthManager::singleton() );
87 
88  $reqs = $provider->getAuthenticationRequests( AuthManager::ACTION_CREATE,
89  [ 'username' => 'Foo' ] );
90 
91  $this->assertCount( 1, $reqs );
92  $this->assertInstanceOf( CaptchaAuthenticationRequest::class, $reqs[0] );
93 
94  $id = $reqs[0]->captchaId;
95  $data = TestingAccessWrapper::newFromObject( $reqs[0] )->captchaData;
96  $this->assertEquals( $captcha->retrieveCaptcha( $id ), $data + [ 'index' => $id ] );
97  }
98 
102  public function testTestForAuthentication( $req, $isBadLoginTriggered,
103  $isBadLoginPerUserTriggered, $result
104  ) {
105  $this->setMwHook( 'PingLimiter', function ( $user, $action, &$result ) {
106  $result = false;
107  return false;
108  } );
109  CaptchaStore::get()->store( '345', [ 'question' => '2+2', 'answer' => '4' ] );
110  $captcha = $this->getMock( SimpleCaptcha::class,
111  [ 'isBadLoginTriggered', 'isBadLoginPerUserTriggered' ] );
112  $captcha->expects( $this->any() )->method( 'isBadLoginTriggered' )
113  ->willReturn( $isBadLoginTriggered );
114  $captcha->expects( $this->any() )->method( 'isBadLoginPerUserTriggered' )
115  ->willReturn( $isBadLoginPerUserTriggered );
116  $this->setMwGlobals( 'wgCaptcha', $captcha );
117  TestingAccessWrapper::newFromClass( ConfirmEditHooks::class )->instanceCreated = true;
118  $provider = new CaptchaPreAuthenticationProvider();
119  $provider->setManager( AuthManager::singleton() );
120 
121  $status = $provider->testForAuthentication( $req ? [ $req ] : [] );
122  $this->assertEquals( $result, $status->isGood() );
123  }
124 
125  public function provideTestForAuthentication() {
127  $fallback->username = 'Foo';
128  return [
129  // [ auth request, bad login?, bad login per user?, result ]
130  'no need to check' => [ $fallback, false, false, true ],
131  'badlogin' => [ $fallback, true, false, false ],
132  'badloginperuser, no username' => [ null, false, true, true ],
133  'badloginperuser' => [ $fallback, false, true, false ],
134  'non-existent captcha' => [ $this->getCaptchaRequest( '123', '4' ), true, true, false ],
135  'wrong captcha' => [ $this->getCaptchaRequest( '345', '6' ), true, true, false ],
136  'correct captcha' => [ $this->getCaptchaRequest( '345', '4' ), true, true, true ],
137  ];
138  }
139 
143  public function testTestForAccountCreation( $req, $creator, $result, $disableTrigger = false ) {
144  $this->setMwHook( 'PingLimiter', function ( &$user, $action, &$result ) {
145  $result = false;
146  return false;
147  } );
148  $this->setTriggers( $disableTrigger ? [] : [ 'createaccount' ] );
149  CaptchaStore::get()->store( '345', [ 'question' => '2+2', 'answer' => '4' ] );
150  $user = User::newFromName( 'Foo' );
151  $provider = new CaptchaPreAuthenticationProvider();
152  $provider->setManager( AuthManager::singleton() );
153 
154  $status = $provider->testForAccountCreation( $user, $creator, $req ? [ $req ] : [] );
155  $this->assertEquals( $result, $status->isGood() );
156  }
157 
158  public function provideTestForAccountCreation() {
159  $user = User::newFromName( 'Bar' );
160  $sysop = User::newFromName( 'UTSysop' );
161  return [
162  // [ auth request, creator, result, disable trigger? ]
163  'no captcha' => [ null, $user, false ],
164  'non-existent captcha' => [ $this->getCaptchaRequest( '123', '4' ), $user, false ],
165  'wrong captcha' => [ $this->getCaptchaRequest( '345', '6' ), $user, false ],
166  'correct captcha' => [ $this->getCaptchaRequest( '345', '4' ), $user, true ],
167  'user is exempt' => [ null, $sysop, true ],
168  'disabled' => [ null, $user, true, 'disable' ],
169  ];
170  }
171 
172  public function testPostAuthentication() {
173  $this->setTriggers( [ 'badlogin', 'badloginperuser' ] );
174  $captcha = new SimpleCaptcha();
175  $user = User::newFromName( 'Foo' );
176  $anotherUser = User::newFromName( 'Bar' );
177  $provider = new CaptchaPreAuthenticationProvider();
178  $provider->setManager( AuthManager::singleton() );
179 
180  $this->assertFalse( $captcha->isBadLoginTriggered() );
181  $this->assertFalse( $captcha->isBadLoginPerUserTriggered( $user ) );
182 
183  $provider->postAuthentication( $user, \MediaWiki\Auth\AuthenticationResponse::newFail(
184  wfMessage( '?' ) ) );
185 
186  $this->assertTrue( $captcha->isBadLoginTriggered() );
187  $this->assertTrue( $captcha->isBadLoginPerUserTriggered( $user ) );
188  $this->assertFalse( $captcha->isBadLoginPerUserTriggered( $anotherUser ) );
189 
190  $provider->postAuthentication( $user, \MediaWiki\Auth\AuthenticationResponse::newPass( 'Foo' ) );
191 
192  $this->assertFalse( $captcha->isBadLoginPerUserTriggered( $user ) );
193  }
194 
196  $this->setTriggers( [] );
197  $captcha = new SimpleCaptcha();
198  $user = User::newFromName( 'Foo' );
199  $provider = new CaptchaPreAuthenticationProvider();
200  $provider->setManager( AuthManager::singleton() );
201 
202  $this->assertFalse( $captcha->isBadLoginTriggered() );
203  $this->assertFalse( $captcha->isBadLoginPerUserTriggered( $user ) );
204 
205  $provider->postAuthentication( $user, \MediaWiki\Auth\AuthenticationResponse::newFail(
206  wfMessage( '?' ) ) );
207 
208  $this->assertFalse( $captcha->isBadLoginTriggered() );
209  $this->assertFalse( $captcha->isBadLoginPerUserTriggered( $user ) );
210  }
211 
215  public function testPingLimiter( array $attempts ) {
216  $this->mergeMwGlobalArrayValue(
217  'wgRateLimits',
218  [
219  'badcaptcha' => [
220  'user' => [ 1, 1 ],
221  ],
222  ]
223  );
224  $provider = new CaptchaPreAuthenticationProvider();
225  $provider->setManager( AuthManager::singleton() );
226  $providerAccess = TestingAccessWrapper::newFromObject( $provider );
227 
228  foreach ( $attempts as $attempt ) {
229  if ( !empty( $attempts[3] ) ) {
230  $this->setMwHook( 'PingLimiter', function ( &$user, $action, &$result ) {
231  $result = false;
232  return false;
233  } );
234  } else {
235  $this->setMwHook( 'PingLimiter', function () {
236  } );
237  }
238 
239  $captcha = new SimpleCaptcha();
240  CaptchaStore::get()->store( '345', [ 'question' => '7+7', 'answer' => '14' ] );
241  $success = $providerAccess->verifyCaptcha( $captcha, [ $attempts[0] ], $attempts[1] );
242  $this->assertEquals( $attempts[2], $success );
243  }
244  }
245 
246  public function providePingLimiter() {
247  $sysop = User::newFromName( 'UTSysop' );
248  return [
249  // sequence of [ auth request, user, result, disable ping limiter? ]
250  'no failure' => [
251  [ $this->getCaptchaRequest( '345', '14' ), new User(), true ],
252  [ $this->getCaptchaRequest( '345', '14' ), new User(), true ],
253  ],
254  'limited' => [
255  [ $this->getCaptchaRequest( '345', '33' ), new User(), false ],
256  [ $this->getCaptchaRequest( '345', '14' ), new User(), false ],
257  ],
258  'exempt user' => [
259  [ $this->getCaptchaRequest( '345', '33' ), $sysop, false ],
260  [ $this->getCaptchaRequest( '345', '14' ), $sysop, true ],
261  ],
262  'pinglimiter disabled' => [
263  [ $this->getCaptchaRequest( '345', '33' ), new User(), false, 'disable' ],
264  [ $this->getCaptchaRequest( '345', '14' ), new User(), true, 'disable' ],
265  ],
266  ];
267  }
268 
269  protected function getCaptchaRequest( $id, $word, $username = null ) {
270  $req = new CaptchaAuthenticationRequest( $id, [ 'question' => '?', 'answer' => $word ] );
271  $req->captchaWord = $word;
272  $req->username = $username;
273  return $req;
274  }
275 
276  protected function blockLogin( $username ) {
277  $captcha = new SimpleCaptcha();
278  $captcha->increaseBadLoginCounter( $username );
279  }
280 
281  protected function flagSession() {
282  RequestContext::getMain()->getRequest()->getSession()
283  ->set( 'ConfirmEdit:loginCaptchaPerUserTriggered', true );
284  }
285 
286  protected function setTriggers( $triggers ) {
287  $types = [ 'edit', 'create', 'sendemail', 'addurl', 'createaccount', 'badlogin',
288  'badloginperuser' ];
289  $captchaTriggers = array_combine( $types, array_map( function ( $type ) use ( $triggers ) {
290  return in_array( $type, $triggers, true );
291  }, $types ) );
292  $this->setMwGlobals( 'wgCaptchaTriggers', $captchaTriggers );
293  }
294 
301  protected function setMwHook( $hook, callable $callback ) {
302  $this->mergeMwGlobalArrayValue( 'wgHooks', [ $hook => $callback ] );
303  }
304 }
CaptchaPreAuthenticationProviderTest\testGetAuthenticationRequests_store
testGetAuthenticationRequests_store()
Definition: CaptchaPreAuthenticationProviderTest.php:82
CaptchaStore\get
static get()
Get somewhere to store captcha data that will persist between requests.
Definition: CaptchaStore.php:42
HashBagOStuff
Simple store for keeping values in an associative array for the current process.
Definition: HashBagOStuff.php:31
CaptchaPreAuthenticationProviderTest\provideTestForAuthentication
provideTestForAuthentication()
Definition: CaptchaPreAuthenticationProviderTest.php:125
CaptchaPreAuthenticationProviderTest\tearDown
tearDown()
Definition: CaptchaPreAuthenticationProviderTest.php:31
true
return true
Definition: router.php:92
$fallback
$fallback
Definition: MessagesAb.php:11
ObjectCache\$instances
static BagOStuff[] $instances
Map of (id => BagOStuff)
Definition: ObjectCache.php:70
User\newFromName
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:515
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1264
CaptchaPreAuthenticationProviderTest\provideTestForAccountCreation
provideTestForAccountCreation()
Definition: CaptchaPreAuthenticationProviderTest.php:158
$success
$success
Definition: NoLocalSettings.php:42
CaptchaStore\unsetInstanceForTests
static unsetInstanceForTests()
Definition: CaptchaStore.php:54
CaptchaPreAuthenticationProviderTest\setMwHook
setMwHook( $hook, callable $callback)
Set a $wgHooks handler for a given hook and remove all other handlers (though not ones set via Hooks:...
Definition: CaptchaPreAuthenticationProviderTest.php:301
CaptchaPreAuthenticationProviderTest\testGetAuthenticationRequests
testGetAuthenticationRequests( $action, $username, $triggers, $needsCaptcha, $preTestCallback=null)
@dataProvider provideGetAuthenticationRequests
Definition: CaptchaPreAuthenticationProviderTest.php:40
CaptchaPreAuthenticationProviderTest\testTestForAuthentication
testTestForAuthentication( $req, $isBadLoginTriggered, $isBadLoginPerUserTriggered, $result)
@dataProvider provideTestForAuthentication
Definition: CaptchaPreAuthenticationProviderTest.php:102
CaptchaPreAuthenticationProviderTest\provideGetAuthenticationRequests
provideGetAuthenticationRequests()
Definition: CaptchaPreAuthenticationProviderTest.php:64
CaptchaPreAuthenticationProviderTest\testTestForAccountCreation
testTestForAccountCreation( $req, $creator, $result, $disableTrigger=false)
@dataProvider provideTestForAccountCreation
Definition: CaptchaPreAuthenticationProviderTest.php:143
CaptchaPreAuthenticationProviderTest\flagSession
flagSession()
Definition: CaptchaPreAuthenticationProviderTest.php:281
CaptchaPreAuthenticationProviderTest\testPostAuthentication
testPostAuthentication()
Definition: CaptchaPreAuthenticationProviderTest.php:172
MediaWiki
This class serves as a utility class for this extension.
MediaWiki\Auth\UsernameAuthenticationRequest
AuthenticationRequest to ensure something with a username is present.
Definition: UsernameAuthenticationRequest.php:29
CaptchaPreAuthenticationProvider
Definition: CaptchaPreAuthenticationProvider.php:9
CaptchaPreAuthenticationProviderTest\testPingLimiter
testPingLimiter(array $attempts)
@dataProvider providePingLimiter
Definition: CaptchaPreAuthenticationProviderTest.php:215
SimpleCaptcha
Demo CAPTCHA (not for production usage) and base class for real CAPTCHAs.
Definition: SimpleCaptcha.php:9
RequestContext\getMain
static getMain()
Get the RequestContext object associated with the main request.
Definition: RequestContext.php:431
CaptchaPreAuthenticationProviderTest\blockLogin
blockLogin( $username)
Definition: CaptchaPreAuthenticationProviderTest.php:276
MediaWiki\Auth\AuthManager
This serves as the entry point to the authentication system.
Definition: AuthManager.php:85
$status
return $status
Definition: SyntaxHighlight.php:347
CaptchaPreAuthenticationProviderTest\getCaptchaRequest
getCaptchaRequest( $id, $word, $username=null)
Definition: CaptchaPreAuthenticationProviderTest.php:269
CaptchaPreAuthenticationProviderTest\providePingLimiter
providePingLimiter()
Definition: CaptchaPreAuthenticationProviderTest.php:246
CaptchaPreAuthenticationProviderTest\testPostAuthentication_disabled
testPostAuthentication_disabled()
Definition: CaptchaPreAuthenticationProviderTest.php:195
CaptchaPreAuthenticationProviderTest
@covers CaptchaPreAuthenticationProvider @group Database
Definition: CaptchaPreAuthenticationProviderTest.php:11
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:51
CaptchaPreAuthenticationProviderTest\setTriggers
setTriggers( $triggers)
Definition: CaptchaPreAuthenticationProviderTest.php:286
CaptchaAuthenticationRequest
Generic captcha authentication request class.
Definition: CaptchaAuthenticationRequest.php:10
CaptchaPreAuthenticationProviderTest\setUp
setUp()
Definition: CaptchaPreAuthenticationProviderTest.php:12
$type
$type
Definition: testCompression.php:48