SecurityHeaderMiddlewareExtensionTest   A
last analyzed

Complexity

Total Complexity 15

Size/Duplication

Total Lines 218
Duplicated Lines 0 %

Importance

Changes 2
Bugs 2 Features 0
Metric Value
eloc 119
dl 0
loc 218
rs 10
c 2
b 2
f 0
wmc 15

11 Methods

Rating   Name   Duplication   Size   Complexity  
A setUpBeforeClass() 0 10 1
A tearDownAfterClass() 0 7 1
A testResponseHeaders() 0 23 3
A endpointExists() 0 8 2
A testReportURIAdded() 0 10 1
A testReportURIAppended() 0 29 1
A testReportToNotAdded() 0 13 1
A testReportToAdded() 0 37 2
A testReportDisabled() 0 25 1
A directiveExists() 0 3 1
A getResponse() 0 5 1
1
<?php
2
3
namespace Signify\Tests;
4
5
use Signify\Extensions\SecurityHeaderSiteconfigExtension;
6
use SilverStripe\Dev\FunctionalTest;
7
use Signify\Middleware\SecurityHeaderMiddleware;
8
use SilverStripe\Config\MergeStrategy\Priority;
9
use SilverStripe\Control\Director;
10
use SilverStripe\SiteConfig\SiteConfig;
11
use SilverStripe\Versioned\Versioned;
12
13
class SecurityHeaderMiddlewareExtensionTest extends FunctionalTest
14
{
15
    protected static $fixture_file = 'fixtures.yml';
16
17
    private static $originalHeaderValues = null;
18
19
    private static $testHeaders = [
20
        'global' => [
21
            'Content-Security-Policy' => 'test-value1',
22
            'Strict-Transport-Security' => 'test-value2',
23
            'X-Frame-Options' => 'test-value3',
24
            'X-XSS-Protection' => 'test-value4',
25
            'X-Content-Type-Options' => 'test-value5'
26
        ]
27
    ];
28
29
    public static function setUpBeforeClass(): void
30
    {
31
        parent::setUpBeforeClass();
32
33
        // Set test header values.
34
        static::$originalHeaderValues = SecurityHeaderMiddleware::config()->get('headers');
0 ignored issues
show
Bug introduced by
Since $originalHeaderValues is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $originalHeaderValues to at least protected.
Loading history...
35
        SecurityHeaderMiddleware::config()->merge('headers', self::$testHeaders);
36
        // Add extension. Note this is needed to ensure the test database is constructed correctly when running both
37
        // test classes together. It's not strictly needed for this test class alone.
38
        SiteConfig::add_extension(SecurityHeaderSiteconfigExtension::class);
39
    }
40
41
    public static function tearDownAfterClass(): void
42
    {
43
        parent::tearDownAfterClass();
44
        // Reset headers to defaults.
45
        SecurityHeaderMiddleware::config()->merge('headers', static::$originalHeaderValues);
0 ignored issues
show
Bug introduced by
Since $originalHeaderValues is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $originalHeaderValues to at least protected.
Loading history...
46
        // Remove extension.
47
        SiteConfig::remove_extension(SecurityHeaderSiteconfigExtension::class);
48
    }
49
50
    public function testResponseHeaders()
51
    {
52
        $response = $this->getResponse();
53
54
        // Test all headers, not just the default ones or just the ones in self::$testHeaders.
55
        $headersSent = TestUtils::arrayChangeKeyCaseDeep(
56
            Priority::mergeArray(self::$testHeaders, SecurityHeaderMiddleware::config()->get('headers')),
57
            CASE_LOWER
58
        );
59
        $headersReceived = array_change_key_case($response->getHeaders(), CASE_LOWER);
60
61
        foreach ($headersReceived as $header => $value) {
62
            if (in_array($header, $headersSent['global'])) {
63
                $this->assertEquals(
64
                    $value,
65
                    $headersSent['global'][$header],
66
                    "Test response value for header '$header' is equal to configured value."
67
                );
68
            }
69
        }
70
71
        $missedHeaders = array_diff_key($headersSent['global'], $headersReceived);
72
        $this->assertEmpty($missedHeaders, 'Test all headers are sent in the response.');
73
    }
74
75
    public function testReportURIAdded()
76
    {
77
        $defaultUri = Director::absoluteURL(SecurityHeaderMiddleware::config()->get('report_uri'));
78
        $response = $this->getResponse();
79
        $csp = $response->getHeader('Content-Security-Policy');
80
81
        $this->assertTrue($this->directiveExists($csp, 'report-uri'), 'Test CSP includes a report-uri directive.');
82
        $this->assertTrue(
83
            $this->endpointExists($csp, 'report-uri', $defaultUri, true),
84
            'Test report-uri is the default endpoint.'
85
        );
86
    }
87
88
    public function testReportURIAppended()
89
    {
90
        $testURI = 'https://example.test/endpoint.aspx';
91
        TestUtils::testWithConfig(
92
            [
93
                SecurityHeaderMiddleware::class => [
94
                    'headers' => [
95
                        'global' => [
96
                            'Content-Security-Policy' => "default-src 'self'; report-uri $testURI;",
97
                        ],
98
                    ],
99
                ],
100
            ],
101
            function () use ($testURI) {
102
                $defaultUri = Director::absoluteURL(SecurityHeaderMiddleware::config()->get('report_uri'));
103
                $response = $this->getResponse();
104
                $csp = $response->getHeader('Content-Security-Policy');
105
106
                $this->assertTrue(
107
                    $this->directiveExists($csp, 'report-uri'),
108
                    'Test CSP includes a report-uri directive.'
109
                );
110
                $this->assertTrue(
111
                    $this->endpointExists($csp, 'report-uri', $testURI),
112
                    'Test report-uri includes the configured endpoint.'
113
                );
114
                $this->assertTrue(
115
                    $this->endpointExists($csp, 'report-uri', $defaultUri),
116
                    'Test report-uri includes the default endpoint.'
117
                );
118
            }
119
        );
120
    }
121
122
    public function testReportDisabled()
123
    {
124
        TestUtils::testWithConfig(
125
            [
126
                SecurityHeaderMiddleware::class => [
127
                    'enable_reporting' => false,
128
                    'use_report_to' => true,
129
                ],
130
            ],
131
            function () {
132
                $response = $this->getResponse();
133
                $csp = $response->getHeader('Content-Security-Policy');
134
                $reportHeaderExists = $response->getHeader('Report-To') !== null;
135
136
                $this->assertFalse(
137
                    $this->directiveExists($csp, 'report-uri'),
138
                    'Test CSP does not include a report-uri directive.'
139
                );
140
                $this->assertFalse(
141
                    $this->directiveExists($csp, 'report-to'),
142
                    'Test CSP does not include a report-to directive.'
143
                );
144
                $this->assertFalse(
145
                    $reportHeaderExists,
146
                    'Test CSP does not include a Report-To header.'
147
                );
148
            }
149
        );
150
    }
151
152
    public function testReportToNotAdded()
153
    {
154
        $response = $this->getResponse();
155
        $csp = $response->getHeader('Content-Security-Policy');
156
        $reportHeaderExists = $response->getHeader('Report-To') !== null;
157
158
        $this->assertFalse(
159
            $this->directiveExists($csp, 'report-to'),
160
            'Test CSP does not include a report-to directive.'
161
        );
162
        $this->assertFalse(
163
            $reportHeaderExists,
164
            'Test CSP does not include a Report-To header.'
165
        );
166
    }
167
168
    public function testReportToAdded()
169
    {
170
        TestUtils::testWithConfig(
171
            [
172
                SecurityHeaderMiddleware::class => [
173
                    'use_report_to' => true,
174
                ],
175
            ],
176
            function () {
177
                $defaultEndpoint = SecurityHeaderMiddleware::config()->get('report_to_group');
178
                $defaultUri = Director::absoluteURL(SecurityHeaderMiddleware::config()->get('report_uri'));
179
                $response = $this->getResponse();
180
                $csp = $response->getHeader('Content-Security-Policy');
181
                $reportHeader = json_decode($response->getHeader('Report-To'), true);
182
183
                $this->assertTrue(
184
                    $this->directiveExists($csp, 'report-to'),
185
                    'Test CSP includes a report-to directive.'
186
                );
187
                $this->assertTrue(
188
                    $this->endpointExists($csp, 'report-to', $defaultEndpoint, true),
189
                    'Test report-to directive is the default endpoint group.'
190
                );
191
                $this->assertTrue(
192
                    $reportHeader !== null,
193
                    'Test CSP includes a Report-To header.'
194
                );
195
                if ($reportHeader !== null) {
196
                    $this->assertEquals(
197
                        $defaultEndpoint,
198
                        $reportHeader['group'],
199
                        'Test Report-To header has correct group name.'
200
                    );
201
                    $this->assertEquals(
202
                        $defaultUri,
203
                        $reportHeader['endpoints'][0]['url'],
204
                        'Test Report-To header has correct endpoint URI'
205
                    );
206
                }
207
            }
208
        );
209
    }
210
211
    protected function getResponse()
212
    {
213
        $page = $this->objFromFixture('Page', 'page');
214
        $page->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
215
        return $this->get($page->Link());
216
    }
217
218
    protected function directiveExists($csp, $directive)
219
    {
220
        return strpos($csp, $directive) !== false;
221
    }
222
223
    protected function endpointExists($csp, $directive, $endpoint, $exactMatch = false)
224
    {
225
        $matches = array();
226
        preg_match('/' . $directive . '\s+(?<endpoints>[^;]+?);/', $csp, $matches);
227
        if ($exactMatch) {
228
            return $matches['endpoints'] === $endpoint;
229
        } else {
230
            return strpos($matches['endpoints'], $endpoint) !== false;
231
        }
232
    }
233
}
234