Passed
Pull Request — master (#205)
by Robbie
02:20
created

testRemoveRegistrationRequiresDeleteHTTPMethod()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 0
dl 0
loc 10
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\MFA\Tests\Controller;
4
5
use PHPUnit_Framework_MockObject_MockObject;
6
use Psr\Log\LoggerInterface;
7
use SilverStripe\Admin\AdminRootController;
8
use SilverStripe\Control\Controller;
9
use SilverStripe\Control\HTTPRequest;
10
use SilverStripe\Core\Config\Config;
11
use SilverStripe\Core\Injector\Injector;
12
use SilverStripe\Dev\FunctionalTest;
13
use SilverStripe\MFA\Controller\AdminRegistrationController;
14
use SilverStripe\MFA\Extension\MemberExtension;
15
use SilverStripe\MFA\Model\RegisteredMethod;
16
use SilverStripe\MFA\Service\MethodRegistry;
17
use SilverStripe\MFA\Service\RegisteredMethodManager;
18
use SilverStripe\MFA\State\AvailableMethodDetails;
19
use SilverStripe\MFA\Store\SessionStore;
20
use SilverStripe\MFA\Tests\Stub\BasicMath\Method as BasicMathMethod;
21
use SilverStripe\ORM\ValidationException;
22
use SilverStripe\Security\Member;
23
use SilverStripe\Security\Security;
24
use SilverStripe\Security\SecurityToken;
25
use SilverStripe\SecurityExtensions\Service\SudoModeServiceInterface;
26
27
class AdminRegistrationControllerTest extends FunctionalTest
28
{
29
    protected static $fixture_file = 'AdminRegistrationControllerTest.yml';
30
31
    protected function setUp()
32
    {
33
        parent::setUp();
34
35
        MethodRegistry::config()->set('methods', [
36
            BasicMathMethod::class,
37
        ]);
38
39
        /** @var SudoModeServiceInterface&PHPUnit_Framework_MockObject_MockObject $sudoModeService */
40
        $sudoModeService = $this->createMock(SudoModeServiceInterface::class);
41
        $sudoModeService->expects($this->any())->method('check')->willReturn(true);
42
        Injector::inst()->registerService($sudoModeService, SudoModeServiceInterface::class);
43
    }
44
45
    public function testStartRegistrationAssertsValidMethod()
46
    {
47
        $this->logInAs($this->objFromFixture(Member::class, 'sally_smith'));
48
49
        $result = $this->get(Controller::join_links(AdminRootController::admin_url(), 'mfa', 'register/foo'));
50
51
        $this->assertSame(400, $result->getStatusCode());
52
        $this->assertContains('No such method is available', $result->getBody());
53
    }
54
55
    public function testStartRegistrationEnforcesSudoMode()
56
    {
57
        $this->logInAs($this->objFromFixture(Member::class, 'sally_smith'));
58
59
        /** @var SudoModeServiceInterface&PHPUnit_Framework_MockObject_MockObject $sudoModeService */
60
        $sudoModeService = $this->createMock(SudoModeServiceInterface::class);
61
        $sudoModeService->expects($this->any())->method('check')->willReturn(false);
62
        Injector::inst()->registerService($sudoModeService, SudoModeServiceInterface::class);
63
64
        $result = $this->get(Controller::join_links(AdminRootController::admin_url(), 'mfa', 'register/foo'));
65
66
        $this->assertSame(400, $result->getStatusCode());
67
        $this->assertContains('Invalid session. Please refresh and try again.', (string) $result->getBody());
68
    }
69
70
    public function testStartRegistrationReturns200Response()
71
    {
72
        $this->logInAs($this->objFromFixture(Member::class, 'sally_smith'));
73
        $method = new BasicMathMethod();
74
75
        $result = $this->get(
76
            Controller::join_links(
77
                AdminRootController::admin_url(),
78
                'mfa',
79
                'register',
80
                $method->getURLSegment()
81
            )
82
        );
83
84
        $this->assertSame(200, $result->getStatusCode());
85
    }
86
87
    public function testFinishRegistrationGracefullyHandlesInvalidSessions()
88
    {
89
        $this->logInAs($this->objFromFixture(Member::class, 'sally_smith'));
90
        $method = new BasicMathMethod();
91
92
        $result = $this->post(
93
            Controller::join_links(
94
                AdminRootController::admin_url(),
95
                'mfa',
96
                'register',
97
                $method->getURLSegment()
98
            ),
99
            ['dummy' => 'data']
100
        );
101
102
        $this->assertSame(400, $result->getStatusCode());
103
        $this->assertContains('Invalid session', $result->getBody());
104
    }
105
106
    public function testFinishRegistrationAssertsValidMethod()
107
    {
108
        /** @var Member $member */
109
        $member = $this->objFromFixture(Member::class, 'sally_smith');
110
        $this->logInAs($member);
111
        $method = new BasicMathMethod();
112
113
        $store = new SessionStore($member);
114
        $store->setMethod($method->getURLSegment());
115
        $this->session()->set(SessionStore::SESSION_KEY, $store);
116
117
        $result = $this->post(
118
            Controller::join_links(
119
                AdminRootController::admin_url(),
120
                'mfa',
121
                'register',
122
                'foo'
123
            ),
124
            ['dummy' => 'data']
125
        );
126
127
        $this->assertSame(400, $result->getStatusCode());
128
        $this->assertContains('No such method is available', $result->getBody());
129
    }
130
131
    public function testFinishRegistrationCompletesWhenValid()
132
    {
133
        /** @var Member $member */
134
        $member = $this->objFromFixture(Member::class, 'sally_smith');
135
        $this->logInAs($member);
136
        $method = new BasicMathMethod();
137
138
        $store = new SessionStore($member);
139
        $store->setMethod($method->getURLSegment());
140
        $this->session()->set(SessionStore::SESSION_KEY, $store);
141
142
        $result = $this->post(
143
            Controller::join_links(
144
                AdminRootController::admin_url(),
145
                'mfa',
146
                'register',
147
                $method->getURLSegment()
148
            ),
149
            ['dummy' => 'data'],
150
            null,
151
            $this->session(),
152
            json_encode(['number' => 7])
153
        );
154
155
        $this->assertSame(201, $result->getStatusCode());
156
    }
157
158
    public function testRemoveRegistrationChecksCSRF()
159
    {
160
        SecurityToken::enable();
161
162
        $controller = new AdminRegistrationController();
163
        $request = new HTTPRequest('DELETE', '');
164
        $response = $controller->removeRegisteredMethod($request);
165
166
        $this->assertSame(400, $response->getStatusCode());
167
        $this->assertContains('Request timed out', $response->getBody());
168
169
        $token = SecurityToken::inst();
170
        $request = new HTTPRequest('DELETE', '', [$token->getName() => $token->getValue()]);
171
172
        $response = $controller->removeRegisteredMethod($request);
173
174
        $this->assertNotContains('Request timed out', $response->getBody());
175
    }
176
177
    public function testRemoveRegistrationRequiresDeleteHTTPMethod()
178
    {
179
        $controller = new AdminRegistrationController();
180
181
        // Method not even provided
182
        $request = new HTTPRequest('POST', '');
183
        $response = $controller->removeRegisteredMethod($request);
184
185
        $this->assertSame(400, $response->getStatusCode());
186
        $this->assertContains('Wrong HTTP request method used', $response->getBody());
187
    }
188
189
    public function testRemoveRegistrationRequiresMethod()
190
    {
191
        $this->logInWithPermission();
192
193
        // Prep a mock for deleting methods
194
        $registeredMethodManager = $this->scaffoldRegisteredMethodManagerMock();
195
196
        $controller = new AdminRegistrationController();
197
198
        // Method not even provided
199
        $request = new HTTPRequest('DELETE', '');
200
        $response = $controller->removeRegisteredMethod($request);
201
202
        $this->assertSame(400, $response->getStatusCode());
203
        $this->assertContains('No such method is available', $response->getBody());
204
205
        // Method provided but non-existing
206
        $request = new HTTPRequest('DELETE', '');
207
        $request->setRouteParams(['Method' => 'fake123']);
208
        $response = $controller->removeRegisteredMethod($request);
209
210
        $this->assertSame(400, $response->getStatusCode());
211
        $this->assertContains('No such method is available', $response->getBody());
212
213
        // Existing method
214
        $request = new HTTPRequest('DELETE', '');
215
        $basicMathMethod = new BasicMathMethod();
216
        $request->setRouteParams(['Method' => $basicMathMethod->getURLSegment()]);
217
        $registeredMethodManager->expects($this->once())->method('deleteFromMember')->willReturn(true);
218
        $response = $controller->removeRegisteredMethod($request);
219
220
        $this->assertSame(200, $response->getStatusCode());
221
        $this->assertTrue(json_decode($response->getBody())->success);
222
    }
223
224
    public function testRemoveRegistrationSuccessIsReflectedInResponse()
225
    {
226
        $this->logInWithPermission();
227
228
        // Prep a mock for deleting methods
229
        $registeredMethodManager = $this->scaffoldRegisteredMethodManagerMock();
230
231
        $controller = new AdminRegistrationController();
232
233
        $request = new HTTPRequest('DELETE', '');
234
        $basicMathMethod = new BasicMathMethod();
235
        $request->setRouteParams(['Method' => $basicMathMethod->getURLSegment()]);
236
237
        $registeredMethodManager->expects($this->exactly(2))->method('deleteFromMember')->willReturn(true, false);
238
239
        $response = $controller->removeRegisteredMethod($request);
240
241
        $this->assertSame(200, $response->getStatusCode());
242
        $this->assertTrue(json_decode($response->getBody())->success);
243
244
        $response = $controller->removeRegisteredMethod($request);
245
246
        $this->assertSame(400, $response->getStatusCode());
247
        $this->assertContains('Could not delete the specified method from the user', $response->getBody());
248
    }
249
250
    public function testRemoveRegistrationSuccessResponseIncludesTheNowAvailableMethod()
251
    {
252
        $this->logInWithPermission();
253
254
        // Prep a mock for deleting methods
255
        $registeredMethodManager = $this->scaffoldRegisteredMethodManagerMock();
256
257
        $controller = new AdminRegistrationController();
258
259
        $request = new HTTPRequest('DELETE', '');
260
        $basicMathMethod = new BasicMathMethod();
261
        $request->setRouteParams(['Method' => $basicMathMethod->getURLSegment()]);
262
        $registeredMethodManager->expects($this->once())->method('deleteFromMember')->willReturn(true);
263
264
        $expectedAvailableMethod = new AvailableMethodDetails($basicMathMethod);
265
266
        $response = $controller->removeRegisteredMethod($request);
267
268
        $this->assertSame(
269
            $expectedAvailableMethod->jsonSerialize(),
270
            json_decode($response->getBody(), true)['availableMethod']
271
        );
272
    }
273
274
    public function testRemoveRegistrationSuccessIndicatesIfTheBackupMethodIsRegistered()
275
    {
276
        $this->logInWithPermission();
277
278
        // Prep a mock for deleting methods
279
        $registeredMethodManager = $this->scaffoldRegisteredMethodManagerMock();
280
281
        $controller = new AdminRegistrationController();
282
283
        $request = new HTTPRequest('DELETE', '');
284
        $basicMathMethod = new BasicMathMethod();
285
        $request->setRouteParams(['Method' => $basicMathMethod->getURLSegment()]);
286
        $registeredMethodManager->expects($this->any())->method('deleteFromMember')->willReturn(true);
287
288
        // Test when there's no backup method registered
289
        Config::modify()->set(MethodRegistry::class, 'default_backup_method', null);
290
        $response = $controller->removeRegisteredMethod($request);
291
        $this->assertFalse(json_decode($response->getBody())->hasBackupMethod);
292
293
        // Make "basic math" the backup method as it's the only available method
294
        Config::modify()->set(MethodRegistry::class, 'default_backup_method', BasicMathMethod::class);
295
296
        // Mock checking for the registered backup method when it's not registered (first) and then when it is (second)
297
        $registeredMethodManager
298
            ->expects($this->exactly(2))
299
            ->method('getFromMember')
300
            ->willReturn(null, new RegisteredMethod());
301
302
        $response = $controller->removeRegisteredMethod($request);
303
        $this->assertFalse(json_decode($response->getBody())->hasBackupMethod);
304
        $response = $controller->removeRegisteredMethod($request);
305
        $this->assertTrue(json_decode($response->getBody())->hasBackupMethod);
306
    }
307
308
    protected function scaffoldRegisteredMethodManagerMock()
309
    {
310
        $mock = $this->createMock(RegisteredMethodManager::class);
311
        Injector::inst()->registerService($mock, RegisteredMethodManager::class);
312
313
        return $mock;
314
    }
315
316
    public function testEnforcesSudoMode()
317
    {
318
        $sudoModeService = $this->createMock(SudoModeServiceInterface::class);
319
        $sudoModeService->expects($this->any())->method('check')->willReturn(false);
320
        Injector::inst()->registerService($sudoModeService, SudoModeServiceInterface::class);
321
322
        /** @var Member $member */
323
        $member = $this->objFromFixture(Member::class, 'sally_smith');
324
        $this->logInAs($member);
325
        $method = new BasicMathMethod();
326
327
        $store = new SessionStore($member);
328
        $store->setMethod($method->getURLSegment());
329
        $this->session()->set(SessionStore::SESSION_KEY, $store);
330
331
        $result = $this->post(
332
            Controller::join_links(
333
                AdminRootController::admin_url(),
334
                'mfa',
335
                'register',
336
                $method->getURLSegment()
337
            ),
338
            ['dummy' => 'data'],
339
            null,
340
            $this->session(),
341
            json_encode(['number' => 7])
342
        );
343
344
        $this->assertSame(400, $result->getStatusCode());
345
        $this->assertContains('Invalid session', $result->getBody());
346
    }
347
348
    /**
349
     * This controller allows any logged in user to access it, since its methods have their own permission
350
     * check validation already.
351
     *
352
     * See: https://github.com/silverstripe/silverstripe-mfa/issues/171
353
     */
354
    public function testAnyUserCanView()
355
    {
356
        $this->assertFalse(AdminRegistrationController::getRequiredPermissions());
357
    }
358
359
    public function testSetDefaultRegisteredMethodChecksCSRF()
360
    {
361
        SecurityToken::enable();
362
363
        $request = new HTTPRequest('POST', '');
364
        $request->setSession($this->session());
365
        $controller = new AdminRegistrationController();
366
367
        $response = $controller->setDefaultRegisteredMethod($request);
368
369
        $this->assertSame(400, $response->getStatusCode());
370
        $this->assertContains('Request timed out', $response->getBody());
371
372
        $token = SecurityToken::inst();
373
        $request = new HTTPRequest('POST', '', [$token->getName() => $token->getValue()]);
374
        $request->setSession($this->session());
375
376
        $response = $controller->setDefaultRegisteredMethod($request);
377
        $this->assertNotContains('Request timed out', $response->getBody());
378
    }
379
380
    public function testSetDefaultRegisteredMethodFailsWhenMethodWasNotFound()
381
    {
382
        $request = new HTTPRequest('POST', '');
383
        $request->setRouteParams(['Method' => 'doesnotexist']);
384
        $request->setSession($this->session());
385
        $controller = new AdminRegistrationController();
386
387
        $response = $controller->setDefaultRegisteredMethod($request);
388
389
        $this->assertSame(400, $response->getStatusCode());
390
        $this->assertContains('No such method is available', $response->getBody());
391
    }
392
393
    public function testSetDefaultRegisteredMethodFailsWhenRegisteredMethodWasNotFoundForUser()
394
    {
395
        // Arbitrary user, no MFA configured for it
396
        $this->logInWithPermission();
397
398
        $request = new HTTPRequest('POST', '');
399
        $basicMathMethod = new BasicMathMethod();
400
        $request->setRouteParams(['Method' => $basicMathMethod->getURLSegment()]);
401
        $request->setSession($this->session());
402
        $controller = new AdminRegistrationController();
403
404
        $response = $controller->setDefaultRegisteredMethod($request);
405
406
        $this->assertSame(400, $response->getStatusCode());
407
        $this->assertContains('No such registered method is available', $response->getBody());
408
    }
409
410
    public function testSetDefaultRegisteredMethodHandlesExceptionsOnWrite()
411
    {
412
        $registeredMethodManager = $this->scaffoldRegisteredMethodManagerMock();
413
414
        $controller = new AdminRegistrationController();
415
        /** @var LoggerInterface&PHPUnit_Framework_MockObject_MockObject $loggerMock */
416
        $loggerMock = $this->createMock(LoggerInterface::class);
417
        $controller->setLogger($loggerMock);
418
419
        $request = new HTTPRequest('POST', '');
420
        $request->setSession($this->session());
421
        $basicMathMethod = new BasicMathMethod();
422
        $request->setRouteParams(['Method' => $basicMathMethod->getURLSegment()]);
423
424
        $registeredMethodManager
425
            ->expects($this->once())
426
            ->method('getFromMember')
427
            ->willReturn(new RegisteredMethod());
428
429
        /** @var Member&PHPUnit_Framework_MockObject_MockObject $memberMock */
430
        $memberMock = $this->createMock(Member::class);
431
        $memberMock->method('write')->willThrowException(new ValidationException());
432
        Security::setCurrentUser($memberMock);
433
434
        $loggerMock->expects($this->once())->method('debug');
435
        $response = $controller->setDefaultRegisteredMethod($request);
436
        $this->assertSame(400, $response->getStatusCode());
437
        $this->assertContains('Could not set the default method for the user', $response->getBody());
438
    }
439
440
    public function testSetDefaultRegisteredMethod()
441
    {
442
        /** @var Member&MemberExtension $member */
443
        $member = $this->objFromFixture(Member::class, 'sally_smith');
444
        $this->logInAs($member);
445
        // Give Sally basic math
446
        $basicMathMethod = new BasicMathMethod();
447
        RegisteredMethodManager::singleton()->registerForMember($member, $basicMathMethod, ['foo' => 'bar']);
448
449
        // Set basic math as the default method
450
        $controller = new AdminRegistrationController();
451
        $request = new HTTPRequest('POST', '');
452
        $request->setSession($this->session());
453
        $request->setRouteParams(['Method' => $basicMathMethod->getURLSegment()]);
454
455
        $response = $controller->setDefaultRegisteredMethod($request);
456
        $this->assertSame(200, $response->getStatusCode());
457
    }
458
}
459