Passed
Push — master ( 83c445...092c55 )
by Kirill
05:01
created

CsrfTest::testGet()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 9
c 1
b 0
f 0
dl 0
loc 16
rs 9.9666
cc 1
nc 1
nop 0
1
<?php
2
3
/**
4
 * Spiral Framework.
5
 *
6
 * @license   MIT
7
 * @author    Anton Titov (Wolfy-J)
8
 */
9
10
declare(strict_types=1);
11
12
namespace Spiral\Tests\Csrf;
13
14
use PHPUnit\Framework\TestCase;
15
use Psr\Http\Message\ResponseFactoryInterface;
16
use Psr\Http\Message\ResponseInterface;
17
use Spiral\Core\Container;
18
use Spiral\Csrf\Config\CsrfConfig;
19
use Spiral\Csrf\Middleware\CsrfFirewall;
20
use Spiral\Csrf\Middleware\CsrfMiddleware;
21
use Spiral\Csrf\Middleware\StrictCsrfFirewall;
22
use Spiral\Http\Config\HttpConfig;
23
use Spiral\Http\Http;
24
use Spiral\Http\Pipeline;
25
use Laminas\Diactoros\ServerRequest;
26
27
class CsrfTest extends TestCase
28
{
29
    private $container;
30
31
    public function setUp(): void
32
    {
33
        $this->container = new Container();
34
        $this->container->bind(
35
            CsrfConfig::class,
36
            new CsrfConfig(
0 ignored issues
show
Bug introduced by
new Spiral\Csrf\Config\C..., 'lifetime' => 86400)) of type Spiral\Csrf\Config\CsrfConfig is incompatible with the type array|callable|string expected by parameter $resolver of Spiral\Core\Container::bind(). ( Ignorable by Annotation )

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

36
            /** @scrutinizer ignore-type */ new CsrfConfig(
Loading history...
37
                [
38
                    'cookie'   => 'csrf-token',
39
                    'length'   => 16,
40
                    'lifetime' => 86400
41
                ]
42
            )
43
        );
44
45
        $this->container->bind(
46
            ResponseFactoryInterface::class,
47
            new TestResponseFactory(new HttpConfig(['headers' => []]))
0 ignored issues
show
Bug introduced by
new Spiral\Tests\Csrf\Te...'headers' => array()))) of type Spiral\Tests\Csrf\TestResponseFactory is incompatible with the type array|callable|string expected by parameter $resolver of Spiral\Core\Container::bind(). ( Ignorable by Annotation )

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

47
            /** @scrutinizer ignore-type */ new TestResponseFactory(new HttpConfig(['headers' => []]))
Loading history...
48
        );
49
    }
50
51
    public function testGet(): void
52
    {
53
        $core = $this->httpCore([CsrfMiddleware::class]);
54
        $core->setHandler(
55
            static function ($r) {
56
                return $r->getAttribute(CsrfMiddleware::ATTRIBUTE);
57
            }
58
        );
59
60
        $response = $this->get($core, '/');
61
        self::assertSame(200, $response->getStatusCode());
62
63
        $cookies = $this->fetchCookies($response);
64
65
        self::assertArrayHasKey('csrf-token', $cookies);
66
        self::assertSame($cookies['csrf-token'], (string)$response->getBody());
67
    }
68
69
    public function testLengthException(): void
70
    {
71
        $this->expectException(\RuntimeException::class);
72
        $this->container->bind(
73
            CsrfConfig::class,
74
            new CsrfConfig(
75
                [
76
                    'cookie'   => 'csrf-token',
77
                    'length'   => 0,
78
                    'lifetime' => 86400
79
                ]
80
            )
81
        );
82
83
        $core = $this->httpCore([CsrfMiddleware::class]);
84
        $core->setHandler(
85
            static function () {
86
                return 'all good';
87
            }
88
        );
89
90
        $response = $this->get($core, '/');
0 ignored issues
show
Unused Code introduced by
The assignment to $response is dead and can be removed.
Loading history...
91
    }
92
93
    public function testPostForbidden(): void
94
    {
95
        $core = $this->httpCore([CsrfMiddleware::class, CsrfFirewall::class]);
96
        $core->setHandler(
97
            static function () {
98
                return 'all good';
99
            }
100
        );
101
102
        $response = $this->post($core, '/');
103
        self::assertSame(412, $response->getStatusCode());
104
    }
105
106
    public function testLogicException(): void
107
    {
108
        $this->expectException(\LogicException::class);
109
        $core = $this->httpCore([CsrfFirewall::class]);
110
        $core->setHandler(
111
            static function () {
112
                return 'all good';
113
            }
114
        );
115
116
        $response = $this->post($core, '/');
0 ignored issues
show
Unused Code introduced by
The assignment to $response is dead and can be removed.
Loading history...
117
    }
118
119
    public function testPostOK(): void
120
    {
121
        $core = $this->httpCore([CsrfMiddleware::class, CsrfFirewall::class]);
122
        $core->setHandler(
123
            static function () {
124
                return 'all good';
125
            }
126
        );
127
128
        $response = $this->get($core, '/');
129
        self::assertSame(200, $response->getStatusCode());
130
        self::assertSame('all good', (string)$response->getBody());
131
132
        $cookies = $this->fetchCookies($response);
133
134
        $response = $this->post($core, '/', [], [], ['csrf-token' => $cookies['csrf-token']]);
135
136
        self::assertSame(412, $response->getStatusCode());
137
138
        $response = $this->post(
139
            $core,
140
            '/',
141
            [
142
                'csrf-token' => $cookies['csrf-token']
143
            ],
144
            [],
145
            ['csrf-token' => $cookies['csrf-token']]
146
        );
147
148
        self::assertSame(200, $response->getStatusCode());
149
        self::assertSame('all good', (string)$response->getBody());
150
    }
151
152
    public function testHeaderOK(): void
153
    {
154
        $core = $this->httpCore([CsrfMiddleware::class, CsrfFirewall::class]);
155
        $core->setHandler(
156
            static function () {
157
                return 'all good';
158
            }
159
        );
160
161
        $response = $this->get($core, '/');
162
        self::assertSame(200, $response->getStatusCode());
163
        self::assertSame('all good', (string)$response->getBody());
164
165
        $cookies = $this->fetchCookies($response);
166
167
        $response = $this->post($core, '/', [], [], ['csrf-token' => $cookies['csrf-token']]);
168
169
        self::assertSame(412, $response->getStatusCode());
170
171
        $response = $this->post(
172
            $core,
173
            '/',
174
            [],
175
            [
176
                'X-CSRF-Token' => $cookies['csrf-token']
177
            ],
178
            ['csrf-token' => $cookies['csrf-token']]
179
        );
180
181
        self::assertSame(200, $response->getStatusCode());
182
        self::assertSame('all good', (string)$response->getBody());
183
    }
184
185
    public function testHeaderOKStrict(): void
186
    {
187
        $core = $this->httpCore([CsrfMiddleware::class, StrictCsrfFirewall::class]);
188
        $core->setHandler(
189
            static function () {
190
                return 'all good';
191
            }
192
        );
193
194
        $response = $this->get($core, '/');
195
        self::assertSame(412, $response->getStatusCode());
196
197
        $cookies = $this->fetchCookies($response);
198
199
        $response = $this->get($core, '/', [], [], ['csrf-token' => $cookies['csrf-token']]);
200
201
        self::assertSame(412, $response->getStatusCode());
202
203
        $response = $this->get(
204
            $core,
205
            '/',
206
            [],
207
            [
208
                'X-CSRF-Token' => $cookies['csrf-token']
209
            ],
210
            ['csrf-token' => $cookies['csrf-token']]
211
        );
212
213
        self::assertSame(200, $response->getStatusCode());
214
        self::assertSame('all good', (string)$response->getBody());
215
    }
216
217
    protected function httpCore(array $middleware = []): Http
218
    {
219
        $config = new HttpConfig(
220
            [
221
                'basePath'   => '/',
222
                'headers'    => [
223
                    'Content-Type' => 'text/html; charset=UTF-8'
224
                ],
225
                'middleware' => $middleware
226
            ]
227
        );
228
229
        return new Http(
230
            $config,
231
            new Pipeline($this->container),
232
            new TestResponseFactory($config),
233
            $this->container
234
        );
235
    }
236
237
    protected function get(
238
        Http $core,
239
        $uri,
240
        array $query = [],
241
        array $headers = [],
242
        array $cookies = []
243
    ): ResponseInterface {
244
        return $core->handle($this->request($uri, 'GET', $query, $headers, $cookies));
245
    }
246
247
    protected function post(
248
        Http $core,
249
        $uri,
250
        array $data = [],
251
        array $headers = [],
252
        array $cookies = []
253
    ): ResponseInterface {
254
        return $core->handle($this->request($uri, 'POST', [], $headers, $cookies)->withParsedBody($data));
255
    }
256
257
    protected function request(
258
        $uri,
259
        string $method,
260
        array $query = [],
261
        array $headers = [],
262
        array $cookies = []
263
    ): ServerRequest {
264
        return new ServerRequest(
265
            [],
266
            [],
267
            $uri,
268
            $method,
269
            'php://input',
270
            $headers,
271
            $cookies,
272
            $query
273
        );
274
    }
275
276
    protected function fetchCookies(ResponseInterface $response): array
277
    {
278
        $result = [];
279
280
        foreach ($response->getHeaders() as $header) {
281
            foreach ($header as $headerLine) {
282
                $chunk = explode(';', $headerLine);
283
                if (!count($chunk) || mb_strpos($chunk[0], '=') === false) {
284
                    continue;
285
                }
286
287
                $cookie = explode('=', $chunk[0]);
288
                $result[$cookie[0]] = rawurldecode($cookie[1]);
289
            }
290
        }
291
292
        return $result;
293
    }
294
}
295