Skip to content

Commit

Permalink
Add E2E tests for HTTP client
Browse files Browse the repository at this point in the history
  • Loading branch information
stayallive committed Nov 6, 2023
1 parent 5fb16a5 commit 43c6770
Show file tree
Hide file tree
Showing 4 changed files with 292 additions and 0 deletions.
98 changes: 98 additions & 0 deletions tests/HttpClient/HttpClientTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

declare(strict_types=1);

namespace Sentry\Tests\HttpClient;

use PHPUnit\Framework\TestCase;
use Sentry\HttpClient\HttpClient;
use Sentry\HttpClient\Request;
use Sentry\Options;
use Sentry\Util\Http;

class HttpClientTest extends TestCase
{
use TestServer;

public function testClientMakesRequestWithCorrectHeadersMethodAndPath(): void
{
$testServer = $this->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);
}
}
145 changes: 145 additions & 0 deletions tests/HttpClient/TestServer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

declare(strict_types=1);

namespace Sentry\Tests\HttpClient;

/**
* This is a test server that can be used to test the HttpClient.
*
* It spawns the PHP development server, captures the output and returns it to the caller.
*
* In your test call `$this->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<string, string>,
* headers: array<string, string>,
* 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);
}
}
1 change: 1 addition & 0 deletions tests/testserver/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
output.json
48 changes: 48 additions & 0 deletions tests/testserver/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

$outputFile = __DIR__ . '/output.json';

// We expect the path to be `/api/<project_id>/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.';

0 comments on commit 43c6770

Please sign in to comment.