diff --git a/composer.json b/composer.json
index 60ef28f..afad724 100644
--- a/composer.json
+++ b/composer.json
@@ -28,6 +28,7 @@
"symfony/runtime": "^5.3 || ^6.0"
},
"require-dev": {
+ "guzzlehttp/test-server": "^0.1",
"illuminate/http": "^8.33",
"swoole/ide-helper": "^4.6"
},
diff --git a/src/bref/composer.json b/src/bref/composer.json
index 96516cf..f19b4c6 100644
--- a/src/bref/composer.json
+++ b/src/bref/composer.json
@@ -20,6 +20,7 @@
},
"require-dev": {
"bref/bref": "^1.3",
+ "guzzlehttp/test-server": "^0.1",
"symfony/http-foundation": "^5.3 || ^6.0",
"symfony/http-kernel": "^5.4 || ^6.0",
"symfony/phpunit-bridge": "^5.3"
diff --git a/src/bref/phpunit.xml.dist b/src/bref/phpunit.xml.dist
index ca58bf8..f1701c2 100644
--- a/src/bref/phpunit.xml.dist
+++ b/src/bref/phpunit.xml.dist
@@ -13,7 +13,6 @@
./tests
- ./tests/phpt
diff --git a/src/bref/tests/Lambda/LambdaClientTest.php b/src/bref/tests/Lambda/LambdaClientTest.php
new file mode 100644
index 0000000..32211a3
--- /dev/null
+++ b/src/bref/tests/Lambda/LambdaClientTest.php
@@ -0,0 +1,339 @@
+lambda = new LambdaClient('localhost:8126', 'phpunit');
+ }
+
+ protected function tearDown(): void
+ {
+ Server::stop();
+ ob_end_clean();
+ }
+
+ public function test basic behavior()
+ {
+ $this->givenAnEvent(['Hello' => 'world!']);
+
+ $output = $this->lambda->processNextEvent(new class() implements Handler {
+ public function handle($event, Context $context)
+ {
+ return ['hello' => 'world'];
+ }
+ });
+
+ $this->assertTrue($output);
+ $this->assertInvocationResult(['hello' => 'world']);
+ }
+
+ public function test handler receives context()
+ {
+ $this->givenAnEvent(['Hello' => 'world!']);
+
+ $this->lambda->processNextEvent(new class() implements Handler {
+ public function handle($event, Context $context)
+ {
+ return ['hello' => 'world', 'received-function-arn' => $context->getInvokedFunctionArn()];
+ }
+ });
+
+ $this->assertInvocationResult([
+ 'hello' => 'world',
+ 'received-function-arn' => 'test-function-name',
+ ]);
+ }
+
+ public function test exceptions in the handler result in an invocation error()
+ {
+ $this->givenAnEvent(['Hello' => 'world!']);
+
+ $output = $this->lambda->processNextEvent(new class() implements Handler {
+ public function handle($event, Context $context)
+ {
+ throw new \RuntimeException('This is an exception');
+ }
+ });
+
+ $this->assertFalse($output);
+ $this->assertInvocationErrorResult('RuntimeException', 'This is an exception');
+ $this->assertErrorInLogs('RuntimeException', 'This is an exception');
+ }
+
+ public function test nested exceptions in the handler result in an invocation error()
+ {
+ $this->givenAnEvent(['Hello' => 'world!']);
+
+ $this->lambda->processNextEvent(new class() implements Handler {
+ public function handle($event, Context $context)
+ {
+ throw new \RuntimeException('This is an exception', 0, new \RuntimeException('The previous exception.', 0, new \Exception('The original exception.')));
+ }
+ });
+
+ $this->assertInvocationErrorResult('RuntimeException', 'This is an exception');
+ $this->assertErrorInLogs('RuntimeException', 'This is an exception');
+ $this->assertPreviousErrorsInLogs([
+ ['errorClass' => 'RuntimeException', 'errorMessage' => 'The previous exception.'],
+ ['errorClass' => 'Exception', 'errorMessage' => 'The original exception.'],
+ ]);
+ }
+
+ public function test an error is thrown if the runtime API returns a wrong response()
+ {
+ $this->expectExceptionMessage('Failed to fetch next Lambda invocation: The requested URL returned error: 404');
+ Server::enqueue([
+ new Response( // lambda event
+ 404, // 404 instead of 200
+ [
+ 'lambda-runtime-aws-request-id' => 1,
+ ],
+ '{ "Hello": "world!"}'
+ ),
+ ]);
+
+ $this->lambda->processNextEvent(new class() implements Handler {
+ public function handle($event, Context $context)
+ {
+ }
+ });
+ }
+
+ public function test an error is thrown if the invocation id is missing()
+ {
+ $this->expectExceptionMessage('Failed to determine the Lambda invocation ID');
+ Server::enqueue([
+ new Response( // lambda event
+ 200,
+ [], // Missing `lambda-runtime-aws-request-id`
+ '{ "Hello": "world!"}'
+ ),
+ ]);
+
+ $this->lambda->processNextEvent(new class() implements Handler {
+ public function handle($event, Context $context)
+ {
+ }
+ });
+ }
+
+ public function test an error is thrown if the invocation body is empty()
+ {
+ $this->expectExceptionMessage('Empty Lambda runtime API response');
+ Server::enqueue([
+ new Response( // lambda event
+ 200,
+ [
+ 'lambda-runtime-aws-request-id' => 1,
+ ]
+ ),
+ ]);
+
+ $this->lambda->processNextEvent(new class() implements Handler {
+ public function handle($event, Context $context)
+ {
+ }
+ });
+ }
+
+ public function test a wrong response from the runtime API turns the invocation into an error()
+ {
+ Server::enqueue([
+ new Response( // lambda event
+ 200,
+ [
+ 'lambda-runtime-aws-request-id' => 1,
+ ],
+ '{ "Hello": "world!"}'
+ ),
+ new Response(400), // The Lambda API returns a 400 instead of a 200
+ new Response(200),
+ ]);
+
+ $this->lambda->processNextEvent(new class() implements Handler {
+ public function handle($event, Context $context)
+ {
+ return $event;
+ }
+ });
+ $requests = Server::received();
+ $this->assertCount(3, $requests);
+
+ [$eventRequest, $eventFailureResponse, $eventFailureLog] = $requests;
+ $this->assertSame('GET', $eventRequest->getMethod());
+ $this->assertSame('http://localhost:8126/2018-06-01/runtime/invocation/next', $eventRequest->getUri()->__toString());
+ $this->assertSame('POST', $eventFailureResponse->getMethod());
+ $this->assertSame('http://localhost:8126/2018-06-01/runtime/invocation/1/response', $eventFailureResponse->getUri()->__toString());
+ $this->assertSame('POST', $eventFailureLog->getMethod());
+ $this->assertSame('http://localhost:8126/2018-06-01/runtime/invocation/1/error', $eventFailureLog->getUri()->__toString());
+
+ // Check the lambda result contains the error message
+ $error = json_decode((string) $eventFailureLog->getBody(), true);
+ $this->assertSame('Error while calling the Lambda runtime API: The requested URL returned error: 400 Bad Request', $error['errorMessage']);
+
+ $this->assertErrorInLogs('Exception', 'Error while calling the Lambda runtime API: The requested URL returned error: 400 Bad Request');
+ }
+
+ public function test function results that cannot be encoded are reported as invocation errors()
+ {
+ $this->givenAnEvent(['hello' => 'world!']);
+
+ $this->lambda->processNextEvent(new class() implements Handler {
+ public function handle($event, Context $context)
+ {
+ return "\xB1\x31";
+ }
+ });
+
+ $message = <<assertInvocationErrorResult('Exception', $message);
+ $this->assertErrorInLogs('Exception', $message);
+ }
+
+ public function test generic event handler()
+ {
+ $handler = new class() implements Handler {
+ /**
+ * @param mixed $event
+ *
+ * @return mixed
+ */
+ public function handle($event, Context $context)
+ {
+ return $event;
+ }
+ };
+
+ $this->givenAnEvent(['foo' => 'bar']);
+
+ $this->lambda->processNextEvent($handler);
+
+ $this->assertInvocationResult(['foo' => 'bar']);
+ }
+
+ /**
+ * @param mixed $event
+ */
+ private function givenAnEvent($event): void
+ {
+ Server::enqueue([
+ new Response( // lambda event
+ 200,
+ [
+ 'lambda-runtime-aws-request-id' => '1',
+ 'lambda-runtime-invoked-function-arn' => 'test-function-name',
+ ],
+ json_encode($event)
+ ),
+ new Response(200), // lambda response accepted
+ ]);
+ }
+
+ /**
+ * @param mixed $result
+ */
+ private function assertInvocationResult($result)
+ {
+ $requests = Server::received();
+ $this->assertCount(2, $requests);
+
+ [$eventRequest, $eventResponse] = $requests;
+ $this->assertSame('GET', $eventRequest->getMethod());
+ $this->assertSame('http://localhost:8126/2018-06-01/runtime/invocation/next', $eventRequest->getUri()->__toString());
+ $this->assertSame('POST', $eventResponse->getMethod());
+ $this->assertSame('http://localhost:8126/2018-06-01/runtime/invocation/1/response', $eventResponse->getUri()->__toString());
+ $this->assertEquals($result, json_decode($eventResponse->getBody()->__toString(), true));
+ }
+
+ private function assertInvocationErrorResult(string $errorClass, string $errorMessage)
+ {
+ $requests = Server::received();
+ $this->assertCount(2, $requests);
+
+ [$eventRequest, $eventResponse] = $requests;
+ $this->assertSame('GET', $eventRequest->getMethod());
+ $this->assertSame('http://localhost:8126/2018-06-01/runtime/invocation/next', $eventRequest->getUri()->__toString());
+ $this->assertSame('POST', $eventResponse->getMethod());
+ $this->assertSame('http://localhost:8126/2018-06-01/runtime/invocation/1/error', $eventResponse->getUri()->__toString());
+
+ // Check the content of the result of the lambda
+ $invocationResult = json_decode($eventResponse->getBody()->__toString(), true);
+ $this->assertSame([
+ 'errorType',
+ 'errorMessage',
+ 'stackTrace',
+ ], array_keys($invocationResult));
+ $this->assertEquals($errorClass, $invocationResult['errorType']);
+ $this->assertEquals($errorMessage, $invocationResult['errorMessage']);
+ $this->assertIsArray($invocationResult['stackTrace']);
+ }
+
+ private function assertErrorInLogs(string $errorClass, string $errorMessage): void
+ {
+ // Decode the logs from stdout
+ $stdout = $this->getActualOutput();
+
+ [$requestId, $message, $json] = explode("\t", $stdout);
+
+ $this->assertSame('Invoke Error', $message);
+
+ // Check the request ID matches a UUID
+ $this->assertNotEmpty($requestId);
+
+ $invocationResult = json_decode($json, true);
+ unset($invocationResult['previous']);
+ $this->assertSame([
+ 'errorType',
+ 'errorMessage',
+ 'stack',
+ ], array_keys($invocationResult));
+ $this->assertEquals($errorClass, $invocationResult['errorType']);
+ $this->assertEquals($errorMessage, $invocationResult['errorMessage']);
+ $this->assertIsArray($invocationResult['stack']);
+ }
+
+ private function assertPreviousErrorsInLogs(array $previousErrors)
+ {
+ // Decode the logs from stdout
+ $stdout = $this->getActualOutput();
+
+ [, , $json] = explode("\t", $stdout);
+
+ ['previous' => $previous] = json_decode($json, true);
+ $this->assertCount(count($previousErrors), $previous);
+ foreach ($previous as $index => $error) {
+ $this->assertSame([
+ 'errorType',
+ 'errorMessage',
+ 'stack',
+ ], array_keys($error));
+ $this->assertEquals($previousErrors[$index]['errorClass'], $error['errorType']);
+ $this->assertEquals($previousErrors[$index]['errorMessage'], $error['errorMessage']);
+ $this->assertIsArray($error['stack']);
+ }
+ }
+}