fix(relay): properly process plain text payload (#39)
* fix(relay): properly process plain text payload non-json text payload is passed as is. * style: apply TS style fixes
This commit is contained in:
@@ -30,7 +30,6 @@ defineOptions({
|
||||
|
||||
const VERSION = 'v0.3.0-alpha'; // x-release-please-version
|
||||
|
||||
|
||||
/*
|
||||
* Stores.
|
||||
*/
|
||||
|
||||
@@ -29,14 +29,14 @@ class NimbusRelayRequest extends FormRequest
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getBody(): array
|
||||
public function getBody(): string|array
|
||||
{
|
||||
$body = $this->validated('body') && filled($this->validated('body'))
|
||||
? $this->validated('body')
|
||||
: [];
|
||||
|
||||
return is_string($body)
|
||||
? json_decode($body, true)
|
||||
? json_decode($body, true) ?? $body
|
||||
: $body;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ class RequestRelayAction
|
||||
$queryParameters = $requestRelayData->queryParameters;
|
||||
$requestBody = $requestRelayData->body;
|
||||
|
||||
if (in_array($requestRelayData->method, ['get', 'head'])) {
|
||||
if (is_array($requestBody) && in_array($requestRelayData->method, ['get', 'head'])) {
|
||||
$queryParameters = array_merge(
|
||||
$queryParameters,
|
||||
$requestBody,
|
||||
@@ -84,10 +84,17 @@ class RequestRelayAction
|
||||
->withQueryParameters($queryParameters)
|
||||
->when(
|
||||
$requestBody !== [],
|
||||
fn (PendingRequest $pendingRequest) => $pendingRequest->withBody(
|
||||
json_encode($requestRelayData->body) ?: throw new RuntimeException('Cannot parse body.'),
|
||||
contentType: $contentType,
|
||||
),
|
||||
function (PendingRequest $pendingRequest) use ($requestBody, $contentType) {
|
||||
$body = match (true) {
|
||||
is_string($requestBody) => $requestBody,
|
||||
default => json_encode($requestBody) ?: throw new RuntimeException('Cannot parse body.'),
|
||||
};
|
||||
|
||||
return $pendingRequest->withBody(
|
||||
$body,
|
||||
contentType: $contentType,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ readonly class RequestRelayData
|
||||
{
|
||||
/**
|
||||
* @param array<string, string> $headers
|
||||
* @param array<string, mixed> $body
|
||||
* @param array<string, mixed>|string $body
|
||||
* @param array<string, string|null> $queryParameters
|
||||
*/
|
||||
public function __construct(
|
||||
@@ -21,7 +21,7 @@ readonly class RequestRelayData
|
||||
public string $endpoint,
|
||||
public AuthorizationCredentials $authorization,
|
||||
public array $headers,
|
||||
public array $body,
|
||||
public array|string $body,
|
||||
public ParameterBag $cookies,
|
||||
public array $queryParameters = [],
|
||||
) {}
|
||||
|
||||
@@ -229,6 +229,59 @@ class RequestRelayActionFunctionalTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
#[TestWith(['get'])]
|
||||
#[TestWith(['head'])]
|
||||
public function test_it_does_not_merge_string_body_into_query_parameters_for_get_and_head_requests(string $method): void
|
||||
{
|
||||
// Arrange
|
||||
|
||||
$bodyData = 'plain text content';
|
||||
|
||||
$queryParameters = ['page' => '1'];
|
||||
|
||||
$requestData = new RequestRelayData(
|
||||
method: $method,
|
||||
endpoint: self::ENDPOINT,
|
||||
authorization: AuthorizationCredentials::none(),
|
||||
headers: [],
|
||||
body: $bodyData,
|
||||
cookies: new ParameterBag,
|
||||
queryParameters: $queryParameters,
|
||||
);
|
||||
|
||||
// Anticipate
|
||||
|
||||
Http::fake(function (Request $request) use ($queryParameters) {
|
||||
// Assert that the request URL contains the original query parameters
|
||||
foreach ($queryParameters as $key => $value) {
|
||||
if (! str_contains($request->url(), "{$key}={$value}")) {
|
||||
return Http::response(['error' => 'Missing query parameter'], 400);
|
||||
}
|
||||
}
|
||||
|
||||
// Assert that the request URL does NOT contain the string body
|
||||
if (str_contains($request->url(), 'plain+text+content') || str_contains($request->url(), 'plain%20text%20content')) {
|
||||
return Http::response(['error' => 'Body should not be in query parameters'], 400);
|
||||
}
|
||||
|
||||
return Http::response([
|
||||
'success' => true,
|
||||
]);
|
||||
});
|
||||
|
||||
$this->mockAuthorizationHandler();
|
||||
|
||||
$requestRelayAction = resolve(RequestRelayAction::class);
|
||||
|
||||
// Act
|
||||
|
||||
$response = $requestRelayAction->execute($requestData);
|
||||
|
||||
// Assert
|
||||
|
||||
$this->assertEquals(200, $response->statusCode);
|
||||
}
|
||||
|
||||
public function test_it_sends_json_body_by_default(): void
|
||||
{
|
||||
// Arrange
|
||||
@@ -270,6 +323,45 @@ class RequestRelayActionFunctionalTest extends TestCase
|
||||
$this->assertEquals($bodyData, $response->body->body['receivedBody']);
|
||||
}
|
||||
|
||||
public function test_it_relays_plain_text_body(): void
|
||||
{
|
||||
// Arrange
|
||||
|
||||
$bodyData = 'plain text content';
|
||||
|
||||
$requestData = new RequestRelayData(
|
||||
method: 'post',
|
||||
endpoint: self::ENDPOINT,
|
||||
authorization: AuthorizationCredentials::none(),
|
||||
headers: ['Content-Type' => 'text/plain'],
|
||||
body: $bodyData,
|
||||
cookies: new ParameterBag,
|
||||
);
|
||||
|
||||
// Anticipate
|
||||
|
||||
Http::fake(function (Request $request) use ($bodyData) {
|
||||
return Http::response([
|
||||
'receivedBody' => $request->body(),
|
||||
'bodyMatches' => $request->body() === $bodyData,
|
||||
], 200);
|
||||
});
|
||||
|
||||
$this->mockAuthorizationHandler();
|
||||
|
||||
$requestRelayAction = resolve(RequestRelayAction::class);
|
||||
|
||||
// Act
|
||||
|
||||
$response = $requestRelayAction->execute($requestData);
|
||||
|
||||
// Assert
|
||||
|
||||
$this->assertTrue($response->body->body['bodyMatches'], 'POST body should be sent as plain text');
|
||||
|
||||
$this->assertEquals($bodyData, $response->body->body['receivedBody']);
|
||||
}
|
||||
|
||||
public function test_it_url_decodes_cookie_values(): void
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -15,11 +15,12 @@ use Symfony\Component\HttpFoundation\InputBag;
|
||||
#[CoversClass(RequestRelayData::class)]
|
||||
class RequestRelayDataUnitTest extends TestCase
|
||||
{
|
||||
#[DataProvider('endpointsDataProvider')]
|
||||
#[DataProvider('relayRequestDataDataProvider')]
|
||||
public function test_it_creates_instance_from_api_request(
|
||||
string $endpoint,
|
||||
string $expectedEndpoint,
|
||||
array $expectedParameters,
|
||||
array|string $body,
|
||||
): void {
|
||||
// Arrange
|
||||
|
||||
@@ -51,7 +52,7 @@ class RequestRelayDataUnitTest extends TestCase
|
||||
['key' => 'Content-Type', 'value' => 'application/json'],
|
||||
['key' => 'X-Custom-Header', 'value' => '::value::'],
|
||||
],
|
||||
'body' => $body = ['test' => 'data'],
|
||||
'body' => $body,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -90,48 +91,62 @@ class RequestRelayDataUnitTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
public static function endpointsDataProvider(): Generator
|
||||
public static function relayRequestDataDataProvider(): Generator
|
||||
{
|
||||
yield 'simple path without params' => [
|
||||
'endpoint' => '/api/test',
|
||||
'expectedEndpoint' => '/api/test',
|
||||
'expectedParameters' => [],
|
||||
'body' => ['test' => 'data'],
|
||||
];
|
||||
|
||||
yield 'simple path with single param' => [
|
||||
'endpoint' => '/api/test?parameter-1=value',
|
||||
'expectedEndpoint' => '/api/test',
|
||||
'expectedParameters' => ['parameter-1' => 'value'],
|
||||
'body' => ['test' => 'data'],
|
||||
];
|
||||
|
||||
yield 'absolute URL without params' => [
|
||||
'endpoint' => 'https://127.0.0.1/api/test',
|
||||
'expectedEndpoint' => 'https://127.0.0.1/api/test',
|
||||
'expectedParameters' => [],
|
||||
'body' => ['test' => 'data'],
|
||||
];
|
||||
|
||||
yield 'absolute URL with multiple params including broken' => [
|
||||
'endpoint' => 'https://127.0.0.1/api/test?key=1&key-2=&broken',
|
||||
'expectedEndpoint' => 'https://127.0.0.1/api/test',
|
||||
'expectedParameters' => ['key' => '1', 'key-2' => ''],
|
||||
'body' => ['test' => 'data'],
|
||||
];
|
||||
|
||||
yield 'absolute URL with multiple valid params' => [
|
||||
'endpoint' => 'https://127.0.0.1/api/test?key=value&key-2=value-2',
|
||||
'expectedEndpoint' => 'https://127.0.0.1/api/test',
|
||||
'expectedParameters' => ['key' => 'value', 'key-2' => 'value-2'],
|
||||
'body' => ['test' => 'data'],
|
||||
];
|
||||
|
||||
yield 'absolute URL with port and param' => [
|
||||
'endpoint' => 'https://127.0.0.1:8000/api/test?key=value',
|
||||
'expectedEndpoint' => 'https://127.0.0.1:8000/api/test',
|
||||
'expectedParameters' => ['key' => 'value'],
|
||||
'body' => ['test' => 'data'],
|
||||
];
|
||||
|
||||
yield 'invalid URL with port and param' => [
|
||||
'endpoint' => 'http://:80?key=value',
|
||||
'expectedEndpoint' => 'http://:80?key=value', // parse_url failed, return as is.
|
||||
'expectedParameters' => [],
|
||||
'body' => ['test' => 'data'],
|
||||
];
|
||||
|
||||
yield 'plain text body' => [
|
||||
'endpoint' => '/api/test',
|
||||
'expectedEndpoint' => '/api/test',
|
||||
'expectedParameters' => [],
|
||||
'body' => 'plain text content',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +148,28 @@ class NimbusRelayTest extends TestCase
|
||||
cookies: [],
|
||||
),
|
||||
];
|
||||
|
||||
yield 'POST request with plain text body' => [
|
||||
'payload' => [
|
||||
'method' => 'POST',
|
||||
'endpoint' => '/test-endpoint',
|
||||
'body' => 'plain text content',
|
||||
'headers' => [
|
||||
['key' => 'Content-Type', 'value' => 'text/plain'],
|
||||
],
|
||||
],
|
||||
'relayResponseStub' => new RelayedRequestResponseData(
|
||||
statusCode: 200,
|
||||
statusText: 'OK',
|
||||
body: new PrintableResponseBody('Received plain text!'),
|
||||
headers: [
|
||||
'header1' => ['value1'],
|
||||
],
|
||||
durationMs: fake('en')->randomFloat(),
|
||||
timestamp: fake('en')->dateTime()->getTimestamp(),
|
||||
cookies: [],
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('validationFailureProvider')]
|
||||
|
||||
Reference in New Issue
Block a user