Completed
Push — master ( c8aa2c...434e3d )
by Robbie
12s
created

SpellControllerTest::langProvider()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 25
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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