Passed
Push — main ( f595fc...f1c997 )
by Daniel
02:01
created

providesRequestsWithAQueryContainingAnInvalidOverrideMethod()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 18
rs 9.9332
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace DanBettles\Defence\Tests\Filter;
6
7
use DanBettles\Defence\Envelope;
8
use DanBettles\Defence\Filter\AbstractFilter;
9
use DanBettles\Defence\Filter\InvalidSymfonyHttpMethodOverrideFilter;
10
use DanBettles\Defence\Logger\NullLogger;
11
use DanBettles\Defence\Tests\AbstractTestCase;
12
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
13
use Symfony\Component\HttpFoundation\Request;
14
15
use function array_filter;
16
use function strtolower;
17
18
use const false;
19
use const true;
20
21
/**
22
 * Start here: https://github.com/symfony/symfony/blob/4.3/src/Symfony/Component/HttpFoundation/Request.php#L1231
23
 */
24
class InvalidSymfonyHttpMethodOverrideFilterTest extends AbstractTestCase
25
{
26
    //###> Factory Methods ###
27
    private function createRequestWithMethodOverrideInTheBody(
28
        string $realMethod,
29
        string $overrideMethod
30
    ): Request {
31
        return $this->getRequestFactory()->create([
32
            'method' => $realMethod,
33
            'body' => ['_method' => $overrideMethod],
34
        ]);
35
    }
36
37
    private function createRequestWithMethodOverrideInTheQuery(
38
        string $realMethod,
39
        string $overrideMethod
40
    ): Request {
41
        return $this->getRequestFactory()->create([
42
            'method' => $realMethod,
43
            'query' => ['_method' => $overrideMethod],
44
        ]);
45
    }
46
47
    private function createRequestWithMethodOverrideInAHeader(
48
        string $realMethod,
49
        string $overrideMethod
50
    ): Request {
51
        return $this->getRequestFactory()->create([
52
            'method' => $realMethod,
53
            'headers' => ['X-Http-Method-Override' => $overrideMethod],
54
        ]);
55
    }
56
    //###< Factory Methods ###
57
58
    public function testIsAFilter(): void
59
    {
60
        $this->assertSubclassOf(AbstractFilter::class, InvalidSymfonyHttpMethodOverrideFilter::class);
61
    }
62
63
    /** @return array<mixed[]> */
64
    public function providesRequests(): array
65
    {
66
        $requestFactory = $this->getRequestFactory();
67
68
        $invalidOverrides = [
69
            'foo',
70
            'FOO',  // Still invalid
71
            '__construct',  // Something we've seen in the wild
72
        ];
73
74
        $argLists = [];
75
76
        // Invalid overrides should *always* yield `true` (i.e. "yes, suspicious")
77
        foreach (InvalidSymfonyHttpMethodOverrideFilter::VALID_METHODS as $validRealMethod) {
78
            foreach ($invalidOverrides as $invalidOverride) {
79
                $argLists[] = [true, $this->createRequestWithMethodOverrideInTheBody($validRealMethod, $invalidOverride)];
80
                $argLists[] = [true, $this->createRequestWithMethodOverrideInTheQuery($validRealMethod, $invalidOverride)];
81
                $argLists[] = [true, $this->createRequestWithMethodOverrideInAHeader($validRealMethod, $invalidOverride)];
82
            }
83
        }
84
85
        $incompatibleRealMethods = array_filter(
86
            InvalidSymfonyHttpMethodOverrideFilter::VALID_METHODS,
87
            fn (string $methodName) => Request::METHOD_POST !== $methodName
88
        );
89
90
        // The method can be overridden only if the 'real' method is `POST`, so other types of request containing
91
        // overrides are suspicious -- irrespective of the value submitted
92
        foreach ($incompatibleRealMethods as $incompatibleRealMethod) {
93
            foreach ($invalidOverrides as $invalidOverride) {
94
                $argLists[] = [true, $this->createRequestWithMethodOverrideInTheBody($incompatibleRealMethod, $invalidOverride)];
95
                $argLists[] = [true, $this->createRequestWithMethodOverrideInTheQuery($incompatibleRealMethod, $invalidOverride)];
96
                $argLists[] = [true, $this->createRequestWithMethodOverrideInAHeader($incompatibleRealMethod, $invalidOverride)];
97
            }
98
99
            foreach (InvalidSymfonyHttpMethodOverrideFilter::VALID_METHODS as $validOverride) {
100
                $argLists[] = [true, $this->createRequestWithMethodOverrideInTheBody($incompatibleRealMethod, $validOverride)];
101
                $argLists[] = [true, $this->createRequestWithMethodOverrideInTheQuery($incompatibleRealMethod, $validOverride)];
102
                $argLists[] = [true, $this->createRequestWithMethodOverrideInAHeader($incompatibleRealMethod, $validOverride)];
103
            }
104
        }
105
106
        $compatibleRealMethod = Request::METHOD_POST;
107
108
        foreach (InvalidSymfonyHttpMethodOverrideFilter::VALID_METHODS as $validOverride) {
109
            $argLists[] = [false, $this->createRequestWithMethodOverrideInTheBody($compatibleRealMethod, $validOverride)];
110
            $argLists[] = [false, $this->createRequestWithMethodOverrideInTheQuery($compatibleRealMethod, $validOverride)];
111
            $argLists[] = [false, $this->createRequestWithMethodOverrideInAHeader($compatibleRealMethod, $validOverride)];
112
113
            $anotherValidOverride = strtolower($validOverride);
114
            $argLists[] = [false, $this->createRequestWithMethodOverrideInTheBody($compatibleRealMethod, $anotherValidOverride)];
115
            $argLists[] = [false, $this->createRequestWithMethodOverrideInTheQuery($compatibleRealMethod, $anotherValidOverride)];
116
            $argLists[] = [false, $this->createRequestWithMethodOverrideInAHeader($compatibleRealMethod, $anotherValidOverride)];
117
        }
118
119
        // *Any* request without an override is okay, too :-)
120
        foreach (InvalidSymfonyHttpMethodOverrideFilter::VALID_METHODS as $validRealMethod) {
121
            $argLists[] = [false, $requestFactory->create(['method' => $validRealMethod])];
122
        }
123
124
        return $argLists;
125
    }
126
127
    /** @dataProvider providesRequests */
128
    public function testInvokeReturnsTrueIfTheMethodOverrideIsInvalid(
129
        bool $expected,
130
        Request $request
131
    ): void {
132
        $envelope = new Envelope($request, new NullLogger());
133
        $filter = new InvalidSymfonyHttpMethodOverrideFilter();
134
135
        $this->assertSame($expected, $filter($envelope));
136
    }
137
138
    /** @return array<mixed[]> */
139
    public function providesRequestsWithAQueryContainingAnInvalidOverrideMethod(): array
140
    {
141
        return [
142
            [
143
                (function () {
144
                    $request = new Request(['_method' => [Request::METHOD_PUT]]);
145
                    $request->setMethod(Request::METHOD_POST);
146
147
                    return $request;
148
                })(),
149
            ],
150
            [
151
                (function () {
152
                    $request = new Request();
153
                    $request->setMethod(Request::METHOD_POST);
154
                    $request->query->set('_method', [Request::METHOD_PUT]);
155
156
                    return $request;
157
                })(),
158
            ],
159
        ];
160
    }
161
162
    /** @dataProvider providesRequestsWithAQueryContainingAnInvalidOverrideMethod */
163
    public function testSymfonyWillNotAllowAnOverrideMethodInAnArrayInTheQuery(Request $request): void
164
    {
165
        $this->expectException(BadRequestException::class);
166
        $this->expectExceptionMessage('Input value "_method" contains a non-scalar value.');
167
168
        $request->query->get('_method');
169
    }
170
171
    /** @return array<mixed[]> */
172
    public function providesRequestsWithABodyContainingAnInvalidOverrideMethod(): array
173
    {
174
        return [
175
            [
176
                (function () {
177
                    $request = new Request([], ['_method' => [Request::METHOD_PUT]]);
178
                    $request->setMethod(Request::METHOD_POST);
179
180
                    return $request;
181
                })(),
182
            ],
183
            [
184
                (function () {
185
                    $request = new Request();
186
                    $request->setMethod(Request::METHOD_POST);
187
                    $request->request->set('_method', [Request::METHOD_PUT]);
188
189
                    return $request;
190
                })(),
191
            ],
192
        ];
193
    }
194
195
    /** @dataProvider providesRequestsWithABodyContainingAnInvalidOverrideMethod */
196
    public function testSymfonyWillNotAllowAnOverrideMethodInAnArrayInTheBody(Request $request): void
197
    {
198
        $this->expectException(BadRequestException::class);
199
        $this->expectExceptionMessage('Input value "_method" contains a non-scalar value.');
200
201
        $request->request->get('_method');
202
    }
203
204
    /** @return array<mixed[]> */
205
    public function providesLogEntries(): array
206
    {
207
        return [
208
            [
209
                'The request contains invalid request-method overrides: `FOO`',
210
                $this->createRequestWithMethodOverrideInTheQuery(Request::METHOD_POST, 'foo'),
211
            ],
212
            [
213
                'The request contains invalid request-method overrides: `BAR`',
214
                $this->createRequestWithMethodOverrideInTheBody(Request::METHOD_POST, 'bar'),
215
            ],
216
            [
217
                'The request contains invalid request-method overrides: `BAZ`',
218
                $this->createRequestWithMethodOverrideInAHeader(Request::METHOD_POST, 'baz'),
219
            ],
220
        ];
221
    }
222
223
    /** @dataProvider providesLogEntries */
224
    public function testInvokeWillAddALogEntryIfTheRequestIsSuspicious(
225
        string $expectedLogMessage,
226
        Request $suspiciousRequest
227
    ): void {
228
        $envelope = new Envelope($suspiciousRequest, new NullLogger());
229
230
        $filterMock = $this
231
            ->getMockBuilder(InvalidSymfonyHttpMethodOverrideFilter::class)
232
            ->onlyMethods(['envelopeAddLogEntry'])
233
            ->getMock()
234
        ;
235
236
        $filterMock
237
            ->expects($this->once())
238
            ->method('envelopeAddLogEntry')
239
            ->with($envelope, $expectedLogMessage)
240
        ;
241
242
        $this->assertTrue($filterMock($envelope));
243
    }
244
}
245