Completed
Push — master ( e3eba2...8f5698 )
by Robbie
10:09
created

SpellControllerTest::testSecurityID()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 34
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 34
rs 8.8571
c 0
b 0
f 0
cc 1
eloc 22
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->errstr);
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->errstr);
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->errstr);
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->errstr);
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->errstr);
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->errstr);
119
    }
120
121
    /**
122
     * Ensure that invalid input is correctly rejected
123
     */
124
    public function testInputRejection()
125
    {
126
        // Disable security ID and permissions for this test
127
        Config::modify()->set(SpellController::class, 'enable_security_token', false);
128
        Config::modify()->set(SpellController::class, 'required_permission', false);
129
        $invalidRequest = _t('SilverStripe\\SpellCheck\\Handling\\SpellController.InvalidRequest', 'Invalid request');
130
131
        // Test checkWords acceptance
132
        $dataCheckWords = array(
133
            'id' => 'c0',
134
            'method' => 'checkWords',
135
            'params' => array(
136
                'en_NZ',
137
                array('collor', 'colour', 'color', 'onee', 'correct')
138
            )
139
        );
140
        $response = $this->post('spellcheck', array('ajax' => 1, 'json_data' => json_encode($dataCheckWords)));
141
        $this->assertEquals(200, $response->getStatusCode());
142
        $jsonBody = json_decode($response->getBody());
143
        $this->assertEquals('c0', $jsonBody->id);
144
        $this->assertEquals(array("collor", "color", "onee"), $jsonBody->result);
145
146
        // Test getSuggestions acceptance
147
        $dataGetSuggestions = array(
148
            'id' => '//c1//', // Should be reduced to only alphanumeric characters
149
            'method' => 'getSuggestions',
150
            'params' => array(
151
                'en_NZ',
152
                'collor'
153
154
            )
155
        );
156
        $response = $this->post('spellcheck', array('ajax' => 1, 'json_data' => json_encode($dataGetSuggestions)));
157
        $this->assertEquals(200, $response->getStatusCode());
158
        $jsonBody = json_decode($response->getBody());
159
        $this->assertEquals('c1', $jsonBody->id);
160
        $this->assertEquals(array('collar', 'colour'), $jsonBody->result);
161
162
        // Test non-ajax rejection
163
        $response = $this->post('spellcheck', array('json_data' => json_encode($dataCheckWords)));
164
        $this->assertEquals(400, $response->getStatusCode());
165
        $jsonBody = json_decode($response->getBody());
166
        $this->assertEquals($invalidRequest, $jsonBody->error->errstr);
167
168
        // Test incorrect method
169
        $dataInvalidMethod = $dataCheckWords;
170
        $dataInvalidMethod['method'] = 'validate';
171
        $response = $this->post('spellcheck', array('ajax' => 1, 'json_data' => json_encode($dataInvalidMethod)));
172
        $this->assertEquals(400, $response->getStatusCode());
173
        $jsonBody = json_decode($response->getBody());
174
        $this->assertEquals(
175
            _t(
176
                'SilverStripe\\SpellCheck\\Handling\\.UnsupportedMethod',
177
                "Unsupported method '{method}'",
178
                array('method' => 'validate')
179
            ),
180
            $jsonBody->error->errstr
181
        );
182
183
        // Test missing method
184
        $dataNoMethod = $dataCheckWords;
185
        unset($dataNoMethod['method']);
186
        $response = $this->post('spellcheck', array('ajax' => 1, 'json_data' => json_encode($dataNoMethod)));
187
        $this->assertEquals(400, $response->getStatusCode());
188
        $jsonBody = json_decode($response->getBody());
189
        $this->assertEquals($invalidRequest, $jsonBody->error->errstr);
190
191
        // Test unsupported locale
192
        $dataWrongLocale = $dataCheckWords;
193
        $dataWrongLocale['params'] = array(
194
            'de_DE',
195
            array('collor', 'colour', 'color', 'onee', 'correct')
196
        );
197
        $response = $this->post('spellcheck', array('ajax' => 1, 'json_data' => json_encode($dataWrongLocale)));
198
        $this->assertEquals(400, $response->getStatusCode());
199
        $jsonBody = json_decode($response->getBody());
200
        $this->assertEquals(_t(
201
            'SilverStripe\\SpellCheck\\Handling\\.InvalidLocale',
202
            'Not supported locale'
203
        ), $jsonBody->error->errstr);
204
    }
205
}
206