Completed
Pull Request — master (#119)
by Filippo
02:21
created

SessionMiddlewareTest   A

Complexity

Total Complexity 36

Size/Duplication

Total Lines 733
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 1

Importance

Changes 0
Metric Value
wmc 36
lcom 2
cbo 1
dl 0
loc 733
rs 9.387
c 0
b 0
f 0

35 Methods

Rating   Name   Duplication   Size   Complexity  
A testDefaultMiddlewareConfiguresASecureCookie() 0 13 1
A testSkipsInjectingSessionCookieOnEmptyContainer() 0 7 1
A testExtractsSessionContainerFromEmptyRequest() 0 5 1
A testInjectsSessionInResponseCookies() 0 14 1
A testSessionContainerCanBeReusedOverMultipleRequests() 0 34 1
A testSessionContainerCanBeCreatedEvenIfTokenDataIsMalformed() 0 37 1
A testWillIgnoreRequestsWithExpiredTokens() 0 14 1
A testWillIgnoreRequestsWithTokensFromFuture() 0 14 1
A testWillIgnoreUnSignedTokens() 0 14 1
A testWillNotRefreshSignedTokensWithoutIssuedAt() 0 14 1
A testWillRefreshTokenWithIssuedAtExactlyAtTokenRefreshTimeThreshold() 0 40 1
A testWillSkipInjectingSessionCookiesWhenSessionIsNotChanged() 0 23 1
A testWillSendExpirationCookieWhenSessionContentsAreCleared() 0 20 1
A testWillIgnoreMalformedTokens() 0 9 1
A testRejectsTokensWithInvalidSignature() 0 20 1
A testAllowsModifyingCookieDetails() 0 35 1
A testSessionTokenParsingIsDelayedWhenSessionIsNotBeingUsed() 0 31 1
A testShouldRegenerateTokenWhenRequestHasATokenThatIsAboutToExpire() 0 37 1
A testShouldNotRegenerateTokenWhenRequestHasATokenThatIsFarFromExpiration() 0 25 1
A validMiddlewaresProvider() 0 17 1
A defaultMiddlewaresProvider() 0 17 1
A testMutableCookieWillNotBeUsed() 0 25 1
A ensureSameResponse() 0 34 2
A ensureClearsSessionCookie() 0 14 1
A createToken() 0 9 1
A createTokenWithCustomClaim() 0 13 1
A emptyValidationMiddleware() 0 13 1
A writingMiddleware() 0 12 1
A fakeDelegate() 0 12 1
A requestWithResponseCookies() 0 6 1
A getCookie() 0 4 1
A getSigner() 0 8 1
A getSignatureKey() 0 8 1
A privateKey() 0 8 1
A publicKey() 0 8 1
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license.
17
 */
18
19
declare(strict_types=1);
20
21
namespace PSR7SessionsTest\Storageless\Http;
22
23
use DateTime;
24
use DateTimeImmutable;
25
use Dflydev\FigCookies\FigResponseCookies;
26
use Dflydev\FigCookies\Modifier\SameSite;
27
use Dflydev\FigCookies\SetCookie;
28
use Laminas\Diactoros\Response;
29
use Laminas\Diactoros\ServerRequest;
30
use Lcobucci\Clock\FrozenClock;
31
use Lcobucci\Clock\SystemClock;
32
use Lcobucci\JWT\Builder;
33
use Lcobucci\JWT\Parser;
34
use Lcobucci\JWT\Signer;
35
use Lcobucci\JWT\Signer\Hmac\Sha256;
36
use PHPUnit\Framework\TestCase;
37
use Psr\Http\Message\RequestInterface;
38
use Psr\Http\Message\ResponseInterface;
39
use Psr\Http\Message\ServerRequestInterface;
40
use Psr\Http\Server\RequestHandlerInterface;
41
use PSR7Sessions\Storageless\Http\SessionMiddleware;
42
use PSR7Sessions\Storageless\Session\DefaultSessionData;
43
use PSR7Sessions\Storageless\Session\SessionInterface;
44
use PSR7SessionsTest\Storageless\Asset\MutableBadCookie;
45
use ReflectionProperty;
46
use function assert;
47
use function file_get_contents;
48
use function random_int;
49
use function time;
50
use function uniqid;
51
52
final class SessionMiddlewareTest extends TestCase
53
{
54
    /**
55
     * @see https://tools.ietf.org/html/rfc6265#section-4.1.2.5 for Secure flag
56
     * @see https://tools.ietf.org/html/rfc6265#section-4.1.2.6 for HttpOnly flag
57
     * @see https://github.com/psr7-sessions/storageless/pull/46 for / path
58
     * @see https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site for SameSite flag
59
     * @see https://tools.ietf.org/html/draft-ietf-httpbis-cookie-prefixes for __Secure- prefix
60
     *
61
     * @param callable(): SessionMiddleware $middlewareFactory
62
     *
63
     * @dataProvider defaultMiddlewaresProvider
64
     * @group #46
65
     */
66
    public function testDefaultMiddlewareConfiguresASecureCookie(callable $middlewareFactory) : void
67
    {
68
        $middleware = $middlewareFactory();
69
        $response   = $middleware->process(new ServerRequest(), $this->writingMiddleware());
70
71
        $cookie = $this->getCookie($response);
72
73
        self::assertTrue($cookie->getSecure());
74
        self::assertTrue($cookie->getHttpOnly());
75
        self::assertSame('/', $cookie->getPath());
76
        self::assertEquals(SameSite::lax(), $cookie->getSameSite());
77
        self::assertStringStartsWith('__Secure-', $cookie->getName());
78
    }
79
80
    /**
81
     * @param callable(): SessionMiddleware $middlewareFactory
82
     *
83
     * @dataProvider validMiddlewaresProvider
84
     */
85
    public function testSkipsInjectingSessionCookieOnEmptyContainer(callable $middlewareFactory) : void
86
    {
87
        $middleware = $middlewareFactory();
88
        $response   = $this->ensureSameResponse($middleware, new ServerRequest(), $this->emptyValidationMiddleware());
89
90
        self::assertNull($this->getCookie($response)->getValue());
91
    }
92
93
    /**
94
     * @param callable(): SessionMiddleware $middlewareFactory
95
     *
96
     * @dataProvider validMiddlewaresProvider
97
     */
98
    public function testExtractsSessionContainerFromEmptyRequest(callable $middlewareFactory) : void
99
    {
100
        $middleware = $middlewareFactory();
101
        $this->ensureSameResponse($middleware, new ServerRequest(), $this->emptyValidationMiddleware());
102
    }
103
104
    /**
105
     * @param callable(): SessionMiddleware $middlewareFactory
106
     *
107
     * @dataProvider validMiddlewaresProvider
108
     */
109
    public function testInjectsSessionInResponseCookies(callable $middlewareFactory) : void
110
    {
111
        $middleware      = $middlewareFactory();
112
        $initialResponse = new Response();
113
        $response        = $middleware->process(new ServerRequest(), $this->writingMiddleware());
114
115
        self::assertNotSame($initialResponse, $response);
116
        self::assertEmpty($this->getCookie($response, 'non-existing')->getValue());
117
118
        $token = $this->getCookie($response)->getValue();
119
120
        self::assertIsString($token);
121
        self::assertEquals((object) ['foo' => 'bar'], (new Parser())->parse($token)->getClaim('session-data'));
122
    }
123
124
    /**
125
     * @param callable(): SessionMiddleware $middlewareFactory
126
     *
127
     * @dataProvider validMiddlewaresProvider
128
     */
129
    public function testSessionContainerCanBeReusedOverMultipleRequests(callable $middlewareFactory) : void
130
    {
131
        $middleware   = $middlewareFactory();
132
        $sessionValue = uniqid('', true);
133
134
        $checkingMiddleware = $this->fakeDelegate(
135
            static function (ServerRequestInterface $request) use ($sessionValue) {
136
                $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE);
137
                assert($session instanceof SessionInterface);
138
139
                self::assertSame($sessionValue, $session->get('foo'));
140
                self::assertFalse($session->hasChanged());
141
142
                $session->set('foo', $sessionValue . 'changed');
143
144
                self::assertTrue(
145
                    $session->hasChanged(),
146
                    'ensuring that the cookie is sent again: '
147
                    . 'non-modified session containers are not to be re-serialized into a token'
148
                );
149
150
                return new Response();
151
            }
152
        );
153
154
        $firstResponse = $middleware->process(new ServerRequest(), $this->writingMiddleware($sessionValue));
155
156
        $response = $middleware->process(
157
            $this->requestWithResponseCookies($firstResponse),
158
            $checkingMiddleware
159
        );
160
161
        self::assertNotSame($response, $firstResponse);
162
    }
163
164
    /**
165
     * @param callable(): SessionMiddleware $middlewareFactory
166
     *
167
     * @dataProvider validMiddlewaresProvider
168
     */
169
    public function testSessionContainerCanBeCreatedEvenIfTokenDataIsMalformed(callable $middlewareFactory) : void
170
    {
171
        $middleware   = $middlewareFactory();
172
        $sessionValue = uniqid('not valid session data', true);
173
174
        $checkingMiddleware = $this->fakeDelegate(
175
            static function (ServerRequestInterface $request) use ($sessionValue) {
176
                $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE);
177
                assert($session instanceof SessionInterface);
178
179
                self::assertSame($sessionValue, $session->get('scalar'));
180
                self::assertFalse($session->hasChanged());
181
182
                return new Response();
183
            }
184
        );
185
186
        $this->createTokenWithCustomClaim(
187
            $middleware,
188
            new DateTime('-1 day'),
189
            new DateTime('+1 day'),
190
            'not valid session data'
191
        );
192
193
        $middleware->process(
194
            (new ServerRequest())
195
                ->withCookieParams([
196
                    SessionMiddleware::DEFAULT_COOKIE => $this->createTokenWithCustomClaim(
197
                        $middleware,
198
                        new DateTime('-1 day'),
199
                        new DateTime('+1 day'),
200
                        $sessionValue
201
                    ),
202
                ]),
203
            $checkingMiddleware
204
        );
205
    }
206
207
    /**
208
     * @param callable(): SessionMiddleware $middlewareFactory
209
     *
210
     * @dataProvider validMiddlewaresProvider
211
     */
212
    public function testWillIgnoreRequestsWithExpiredTokens(callable $middlewareFactory) : void
213
    {
214
        $middleware   = $middlewareFactory();
215
        $expiredToken = (new ServerRequest())
216
            ->withCookieParams([
217
                SessionMiddleware::DEFAULT_COOKIE => $this->createToken(
218
                    $middleware,
219
                    new DateTime('-1 day'),
220
                    new DateTime('-2 day')
221
                ),
222
            ]);
223
224
        $this->ensureSameResponse($middleware, $expiredToken, $this->emptyValidationMiddleware());
225
    }
226
227
    /**
228
     * @param callable(): SessionMiddleware $middlewareFactory
229
     *
230
     * @dataProvider validMiddlewaresProvider
231
     */
232
    public function testWillIgnoreRequestsWithTokensFromFuture(callable $middlewareFactory) : void
233
    {
234
        $middleware    = $middlewareFactory();
235
        $tokenInFuture = (new ServerRequest())
236
            ->withCookieParams([
237
                SessionMiddleware::DEFAULT_COOKIE => $this->createToken(
238
                    $middleware,
239
                    new DateTime('+1 day'),
240
                    new DateTime('-2 day')
241
                ),
242
            ]);
243
244
        $this->ensureSameResponse($middleware, $tokenInFuture, $this->emptyValidationMiddleware());
245
    }
246
247
    /**
248
     * @param callable(): SessionMiddleware $middlewareFactory
249
     *
250
     * @dataProvider validMiddlewaresProvider
251
     */
252
    public function testWillIgnoreUnSignedTokens(callable $middlewareFactory) : void
253
    {
254
        $middleware    = $middlewareFactory();
255
        $unsignedToken = (new ServerRequest())
256
            ->withCookieParams([
257
                SessionMiddleware::DEFAULT_COOKIE => (string) (new Builder())
258
                    ->setIssuedAt((new DateTime('-1 day'))->getTimestamp())
259
                    ->setExpiration((new DateTime('+1 day'))->getTimestamp())
260
                    ->set(SessionMiddleware::SESSION_CLAIM, DefaultSessionData::fromTokenData(['foo' => 'bar']))
261
                    ->getToken(),
262
            ]);
263
264
        $this->ensureSameResponse($middleware, $unsignedToken, $this->emptyValidationMiddleware());
265
    }
266
267
    /**
268
     * @param callable(): SessionMiddleware $middlewareFactory
269
     *
270
     * @dataProvider validMiddlewaresProvider
271
     */
272
    public function testWillNotRefreshSignedTokensWithoutIssuedAt(callable $middlewareFactory) : void
273
    {
274
        $middleware    = $middlewareFactory();
275
        $unsignedToken = (new ServerRequest())
276
            ->withCookieParams([
277
                SessionMiddleware::DEFAULT_COOKIE => (string) (new Builder())
278
                    ->setExpiration((new DateTime('+1 day'))->getTimestamp())
279
                    ->set(SessionMiddleware::SESSION_CLAIM, DefaultSessionData::fromTokenData(['foo' => 'bar']))
280
                    ->sign($this->getSigner($middleware), $this->getSignatureKey($middleware))
281
                    ->getToken(),
282
            ]);
283
284
        $this->ensureSameResponse($middleware, $unsignedToken);
285
    }
286
287
    public function testWillRefreshTokenWithIssuedAtExactlyAtTokenRefreshTimeThreshold() : void
288
    {
289
        // forcing ourselves to think of time as a mutable value:
290
        $time = time() + random_int(-100, +100);
291
292
        $clock = new FrozenClock(new DateTimeImmutable('@' . $time));
293
294
        $middleware = new SessionMiddleware(
295
            new Sha256(),
296
            'foo',
297
            'foo',
298
            SetCookie::create(SessionMiddleware::DEFAULT_COOKIE),
299
            new Parser(),
300
            1000,
301
            $clock,
302
            100
303
        );
304
305
        $requestWithTokenIssuedInThePast = (new ServerRequest())
306
            ->withCookieParams([
307
                SessionMiddleware::DEFAULT_COOKIE => (string) (new Builder())
308
                    ->setExpiration($time + 10000)
309
                    ->setIssuedAt($time - 100)
310
                    ->set(SessionMiddleware::SESSION_CLAIM, DefaultSessionData::fromTokenData(['foo' => 'bar']))
311
                    ->sign($this->getSigner($middleware), $this->getSignatureKey($middleware))
312
                    ->getToken(),
313
            ]);
314
315
        $tokenString = $this
316
            ->getCookie($middleware->process($requestWithTokenIssuedInThePast, $this->fakeDelegate(static function () {
317
                return new Response();
318
            })))
319
            ->getValue();
320
321
        self::assertIsString($tokenString);
322
323
        $token = (new Parser())->parse($tokenString);
324
325
        self::assertEquals($time, $token->getClaim(SessionMiddleware::ISSUED_AT_CLAIM), 'Token was refreshed');
326
    }
327
328
    /**
329
     * @param callable(): SessionMiddleware $middlewareFactory
330
     *
331
     * @dataProvider validMiddlewaresProvider
332
     */
333
    public function testWillSkipInjectingSessionCookiesWhenSessionIsNotChanged(callable $middlewareFactory) : void
334
    {
335
        $middleware = $middlewareFactory();
336
        $this->ensureSameResponse(
337
            $middleware,
338
            $this->requestWithResponseCookies(
339
                $middleware->process(new ServerRequest(), $this->writingMiddleware())
340
            ),
341
            $this->fakeDelegate(
342
                static function (ServerRequestInterface $request) {
343
                    $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE);
344
                    assert($session instanceof SessionInterface);
345
346
                    // note: we set the same data just to make sure that we are indeed interacting with the session
347
                    $session->set('foo', 'bar');
348
349
                    self::assertFalse($session->hasChanged());
350
351
                    return new Response();
352
                }
353
            )
354
        );
355
    }
356
357
    /**
358
     * @param callable(): SessionMiddleware $middlewareFactory
359
     *
360
     * @dataProvider validMiddlewaresProvider
361
     */
362
    public function testWillSendExpirationCookieWhenSessionContentsAreCleared(callable $middlewareFactory) : void
363
    {
364
        $middleware = $middlewareFactory();
365
        $this->ensureClearsSessionCookie(
366
            $middleware,
367
            $this->requestWithResponseCookies(
368
                $middleware->process(new ServerRequest(), $this->writingMiddleware())
369
            ),
370
            $this->fakeDelegate(
371
                static function (ServerRequestInterface $request) {
372
                    $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE);
373
                    assert($session instanceof SessionInterface);
374
375
                    $session->clear();
376
377
                    return new Response();
378
                }
379
            )
380
        );
381
    }
382
383
    /**
384
     * @param callable(): SessionMiddleware $middlewareFactory
385
     *
386
     * @dataProvider validMiddlewaresProvider
387
     */
388
    public function testWillIgnoreMalformedTokens(callable $middlewareFactory) : void
389
    {
390
        $middleware = $middlewareFactory();
391
        $this->ensureSameResponse(
392
            $middleware,
393
            (new ServerRequest())->withCookieParams([SessionMiddleware::DEFAULT_COOKIE => 'malformed content']),
394
            $this->emptyValidationMiddleware()
395
        );
396
    }
397
398
    public function testRejectsTokensWithInvalidSignature() : void
399
    {
400
        $middleware = new SessionMiddleware(
401
            new Sha256(),
402
            'foo',
403
            'bar', // wrong symmetric key (on purpose)
404
            SetCookie::create(SessionMiddleware::DEFAULT_COOKIE),
405
            new Parser(),
406
            100,
407
            new SystemClock()
408
        );
409
410
        $this->ensureSameResponse(
411
            $middleware,
412
            $this->requestWithResponseCookies(
413
                $middleware->process(new ServerRequest(), $this->writingMiddleware())
414
            ),
415
            $this->emptyValidationMiddleware()
416
        );
417
    }
418
419
    public function testAllowsModifyingCookieDetails() : void
420
    {
421
        $defaultCookie = SetCookie::create('a-different-cookie-name')
422
            ->withDomain('foo.bar')
423
            ->withPath('/yadda')
424
            ->withHttpOnly(false)
425
            ->withMaxAge(123123)
426
            ->withValue('a-random-value');
427
428
        $dateTime   = new DateTimeImmutable();
429
        $middleware = new SessionMiddleware(
430
            new Sha256(),
431
            'foo',
432
            'foo',
433
            $defaultCookie,
434
            new Parser(),
435
            123456,
436
            new FrozenClock($dateTime),
437
            123
438
        );
439
440
        $response = $middleware->process(new ServerRequest(), $this->writingMiddleware());
441
442
        self::assertNull($this->getCookie($response)->getValue());
443
444
        $tokenCookie = $this->getCookie($response, 'a-different-cookie-name');
445
446
        self::assertNotEmpty($tokenCookie->getValue());
447
        self::assertNotSame($defaultCookie->getValue(), $tokenCookie->getValue());
448
        self::assertSame($defaultCookie->getDomain(), $tokenCookie->getDomain());
449
        self::assertSame($defaultCookie->getPath(), $tokenCookie->getPath());
450
        self::assertSame($defaultCookie->getHttpOnly(), $tokenCookie->getHttpOnly());
451
        self::assertSame($defaultCookie->getMaxAge(), $tokenCookie->getMaxAge());
452
        self::assertEquals($dateTime->getTimestamp() + 123456, $tokenCookie->getExpires());
453
    }
454
455
    public function testSessionTokenParsingIsDelayedWhenSessionIsNotBeingUsed() : void
456
    {
457
        $signer = $this->createMock(Signer::class);
458
459
        $signer->expects(self::never())->method('verify');
460
        $signer->method('getAlgorithmId')->willReturn('HS256');
461
462
        $currentTimeProvider = new SystemClock();
463
        $setCookie           = SetCookie::create(SessionMiddleware::DEFAULT_COOKIE);
464
        $middleware          = new SessionMiddleware($signer, 'foo', 'foo', $setCookie, new Parser(), 100, $currentTimeProvider);
465
        $request             = (new ServerRequest())
466
            ->withCookieParams([
467
                SessionMiddleware::DEFAULT_COOKIE => (string) (new Builder())
468
                    ->set(SessionMiddleware::SESSION_CLAIM, DefaultSessionData::fromTokenData(['foo' => 'bar']))
469
                    ->setIssuedAt(time())
470
                    ->sign(new Sha256(), 'foo')
471
                    ->getToken(),
472
            ]);
473
474
        $middleware->process(
475
            $request,
476
            $this->fakeDelegate(static function (ServerRequestInterface $request) {
477
                self::assertInstanceOf(
478
                    SessionInterface::class,
479
                    $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE)
480
                );
481
482
                return new Response();
483
            })
484
        );
485
    }
486
487
    public function testShouldRegenerateTokenWhenRequestHasATokenThatIsAboutToExpire() : void
488
    {
489
        $dateTime   = new DateTimeImmutable();
490
        $middleware = new SessionMiddleware(
491
            new Sha256(),
492
            'foo',
493
            'foo',
494
            SetCookie::create(SessionMiddleware::DEFAULT_COOKIE),
495
            new Parser(),
496
            1000,
497
            new FrozenClock($dateTime),
498
            300
499
        );
500
501
        $expiringToken = (new ServerRequest())
502
            ->withCookieParams([
503
                SessionMiddleware::DEFAULT_COOKIE => (string) (new Builder())
504
                    ->setIssuedAt((new DateTime('-800 second'))->getTimestamp())
505
                    ->setExpiration((new DateTime('+200 second'))->getTimestamp())
506
                    ->set(SessionMiddleware::SESSION_CLAIM, DefaultSessionData::fromTokenData(['foo' => 'bar']))
507
                    ->sign($this->getSigner($middleware), $this->getSignatureKey($middleware))
508
                    ->getToken(),
509
            ]);
510
511
        $initialResponse = new Response();
512
513
        $response = $middleware->process($expiringToken, $this->fakeDelegate(static function () use ($initialResponse) {
514
            return $initialResponse;
515
        }));
516
517
        self::assertNotSame($initialResponse, $response);
518
519
        $tokenCookie = $this->getCookie($response);
520
521
        self::assertNotEmpty($tokenCookie->getValue());
522
        self::assertEquals($dateTime->getTimestamp() + 1000, $tokenCookie->getExpires());
523
    }
524
525
    public function testShouldNotRegenerateTokenWhenRequestHasATokenThatIsFarFromExpiration() : void
526
    {
527
        $middleware = new SessionMiddleware(
528
            new Sha256(),
529
            'foo',
530
            'foo',
531
            SetCookie::create(SessionMiddleware::DEFAULT_COOKIE),
532
            new Parser(),
533
            1000,
534
            new SystemClock(),
535
            300
536
        );
537
538
        $validToken = (new ServerRequest())
539
            ->withCookieParams([
540
                SessionMiddleware::DEFAULT_COOKIE => (string) (new Builder())
541
                    ->setIssuedAt((new DateTime('-100 second'))->getTimestamp())
542
                    ->setExpiration((new DateTime('+900 second'))->getTimestamp())
543
                    ->set(SessionMiddleware::SESSION_CLAIM, DefaultSessionData::fromTokenData(['foo' => 'bar']))
544
                    ->sign($this->getSigner($middleware), $this->getSignatureKey($middleware))
545
                    ->getToken(),
546
            ]);
547
548
        $this->ensureSameResponse($middleware, $validToken);
549
    }
550
551
    /**
552
     * @return array<array<callable(): SessionMiddleware>>
0 ignored issues
show
Documentation introduced by
The doc-type array<array<callable(): could not be parsed: Expected ">" at position 5, but found "(". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
553
     */
554
    public function validMiddlewaresProvider() : array
555
    {
556
        return $this->defaultMiddlewaresProvider() + [
557
            [static function () : SessionMiddleware {
558
                return new SessionMiddleware(
559
                    new Sha256(),
560
                    'foo',
561
                    'foo',
562
                    SetCookie::create(SessionMiddleware::DEFAULT_COOKIE),
563
                    new Parser(),
564
                    100,
565
                    new SystemClock()
566
                );
567
            },
568
            ],
569
        ];
570
    }
571
572
    /**
573
     * @return array<array<callable(): SessionMiddleware>>
0 ignored issues
show
Documentation introduced by
The doc-type array<array<callable(): could not be parsed: Expected ">" at position 5, but found "(". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
574
     */
575
    public function defaultMiddlewaresProvider() : array
576
    {
577
        return [
578
            [static function () : SessionMiddleware {
579
                return SessionMiddleware::fromSymmetricKeyDefaults('not relevant', 100);
580
            },
581
            ],
582
            [static function () : SessionMiddleware {
583
                return SessionMiddleware::fromAsymmetricKeyDefaults(
584
                    self::privateKey(),
585
                    self::publicKey(),
586
                    200
587
                );
588
            },
589
            ],
590
        ];
591
    }
592
593
    public function testMutableCookieWillNotBeUsed() : void
594
    {
595
        $cookie = MutableBadCookie::create(SessionMiddleware::DEFAULT_COOKIE);
596
597
        assert($cookie instanceof MutableBadCookie);
0 ignored issues
show
Bug introduced by
The class PSR7SessionsTest\Storage...\Asset\MutableBadCookie does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
598
599
        $middleware = new SessionMiddleware(
600
            new Sha256(),
601
            'foo',
602
            'foo',
603
            $cookie,
604
            new Parser(),
605
            1000,
606
            new SystemClock()
607
        );
608
609
        $cookie->mutated = true;
610
611
        self::assertStringStartsWith(
612
            '__Secure-slsession=',
613
            $middleware
614
                ->process(new ServerRequest(), $this->writingMiddleware())
615
                ->getHeaderLine('Set-Cookie')
616
        );
617
    }
618
619
    private function ensureSameResponse(
620
        SessionMiddleware $middleware,
621
        ServerRequestInterface $request,
622
        ?RequestHandlerInterface $next = null
623
    ) : ResponseInterface {
624
        $initialResponse = new Response();
625
626
        $handleRequest = $this->createMock(RequestHandlerInterface::class);
627
628
        if ($next === null) {
629
            $handleRequest
630
                ->expects(self::once())
631
                ->method('handle')
632
                ->willReturn($initialResponse);
633
        } else {
634
            // capturing `$initialResponse` from the `$next` handler
635
            $handleRequest
636
                ->expects(self::once())
637
                ->method('handle')
638
                ->willReturnCallback(static function (ServerRequestInterface $serverRequest) use ($next, & $initialResponse) {
639
                    $response = $next->handle($serverRequest);
640
641
                    $initialResponse = $response;
642
643
                    return $response;
644
                });
645
        }
646
647
        $response = $middleware->process($request, $handleRequest);
648
649
        self::assertSame($initialResponse, $response);
650
651
        return $response;
652
    }
653
654
    private function ensureClearsSessionCookie(
655
        SessionMiddleware $middleware,
656
        ServerRequestInterface $request,
657
        RequestHandlerInterface $next
658
    ) : ResponseInterface {
659
        $response = $middleware->process($request, $next);
660
661
        $cookie = $this->getCookie($response);
662
663
        self::assertLessThan((new DateTime('-29 day'))->getTimestamp(), $cookie->getExpires());
664
        self::assertEmpty($cookie->getValue());
665
666
        return $response;
667
    }
668
669
    private function createToken(SessionMiddleware $middleware, DateTime $issuedAt, DateTime $expiration) : string
670
    {
671
        return (string) (new Builder())
672
            ->setIssuedAt($issuedAt->getTimestamp())
673
            ->setExpiration($expiration->getTimestamp())
674
            ->set(SessionMiddleware::SESSION_CLAIM, DefaultSessionData::fromTokenData(['foo' => 'bar']))
675
            ->sign($this->getSigner($middleware), $this->getSignatureKey($middleware))
676
            ->getToken();
677
    }
678
679
    /** @param mixed $claim */
680
    private function createTokenWithCustomClaim(
681
        SessionMiddleware $middleware,
682
        DateTime $issuedAt,
683
        DateTime $expiration,
684
        $claim
685
    ) : string {
686
        return (string) (new Builder())
687
            ->setIssuedAt($issuedAt->getTimestamp())
688
            ->setExpiration($expiration->getTimestamp())
689
            ->set(SessionMiddleware::SESSION_CLAIM, $claim)
690
            ->sign($this->getSigner($middleware), $this->getSignatureKey($middleware))
691
            ->getToken();
692
    }
693
694
    private function emptyValidationMiddleware() : RequestHandlerInterface
695
    {
696
        return $this->fakeDelegate(
697
            static function (ServerRequestInterface $request) {
698
                $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE);
699
700
                self::assertInstanceOf(SessionInterface::class, $session);
701
                self::assertTrue($session->isEmpty());
702
703
                return new Response();
704
            }
705
        );
706
    }
707
708
    private function writingMiddleware(string $value = 'bar') : RequestHandlerInterface
709
    {
710
        return $this->fakeDelegate(
711
            static function (ServerRequestInterface $request) use ($value) {
712
                $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE);
713
                assert($session instanceof SessionInterface);
714
                $session->set('foo', $value);
715
716
                return new Response();
717
            }
718
        );
719
    }
720
721
    private function fakeDelegate(callable $callback) : RequestHandlerInterface
722
    {
723
        $middleware = $this->createMock(RequestHandlerInterface::class);
724
725
        $middleware
726
            ->expects(self::once())
727
           ->method('handle')
728
           ->willReturnCallback($callback)
729
           ->with(self::isInstanceOf(RequestInterface::class));
730
731
        return $middleware;
732
    }
733
734
    /**
735
     * @return ServerRequest
736
     */
737
    private function requestWithResponseCookies(ResponseInterface $response) : ServerRequestInterface
738
    {
739
        return (new ServerRequest())->withCookieParams([
740
            SessionMiddleware::DEFAULT_COOKIE => $this->getCookie($response)->getValue(),
741
        ]);
742
    }
743
744
    private function getCookie(ResponseInterface $response, string $name = SessionMiddleware::DEFAULT_COOKIE) : SetCookie
745
    {
746
        return FigResponseCookies::get($response, $name);
747
    }
748
749
    private function getSigner(SessionMiddleware $middleware) : Signer
750
    {
751
        $property = new ReflectionProperty(SessionMiddleware::class, 'signer');
752
753
        $property->setAccessible(true);
754
755
        return $property->getValue($middleware);
756
    }
757
758
    private function getSignatureKey(SessionMiddleware $middleware) : string
759
    {
760
        $property = new ReflectionProperty(SessionMiddleware::class, 'signatureKey');
761
762
        $property->setAccessible(true);
763
764
        return $property->getValue($middleware);
765
    }
766
767
    private static function privateKey() : string
768
    {
769
        $key = file_get_contents(__DIR__ . '/../../keys/private_key.pem');
770
771
        self::assertIsString($key);
772
773
        return $key;
774
    }
775
776
    private static function publicKey() : string
777
    {
778
        $key = file_get_contents(__DIR__ . '/../../keys/public_key.pem');
779
780
        self::assertIsString($key);
781
782
        return $key;
783
    }
784
}
785