Passed
Push — main ( d1c8bb...2cd0ba )
by Daniel
12:40
created

InvalidSymfonyHttpMethodOverrideFilterTest   A

Complexity

Total Complexity 15

Size/Duplication

Total Lines 178
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 15
eloc 77
dl 0
loc 178
rs 10
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A providesLogEntries() 0 23 1
A testInvokeWillAddALogEntryIfTheRequestIsSuspicious() 0 19 1
A createRequestWithMethodOverrideInTheQuery() 0 7 1
A createRequestWithMethodOverrideInTheBody() 0 7 1
B providesRequests() 0 69 8
A testIsAFilter() 0 3 1
A testInvokeReturnsTrueIfTheMethodOverrideIsInvalid() 0 8 1
A createRequestWithMethodOverrideInAHeader() 0 6 1
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\Request;
13
14
use function array_filter;
15
use function strtolower;
16
17
use const false;
18
use const true;
19
20
/**
21
 * Start here: https://github.com/symfony/symfony/blob/4.3/src/Symfony/Component/HttpFoundation/Request.php#L1231
22
 */
23
class InvalidSymfonyHttpMethodOverrideFilterTest extends AbstractTestCase
24
{
25
    //###> Factory Methods ###
26
    /**
27
     * @param mixed $overrideMethod Allow anything so we can try to break other code
28
     */
29
    private function createRequestWithMethodOverrideInTheBody(string $realMethod, $overrideMethod): Request
30
    {
31
        /** @var string $overrideMethod To keep PHPStan off our backs */
32
        return $this->getRequestFactory()->create([
33
            'method' => $realMethod,
34
            'body' => [
35
                '_method' => $overrideMethod,
36
            ],
37
        ]);
38
    }
39
40
    /**
41
     * @param mixed $overrideMethod Allow anything so we can try to break other code
42
     */
43
    private function createRequestWithMethodOverrideInTheQuery(string $realMethod, $overrideMethod): Request
44
    {
45
        /** @var string $overrideMethod To keep PHPStan off our backs */
46
        return $this->getRequestFactory()->create([
47
            'method' => $realMethod,
48
            'query' => [
49
                '_method' => $overrideMethod,
50
            ],
51
        ]);
52
    }
53
54
    private function createRequestWithMethodOverrideInAHeader(string $realMethod, string $overrideMethod): Request
55
    {
56
        return $this->getRequestFactory()->create([
57
            'method' => $realMethod,
58
            'headers' => [
59
                'X-Http-Method-Override' => $overrideMethod,
60
            ],
61
        ]);
62
    }
63
    //###< Factory Methods ###
64
65
    public function testIsAFilter(): void
66
    {
67
        $this->assertSubclassOf(AbstractFilter::class, InvalidSymfonyHttpMethodOverrideFilter::class);
68
    }
69
70
    /** @return array<mixed[]> */
71
    public function providesRequests(): array
72
    {
73
        $requestFactory = $this->getRequestFactory();
74
75
        $invalidOverrides = [
76
            'foo',
77
            'FOO',  // Still invalid
78
            '__construct',  // Something we've seen in the wild
79
        ];
80
81
        $invalidOverrideInArray = ['foo' => 'bar'];
82
83
        $argLists = [];
84
85
        // Invalid overrides should *always* yield `true` (i.e. "yes, suspicious")
86
        foreach (InvalidSymfonyHttpMethodOverrideFilter::VALID_METHODS as $validRealMethod) {
87
            foreach ($invalidOverrides as $invalidOverride) {
88
                $argLists[] = [true, $this->createRequestWithMethodOverrideInTheBody($validRealMethod, $invalidOverride)];
89
                $argLists[] = [true, $this->createRequestWithMethodOverrideInTheQuery($validRealMethod, $invalidOverride)];
90
                $argLists[] = [true, $this->createRequestWithMethodOverrideInAHeader($validRealMethod, $invalidOverride)];
91
            }
92
93
            $argLists[] = [true, $this->createRequestWithMethodOverrideInTheBody($validRealMethod, $invalidOverrideInArray)];
94
            $argLists[] = [true, $this->createRequestWithMethodOverrideInTheQuery($validRealMethod, $invalidOverrideInArray)];
95
        }
96
97
        $incompatibleRealMethods = array_filter(
98
            InvalidSymfonyHttpMethodOverrideFilter::VALID_METHODS,
99
            fn (string $methodName) => Request::METHOD_POST !== $methodName
100
        );
101
102
        // The method can be overridden only if the 'real' method is `POST`, so other types of request containing
103
        // overrides are suspicious -- irrespective of the value submitted
104
        foreach ($incompatibleRealMethods as $incompatibleRealMethod) {
105
            foreach ($invalidOverrides as $invalidOverride) {
106
                $argLists[] = [true, $this->createRequestWithMethodOverrideInTheBody($incompatibleRealMethod, $invalidOverride)];
107
                $argLists[] = [true, $this->createRequestWithMethodOverrideInTheQuery($incompatibleRealMethod, $invalidOverride)];
108
                $argLists[] = [true, $this->createRequestWithMethodOverrideInAHeader($incompatibleRealMethod, $invalidOverride)];
109
            }
110
111
            $argLists[] = [true, $this->createRequestWithMethodOverrideInTheBody($incompatibleRealMethod, $invalidOverrideInArray)];
112
            $argLists[] = [true, $this->createRequestWithMethodOverrideInTheQuery($incompatibleRealMethod, $invalidOverrideInArray)];
113
114
            foreach (InvalidSymfonyHttpMethodOverrideFilter::VALID_METHODS as $validOverride) {
115
                $argLists[] = [true, $this->createRequestWithMethodOverrideInTheBody($incompatibleRealMethod, $validOverride)];
116
                $argLists[] = [true, $this->createRequestWithMethodOverrideInTheQuery($incompatibleRealMethod, $validOverride)];
117
                $argLists[] = [true, $this->createRequestWithMethodOverrideInAHeader($incompatibleRealMethod, $validOverride)];
118
            }
119
        }
120
121
        $compatibleRealMethod = Request::METHOD_POST;
122
123
        foreach (InvalidSymfonyHttpMethodOverrideFilter::VALID_METHODS as $validOverride) {
124
            $argLists[] = [false, $this->createRequestWithMethodOverrideInTheBody($compatibleRealMethod, $validOverride)];
125
            $argLists[] = [false, $this->createRequestWithMethodOverrideInTheQuery($compatibleRealMethod, $validOverride)];
126
            $argLists[] = [false, $this->createRequestWithMethodOverrideInAHeader($compatibleRealMethod, $validOverride)];
127
128
            $anotherValidOverride = strtolower($validOverride);
129
            $argLists[] = [false, $this->createRequestWithMethodOverrideInTheBody($compatibleRealMethod, $anotherValidOverride)];
130
            $argLists[] = [false, $this->createRequestWithMethodOverrideInTheQuery($compatibleRealMethod, $anotherValidOverride)];
131
            $argLists[] = [false, $this->createRequestWithMethodOverrideInAHeader($compatibleRealMethod, $anotherValidOverride)];
132
        }
133
134
        // *Any* request without an override is okay, too :-)
135
        foreach (InvalidSymfonyHttpMethodOverrideFilter::VALID_METHODS as $validRealMethod) {
136
            $argLists[] = [false, $requestFactory->create(['method' => $validRealMethod])];
137
        }
138
139
        return $argLists;
140
    }
141
142
    /** @dataProvider providesRequests */
143
    public function testInvokeReturnsTrueIfTheMethodOverrideIsInvalid(
144
        bool $expected,
145
        Request $request
146
    ): void {
147
        $envelope = new Envelope($request, new NullLogger());
148
        $filter = new InvalidSymfonyHttpMethodOverrideFilter();
149
150
        $this->assertSame($expected, $filter($envelope));
151
    }
152
153
    /** @return array<mixed[]> */
154
    public function providesLogEntries(): array
155
    {
156
        return [
157
            [
158
                'The request contains invalid request-method overrides: `FOO`',
159
                $this->createRequestWithMethodOverrideInTheQuery(Request::METHOD_POST, 'foo'),
160
            ],
161
            [
162
                'The request contains invalid request-method overrides: `BAR`',
163
                $this->createRequestWithMethodOverrideInTheBody(Request::METHOD_POST, 'bar'),
164
            ],
165
            [
166
                'The request contains invalid request-method overrides: `BAZ`',
167
                $this->createRequestWithMethodOverrideInAHeader(Request::METHOD_POST, 'baz'),
168
            ],
169
            // An array is not permitted:
170
            [
171
                'The type of the request-method override is invalid',
172
                $this->createRequestWithMethodOverrideInTheQuery(Request::METHOD_POST, []),
173
            ],
174
            [
175
                'The type of the request-method override is invalid',
176
                $this->createRequestWithMethodOverrideInTheBody(Request::METHOD_POST, []),
177
            ],
178
        ];
179
    }
180
181
    /** @dataProvider providesLogEntries */
182
    public function testInvokeWillAddALogEntryIfTheRequestIsSuspicious(
183
        string $expectedLogMessage,
184
        Request $suspiciousRequest
185
    ): void {
186
        $envelope = new Envelope($suspiciousRequest, new NullLogger());
187
188
        $filterMock = $this
189
            ->getMockBuilder(InvalidSymfonyHttpMethodOverrideFilter::class)
190
            ->onlyMethods(['envelopeAddLogEntry'])
191
            ->getMock()
192
        ;
193
194
        $filterMock
195
            ->expects($this->once())
196
            ->method('envelopeAddLogEntry')
197
            ->with($envelope, $expectedLogMessage)
198
        ;
199
200
        $this->assertTrue($filterMock($envelope));
201
    }
202
}
203