Passed
Push — 4 ( 29943f...c31de7 )
by Guy
08:24 queued 10s
created

ControllerTest::testSpecificHTTPMethods()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 9
rs 10
1
<?php
2
3
namespace SilverStripe\Control\Tests;
4
5
use SilverStripe\Control\Controller;
6
use SilverStripe\Control\Director;
7
use SilverStripe\Control\Tests\ControllerTest\AccessBaseController;
8
use SilverStripe\Control\Tests\ControllerTest\AccessSecuredController;
9
use SilverStripe\Control\Tests\ControllerTest\AccessWildcardSecuredController;
10
use SilverStripe\Control\Tests\ControllerTest\ContainerController;
11
use SilverStripe\Control\Tests\ControllerTest\HasAction;
12
use SilverStripe\Control\Tests\ControllerTest\HasAction_Unsecured;
13
use SilverStripe\Control\Tests\ControllerTest\IndexSecuredController;
14
use SilverStripe\Control\Tests\ControllerTest\SubController;
15
use SilverStripe\Control\Tests\ControllerTest\TestController;
16
use SilverStripe\Control\Tests\ControllerTest\UnsecuredController;
17
use SilverStripe\Control\Tests\RequestHandlingTest\HTTPMethodTestController;
18
use SilverStripe\Dev\FunctionalTest;
19
use SilverStripe\Security\Member;
20
use SilverStripe\View\SSViewer;
21
22
class ControllerTest extends FunctionalTest
23
{
24
25
    protected static $fixture_file = 'ControllerTest.yml';
26
27
    protected $autoFollowRedirection = false;
28
29
    protected $depSettings = null;
30
31
    protected static $extra_controllers = [
32
        AccessBaseController::class,
33
        AccessSecuredController::class,
34
        AccessWildcardSecuredController::class,
35
        ContainerController::class,
36
        HasAction::class,
37
        HasAction_Unsecured::class,
38
        HTTPMethodTestController::class,
39
        IndexSecuredController::class,
40
        SubController::class,
41
        TestController::class,
42
        UnsecuredController::class,
43
    ];
44
45
    protected function setUp()
46
    {
47
        parent::setUp();
48
        Director::config()->update('alternate_base_url', '/');
49
50
        // Add test theme
51
        $themeDir = substr(__DIR__, strlen(FRAMEWORK_DIR)) . '/ControllerTest/';
52
        $themes = [
53
            "silverstripe/framework:{$themeDir}",
54
            SSViewer::DEFAULT_THEME,
55
        ];
56
        SSViewer::set_themes($themes);
57
    }
58
59
    public function testDefaultAction()
60
    {
61
        /* For a controller with a template, the default action will simple run that template. */
62
        $response = $this->get("TestController/");
63
        $this->assertContains("This is the main template. Content is 'default content'", $response->getBody());
64
    }
65
66
    public function testMethodActions()
67
    {
68
        /* The Action can refer to a method that is called on the object.  If a method returns an array, then it
69
        * will be used to customise the template data */
70
        $response = $this->get("TestController/methodaction");
71
        $this->assertContains("This is the main template. Content is 'methodaction content'.", $response->getBody());
72
73
        /* If the method just returns a string, then that will be used as the response */
74
        $response = $this->get("TestController/stringaction");
75
        $this->assertContains("stringaction was called.", $response->getBody());
76
    }
77
78
    public function testTemplateActions()
79
    {
80
        /* If there is no method, it can be used to point to an alternative template. */
81
        $response = $this->get("TestController/templateaction");
82
        $this->assertContains(
83
            "This is the template for templateaction. Content is 'default content'.",
84
            $response->getBody()
85
        );
86
    }
87
88
    public function testUndefinedActions()
89
    {
90
        $response = $this->get('IndexSecuredController/undefinedaction');
91
        $this->assertInstanceOf('SilverStripe\\Control\\HTTPResponse', $response);
92
        $this->assertEquals(404, $response->getStatusCode(), 'Undefined actions return a not found response.');
93
    }
94
95
    public function testAllowedActions()
96
    {
97
        $adminUser = $this->objFromFixture(Member::class, 'admin');
98
99
        $response = $this->get("UnsecuredController/");
100
        $this->assertEquals(
101
            200,
102
            $response->getStatusCode(),
103
            'Access granted on index action without $allowed_actions on defining controller, ' . 'when called without an action in the URL'
104
        );
105
106
        $response = $this->get("UnsecuredController/index");
107
        $this->assertEquals(
108
            200,
109
            $response->getStatusCode(),
110
            'Access denied on index action without $allowed_actions on defining controller, ' . 'when called with an action in the URL'
111
        );
112
113
        $response = $this->get("UnsecuredController/method1");
114
        $this->assertEquals(
115
            403,
116
            $response->getStatusCode(),
117
            'Access denied on action without $allowed_actions on defining controller, ' . 'when called without an action in the URL'
118
        );
119
120
        $response = $this->get("AccessBaseController/");
121
        $this->assertEquals(
122
            200,
123
            $response->getStatusCode(),
124
            'Access granted on index with empty $allowed_actions on defining controller, ' . 'when called without an action in the URL'
125
        );
126
127
        $response = $this->get("AccessBaseController/index");
128
        $this->assertEquals(
129
            200,
130
            $response->getStatusCode(),
131
            'Access granted on index with empty $allowed_actions on defining controller, ' . 'when called with an action in the URL'
132
        );
133
134
        $response = $this->get("AccessBaseController/method1");
135
        $this->assertEquals(
136
            403,
137
            $response->getStatusCode(),
138
            'Access denied on action with empty $allowed_actions on defining controller'
139
        );
140
141
        $response = $this->get("AccessBaseController/method2");
142
        $this->assertEquals(
143
            403,
144
            $response->getStatusCode(),
145
            'Access denied on action with empty $allowed_actions on defining controller, ' . 'even when action is allowed in subclasses (allowed_actions don\'t inherit)'
146
        );
147
148
        $response = $this->get("AccessSecuredController/");
149
        $this->assertEquals(
150
            200,
151
            $response->getStatusCode(),
152
            'Access granted on index with non-empty $allowed_actions on defining controller, ' . 'even when index isn\'t specifically mentioned in there'
153
        );
154
155
        $response = $this->get("AccessSecuredController/method1");
156
        $this->assertEquals(
157
            403,
158
            $response->getStatusCode(),
159
            'Access denied on action which is only defined in parent controller, ' . 'even when action is allowed in currently called class (allowed_actions don\'t inherit)'
160
        );
161
162
        $response = $this->get("AccessSecuredController/method2");
163
        $this->assertEquals(
164
            200,
165
            $response->getStatusCode(),
166
            'Access granted on action originally defined with empty $allowed_actions on parent controller, ' . 'because it has been redefined in the subclass'
167
        );
168
169
        $response = $this->get("AccessSecuredController/templateaction");
170
        $this->assertEquals(
171
            403,
172
            $response->getStatusCode(),
173
            'Access denied on action with $allowed_actions on defining controller, ' . 'if action is not a method but rather a template discovered by naming convention'
174
        );
175
176
        $response = $this->get("AccessSecuredController/templateaction");
177
        $this->assertEquals(
178
            403,
179
            $response->getStatusCode(),
180
            'Access denied on action with $allowed_actions on defining controller, ' . 'if action is not a method but rather a template discovered by naming convention'
181
        );
182
183
        Member::actAs($adminUser, function () {
184
            $response = $this->get("AccessSecuredController/templateaction");
185
            $this->assertEquals(
186
                200,
187
                $response->getStatusCode(),
188
                'Access granted for logged in admin on action with $allowed_actions on defining controller, ' . 'if action is not a method but rather a template discovered by naming convention'
189
            );
190
        });
191
192
        $response = $this->get("AccessSecuredController/adminonly");
193
        $this->assertEquals(
194
            403,
195
            $response->getStatusCode(),
196
            'Access denied on action with $allowed_actions on defining controller, ' . 'when restricted by unmatched permission code'
197
        );
198
199
        $response = $this->get("AccessSecuredController/aDmiNOnlY");
200
        $this->assertEquals(
201
            403,
202
            $response->getStatusCode(),
203
            'Access denied on action with $allowed_actions on defining controller, ' . 'regardless of capitalization'
204
        );
205
206
        $response = $this->get('AccessSecuredController/protectedmethod');
207
        $this->assertEquals(
208
            404,
209
            $response->getStatusCode(),
210
            "Access denied to protected method even if its listed in allowed_actions"
211
        );
212
213
        Member::actAs($adminUser, function () {
214
            $response = $this->get("AccessSecuredController/adminonly");
215
            $this->assertEquals(
216
                200,
217
                $response->getStatusCode(),
218
                "Permission codes are respected when set in \$allowed_actions"
219
            );
220
        });
221
222
        $response = $this->get('AccessBaseController/extensionmethod1');
223
        $this->assertEquals(
224
            200,
225
            $response->getStatusCode(),
226
            "Access granted to method defined in allowed_actions on extension, " . "where method is also defined on extension"
227
        );
228
229
        $response = $this->get('AccessSecuredController/extensionmethod1');
230
        $this->assertEquals(
231
            200,
232
            $response->getStatusCode(),
233
            "Access granted to method defined in allowed_actions on extension, " . "where method is also defined on extension, even when called in a subclass"
234
        );
235
236
        $response = $this->get('AccessBaseController/extensionmethod2');
237
        $this->assertEquals(
238
            404,
239
            $response->getStatusCode(),
240
            "Access denied to method not defined in allowed_actions on extension, " . "where method is also defined on extension"
241
        );
242
243
        $response = $this->get('IndexSecuredController/');
244
        $this->assertEquals(
245
            403,
246
            $response->getStatusCode(),
247
            "Access denied when index action is limited through allowed_actions, " . "and doesn't satisfy checks, and action is empty"
248
        );
249
250
        $response = $this->get('IndexSecuredController/index');
251
        $this->assertEquals(
252
            403,
253
            $response->getStatusCode(),
254
            "Access denied when index action is limited through allowed_actions, " . "and doesn't satisfy checks"
255
        );
256
257
        Member::actAs($adminUser, function () {
258
            $response = $this->get('IndexSecuredController/');
259
            $this->assertEquals(
260
                200,
261
                $response->getStatusCode(),
262
                "Access granted when index action is limited through allowed_actions, " . "and does satisfy checks"
263
            );
264
        });
265
    }
266
267
    /**
268
     * @expectedException \InvalidArgumentException
269
     * @expectedExceptionMessage Invalid allowed_action '*'
270
     */
271
    public function testWildcardAllowedActions()
272
    {
273
        $this->get('AccessWildcardSecuredController');
274
    }
275
276
    /**
277
     * Test Controller::join_links()
278
     */
279
    public function testJoinLinks()
280
    {
281
        /* Controller::join_links() will reliably join two URL-segments together so that they will be
282
        * appropriately parsed by the URL parser */
283
        $this->assertEquals("admin/crm/MyForm", Controller::join_links("admin/crm", "MyForm"));
284
        $this->assertEquals("admin/crm/MyForm", Controller::join_links("admin/crm/", "MyForm"));
285
286
        /* It will also handle appropriate combination of querystring variables */
287
        $this->assertEquals("admin/crm/MyForm?flush=1", Controller::join_links("admin/crm/?flush=1", "MyForm"));
288
        $this->assertEquals("admin/crm/MyForm?flush=1", Controller::join_links("admin/crm/", "MyForm?flush=1"));
289
        $this->assertEquals(
290
            "admin/crm/MyForm?field=1&other=1",
291
            Controller::join_links("admin/crm/?field=1", "MyForm?other=1")
292
        );
293
294
        /* It can handle arbitrary numbers of components, and will ignore empty ones */
295
        $this->assertEquals("admin/crm/MyForm/", Controller::join_links("admin/", "crm", "", "MyForm/"));
296
        $this->assertEquals(
297
            "admin/crm/MyForm/?a=1&b=2",
298
            Controller::join_links("admin/?a=1", "crm", "", "MyForm/?b=2")
299
        );
300
301
        /* It can also be used to attach additional get variables to a link */
302
        $this->assertEquals("admin/crm?flush=1", Controller::join_links("admin/crm", "?flush=1"));
303
        $this->assertEquals("admin/crm?existing=1&flush=1", Controller::join_links("admin/crm?existing=1", "?flush=1"));
304
        $this->assertEquals(
305
            "admin/crm/MyForm?a=1&b=2&c=3",
306
            Controller::join_links("?a=1", "admin/crm", "?b=2", "MyForm?c=3")
307
        );
308
309
        // And duplicates are handled nicely
310
        $this->assertEquals(
311
            "admin/crm?foo=2&bar=3&baz=1",
312
            Controller::join_links("admin/crm?foo=1&bar=1&baz=1", "?foo=2&bar=3")
313
        );
314
315
        $this->assertEquals(
316
            'admin/action',
317
            Controller::join_links('admin/', '/', '/action'),
318
            'Test that multiple slashes are trimmed.'
319
        );
320
321
        $this->assertEquals('/admin/action', Controller::join_links('/admin', 'action'));
322
323
        /* One fragment identifier is handled as you would expect */
324
        $this->assertEquals("my-page?arg=var#subsection", Controller::join_links("my-page#subsection", "?arg=var"));
325
326
        /* If there are multiple, it takes the last one */
327
        $this->assertEquals(
328
            "my-page?arg=var#second-section",
329
            Controller::join_links("my-page#subsection", "?arg=var", "#second-section")
330
        );
331
332
        /* Does type-safe checks for zero value */
333
        $this->assertEquals("my-page/0", Controller::join_links("my-page", 0));
334
335
        // Test array args
336
        $this->assertEquals(
337
            "admin/crm/MyForm?a=1&b=2&c=3",
338
            Controller::join_links(["?a=1", "admin/crm", "?b=2", "MyForm?c=3"])
339
        );
340
    }
341
342
    public function testLink()
343
    {
344
        $controller = new HasAction();
345
        $this->assertEquals('HasAction/', $controller->Link());
346
        $this->assertEquals('HasAction/', $controller->Link(null));
347
        $this->assertEquals('HasAction/', $controller->Link(false));
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type string expected by parameter $action of SilverStripe\Control\RequestHandler::Link(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

347
        $this->assertEquals('HasAction/', $controller->Link(/** @scrutinizer ignore-type */ false));
Loading history...
348
        $this->assertEquals('HasAction/allowed-action/', $controller->Link('allowed-action'));
349
    }
350
351
    /**
352
     * @covers \SilverStripe\Control\Controller::hasAction
353
     */
354
    public function testHasAction()
355
    {
356
        $controller = new HasAction();
357
        $unsecuredController = new HasAction_Unsecured();
358
        $securedController = new AccessSecuredController();
359
360
        $this->assertFalse(
361
            $controller->hasAction('1'),
362
            'Numeric actions do not slip through.'
363
        );
364
        //$this->assertFalse(
365
        //  $controller->hasAction('lowercase_permission'),
366
        //  'Lowercase permission does not slip through.'
367
        //);
368
        $this->assertFalse(
369
            $controller->hasAction('undefined'),
370
            'undefined actions do not exist'
371
        );
372
        $this->assertTrue(
373
            $controller->hasAction('allowed_action'),
374
            'allowed actions are recognised'
375
        );
376
        $this->assertTrue(
377
            $controller->hasAction('template_action'),
378
            'action-specific templates are recognised'
379
        );
380
381
        $this->assertTrue(
382
            $unsecuredController->hasAction('defined_action'),
383
            'Without an allowed_actions, any defined methods are recognised as actions'
384
        );
385
386
        $this->assertTrue(
387
            $securedController->hasAction('adminonly'),
388
            'Method is generally visible even if its denied via allowed_actions'
389
        );
390
391
        $this->assertFalse(
392
            $securedController->hasAction('protectedmethod'),
393
            'Method is not visible when protected, even if its defined in allowed_actions'
394
        );
395
396
        $this->assertTrue(
397
            $securedController->hasAction('extensionmethod1'),
398
            'Method is visible when defined on an extension and part of allowed_actions'
399
        );
400
401
        $this->assertFalse(
402
            $securedController->hasAction('internalextensionmethod'),
403
            'Method is not visible when defined on an extension, but not part of allowed_actions'
404
        );
405
406
        $this->assertFalse(
407
            $securedController->hasAction('protectedextensionmethod'),
408
            'Method is not visible when defined on an extension, part of allowed_actions, ' . 'but with protected visibility'
409
        );
410
    }
411
412
    public function testRedirectBackByReferer()
413
    {
414
        $internalRelativeUrl = Controller::join_links(Director::baseURL(), '/some-url');
415
        $internalAbsoluteUrl = Controller::join_links(Director::absoluteBaseURL(), '/some-url');
416
417
        $response = $this->get(
418
            'TestController/redirectbacktest',
419
            null,
420
            array('Referer' => $internalRelativeUrl)
421
        );
422
        $this->assertEquals(302, $response->getStatusCode());
423
        $this->assertEquals(
424
            $internalAbsoluteUrl,
425
            $response->getHeader('Location'),
426
            "Redirects on internal relative URLs"
427
        );
428
429
        $response = $this->get(
430
            'TestController/redirectbacktest',
431
            null,
432
            array('Referer' => $internalAbsoluteUrl)
433
        );
434
        $this->assertEquals(302, $response->getStatusCode());
435
        $this->assertEquals(
436
            $internalAbsoluteUrl,
437
            $response->getHeader('Location'),
438
            "Redirects on internal absolute URLs"
439
        );
440
441
        $externalAbsoluteUrl = 'http://myhost.com/some-url';
442
        $response = $this->get(
443
            'TestController/redirectbacktest',
444
            null,
445
            array('Referer' => $externalAbsoluteUrl)
446
        );
447
        $this->assertEquals(
448
            Director::absoluteBaseURL(),
449
            $response->getHeader('Location'),
450
            "Redirects back to home page on external url"
451
        );
452
    }
453
454
    public function testRedirectBackByBackUrl()
455
    {
456
        $internalRelativeUrl = Controller::join_links(Director::baseURL(), '/some-url');
457
        $internalAbsoluteUrl = Controller::join_links(Director::absoluteBaseURL(), '/some-url');
458
459
        $response = $this->get('TestController/redirectbacktest?BackURL=' . urlencode($internalRelativeUrl));
460
        $this->assertEquals(302, $response->getStatusCode());
461
        $this->assertEquals(
462
            $internalAbsoluteUrl,
463
            $response->getHeader('Location'),
464
            "Redirects on internal relative URLs"
465
        );
466
467
        // BackURL is internal link
468
        $internalAbsoluteUrl = Director::absoluteBaseURL() . '/some-url';
469
        $link = 'TestController/redirectbacktest?BackURL=' . urlencode($internalAbsoluteUrl);
470
        $response = $this->get($link);
471
        $this->assertEquals($internalAbsoluteUrl, $response->getHeader('Location'));
472
        $this->assertEquals(
473
            302,
474
            $response->getStatusCode(),
475
            "Redirects on internal absolute URLs"
476
        );
477
478
        // Note that this test is affected by the prior ->get()
479
        $externalAbsoluteUrl = 'http://myhost.com/some-url';
480
        $response = $this->get('TestController/redirectbacktest?BackURL=' . urlencode($externalAbsoluteUrl));
481
        $this->assertEquals(
482
            Director::absoluteURL($link),
483
            $response->getHeader('Location'),
484
            "If BackURL Is external link, fall back to last url (Referer)"
485
        );
486
    }
487
488
    public function testSubActions()
489
    {
490
        // If a controller action returns another controller, ensure that the $action variable is correctly forwarded
491
        $response = $this->get("ContainerController/subcontroller/subaction");
492
        $this->assertEquals('subaction', $response->getBody());
493
494
        // Handle nested action
495
        $response = $this->get('ContainerController/subcontroller/substring/subvieweraction');
496
        $this->assertEquals('Hope this works', $response->getBody());
497
    }
498
499
    public function testSpecificHTTPMethods()
500
    {
501
        // 'GET /'
502
        $response = $this->get('HTTPMethodTestController');
503
        $this->assertEquals('Routed to getRoot', $response->getBody());
504
505
        // 'POST ' (legacy method of specifying root route)
506
        $response = $this->post('HTTPMethodTestController', ['dummy' => 'example']);
507
        $this->assertEquals('Routed to postLegacyRoot', $response->getBody());
508
    }
509
}
510