RequestValidatorTest   A
last analyzed

Complexity

Total Complexity 3

Size/Duplication

Total Lines 283
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
eloc 161
c 2
b 0
f 1
dl 0
loc 283
rs 10
wmc 3

31 Methods

Rating   Name   Duplication   Size   Complexity  
A testInvalidSignatureCertChainUrl() 0 14 1
A testWrongSignatureCertChainUrl() 0 27 1
A testWrongSignatureCertChainUrlCallError() 0 22 1
A testInvalidRequestTime() 0 12 1
testTimestampExactlyOnToleranceBoundaryPasses() 0 24 ?
A hp$2 ➔ validateSignature() 0 3 1
A hp$1 ➔ validateSignature() 0 3 1
A hp$3 ➔ validateSignature() 0 3 1
A hp$1 ➔ __construct() 0 4 1
A hp$2 ➔ testSkipTimestampValidationWhenRequestDisablesIt() 0 23 1
A hp$2 ➔ validateTimestamp() 0 3 1
A hp$3 ➔ __construct() 0 4 1
testSkipTimestampValidationWhenRequestDisablesIt() 0 23 ?
A hp$2 ➔ __construct() 0 4 1
A hp$3 ➔ testSkipSignatureValidationWhenRequestDisablesIt() 0 26 1
A hp$0 ➔ testValidTimestampWithinTolerance() 0 27 1
testValidTimestampWithinTolerance() 0 27 ?
A hp$0 ➔ __construct() 0 4 1
A hp$1 ➔ testTimestampExactlyOnToleranceBoundaryPasses() 0 24 1
A hp$0 ➔ validateSignature() 0 3 1
testSkipSignatureValidationWhenRequestDisablesIt() 0 26 ?
testTimestampJustOverToleranceFails() 0 12 ?
A hp$1 ➔ testTimestampJustOverToleranceFails() 0 12 1
A hp$5 ➔ testSignatureValidationUsesCachedCertWithoutHttpCall() 0 34 1
testSignatureValidationUsesCachedCertWithoutHttpCall() 0 34 ?
A hp$4 ➔ testSignatureValidationPerformsHttpRequest() 0 40 1
A hp$5 ➔ validateSignature() 0 3 1
A hp$5 ➔ __construct() 0 4 1
testSignatureValidationPerformsHttpRequest() 0 40 ?
A hp$4 ➔ __construct() 0 4 1
A hp$4 ➔ validateSignature() 0 3 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace MaxBeckers\AmazonAlexa\Test\Validation;
6
7
use GuzzleHttp\Client;
8
use MaxBeckers\AmazonAlexa\Exception\RequestInvalidSignatureException;
9
use MaxBeckers\AmazonAlexa\Exception\RequestInvalidTimestampException;
10
use MaxBeckers\AmazonAlexa\Request\Request;
11
use MaxBeckers\AmazonAlexa\Request\Request\Standard\IntentRequest;
12
use MaxBeckers\AmazonAlexa\Validation\RequestValidator;
13
use PHPUnit\Framework\TestCase;
14
use Psr\Http\Message\ResponseInterface;
15
use Psr\Http\Message\StreamInterface;
16
17
class RequestValidatorTest extends TestCase
18
{
19
    public function testInvalidRequestTime(): void
20
    {
21
        $requestValidator = new RequestValidator();
22
23
        $intentRequest = new IntentRequest();
24
        $intentRequest->type = 'test';
25
        $intentRequest->timestamp = new \DateTime('-1 hour');
26
        $request = new Request();
27
        $request->request = $intentRequest;
28
29
        $this->expectException(RequestInvalidTimestampException::class);
30
        $requestValidator->validate($request);
31
    }
32
33
    public function testInvalidSignatureCertChainUrl(): void
34
    {
35
        $requestValidator = new RequestValidator();
36
37
        $intentRequest = new IntentRequest();
38
        $intentRequest->type = 'test';
39
        $intentRequest->timestamp = new \DateTime();
40
        $request = new Request();
41
        $request->request = $intentRequest;
42
        $request->signatureCertChainUrl = 'wrong path';
43
        $request->signature = 'none';
44
45
        $this->expectException(RequestInvalidSignatureException::class);
46
        $requestValidator->validate($request);
47
    }
48
49
    public function testWrongSignatureCertChainUrl(): void
50
    {
51
        $client = $this->createMock(Client::class);
52
        $apiResponse = $this->createMock(ResponseInterface::class);
53
        $apiResponseBody = $this->createMock(StreamInterface::class);
54
        $requestValidator = new RequestValidator(RequestValidator::TIMESTAMP_VALID_TOLERANCE_SECONDS, $client);
55
56
        $client->method('request')
57
               ->willReturn($apiResponse);
58
        $apiResponse->method('getStatusCode')
59
                    ->willReturn(200);
60
        $apiResponse->method('getBody')
61
                    ->willReturn($apiResponseBody);
62
        $apiResponseBody->method('getContents')
63
                        ->willReturn('cert content');
64
65
        $intentRequest = new IntentRequest();
66
        $intentRequest->type = 'test';
67
        $intentRequest->timestamp = new \DateTime();
68
        $request = new Request();
69
        $request->request = $intentRequest;
70
        $request->signatureCertChainUrl = 'https://s3.amazonaws.com/echo.api/test.pem';
71
        $request->signature = 'none';
72
        $request->amazonRequestBody = '';
73
74
        $this->expectException(RequestInvalidSignatureException::class);
75
        $requestValidator->validate($request);
76
    }
77
78
    public function testWrongSignatureCertChainUrlCallError(): void
79
    {
80
        $client = $this->createMock(Client::class);
81
        $apiResponse = $this->createMock(ResponseInterface::class);
82
        $requestValidator = new RequestValidator(RequestValidator::TIMESTAMP_VALID_TOLERANCE_SECONDS, $client);
83
84
        $client->method('request')
85
               ->willReturn($apiResponse);
86
        $apiResponse->method('getStatusCode')
87
                    ->willReturn(400);
88
89
        $intentRequest = new IntentRequest();
90
        $intentRequest->type = 'test';
91
        $intentRequest->timestamp = new \DateTime();
92
        $request = new Request();
93
        $request->request = $intentRequest;
94
        $request->signatureCertChainUrl = 'https://s3.amazonaws.com/echo.api/test.pem';
95
        $request->signature = 'none';
96
        $request->amazonRequestBody = '';
97
98
        $this->expectException(RequestInvalidSignatureException::class);
99
        $requestValidator->validate($request);
100
    }
101
102
    public function testValidTimestampWithinTolerance(): void
103
    {
104
        $validator = new RequestValidator();
105
        $intent = new IntentRequest();
106
        $intent->timestamp = new \DateTime('-10 seconds');
107
        $intent->type = 'test';
108
        $r = new Request();
109
        $r->request = $intent;
110
        $r->signatureCertChainUrl = 'https://s3.amazonaws.com/echo.api/echo-api-cert.pem';
111
        $r->signature = 'SGVsbG8='; // base64 "Hello"
112
113
        // validateSignature will fail (no real cert), so disable signature path by overriding property
114
        $intentWithNoSignature = new class ($intent) extends IntentRequest {
115
            public function __construct(IntentRequest $base)
116
            {
117
                $this->timestamp = $base->timestamp;
118
                $this->type = $base->type;
119
            }
120
            public function validateSignature(): bool
121
            {
122
                return false;
123
            }
124
        };
125
        $r->request = $intentWithNoSignature;
126
127
        $validator->validate($r);
128
        $this->assertTrue(true);
129
    }
130
131
    public function testTimestampExactlyOnToleranceBoundaryPasses(): void
132
    {
133
        $tolerance = 50;
134
        $validator = new RequestValidator($tolerance);
135
        $intent = new IntentRequest();
136
        $intent->timestamp = new \DateTime("-{$tolerance} seconds");
137
        $intent->type = 'test';
138
        $r = new Request();
139
        $r->request = $intent;
140
        $intent = new class ($intent) extends IntentRequest {
141
            public function __construct(IntentRequest $base)
142
            {
143
                $this->timestamp = $base->timestamp;
144
                $this->type = $base->type;
145
            }
146
            public function validateSignature(): bool
147
            {
148
                return false;
149
            }
150
        };
151
        $r->request = $intent;
152
153
        $validator->validate($r);
154
        $this->assertTrue(true);
155
    }
156
157
    public function testTimestampJustOverToleranceFails(): void
158
    {
159
        $tolerance = 30;
160
        $validator = new RequestValidator($tolerance);
161
        $intent = new IntentRequest();
162
        $intent->timestamp = new \DateTime('-' . ($tolerance + 1) . ' seconds');
163
        $intent->type = 'test';
164
        $r = new Request();
165
        $r->request = $intent;
166
167
        $this->expectException(RequestInvalidTimestampException::class);
168
        $validator->validate($r);
169
    }
170
171
    public function testSkipTimestampValidationWhenRequestDisablesIt(): void
172
    {
173
        $validator = new RequestValidator();
174
        $intent = new class () extends IntentRequest {
175
            public function __construct()
176
            {
177
                $this->timestamp = new \DateTime('-5 hours'); // would normally fail
178
                $this->type = 'test';
179
            }
180
            public function validateTimestamp(): bool
181
            {
182
                return false;
183
            }
184
            public function validateSignature(): bool
185
            {
186
                return false;
187
            }
188
        };
189
        $r = new Request();
190
        $r->request = $intent;
191
192
        $validator->validate($r);
193
        $this->assertTrue(true);
194
    }
195
196
    public function testSkipSignatureValidationWhenRequestDisablesIt(): void
197
    {
198
        $client = $this->createMock(Client::class);
199
        $client->expects($this->never())->method('request');
200
201
        $validator = new RequestValidator(RequestValidator::TIMESTAMP_VALID_TOLERANCE_SECONDS, $client);
202
203
        $intent = new class () extends IntentRequest {
204
            public function __construct()
205
            {
206
                $this->timestamp = new \DateTime();
207
                $this->type = 'test';
208
            }
209
            public function validateSignature(): bool
210
            {
211
                return false;
212
            }
213
        };
214
215
        $r = new Request();
216
        $r->request = $intent;
217
        $r->signatureCertChainUrl = 'https://s3.amazonaws.com/echo.api/echo-api-cert.pem';
218
        $r->signature = 'AA==';
219
        $validator->validate($r);
220
221
        $this->assertTrue(true);
222
    }
223
224
    public function testSignatureValidationPerformsHttpRequest(): void
225
    {
226
        $client = $this->createMock(Client::class);
227
        $response = $this->createMock(ResponseInterface::class);
228
        $stream = $this->createMock(StreamInterface::class);
229
230
        // Use a unique URL to avoid cached certificates
231
        $uniqueUrl = 'https://s3.amazonaws.com/echo.api/echo-api-cert-' . uniqid() . '.pem';
232
        $localPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . md5($uniqueUrl) . '.pem';
233
234
        // Ensure no cached certificate exists
235
        @unlink($localPath);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

235
        /** @scrutinizer ignore-unhandled */ @unlink($localPath);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
236
237
        $client->expects($this->once())->method('request')->with('GET', $uniqueUrl)->willReturn($response);
238
        $response->method('getStatusCode')->willReturn(200);
239
        $response->method('getBody')->willReturn($stream);
240
        $stream->method('getContents')->willReturn("-----BEGIN CERTIFICATE-----\nFAKE\n-----END CERTIFICATE-----");
241
242
        $validator = new RequestValidator(RequestValidator::TIMESTAMP_VALID_TOLERANCE_SECONDS, $client);
243
244
        $intent = new class () extends IntentRequest {
245
            public function __construct()
246
            {
247
                $this->timestamp = new \DateTime();
248
                $this->type = 'test';
249
            }
250
            public function validateSignature(): bool
251
            {
252
                return true; // Enable signature validation to trigger HTTP request
253
            }
254
        };
255
256
        $r = new Request();
257
        $r->request = $intent;
258
        $r->amazonRequestBody = 'BODY';
259
        $r->signature = base64_encode('sig');
260
        $r->signatureCertChainUrl = $uniqueUrl;
261
262
        $this->expectException(RequestInvalidSignatureException::class);
263
        $validator->validate($r);
264
    }
265
266
    public function testSignatureValidationUsesCachedCertWithoutHttpCall(): void
267
    {
268
        $url = 'https://s3.amazonaws.com/echo.api/cached-cert.pem';
269
        $localPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . md5($url) . '.pem';
270
        file_put_contents($localPath, "-----BEGIN CERTIFICATE-----\nCACHED\n-----END CERTIFICATE-----");
271
272
        $client = $this->createMock(Client::class);
273
        $client->expects($this->never())->method('request');
274
275
        $validator = new RequestValidator(RequestValidator::TIMESTAMP_VALID_TOLERANCE_SECONDS, $client);
276
277
        $intent = new class () extends IntentRequest {
278
            public function __construct()
279
            {
280
                $this->timestamp = new \DateTime();
281
                $this->type = 'test';
282
            }
283
            public function validateSignature(): bool
284
            {
285
                return true;
286
            }
287
        };
288
289
        $r = new Request();
290
        $r->request = $intent;
291
        $r->amazonRequestBody = 'BODY';
292
        $r->signature = base64_encode('sig');
293
        $r->signatureCertChainUrl = $url;
294
295
        $this->expectException(RequestInvalidSignatureException::class);
296
        try {
297
            $validator->validate($r);
298
        } finally {
299
            @unlink($localPath);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

299
            /** @scrutinizer ignore-unhandled */ @unlink($localPath);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
300
        }
301
    }
302
}
303