diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php index 014f4fb..26e01f3 100644 --- a/features/bootstrap/FeatureContext.php +++ b/features/bootstrap/FeatureContext.php @@ -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()); @@ -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:')] @@ -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); } } diff --git a/features/test.feature b/features/test.feature index 0b86897..9ab4762 100644 --- a/features/test.feature +++ b/features/test.feature @@ -111,6 +111,29 @@ Feature: Test this extension | key | value | | data | [{"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":,"secondaryId":}] | + 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: """ diff --git a/src/NextcloudApiContext.php b/src/NextcloudApiContext.php index 52ed147..379b4e4 100644 --- a/src/NextcloudApiContext.php +++ b/src/NextcloudApiContext.php @@ -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) { @@ -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)) { @@ -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; @@ -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; } @@ -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); @@ -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')]