Passed
Push — master ( f3ad4b...c4e72e )
by Robbie
05:06
created

RegisterHandlerTest::registerProvider()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 91
Code Lines 58

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 58
c 2
b 0
f 0
nc 1
nop 0
dl 0
loc 91
rs 8.9163

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\WebAuthn\Tests;
4
5
use Exception;
6
use PHPUnit_Framework_MockObject_MockObject;
7
use Psr\Log\LoggerInterface;
8
use SilverStripe\Control\HTTPRequest;
9
use SilverStripe\Core\Injector\Injector;
10
use SilverStripe\Dev\SapphireTest;
11
use SilverStripe\MFA\State\Result;
12
use SilverStripe\MFA\Store\SessionStore;
13
use SilverStripe\Security\Member;
14
use SilverStripe\WebAuthn\CredentialRepository;
15
use SilverStripe\WebAuthn\RegisterHandler;
16
use Webauthn\AttestationStatement\AttestationObject;
17
use Webauthn\AttestedCredentialData;
18
use Webauthn\AuthenticatorAssertionResponse;
19
use Webauthn\AuthenticatorAttestationResponse;
20
use Webauthn\AuthenticatorAttestationResponseValidator;
21
use Webauthn\AuthenticatorData;
22
use Webauthn\AuthenticatorResponse;
23
use Webauthn\AuthenticatorSelectionCriteria;
24
use Webauthn\PublicKeyCredential;
25
use Webauthn\PublicKeyCredentialCreationOptions;
26
use Webauthn\PublicKeyCredentialLoader;
27
use Webauthn\PublicKeyCredentialSource;
28
29
class RegisterHandlerTest extends SapphireTest
30
{
31
    protected $usesDatabase = true;
32
33
    /**
34
     * @var RegisterHandler
35
     */
36
    protected $handler;
37
38
    /**
39
     * @var Member
40
     */
41
    protected $member;
42
43
    /**
44
     * @var HTTPRequest
45
     */
46
    protected $request;
47
48
    /**
49
     * @var SessionStore
50
     */
51
    protected $store;
52
53
    /**
54
     * @var array
55
     */
56
    protected $originalServer;
57
58
    protected function setUp()
59
    {
60
        parent::setUp();
61
62
        $this->request = new HTTPRequest('GET', '/');
63
        $this->handler = Injector::inst()->create(RegisterHandler::class);
64
65
        $memberID = $this->logInWithPermission();
66
        /** @var Member $member */
67
        $this->member = Member::get()->byID($memberID);
68
69
        $this->store = new SessionStore($this->member);
70
71
        $this->originalServer = $_SERVER;
72
73
        // Set default configuration settings
74
        RegisterHandler::config()->set(
75
            'authenticator_attachment',
76
            AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM
77
        );
78
    }
79
80
    protected function tearDown()
81
    {
82
        $_SERVER = $this->originalServer;
83
84
        parent::tearDown();
85
    }
86
87
    /**
88
     * @param string $baseUrl
89
     * @param string $expected
90
     * @dataProvider hostProvider
91
     */
92
    public function testRelyingPartyEntityDomainIncludesSilverStripeDomain(string $baseUrl, string $expected)
93
    {
94
        $_SERVER['HTTP_HOST'] = $baseUrl;
95
96
        $result = $this->handler->start($this->store);
97
        $this->assertArrayHasKey('keyData', $result);
98
99
        /** @var PublicKeyCredentialCreationOptions $options */
100
        $options = $result['keyData'];
101
        $this->assertInstanceOf(PublicKeyCredentialCreationOptions::class, $options);
102
103
        $relyingPartyEntity = $options->getRp();
104
        $this->assertSame(
105
            $expected,
106
            $relyingPartyEntity->getId(),
107
            'Relying party entity should identify the current SilverStripe domain'
108
        );
109
    }
110
111
    /**
112
     * @return array
113
     */
114
    public function hostProvider(): array
115
    {
116
        return [
117
            'domain only' => ['http://example.com', 'example.com'],
118
            'domain with port' => ['https://example.com:8080', 'example.com'],
119
            'subdomain' => ['https://www.example.com', 'www.example.com'],
120
            'subdomain with port' => ['http://my.example.com:8887', 'my.example.com'],
121
            'subfolder' => ['https://example.com/mysite', 'example.com'],
122
            'subfolder with port' => ['http://example.com:8080/mysite', 'example.com'],
123
            'subdomain with subfolder' => ['http://my.example.com/mysite', 'my.example.com'],
124
            'subdomain with port and subfolder' => ['https://my.example.com:8080/mysite', 'my.example.com'],
125
            'credentials with domain and trailing slash' => ['http://foo:[email protected]/', 'example.com'],
126
        ];
127
    }
128
129
    public function testAuthenticatorSelectionCriteriaRequiresCrossPlatformAttachmentByDefault()
130
    {
131
        $result = $this->handler->start($this->store);
132
        $this->assertArrayHasKey('keyData', $result);
133
134
        /** @var PublicKeyCredentialCreationOptions $options */
135
        $options = $result['keyData'];
136
        $this->assertInstanceOf(PublicKeyCredentialCreationOptions::class, $options);
137
138
        $authenticatorSelection = $options->getAuthenticatorSelection();
139
        $this->assertSame(
140
            AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM,
141
            $authenticatorSelection->getAuthenticatorAttachment()
142
        );
143
    }
144
145
    public function testStart()
146
    {
147
        $result = $this->handler->start($this->store);
148
        $this->assertArrayHasKey('keyData', $result);
149
150
        /** @var PublicKeyCredentialCreationOptions $options */
151
        $options = $result['keyData'];
152
        $this->assertInstanceOf(PublicKeyCredentialCreationOptions::class, $options);
153
    }
154
155
    public function testRegisterReturnsErrorWhenRequiredInformationIsMissing()
156
    {
157
        $result = $this->handler->register($this->request, $this->store);
158
159
        $this->assertFalse($result->isSuccessful());
160
        $this->assertContains('Incomplete data', $result->getMessage());
161
    }
162
163
    /**
164
     * @param AuthenticatorResponse $mockResponse
165
     * @param Result $expectedResult
166
     * @param int $expectedCredentialCount
167
     * @param callable $responseValidatorMockCallback
168
     * @throws Exception
169
     * @dataProvider registerProvider
170
     */
171
    public function testRegister(
172
        $mockResponse,
173
        $expectedResult,
174
        $expectedCredentialCount,
175
        callable $responseValidatorMockCallback = null,
176
        callable $storeModifier = null
177
    ) {
178
        /** @var RegisterHandler&PHPUnit_Framework_MockObject_MockObject $handlerMock */
179
        $handlerMock = $this->getMockBuilder(RegisterHandler::class)
180
            ->setMethods(['getPublicKeyCredentialLoader', 'getAuthenticatorAttestationResponseValidator'])
181
            ->getMock();
182
183
        $responseValidatorMock = $this->createMock(AuthenticatorAttestationResponseValidator::class);
184
        // Allow the data provider to customise the validation check handling
185
        if ($responseValidatorMockCallback) {
186
            $responseValidatorMockCallback($responseValidatorMock);
187
        }
188
        $handlerMock->expects($this->any())->method('getAuthenticatorAttestationResponseValidator')
189
            ->willReturn($responseValidatorMock);
190
191
        $loggerMock = $this->createMock(LoggerInterface::class);
192
        $handlerMock->setLogger($loggerMock);
193
194
        $loaderMock = $this->createMock(PublicKeyCredentialLoader::class);
195
        $handlerMock->expects($this->once())->method('getPublicKeyCredentialLoader')->willReturn($loaderMock);
196
197
        $publicKeyCredentialMock = $this->createMock(PublicKeyCredential::class);
198
        $loaderMock->expects($this->once())->method('load')->with('example')->willReturn(
199
            $publicKeyCredentialMock
200
        );
201
202
        $publicKeyCredentialMock->expects($this->any())->method('getResponse')->willReturn($mockResponse);
203
204
        $this->request->setBody(json_encode([
205
            'credentials' => base64_encode('example'),
206
        ]));
207
208
        if ($storeModifier) {
209
            $storeModifier($this->store);
210
        }
211
212
        $result = $handlerMock->register($this->request, $this->store);
213
214
        $this->assertSame($expectedResult->isSuccessful(), $result->isSuccessful());
215
        if ($expectedResult->getMessage()) {
216
            $this->assertContains($expectedResult->getMessage(), $result->getMessage());
217
        }
218
219
        $this->assertCount(
220
            $expectedCredentialCount,
221
            $result->getContext(),
222
            'The number of credentials stored is expected'
223
        );
224
    }
225
226
    /**
227
     * Some centralised or reusable logic for testRegister. Note that some of the mocks are only used in some of the
228
     * provided data scenarios, but any expected call numbers are based on all scenarios being run.
229
     *
230
     * @return array[]
231
     */
232
    public function registerProvider()
233
    {
234
        // phpcs:disable
235
        $testSource = PublicKeyCredentialSource::createFromArray([
236
            'publicKeyCredentialId' => 'g8e1UH4B1gUYl_7AiDXHTp8SE3cxYnpC6jF3Fo0KMm79FNN_e34hDE1Mnd4FSOoNW6B-p7xB2tqj28svkJQh1Q',
237
            'type' => 'public-key',
238
            'transports' => [],
239
            'attestationType' => 'none',
240
            'trustPath' => [
241
                'type' => 'empty',
242
            ],
243
            'aaguid' => 'AAAAAAAAAAAAAAAAAAAAAA',
244
            'credentialPublicKey' => 'pQECAyYgASFYII3gDdvOBje5JfjNO0VhxE2RrV5XoKqWmCZAmR0f9nFaIlggZOUvkovGH9cfeyfXEpJAVOzR1d-rVRZJvwWJf444aLo',
245
            'userHandle' => 'MQ',
246
            'counter' => 268,
247
        ]);
248
        // phpcs:enable
249
250
        $authDataMock = $this->createMock(AuthenticatorData::class);
251
        $authDataMock->expects($this->exactly(4))->method('hasAttestedCredentialData')
252
            // The first call is the "response indicates incomplete data" test case, second is "valid response",
253
            // third is "invalid response"
254
            ->willReturnOnConsecutiveCalls(false, true, true, true);
255
        $authDataMock->expects($this->any())->method('getAttestedCredentialData')->willReturn(
256
            $testSource->getAttestedCredentialData()
257
        );
258
        $authDataMock->expects($this->any())->method('getSignCount')->willReturn(1);
259
260
        $attestationMock = $this->createMock(AttestationObject::class);
261
        $attestationMock->expects($this->any())->method('getAuthData')->willReturn($authDataMock);
262
263
        $responseMock = $this->createMock(AuthenticatorAttestationResponse::class);
264
        $responseMock->expects($this->any())->method('getAttestationObject')->willReturn($attestationMock);
265
266
        return [
267
            'wrong response return type' => [
268
                // Deliberately the wrong child implementation of \Webauthn\AuthenticatorResponse
269
                $this->createMock(AuthenticatorAssertionResponse::class),
270
                new Result(false, 'Unexpected response type found'),
271
                0,
272
            ],
273
            'response indicates incomplete data' => [
274
                $responseMock,
275
                new Result(false, 'Incomplete data, required information missing'),
276
                0,
277
            ],
278
            'valid response' => [
279
                $responseMock,
280
                new Result(true),
281
                1,
282
                function (PHPUnit_Framework_MockObject_MockObject $responseValidatorMock) {
283
                    // Specifically setting expectations for the result of the response validator's "check" call
284
                    $responseValidatorMock->expects($this->once())->method('check')->willReturn(true);
285
                },
286
            ],
287
            'valid response with existing credential' => [
288
                $responseMock,
289
                new Result(true),
290
                1,
291
                function (PHPUnit_Framework_MockObject_MockObject $responseValidatorMock) {
292
                    // Specifically setting expectations for the result of the response validator's "check" call
293
                    $responseValidatorMock->expects($this->once())->method('check')->willReturn(true);
294
                },
295
                function (SessionStore $store) use ($testSource) {
0 ignored issues
show
Unused Code introduced by
The import $testSource is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
296
                    $repo = new CredentialRepository((string) $store->getMember()->ID);
297
                    // phpcs:disable
298
                    $repo->saveCredentialSource(PublicKeyCredentialSource::createFromArray([
299
                        'publicKeyCredentialId' => 'g8e1UH4B1gUYl_7AiDXHTp8SE3cxYnpC6jF3Fo0KMm79FNN_e34hDE1Mnd4FSOoNW245125129518925891',
300
                        'type' => 'public-key',
301
                        'transports' => [],
302
                        'attestationType' => 'none',
303
                        'trustPath' => [
304
                            'type' => 'empty',
305
                        ],
306
                        'aaguid' => 'AAAAAAAAAAAAAAAAAAAAAA',
307
                        'credentialPublicKey' => 'pQECAyYgASFYII3gDdvOBje5JfjNO0VhxE2RrV5XoKqWmCZAmR0f9nFaIlggZOUvkovGH9cfeyfXEpJAVOzR1d-rVRZJvwWJf444aLo',
308
                        'userHandle' => 'MQ',
309
                        'counter' => 268,
310
                    ]));
311
                    // phpcs:enable
312
                    $store->addState(['repository' => $repo]);
313
                },
314
            ],
315
            'invalid response' => [
316
                $responseMock,
317
                new Result(false, 'I am a test'),
318
                0,
319
                function (PHPUnit_Framework_MockObject_MockObject $responseValidatorMock) {
320
                    // Specifically setting expectations for the result of the response validator's "check" call
321
                    $responseValidatorMock->expects($this->once())->method('check')
322
                        ->willThrowException(new Exception('I am a test'));
323
                },
324
            ],
325
        ];
326
    }
327
}
328