Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 37 additions & 7 deletions features/bootstrap/FeatureContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public static function findParentDirContainingFile(string $filename): string {
public function sendRequest(string $verb, string $url, $body = null, array $headers = [], array $options = []): void {
parent::sendRequest($verb, $url, $body, $headers, $options);
$lastRequest = $this->getLastRequest();
$parsedInput = $this->getParsedInputFromRequest($lastRequest);

// Verb
Assert::assertEquals($verb, $lastRequest->getRequestMethod());
Expand All @@ -88,8 +89,37 @@ public function sendRequest(string $verb, string $url, $body = null, array $head

// Form params
if (array_key_exists('form_params', $this->requestOptions)) {
Assert::assertEquals($this->requestOptions['form_params'], $lastRequest->getParsedInput());
Assert::assertEquals($this->requestOptions['form_params'], $parsedInput);
}

// JSON payload
if (array_key_exists('json', $this->requestOptions)) {
Assert::assertEquals($this->requestOptions['json'], $parsedInput);
}
}

private function getParsedInputFromRequest(RequestInfo $requestInfo): array {
$headers = $requestInfo->getHeaders();
$contentType = $headers['Content-Type'] ?? $headers['CONTENT_TYPE'] ?? '';
$input = $requestInfo->getInput();
if (str_contains((string)$contentType, 'application/json') || $this->isJson($input)) {
$decoded = json_decode($input, true);
if (is_array($decoded)) {
return $decoded;
}
}

return $requestInfo->getParsedInput() ?? [];
}

private function hasNestedPayload(array $payload): bool {
foreach ($payload as $value) {
if (is_array($value) || $value instanceof \stdClass) {
return true;
}
}

return false;
}

#[Given('set the response to:')]
Expand All @@ -106,12 +136,12 @@ public function setTheResponseTo(PyStringNode $response): void {
#[\Override]
public function theResponseShouldBeAJsonArrayWithTheFollowingMandatoryValues(TableNode $table): void {
$lastRequest = $this->getLastRequest();
$body = json_encode($lastRequest->getParsedInput());
Assert::assertIsString($body);
// Mock response to be equal to body of request
$this->mockServer->setDefaultResponse(new MockWebServerResponse(
$body
));
$parsedInput = $this->getParsedInputFromRequest($lastRequest);
if ($this->hasNestedPayload($parsedInput)) {
$body = json_encode($parsedInput);
Assert::assertIsString($body);
$this->response = new Response(200, [], $body);
}
parent::theResponseShouldBeAJsonArrayWithTheFollowingMandatoryValues($table);
}
}
23 changes: 23 additions & 0 deletions features/test.feature
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,29 @@ Feature: Test this extension
| key | value |
| data | [{"foo":"<FIELD_FOO>"}] |

Scenario: Test placeholder decode keeps numeric types in complex payload
When set the response to:
"""
{
"primaryId": 639,
"secondaryId": 690
}
"""
And sending "POST" to "/"
And fetch field "(PRIMARY_ID)primaryId" from previous JSON response
And fetch field "(SECONDARY_ID)secondaryId" from previous JSON response
And sending "PATCH" to "/"
| items | [{"kind":"entry","metrics":{"index":1,"size":350,"offset":166},"primaryId":<PRIMARY_ID>,"secondaryId":<SECONDARY_ID>}] |
Then the response should be a JSON array with the following mandatory values
| key | value |
| (jq).items[0].metrics.index | 1 |
| (jq).items[0].metrics.size | 350 |
| (jq).items[0].primaryId | 639 |
| (jq).items[0].secondaryId | 690 |
| (jq).items[0].metrics.index | (jq)type == "number" |
| (jq).items[0].primaryId | (jq)type == "number" |
| (jq).items[0].secondaryId | (jq)type == "number" |

Scenario: Test initial state with string
When set the response to:
"""
Expand Down
63 changes: 55 additions & 8 deletions src/NextcloudApiContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ public function sendRequest(string $verb, string $url, $body = null, array $head

try {
list($fullUrl, $options) = $this->beforeRequest($fullUrl, $options);
$options = $this->normalizePayloadForRequest($verb, $options);
$this->requestOptions = $options;
$this->response = $client->{$verb}($fullUrl, $options);
} catch (ClientException $ex) {
Expand All @@ -222,6 +223,42 @@ public function sendRequest(string $verb, string $url, $body = null, array $head
}
}

private function normalizePayloadForRequest(string $verb, array $options): array {
if (empty($options['form_params'])) {
return $options;
}

$writeVerbs = ['post', 'put', 'patch'];
if (!in_array(strtolower($verb), $writeVerbs, true)) {
return $options;
}

$hasComplexPayload = false;
foreach ($options['form_params'] as $value) {
if (is_array($value) || $value instanceof \stdClass) {
$hasComplexPayload = true;
break;
}
}

if (!$hasComplexPayload) {
return $options;
}

$encoded = json_encode($options['form_params']);
Assert::assertIsString($encoded);
$decoded = json_decode($encoded, true);
Assert::assertIsArray($decoded);

$options['json'] = $decoded;
unset($options['form_params']);
if (!isset($options['headers']['Content-Type'])) {
$options['headers']['Content-Type'] = 'application/json';
}

return $options;
}

#[Given('/^set the custom http header "([^"]*)" with "([^"]*)" as value to next request$/')]
public function setTheCustomHttpHeaderAsValueToNextRequest(string $header, string $value):void {
if (empty($value)) {
Expand All @@ -239,11 +276,15 @@ protected function beforeRequest(string $fullUrl, array $options): array {

protected function decodeIfIsJsonString(array $list): array {
foreach ($list as $key => $value) {
if ($this->isJson($value)) {
$list[$key] = json_decode($value);
if (!is_string($value)) {
continue;
}
if (str_starts_with($value, '(string)')) {
$list[$key] = substr($value, strlen('(string)'));
continue;
}
if ($this->isJson($value)) {
$list[$key] = json_decode($value);
}
}
return $list;
Expand Down Expand Up @@ -447,6 +488,7 @@ public function setAppConfig(string $appId, TableNode $formData): void {
protected function parseFormParams(array $options): array {
if (!empty($options['form_params'])) {
$this->parseTextRcursive($options['form_params']);
$options['form_params'] = $this->decodeIfIsJsonString($options['form_params']);
}
return $options;
}
Expand Down Expand Up @@ -489,12 +531,17 @@ public static function runCommand(string $command): array {
if ($owner === false) {
throw new \Exception('Could not retrieve owner information for UID ' . $fileOwnerUid);
}
$fullCommand = 'php ' . $console . ' ' . $command;
if (!empty(self::$environments)) {
$fullCommand = http_build_query(self::$environments, '', ' ') . ' ' . $fullCommand;
}
$baseCommand = 'php ' . $console . ' ' . $command;
$environmentPrefix = !empty(self::$environments)
? http_build_query(self::$environments, '', ' ')
: '';

if (posix_getuid() !== $owner['uid']) {
$fullCommand = 'runuser -u ' . $owner['name'] . ' -- ' . $fullCommand;
$fullCommand = 'runuser -u ' . $owner['name'] . ' -- '
. ($environmentPrefix !== '' ? 'env ' . $environmentPrefix . ' ' : '')
. $baseCommand;
} else {
$fullCommand = ($environmentPrefix !== '' ? $environmentPrefix . ' ' : '') . $baseCommand;
}
$fullCommand .= ' 2>&1';
return self::runBashCommand($fullCommand);
Expand Down Expand Up @@ -544,7 +591,7 @@ private static function runBashCommand(string $command): array {

#[Given('the output of the last command should contain the following text:')]
public static function theOutputOfTheLastCommandContains(PyStringNode $text): void {
Assert::assertStringContainsString((string) $text, self::$commandOutput, 'The output of the last command does not contain: ' . $text);
Assert::assertStringContainsString((string) $text, self::$commandOutput, 'The output of the last command does not contain: ' . (string) $text);
}

#[Given('the output of the last command should be empty')]
Expand Down