From 43c677059cc78e9489c5e2a1272c471185751143 Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Mon, 6 Nov 2023 09:59:58 +0100 Subject: [PATCH] Add E2E tests for HTTP client --- tests/HttpClient/HttpClientTest.php | 98 +++++++++++++++++++ tests/HttpClient/TestServer.php | 145 ++++++++++++++++++++++++++++ tests/testserver/.gitignore | 1 + tests/testserver/index.php | 48 +++++++++ 4 files changed, 292 insertions(+) create mode 100644 tests/HttpClient/HttpClientTest.php create mode 100644 tests/HttpClient/TestServer.php create mode 100644 tests/testserver/.gitignore create mode 100644 tests/testserver/index.php diff --git a/tests/HttpClient/HttpClientTest.php b/tests/HttpClient/HttpClientTest.php new file mode 100644 index 000000000..e6b0d23f0 --- /dev/null +++ b/tests/HttpClient/HttpClientTest.php @@ -0,0 +1,98 @@ +startTestServer(); + + $options = new Options([ + 'dsn' => "http://publicKey@{$testServer}/200", + ]); + + $request = new Request(); + $request->setStringBody('test'); + + $client = new HttpClient($sdkIdentifier = 'sentry.php', $sdkVersion = 'testing'); + $response = $client->sendRequest($request, $options); + + $serverOutput = $this->stopTestServer(); + + $this->assertTrue($response->isSuccess()); + $this->assertEquals(200, $response->getStatusCode()); + + // This assertion is here to test that the response headers are correctly parsed + $this->assertEquals(200, (int) $response->getHeaderLine('x-sentry-test-server-status-code')); + + $this->assertTrue($serverOutput['compressed']); + $this->assertEquals($response->getStatusCode(), $serverOutput['status']); + $this->assertEquals($request->getStringBody(), $serverOutput['body']); + $this->assertEquals('/api/200/envelope/', $serverOutput['server']['REQUEST_URI']); + $this->assertEquals("{$sdkIdentifier}/{$sdkVersion}", $serverOutput['headers']['User-Agent']); + + $expectedHeaders = Http::getRequestHeaders($options->getDsn(), $sdkIdentifier, $sdkVersion); + foreach ($expectedHeaders as $expectedHeader) { + [$headerName, $headerValue] = explode(': ', $expectedHeader); + $this->assertEquals($headerValue, $serverOutput['headers'][$headerName]); + } + } + + public function testClientMakesUncompressedRequestWhenCompressionDisabled(): void + { + $testServer = $this->startTestServer(); + + $options = new Options([ + 'dsn' => "http://publicKey@{$testServer}/200", + 'http_compression' => false, + ]); + + $request = new Request(); + $request->setStringBody('test'); + + $client = new HttpClient('sentry.php', 'testing'); + $response = $client->sendRequest($request, $options); + + $serverOutput = $this->stopTestServer(); + + $this->assertTrue($response->isSuccess()); + $this->assertFalse($serverOutput['compressed']); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals($response->getStatusCode(), $serverOutput['status']); + $this->assertEquals($request->getStringBody(), $serverOutput['body']); + $this->assertEquals(\strlen($request->getStringBody()), $serverOutput['headers']['Content-Length']); + } + + public function testThrowsExceptionIfDsnOptionIsNotSet(): void + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('The DSN option must be set to use the HttpClient.'); + + $options = new Options(['dsn' => null]); + + $client = new HttpClient('sentry.php', 'testing'); + $client->sendRequest(new Request(), $options); + } + + public function testThrowsExceptionIfRequestDataIsEmpty(): void + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('The request data is empty.'); + + $options = new Options(['dsn' => 'https://publicKey@example.com/1']); + + $client = new HttpClient('sentry.php', 'testing'); + $client->sendRequest(new Request(), $options); + } +} diff --git a/tests/HttpClient/TestServer.php b/tests/HttpClient/TestServer.php new file mode 100644 index 000000000..5a12b2cf8 --- /dev/null +++ b/tests/HttpClient/TestServer.php @@ -0,0 +1,145 @@ +startTestServer()` to start the server and get the address. + * After you have made your request, call `$this->stopTestServer()` to stop the server and get the output. + * + * Thanks to Stripe for the inspiration: https://github.com/stripe/stripe-php/blob/e0a960c8655b21b21c3ba2e5927f432eeda9105f/tests/TestServer.php + */ +trait TestServer +{ + /** + * @var string the path to the output file + */ + protected static $serverOutputFile = __DIR__ . '/../testserver/output.json'; + + /** + * @var resource|null the server process handle + */ + protected $serverProcess; + + /** + * @var resource|null the server stderr handle + */ + protected $serverStderr; + + /** + * @var int the port on which the server is listening, this default value was randomly chosen + */ + protected $serverPort = 44884; + + public function startTestServer(): string + { + if ($this->serverProcess !== null) { + throw new \RuntimeException('There is already a test server instance running.'); + } + + if (file_exists(self::$serverOutputFile)) { + unlink(self::$serverOutputFile); + } + + $pipes = []; + + $this->serverProcess = proc_open( + $command = sprintf( + 'php -S localhost:%d -t %s', + $this->serverPort, + realpath(__DIR__ . '/../testserver') + ), + [2 => ['pipe', 'w']], + $pipes + ); + + $this->serverStderr = $pipes[2]; + + $pid = proc_get_status($this->serverProcess)['pid']; + + if (!\is_resource($this->serverProcess)) { + throw new \RuntimeException("Error starting test server on pid {$pid}, command failed: {$command}"); + } + + while (true) { + $conn = @fsockopen('localhost', $this->serverPort); + if (\is_resource($conn)) { + fclose($conn); + + break; + } + } + + return "localhost:{$this->serverPort}"; + } + + /** + * Stop the test server and return the output from the server. + * + * @return array{ + * body: string, + * status: int, + * server: array, + * headers: array, + * compressed: bool, + * } + */ + public function stopTestServer(): array + { + if (!$this->serverProcess) { + throw new \RuntimeException('There is no test server instance running.'); + } + + for ($i = 0; $i < 20; ++$i) { + $status = proc_get_status($this->serverProcess); + + if (!$status['running']) { + break; + } + + $this->killServerProcess($status['pid']); + + usleep(10000); + } + + if ($status['running']) { + throw new \RuntimeException('Could not kill test server'); + } + + if (!file_exists(self::$serverOutputFile)) { + stream_set_blocking($this->serverStderr, false); + $stderrOutput = stream_get_contents($this->serverStderr); + + echo $stderrOutput . \PHP_EOL; + + throw new \RuntimeException('Test server did not write output file'); + } + + proc_close($this->serverProcess); + + $this->serverProcess = null; + $this->serverStderr = null; + + return json_decode(file_get_contents(self::$serverOutputFile), true); + } + + private function killServerProcess(int $pid): void + { + if (\PHP_OS_FAMILY === 'Windows') { + exec("taskkill /pid {$pid} /f /t"); + } else { + // Kills any child processes -- the php test server appears to start up a child. + exec("pkill -P {$pid}"); + + // Kill the parent process. + exec("kill {$pid}"); + } + + proc_terminate($this->serverProcess, 9); + } +} diff --git a/tests/testserver/.gitignore b/tests/testserver/.gitignore new file mode 100644 index 000000000..e102ff534 --- /dev/null +++ b/tests/testserver/.gitignore @@ -0,0 +1 @@ +output.json diff --git a/tests/testserver/index.php b/tests/testserver/index.php new file mode 100644 index 000000000..ed763a59d --- /dev/null +++ b/tests/testserver/index.php @@ -0,0 +1,48 @@ +/envelope/`. +// We use the project ID to determine the status code so we need to extract it from the path +$path = trim(parse_url($_SERVER['REQUEST_URI'], \PHP_URL_PATH), '/'); + +if (!preg_match('/api\/\d+\/envelope/', $path)) { + http_response_code(204); + + return; +} + +$status = (int) explode('/', $path)[1]; + +$headers = getallheaders(); + +$rawBody = file_get_contents('php://input'); + +$compressed = false; + +if (!isset($headers['Content-Encoding'])) { + $body = $rawBody; +} elseif ($headers['Content-Encoding'] === 'gzip') { + $body = gzdecode($rawBody); + $compressed = true; +} else { + $body = '__unable to decode body__'; +} + +$output = [ + 'body' => $body, + 'status' => $status, + 'server' => $_SERVER, + 'headers' => $headers, + 'compressed' => $compressed, +]; + +file_put_contents(__DIR__ . '/output.json', json_encode($output, \JSON_PRETTY_PRINT)); + +header('X-Sentry-Test-Server-Status-Code: ' . $status); + +http_response_code($status); + +return 'Processed.';