SpellControllerTest   A
last analyzed

Complexity

Total Complexity 8

Size/Duplication

Total Lines 227
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 7

Importance

Changes 0
Metric Value
wmc 8
lcom 2
cbo 7
dl 0
loc 227
rs 10
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A setUp() 0 20 1
A tearDown() 0 10 2
A testSecurityID() 0 34 1
A testPermissions() 0 26 1
A testBothLangAndLocaleInputResolveToLocale() 0 16 1
A langProvider() 0 30 1
B testInputRejection() 0 61 1
1
<?php
2
3
namespace SilverStripe\SpellCheck\Tests;
4
5
use SilverStripe\Control\Session;
6
use SilverStripe\Core\Config\Config;
7
use SilverStripe\Core\Injector\Injector;
8
use SilverStripe\Dev\FunctionalTest;
9
use SilverStripe\Security\RandomGenerator;
10
use SilverStripe\Security\SecurityToken;
11
use SilverStripe\SpellCheck\Data\SpellProvider;
12
use SilverStripe\SpellCheck\Handling\SpellController;
13
use SilverStripe\SpellCheck\Tests\Stub\SpellProviderStub;
14
15
/**
16
 * Tests the {@see SpellController} class
17
 */
18
class SpellControllerTest extends FunctionalTest
19
{
20
    protected $usesDatabase = true;
21
22
    protected $securityWasEnabled = false;
23
24
    protected function setUp()
25
    {
26
        parent::setUp();
27
28
        $this->securityWasEnabled = SecurityToken::is_enabled();
29
30
        // Reset config
31
        Config::modify()->set(SpellController::class, 'required_permission', 'CMS_ACCESS_CMSMain');
32
        Config::inst()->remove(SpellController::class, 'locales');
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface SilverStripe\Config\Coll...nfigCollectionInterface as the method remove() does only exist in the following implementations of said interface: SilverStripe\Config\Coll...s\DeltaConfigCollection, SilverStripe\Config\Coll...\MemoryConfigCollection.

Let’s take a look at an example:

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

class MyUser implements 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 implementation 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 interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
33
        Config::modify()
34
            ->set(SpellController::class, 'locales', array('en_US', 'en_NZ', 'fr_FR'))
35
            ->set(SpellController::class, 'enable_security_token', true)
36
            ->set(SpellController::class, 'return_errors_as_ok', false);
37
38
        SecurityToken::enable();
39
40
        // Setup mock for testing provider
41
        $spellChecker = new SpellProviderStub;
42
        Injector::inst()->registerService($spellChecker, SpellProvider::class);
43
    }
44
45
    protected function tearDown()
46
    {
47
        if ($this->securityWasEnabled) {
48
            SecurityToken::enable();
49
        } else {
50
            SecurityToken::disable();
51
        }
52
53
        parent::tearDown();
54
    }
55
56
    /**
57
     * Tests security ID check
58
     */
59
    public function testSecurityID()
60
    {
61
        // Mock token
62
        $securityToken = SecurityToken::inst();
63
        $generator = new RandomGenerator();
64
        $token = $generator->randomToken('sha1');
65
        $session = array(
66
            $securityToken->getName() => $token
67
        );
68
        $tokenError = _t(
69
            'SilverStripe\\SpellCheck\\Handling\\SpellController.SecurityMissing',
70
            'Your session has expired. Please refresh your browser to continue.'
71
        );
72
73
        // Test request sans token
74
        $response = $this->get('spellcheck', Injector::inst()->create(Session::class, $session));
75
        $this->assertEquals(400, $response->getStatusCode());
76
        $jsonBody = json_decode($response->getBody());
77
        $this->assertEquals($tokenError, $jsonBody->error);
78
79
        // Test request with correct token (will fail with an unrelated error)
80
        $response = $this->get(
81
            'spellcheck/?SecurityID='.urlencode($token),
82
            Injector::inst()->create(Session::class, $session)
83
        );
84
        $jsonBody = json_decode($response->getBody());
85
        $this->assertNotEquals($tokenError, $jsonBody->error);
86
87
        // Test request with check disabled
88
        Config::modify()->set(SpellController::class, 'enable_security_token', false);
89
        $response = $this->get('spellcheck', Injector::inst()->create(Session::class, $session));
90
        $jsonBody = json_decode($response->getBody());
91
        $this->assertNotEquals($tokenError, $jsonBody->error);
92
    }
93
94
    /**
95
     * Tests permission check
96
     */
97
    public function testPermissions()
98
    {
99
        // Disable security ID for this test
100
        Config::modify()->set(SpellController::class, 'enable_security_token', false);
101
        $securityError = _t('SilverStripe\\SpellCheck\\Handling\\SpellController.SecurityDenied', 'Permission Denied');
102
103
        // Test admin permissions
104
        Config::modify()->set(SpellController::class, 'required_permission', 'ADMIN');
105
        $this->logInWithPermission('ADMIN');
106
        $response = $this->get('spellcheck');
107
        $jsonBody = json_decode($response->getBody());
108
        $this->assertNotEquals($securityError, $jsonBody->error);
109
110
        // Test insufficient permissions
111
        $this->logInWithPermission('CMS_ACCESS_CMSMain');
112
        $response = $this->get('spellcheck');
113
        $this->assertEquals(403, $response->getStatusCode());
114
        $jsonBody = json_decode($response->getBody());
115
        $this->assertEquals($securityError, $jsonBody->error);
116
117
        // Test disabled permissions
118
        Config::modify()->set(SpellController::class, 'required_permission', false);
119
        $response = $this->get('spellcheck');
120
        $jsonBody = json_decode($response->getBody());
121
        $this->assertNotEquals($securityError, $jsonBody->error);
122
    }
123
124
    /**
125
     * @param string $lang
126
     * @param int $expectedStatusCode
127
     * @dataProvider langProvider
128
     */
129
    public function testBothLangAndLocaleInputResolveToLocale($lang, $expectedStatusCode, $errorsAreOk = false)
130
    {
131
        $this->logInWithPermission('ADMIN');
132
        Config::modify()
133
            ->set(SpellController::class, 'enable_security_token', false)
134
            ->set(SpellController::class, 'return_errors_as_ok', $errorsAreOk);
135
136
        $mockData = [
137
            'ajax' => true,
138
            'method' => 'spellcheck',
139
            'lang' => $lang,
140
            'text' => 'Collor is everywhere',
141
        ];
142
        $response = $this->post('spellcheck', $mockData);
143
        $this->assertEquals($expectedStatusCode, $response->getStatusCode());
144
    }
145
146
    /**
147
     * @return array[]
148
     */
149
    public function langProvider()
150
    {
151
        return [
152
            'english_language' => [
153
                'en', // assumes en_US is the default locale for "en" language
154
                200,
155
            ],
156
            'english_locale' => [
157
                'en_NZ',
158
                200,
159
            ],
160
            'invalid_language' => [
161
                'ru',
162
                400,
163
            ],
164
            'invalid_language_returned_as_ok' => [
165
                'ru',
166
                200,
167
                true
168
            ],
169
            'other_valid_language' => [
170
                'fr', // assumes fr_FR is the default locale for "en" language
171
                200,
172
            ],
173
            'other_valid_locale' => [
174
                'fr_FR',
175
                200,
176
            ],
177
        ];
178
    }
179
180
    /**
181
     * Ensure that invalid input is correctly rejected
182
     */
183
    public function testInputRejection()
184
    {
185
        // Disable security ID and permissions for this test
186
        Config::modify()->set(SpellController::class, 'enable_security_token', false);
187
        Config::modify()->set(SpellController::class, 'required_permission', false);
188
        $invalidRequest = _t('SilverStripe\\SpellCheck\\Handling\\SpellController.InvalidRequest', 'Invalid request');
189
190
        // Test spellcheck acceptance
191
        $mockData = [
192
            'method' => 'spellcheck',
193
            'lang' => 'en_NZ',
194
            'text' => 'Collor is everywhere',
195
        ];
196
        $response = $this->post('spellcheck', ['ajax' => true] + $mockData);
197
        $this->assertEquals(200, $response->getStatusCode());
198
        $jsonBody = json_decode($response->getBody());
199
        $this->assertNotEmpty($jsonBody->words);
200
        $this->assertNotEmpty($jsonBody->words->collor);
201
        $this->assertEquals(['collar', 'colour'], $jsonBody->words->collor);
202
203
        // Test non-ajax rejection
204
        $response = $this->post('spellcheck', $mockData);
205
        $this->assertEquals(400, $response->getStatusCode());
206
        $jsonBody = json_decode($response->getBody());
207
        $this->assertEquals($invalidRequest, $jsonBody->error);
208
209
        // Test incorrect method
210
        $dataInvalidMethod = $mockData;
211
        $dataInvalidMethod['method'] = 'validate';
212
        $response = $this->post('spellcheck', ['ajax' => true] + $dataInvalidMethod);
213
        $this->assertEquals(400, $response->getStatusCode());
214
        $jsonBody = json_decode($response->getBody());
215
        $this->assertEquals(
216
            _t(
217
                'SilverStripe\\SpellCheck\\Handling\\SpellController.UnsupportedMethod',
218
                "Unsupported method '{method}'",
219
                array('method' => 'validate')
220
            ),
221
            $jsonBody->error
222
        );
223
224
        // Test missing method
225
        $dataNoMethod = $mockData;
226
        unset($dataNoMethod['method']);
227
        $response = $this->post('spellcheck', ['ajax' => true] + $dataNoMethod);
228
        $this->assertEquals(400, $response->getStatusCode());
229
        $jsonBody = json_decode($response->getBody());
230
        $this->assertEquals($invalidRequest, $jsonBody->error);
231
232
        // Test unsupported locale
233
        $dataWrongLocale = $mockData;
234
        $dataWrongLocale['lang'] = 'de_DE';
235
236
        $response = $this->post('spellcheck', ['ajax' => true] + $dataWrongLocale);
237
        $this->assertEquals(400, $response->getStatusCode());
238
        $jsonBody = json_decode($response->getBody());
239
        $this->assertEquals(_t(
240
            'SilverStripe\\SpellCheck\\Handling\\SpellController.InvalidLocale',
241
            'Not a supported locale'
242
        ), $jsonBody->error);
243
    }
244
}
245