ApiAuthenticatorTest   A
last analyzed

Complexity

Total Complexity 25

Size/Duplication

Total Lines 295
Duplicated Lines 15.59 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 0
Metric Value
wmc 25
lcom 1
cbo 11
dl 46
loc 295
rs 10
c 0
b 0
f 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
A setUp() 0 18 1
A testCreateTokenNoHeaders() 0 4 1
A testCreateTokenTwoHeaders() 0 6 1
A testCreateTokenInvalidUserId() 0 6 1
A testCreateTokenInvalidTimestamp() 0 6 1
A testCreateToken() 0 15 1
A testAuthenticateTokenOldTimestamp() 10 10 1
A testAuthenticateTokenFutureTimestamp() 10 10 1
A testAuthenticateTokenNoUser() 0 10 1
A testAuthenticateTokenLockedUser() 0 10 1
A testAuthenticateTokenBadToken() 0 8 1
B testAuthenticateToken() 0 28 1
A testSupportsTokenWrongProviderKey() 0 4 1
A testSupportsTokenWrongType() 0 4 1
A testSupportsToken() 0 4 1
A testOnAuthenticationFailureWithAuthenticationException() 13 13 1
A testOnAuthenticationFailureWithBadCredentialsException() 13 13 1
A createRequestMock() 0 12 1
A createToken() 0 8 1
A createUserMock() 0 11 1
A createUserProviderMock() 0 4 1
A mergeHeaders() 0 20 4

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

1
<?php
2
3
namespace Overwatch\UserBundle\Security;
4
5
use Overwatch\UserBundle\Entity\User;
6
use Overwatch\UserBundle\Security\ApiAuthenticator;
7
use Symfony\Component\HttpFoundation\HeaderBag;
8
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
9
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
10
use Symfony\Component\Security\Core\Exception\AuthenticationException;
11
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
12
13
/**
14
 * ApiAuthenticatorTest
15
 *
16
 * @author Zac Sturgess <[email protected]>
17
 */
18
class ApiAuthenticatorTest extends \PHPUnit_Framework_TestCase
19
{
20
    const PROVIDER_KEY = 'overwatch_test';
21
    
22
    private $apiAuth;
23
    private $user;
24
    
25
    public function setUp()
26
    {
27
        $fakeEm = $this->getMockBuilder('Doctrine\ORM\EntityManager')->disableOriginalConstructor()->getMock();
28
        $this->user = $this->createUserMock();
29
        
30
        $fakeEm
31
            ->method('find')
32
            ->will(
33
                $this->returnValueMap([
34
                    ['OverwatchUserBundle:User', 1, null, null, $this->user],
35
                    ['OverwatchUserBundle:User', 2, null, null, null],
36
                    ['OverwatchUserBundle:User', 3, null, null, $this->createUserMock(true)]
37
                ])
38
            )
39
        ;
40
        
41
        $this->apiAuth = new ApiAuthenticator($fakeEm);
42
    }
43
    
44
    /**
45
     * @expectedException Symfony\Component\Security\Core\Exception\BadCredentialsException
46
     * @expectedExceptionMessage API credentials invalid. The user ID, timestamp and token should given.
47
     */
48
    public function testCreateTokenNoHeaders()
49
    {
50
        $this->apiAuth->createToken($this->createRequestMock(), self::PROVIDER_KEY);
51
    }
52
    
53
    /**
54
     * @expectedException Symfony\Component\Security\Core\Exception\BadCredentialsException
55
     * @expectedExceptionMessage API credentials invalid. The user ID, timestamp and token should given.
56
     */
57
    public function testCreateTokenTwoHeaders()
58
    {
59
        $this->apiAuth->createToken($this->createRequestMock([
60
            ApiAuthenticator::USER_ID => null
61
        ]), self::PROVIDER_KEY);
62
    }
63
    
64
    /**
65
     * @expectedException Symfony\Component\Security\Core\Exception\BadCredentialsException
66
     * @expectedExceptionMessage API credentials invalid. The user ID should be an integer.
67
     */
68
    public function testCreateTokenInvalidUserId()
69
    {
70
        $this->apiAuth->createToken($this->createRequestMock([
71
            ApiAuthenticator::USER_ID => 'overwatch_test'
72
        ]), self::PROVIDER_KEY);
73
    }
74
    
75
    /**
76
     * @expectedException Symfony\Component\Security\Core\Exception\BadCredentialsException
77
     * @expectedExceptionMessage API credentials invalid. The timestamp should be an integer.
78
     */
79
    public function testCreateTokenInvalidTimestamp()
80
    {
81
        $this->apiAuth->createToken($this->createRequestMock([
82
            ApiAuthenticator::TIMESTAMP => 'overwatch_test'
83
        ]), self::PROVIDER_KEY);
84
    }
85
    
86
    public function testCreateToken()
87
    {
88
        $token = $this->apiAuth->createToken($this->createRequestMock([
89
            ApiAuthenticator::TIMESTAMP => '111'
90
        ]), self::PROVIDER_KEY);
91
        
92
        $this->assertInstanceOf("Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken", $token);
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal Symfony\Component\Securi...n\PreAuthenticatedToken does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
93
        $this->assertEquals('anon.', $token->getUser());
94
        $this->assertEquals(self::PROVIDER_KEY, $token->getProviderKey());
95
        $this->assertEquals([
96
            ApiAuthenticator::USER_ID   => 1,
97
            ApiAuthenticator::TIMESTAMP => '111',
98
            ApiAuthenticator::TOKEN     => 'abc123'
99
        ], $token->getCredentials());
100
    }
101
    
102
    /**
103
     * @expectedException Symfony\Component\Security\Core\Exception\AuthenticationException
104
     * @expectedExceptionMessage API credentials invalid. The timestamp is more than 60 seconds old.
105
     */
106 View Code Duplication
    public function testAuthenticateTokenOldTimestamp()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
107
    {
108
        $this->apiAuth->authenticateToken(
109
            $this->createToken([
110
                ApiAuthenticator::TIMESTAMP => time() - 61
111
            ]),
112
            $this->createUserProviderMock(),
113
            self::PROVIDER_KEY
114
        );
115
    }
116
    
117
    /**
118
     * @expectedException Symfony\Component\Security\Core\Exception\AuthenticationException
119
     * @expectedExceptionMessage API credentials invalid. The timestamp is more than 60 seconds old.
120
     */
121 View Code Duplication
    public function testAuthenticateTokenFutureTimestamp()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
122
    {
123
        $this->apiAuth->authenticateToken(
124
            $this->createToken([
125
                ApiAuthenticator::TIMESTAMP => time() + 61
126
            ]),
127
            $this->createUserProviderMock(),
128
            self::PROVIDER_KEY
129
        );
130
    }
131
    
132
    /**
133
     * @expectedException Symfony\Component\Security\Core\Exception\AuthenticationException
134
     * @expectedExceptionMessage API credentials invalid. User not found.
135
     */
136
    public function testAuthenticateTokenNoUser()
137
    {
138
        $this->apiAuth->authenticateToken(
139
            $this->createToken([
140
                ApiAuthenticator::USER_ID => 2
141
            ]),
142
            $this->createUserProviderMock(),
143
            self::PROVIDER_KEY
144
        );
145
    }
146
    
147
    /**
148
     * @expectedException Symfony\Component\Security\Core\Exception\AuthenticationException
149
     * @expectedExceptionMessage API credentials invalid. User not found.
150
     */
151
    public function testAuthenticateTokenLockedUser()
152
    {
153
        $this->apiAuth->authenticateToken(
154
            $this->createToken([
155
                ApiAuthenticator::USER_ID => 3
156
            ]),
157
            $this->createUserProviderMock(),
158
            self::PROVIDER_KEY
159
        );
160
    }
161
    
162
    /**
163
     * @expectedException Symfony\Component\Security\Core\Exception\AuthenticationException
164
     * @expectedExceptionMessage API credentials invalid. Token verification failed
165
     */
166
    public function testAuthenticateTokenBadToken()
167
    {
168
        $this->apiAuth->authenticateToken(
169
            $this->createToken([]),
170
            $this->createUserProviderMock(),
171
            self::PROVIDER_KEY
172
        );
173
    }
174
    
175
    public function testAuthenticateToken()
176
    {
177
        $timestamp = time();
178
        $apiToken = hash_hmac(
179
            'sha256',
180
            'timestamp=' . $timestamp,
181
            $this->user->getApiKey()
182
        );
183
        
184
        $token = $this->apiAuth->authenticateToken(
185
            $this->createToken([
186
                ApiAuthenticator::TIMESTAMP => $timestamp,
187
                ApiAuthenticator::TOKEN     => $apiToken
188
            ]),
189
            $this->createUserProviderMock(),
190
            self::PROVIDER_KEY
191
        );
192
        
193
        $this->assertInstanceOf("Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken", $token);
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal Symfony\Component\Securi...n\PreAuthenticatedToken does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
194
        $this->assertEquals($this->user, $token->getUser());
195
        $this->assertEquals(self::PROVIDER_KEY, $token->getProviderKey());
196
        $this->assertEquals([
197
            ApiAuthenticator::USER_ID   => 1,
198
            ApiAuthenticator::TIMESTAMP => $timestamp,
199
            ApiAuthenticator::TOKEN     => $apiToken
200
        ], $token->getCredentials());
201
        $this->assertEquals($this->user->getRoles()[0], $token->getRoles()[0]->getRole());
202
    }
203
    
204
    public function testSupportsTokenWrongProviderKey()
205
    {
206
        $this->assertFalse($this->apiAuth->supportsToken($this->createToken([]), 'totally_not_correct_provider_key'));
207
    }
208
    
209
    public function testSupportsTokenWrongType()
210
    {
211
        $this->assertFalse($this->apiAuth->supportsToken(new UsernamePasswordToken('anon', [], self::PROVIDER_KEY), self::PROVIDER_KEY));
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
212
    }
213
    
214
    public function testSupportsToken()
215
    {
216
        $this->assertTrue($this->apiAuth->supportsToken($this->createToken([]), self::PROVIDER_KEY));
217
    }
218
    
219 View Code Duplication
    public function testOnAuthenticationFailureWithAuthenticationException()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
220
    {
221
        $message = 'Bacon ipsum dolor sit amet';
222
        
223
        $response = $this->apiAuth->onAuthenticationFailure(
224
            $this->createRequestMock(),
225
            new AuthenticationException($message)
226
        );
227
        
228
        $this->assertInstanceOf('Symfony\Component\HttpFoundation\JsonResponse', $response);
229
        $this->assertEquals(json_encode($message), $response->getContent());
230
        $this->assertEquals(401, $response->getStatusCode());
231
    }
232
    
233 View Code Duplication
    public function testOnAuthenticationFailureWithBadCredentialsException()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
234
    {
235
        $message = 'Bacon ipsum dolor sit amet';
236
        
237
        $response = $this->apiAuth->onAuthenticationFailure(
238
            $this->createRequestMock(),
239
            new BadCredentialsException($message)
240
        );
241
        
242
        $this->assertInstanceOf('Symfony\Component\HttpFoundation\JsonResponse', $response);
243
        $this->assertEquals(json_encode($message), $response->getContent());
244
        $this->assertEquals(401, $response->getStatusCode());
245
    }
246
    
247
    /**
248
     * @return \Symfony\Component\HttpFoundation\Request
249
     */
250
    private function createRequestMock(array $headers = null)
251
    {
252
        $headers = $this->mergeHeaders($headers);
253
        
254
        $headerBag = new HeaderBag;
255
        $headerBag->add($headers);
256
        
257
        $fakeReq = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->disableOriginalConstructor()->getMock();
258
        $fakeReq->headers = $headerBag;
259
        
260
        return $fakeReq;
261
    }
262
    
263
    private function createToken(array $headers = null)
264
    {
265
        return new PreAuthenticatedToken(
266
            'anon.',
267
            $this->mergeHeaders($headers),
268
            self::PROVIDER_KEY
269
        );
270
    }
271
    
272
    private function createUserMock($locked = false)
273
    {
274
        $user = new User;
275
        $user
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class FOS\UserBundle\Model\User as the method resetApiKey() does only exist in the following sub-classes of FOS\UserBundle\Model\User: Overwatch\UserBundle\Entity\User. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
276
            ->setEmail('[email protected]')
277
            ->setLocked($locked)
278
            ->resetApiKey()
279
        ;
280
        
281
        return $user;
282
    }
283
    
284
    /**
285
     * @return \Symfony\Component\Security\Core\User\UserProviderInterface
286
     */
287
    private function createUserProviderMock()
288
    {
289
        return $this->getMockBuilder('FOS\UserBundle\Security\UserProvider')->disableOriginalConstructor()->getMock();
290
    }
291
    
292
    private function mergeHeaders(array $headers = null)
293
    {
294
        if ($headers === null) {
295
            $headers = [];
296
        } else {
297
            $headers = array_merge([
298
                ApiAuthenticator::USER_ID   => 1,
299
                ApiAuthenticator::TIMESTAMP => time(),
300
                ApiAuthenticator::TOKEN     => 'abc123'
301
            ], $headers);
302
            
303
            foreach ($headers as $header => $headerValue) {
304
                if ($headerValue === null) {
305
                    unset($headers[$header]);
306
                }
307
            }
308
        }
309
        
310
        return $headers;
311
    }
312
}
313