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
|
const VERSION = 'v0.3.0-alpha'; // x-release-please-version
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Stores.
|
* Stores.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -29,14 +29,14 @@ class NimbusRelayRequest extends FormRequest
|
|||||||
/**
|
/**
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
public function getBody(): array
|
public function getBody(): string|array
|
||||||
{
|
{
|
||||||
$body = $this->validated('body') && filled($this->validated('body'))
|
$body = $this->validated('body') && filled($this->validated('body'))
|
||||||
? $this->validated('body')
|
? $this->validated('body')
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
return is_string($body)
|
return is_string($body)
|
||||||
? json_decode($body, true)
|
? json_decode($body, true) ?? $body
|
||||||
: $body;
|
: $body;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class RequestRelayAction
|
|||||||
$queryParameters = $requestRelayData->queryParameters;
|
$queryParameters = $requestRelayData->queryParameters;
|
||||||
$requestBody = $requestRelayData->body;
|
$requestBody = $requestRelayData->body;
|
||||||
|
|
||||||
if (in_array($requestRelayData->method, ['get', 'head'])) {
|
if (is_array($requestBody) && in_array($requestRelayData->method, ['get', 'head'])) {
|
||||||
$queryParameters = array_merge(
|
$queryParameters = array_merge(
|
||||||
$queryParameters,
|
$queryParameters,
|
||||||
$requestBody,
|
$requestBody,
|
||||||
@@ -84,10 +84,17 @@ class RequestRelayAction
|
|||||||
->withQueryParameters($queryParameters)
|
->withQueryParameters($queryParameters)
|
||||||
->when(
|
->when(
|
||||||
$requestBody !== [],
|
$requestBody !== [],
|
||||||
fn (PendingRequest $pendingRequest) => $pendingRequest->withBody(
|
function (PendingRequest $pendingRequest) use ($requestBody, $contentType) {
|
||||||
json_encode($requestRelayData->body) ?: throw new RuntimeException('Cannot parse body.'),
|
$body = match (true) {
|
||||||
contentType: $contentType,
|
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, string> $headers
|
||||||
* @param array<string, mixed> $body
|
* @param array<string, mixed>|string $body
|
||||||
* @param array<string, string|null> $queryParameters
|
* @param array<string, string|null> $queryParameters
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -21,7 +21,7 @@ readonly class RequestRelayData
|
|||||||
public string $endpoint,
|
public string $endpoint,
|
||||||
public AuthorizationCredentials $authorization,
|
public AuthorizationCredentials $authorization,
|
||||||
public array $headers,
|
public array $headers,
|
||||||
public array $body,
|
public array|string $body,
|
||||||
public ParameterBag $cookies,
|
public ParameterBag $cookies,
|
||||||
public array $queryParameters = [],
|
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
|
public function test_it_sends_json_body_by_default(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -270,6 +323,45 @@ class RequestRelayActionFunctionalTest extends TestCase
|
|||||||
$this->assertEquals($bodyData, $response->body->body['receivedBody']);
|
$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
|
public function test_it_url_decodes_cookie_values(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|||||||
@@ -15,11 +15,12 @@ use Symfony\Component\HttpFoundation\InputBag;
|
|||||||
#[CoversClass(RequestRelayData::class)]
|
#[CoversClass(RequestRelayData::class)]
|
||||||
class RequestRelayDataUnitTest extends TestCase
|
class RequestRelayDataUnitTest extends TestCase
|
||||||
{
|
{
|
||||||
#[DataProvider('endpointsDataProvider')]
|
#[DataProvider('relayRequestDataDataProvider')]
|
||||||
public function test_it_creates_instance_from_api_request(
|
public function test_it_creates_instance_from_api_request(
|
||||||
string $endpoint,
|
string $endpoint,
|
||||||
string $expectedEndpoint,
|
string $expectedEndpoint,
|
||||||
array $expectedParameters,
|
array $expectedParameters,
|
||||||
|
array|string $body,
|
||||||
): void {
|
): void {
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ class RequestRelayDataUnitTest extends TestCase
|
|||||||
['key' => 'Content-Type', 'value' => 'application/json'],
|
['key' => 'Content-Type', 'value' => 'application/json'],
|
||||||
['key' => 'X-Custom-Header', 'value' => '::value::'],
|
['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' => [
|
yield 'simple path without params' => [
|
||||||
'endpoint' => '/api/test',
|
'endpoint' => '/api/test',
|
||||||
'expectedEndpoint' => '/api/test',
|
'expectedEndpoint' => '/api/test',
|
||||||
'expectedParameters' => [],
|
'expectedParameters' => [],
|
||||||
|
'body' => ['test' => 'data'],
|
||||||
];
|
];
|
||||||
|
|
||||||
yield 'simple path with single param' => [
|
yield 'simple path with single param' => [
|
||||||
'endpoint' => '/api/test?parameter-1=value',
|
'endpoint' => '/api/test?parameter-1=value',
|
||||||
'expectedEndpoint' => '/api/test',
|
'expectedEndpoint' => '/api/test',
|
||||||
'expectedParameters' => ['parameter-1' => 'value'],
|
'expectedParameters' => ['parameter-1' => 'value'],
|
||||||
|
'body' => ['test' => 'data'],
|
||||||
];
|
];
|
||||||
|
|
||||||
yield 'absolute URL without params' => [
|
yield 'absolute URL without params' => [
|
||||||
'endpoint' => 'https://127.0.0.1/api/test',
|
'endpoint' => 'https://127.0.0.1/api/test',
|
||||||
'expectedEndpoint' => 'https://127.0.0.1/api/test',
|
'expectedEndpoint' => 'https://127.0.0.1/api/test',
|
||||||
'expectedParameters' => [],
|
'expectedParameters' => [],
|
||||||
|
'body' => ['test' => 'data'],
|
||||||
];
|
];
|
||||||
|
|
||||||
yield 'absolute URL with multiple params including broken' => [
|
yield 'absolute URL with multiple params including broken' => [
|
||||||
'endpoint' => 'https://127.0.0.1/api/test?key=1&key-2=&broken',
|
'endpoint' => 'https://127.0.0.1/api/test?key=1&key-2=&broken',
|
||||||
'expectedEndpoint' => 'https://127.0.0.1/api/test',
|
'expectedEndpoint' => 'https://127.0.0.1/api/test',
|
||||||
'expectedParameters' => ['key' => '1', 'key-2' => ''],
|
'expectedParameters' => ['key' => '1', 'key-2' => ''],
|
||||||
|
'body' => ['test' => 'data'],
|
||||||
];
|
];
|
||||||
|
|
||||||
yield 'absolute URL with multiple valid params' => [
|
yield 'absolute URL with multiple valid params' => [
|
||||||
'endpoint' => 'https://127.0.0.1/api/test?key=value&key-2=value-2',
|
'endpoint' => 'https://127.0.0.1/api/test?key=value&key-2=value-2',
|
||||||
'expectedEndpoint' => 'https://127.0.0.1/api/test',
|
'expectedEndpoint' => 'https://127.0.0.1/api/test',
|
||||||
'expectedParameters' => ['key' => 'value', 'key-2' => 'value-2'],
|
'expectedParameters' => ['key' => 'value', 'key-2' => 'value-2'],
|
||||||
|
'body' => ['test' => 'data'],
|
||||||
];
|
];
|
||||||
|
|
||||||
yield 'absolute URL with port and param' => [
|
yield 'absolute URL with port and param' => [
|
||||||
'endpoint' => 'https://127.0.0.1:8000/api/test?key=value',
|
'endpoint' => 'https://127.0.0.1:8000/api/test?key=value',
|
||||||
'expectedEndpoint' => 'https://127.0.0.1:8000/api/test',
|
'expectedEndpoint' => 'https://127.0.0.1:8000/api/test',
|
||||||
'expectedParameters' => ['key' => 'value'],
|
'expectedParameters' => ['key' => 'value'],
|
||||||
|
'body' => ['test' => 'data'],
|
||||||
];
|
];
|
||||||
|
|
||||||
yield 'invalid URL with port and param' => [
|
yield 'invalid URL with port and param' => [
|
||||||
'endpoint' => 'http://:80?key=value',
|
'endpoint' => 'http://:80?key=value',
|
||||||
'expectedEndpoint' => 'http://:80?key=value', // parse_url failed, return as is.
|
'expectedEndpoint' => 'http://:80?key=value', // parse_url failed, return as is.
|
||||||
'expectedParameters' => [],
|
'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: [],
|
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')]
|
#[DataProvider('validationFailureProvider')]
|
||||||
|
|||||||
Reference in New Issue
Block a user