Skip to content

Commit 9be7fc0

Browse files
authored
[Bref] Port test for LambdaClient from Bref (#94)
* Port test for LambdaClient from Bref * guzzlehttp/test-server is public now * minor
1 parent de921ac commit 9be7fc0

File tree

4 files changed

+341
-1
lines changed

4 files changed

+341
-1
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"symfony/runtime": "^5.3 || ^6.0"
2929
},
3030
"require-dev": {
31+
"guzzlehttp/test-server": "^0.1",
3132
"illuminate/http": "^8.33",
3233
"swoole/ide-helper": "^4.6"
3334
},

src/bref/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
},
2121
"require-dev": {
2222
"bref/bref": "^1.3",
23+
"guzzlehttp/test-server": "^0.1",
2324
"symfony/http-foundation": "^5.3 || ^6.0",
2425
"symfony/http-kernel": "^5.4 || ^6.0",
2526
"symfony/phpunit-bridge": "^5.3"

src/bref/phpunit.xml.dist

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
<testsuites>
1414
<testsuite name="Test Suite">
1515
<directory>./tests</directory>
16-
<directory suffix=".phpt">./tests/phpt</directory>
1716
</testsuite>
1817
</testsuites>
1918
</phpunit>
Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
<?php
2+
3+
namespace Runtime\Bref\Tests;
4+
5+
use Bref\Context\Context;
6+
use Bref\Event\Handler;
7+
use GuzzleHttp\Psr7\Response;
8+
use GuzzleHttp\Server\Server;
9+
use PHPUnit\Framework\TestCase;
10+
use Runtime\Bref\Lambda\LambdaClient;
11+
12+
/**
13+
* Tests the communication between `LambdaClient` and the Lambda Runtime HTTP API.
14+
*
15+
* The API is mocked using a fake HTTP server.
16+
*/
17+
class LambdaClientTest extends TestCase
18+
{
19+
/** @var LambdaClient */
20+
private $lambda;
21+
22+
protected function setUp(): void
23+
{
24+
ob_start();
25+
Server::start();
26+
$this->lambda = new LambdaClient('localhost:8126', 'phpunit');
27+
}
28+
29+
protected function tearDown(): void
30+
{
31+
Server::stop();
32+
ob_end_clean();
33+
}
34+
35+
public function test basic behavior()
36+
{
37+
$this->givenAnEvent(['Hello' => 'world!']);
38+
39+
$output = $this->lambda->processNextEvent(new class() implements Handler {
40+
public function handle($event, Context $context)
41+
{
42+
return ['hello' => 'world'];
43+
}
44+
});
45+
46+
$this->assertTrue($output);
47+
$this->assertInvocationResult(['hello' => 'world']);
48+
}
49+
50+
public function test handler receives context()
51+
{
52+
$this->givenAnEvent(['Hello' => 'world!']);
53+
54+
$this->lambda->processNextEvent(new class() implements Handler {
55+
public function handle($event, Context $context)
56+
{
57+
return ['hello' => 'world', 'received-function-arn' => $context->getInvokedFunctionArn()];
58+
}
59+
});
60+
61+
$this->assertInvocationResult([
62+
'hello' => 'world',
63+
'received-function-arn' => 'test-function-name',
64+
]);
65+
}
66+
67+
public function test exceptions in the handler result in an invocation error()
68+
{
69+
$this->givenAnEvent(['Hello' => 'world!']);
70+
71+
$output = $this->lambda->processNextEvent(new class() implements Handler {
72+
public function handle($event, Context $context)
73+
{
74+
throw new \RuntimeException('This is an exception');
75+
}
76+
});
77+
78+
$this->assertFalse($output);
79+
$this->assertInvocationErrorResult('RuntimeException', 'This is an exception');
80+
$this->assertErrorInLogs('RuntimeException', 'This is an exception');
81+
}
82+
83+
public function test nested exceptions in the handler result in an invocation error()
84+
{
85+
$this->givenAnEvent(['Hello' => 'world!']);
86+
87+
$this->lambda->processNextEvent(new class() implements Handler {
88+
public function handle($event, Context $context)
89+
{
90+
throw new \RuntimeException('This is an exception', 0, new \RuntimeException('The previous exception.', 0, new \Exception('The original exception.')));
91+
}
92+
});
93+
94+
$this->assertInvocationErrorResult('RuntimeException', 'This is an exception');
95+
$this->assertErrorInLogs('RuntimeException', 'This is an exception');
96+
$this->assertPreviousErrorsInLogs([
97+
['errorClass' => 'RuntimeException', 'errorMessage' => 'The previous exception.'],
98+
['errorClass' => 'Exception', 'errorMessage' => 'The original exception.'],
99+
]);
100+
}
101+
102+
public function test an error is thrown if the runtime API returns a wrong response()
103+
{
104+
$this->expectExceptionMessage('Failed to fetch next Lambda invocation: The requested URL returned error: 404');
105+
Server::enqueue([
106+
new Response( // lambda event
107+
404, // 404 instead of 200
108+
[
109+
'lambda-runtime-aws-request-id' => 1,
110+
],
111+
'{ "Hello": "world!"}'
112+
),
113+
]);
114+
115+
$this->lambda->processNextEvent(new class() implements Handler {
116+
public function handle($event, Context $context)
117+
{
118+
}
119+
});
120+
}
121+
122+
public function test an error is thrown if the invocation id is missing()
123+
{
124+
$this->expectExceptionMessage('Failed to determine the Lambda invocation ID');
125+
Server::enqueue([
126+
new Response( // lambda event
127+
200,
128+
[], // Missing `lambda-runtime-aws-request-id`
129+
'{ "Hello": "world!"}'
130+
),
131+
]);
132+
133+
$this->lambda->processNextEvent(new class() implements Handler {
134+
public function handle($event, Context $context)
135+
{
136+
}
137+
});
138+
}
139+
140+
public function test an error is thrown if the invocation body is empty()
141+
{
142+
$this->expectExceptionMessage('Empty Lambda runtime API response');
143+
Server::enqueue([
144+
new Response( // lambda event
145+
200,
146+
[
147+
'lambda-runtime-aws-request-id' => 1,
148+
]
149+
),
150+
]);
151+
152+
$this->lambda->processNextEvent(new class() implements Handler {
153+
public function handle($event, Context $context)
154+
{
155+
}
156+
});
157+
}
158+
159+
public function test a wrong response from the runtime API turns the invocation into an error()
160+
{
161+
Server::enqueue([
162+
new Response( // lambda event
163+
200,
164+
[
165+
'lambda-runtime-aws-request-id' => 1,
166+
],
167+
'{ "Hello": "world!"}'
168+
),
169+
new Response(400), // The Lambda API returns a 400 instead of a 200
170+
new Response(200),
171+
]);
172+
173+
$this->lambda->processNextEvent(new class() implements Handler {
174+
public function handle($event, Context $context)
175+
{
176+
return $event;
177+
}
178+
});
179+
$requests = Server::received();
180+
$this->assertCount(3, $requests);
181+
182+
[$eventRequest, $eventFailureResponse, $eventFailureLog] = $requests;
183+
$this->assertSame('GET', $eventRequest->getMethod());
184+
$this->assertSame('http://localhost:8126/2018-06-01/runtime/invocation/next', $eventRequest->getUri()->__toString());
185+
$this->assertSame('POST', $eventFailureResponse->getMethod());
186+
$this->assertSame('http://localhost:8126/2018-06-01/runtime/invocation/1/response', $eventFailureResponse->getUri()->__toString());
187+
$this->assertSame('POST', $eventFailureLog->getMethod());
188+
$this->assertSame('http://localhost:8126/2018-06-01/runtime/invocation/1/error', $eventFailureLog->getUri()->__toString());
189+
190+
// Check the lambda result contains the error message
191+
$error = json_decode((string) $eventFailureLog->getBody(), true);
192+
$this->assertSame('Error while calling the Lambda runtime API: The requested URL returned error: 400 Bad Request', $error['errorMessage']);
193+
194+
$this->assertErrorInLogs('Exception', 'Error while calling the Lambda runtime API: The requested URL returned error: 400 Bad Request');
195+
}
196+
197+
public function test function results that cannot be encoded are reported as invocation errors()
198+
{
199+
$this->givenAnEvent(['hello' => 'world!']);
200+
201+
$this->lambda->processNextEvent(new class() implements Handler {
202+
public function handle($event, Context $context)
203+
{
204+
return "\xB1\x31";
205+
}
206+
});
207+
208+
$message = <<<ERROR
209+
The Lambda response cannot be encoded to JSON.
210+
This error usually happens when you try to return binary content. If you are writing an HTTP application and you want to return a binary HTTP response (like an image, a PDF, etc.), please read this guide: https://bref.sh/docs/runtimes/http.html#binary-responses
211+
Here is the original JSON error: 'Malformed UTF-8 characters, possibly incorrectly encoded'
212+
ERROR;
213+
$this->assertInvocationErrorResult('Exception', $message);
214+
$this->assertErrorInLogs('Exception', $message);
215+
}
216+
217+
public function test generic event handler()
218+
{
219+
$handler = new class() implements Handler {
220+
/**
221+
* @param mixed $event
222+
*
223+
* @return mixed
224+
*/
225+
public function handle($event, Context $context)
226+
{
227+
return $event;
228+
}
229+
};
230+
231+
$this->givenAnEvent(['foo' => 'bar']);
232+
233+
$this->lambda->processNextEvent($handler);
234+
235+
$this->assertInvocationResult(['foo' => 'bar']);
236+
}
237+
238+
/**
239+
* @param mixed $event
240+
*/
241+
private function givenAnEvent($event): void
242+
{
243+
Server::enqueue([
244+
new Response( // lambda event
245+
200,
246+
[
247+
'lambda-runtime-aws-request-id' => '1',
248+
'lambda-runtime-invoked-function-arn' => 'test-function-name',
249+
],
250+
json_encode($event)
251+
),
252+
new Response(200), // lambda response accepted
253+
]);
254+
}
255+
256+
/**
257+
* @param mixed $result
258+
*/
259+
private function assertInvocationResult($result)
260+
{
261+
$requests = Server::received();
262+
$this->assertCount(2, $requests);
263+
264+
[$eventRequest, $eventResponse] = $requests;
265+
$this->assertSame('GET', $eventRequest->getMethod());
266+
$this->assertSame('http://localhost:8126/2018-06-01/runtime/invocation/next', $eventRequest->getUri()->__toString());
267+
$this->assertSame('POST', $eventResponse->getMethod());
268+
$this->assertSame('http://localhost:8126/2018-06-01/runtime/invocation/1/response', $eventResponse->getUri()->__toString());
269+
$this->assertEquals($result, json_decode($eventResponse->getBody()->__toString(), true));
270+
}
271+
272+
private function assertInvocationErrorResult(string $errorClass, string $errorMessage)
273+
{
274+
$requests = Server::received();
275+
$this->assertCount(2, $requests);
276+
277+
[$eventRequest, $eventResponse] = $requests;
278+
$this->assertSame('GET', $eventRequest->getMethod());
279+
$this->assertSame('http://localhost:8126/2018-06-01/runtime/invocation/next', $eventRequest->getUri()->__toString());
280+
$this->assertSame('POST', $eventResponse->getMethod());
281+
$this->assertSame('http://localhost:8126/2018-06-01/runtime/invocation/1/error', $eventResponse->getUri()->__toString());
282+
283+
// Check the content of the result of the lambda
284+
$invocationResult = json_decode($eventResponse->getBody()->__toString(), true);
285+
$this->assertSame([
286+
'errorType',
287+
'errorMessage',
288+
'stackTrace',
289+
], array_keys($invocationResult));
290+
$this->assertEquals($errorClass, $invocationResult['errorType']);
291+
$this->assertEquals($errorMessage, $invocationResult['errorMessage']);
292+
$this->assertIsArray($invocationResult['stackTrace']);
293+
}
294+
295+
private function assertErrorInLogs(string $errorClass, string $errorMessage): void
296+
{
297+
// Decode the logs from stdout
298+
$stdout = $this->getActualOutput();
299+
300+
[$requestId, $message, $json] = explode("\t", $stdout);
301+
302+
$this->assertSame('Invoke Error', $message);
303+
304+
// Check the request ID matches a UUID
305+
$this->assertNotEmpty($requestId);
306+
307+
$invocationResult = json_decode($json, true);
308+
unset($invocationResult['previous']);
309+
$this->assertSame([
310+
'errorType',
311+
'errorMessage',
312+
'stack',
313+
], array_keys($invocationResult));
314+
$this->assertEquals($errorClass, $invocationResult['errorType']);
315+
$this->assertEquals($errorMessage, $invocationResult['errorMessage']);
316+
$this->assertIsArray($invocationResult['stack']);
317+
}
318+
319+
private function assertPreviousErrorsInLogs(array $previousErrors)
320+
{
321+
// Decode the logs from stdout
322+
$stdout = $this->getActualOutput();
323+
324+
[, , $json] = explode("\t", $stdout);
325+
326+
['previous' => $previous] = json_decode($json, true);
327+
$this->assertCount(count($previousErrors), $previous);
328+
foreach ($previous as $index => $error) {
329+
$this->assertSame([
330+
'errorType',
331+
'errorMessage',
332+
'stack',
333+
], array_keys($error));
334+
$this->assertEquals($previousErrors[$index]['errorClass'], $error['errorType']);
335+
$this->assertEquals($previousErrors[$index]['errorMessage'], $error['errorMessage']);
336+
$this->assertIsArray($error['stack']);
337+
}
338+
}
339+
}

0 commit comments

Comments
 (0)