Skip to content

Commit 2b6e095

Browse files
authored
Merge pull request #106 from LibreSign/fix/psalm-and-payload-regression
fix: psalm and payload regression
2 parents 39de56b + 82d5024 commit 2b6e095

3 files changed

Lines changed: 115 additions & 15 deletions

File tree

features/bootstrap/FeatureContext.php

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ public static function findParentDirContainingFile(string $filename): string {
7171
public function sendRequest(string $verb, string $url, $body = null, array $headers = [], array $options = []): void {
7272
parent::sendRequest($verb, $url, $body, $headers, $options);
7373
$lastRequest = $this->getLastRequest();
74+
$parsedInput = $this->getParsedInputFromRequest($lastRequest);
7475

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

8990
// Form params
9091
if (array_key_exists('form_params', $this->requestOptions)) {
91-
Assert::assertEquals($this->requestOptions['form_params'], $lastRequest->getParsedInput());
92+
Assert::assertEquals($this->requestOptions['form_params'], $parsedInput);
9293
}
94+
95+
// JSON payload
96+
if (array_key_exists('json', $this->requestOptions)) {
97+
Assert::assertEquals($this->requestOptions['json'], $parsedInput);
98+
}
99+
}
100+
101+
private function getParsedInputFromRequest(RequestInfo $requestInfo): array {
102+
$headers = $requestInfo->getHeaders();
103+
$contentType = $headers['Content-Type'] ?? $headers['CONTENT_TYPE'] ?? '';
104+
$input = $requestInfo->getInput();
105+
if (str_contains((string)$contentType, 'application/json') || $this->isJson($input)) {
106+
$decoded = json_decode($input, true);
107+
if (is_array($decoded)) {
108+
return $decoded;
109+
}
110+
}
111+
112+
return $requestInfo->getParsedInput() ?? [];
113+
}
114+
115+
private function hasNestedPayload(array $payload): bool {
116+
foreach ($payload as $value) {
117+
if (is_array($value) || $value instanceof \stdClass) {
118+
return true;
119+
}
120+
}
121+
122+
return false;
93123
}
94124

95125
#[Given('set the response to:')]
@@ -106,12 +136,12 @@ public function setTheResponseTo(PyStringNode $response): void {
106136
#[\Override]
107137
public function theResponseShouldBeAJsonArrayWithTheFollowingMandatoryValues(TableNode $table): void {
108138
$lastRequest = $this->getLastRequest();
109-
$body = json_encode($lastRequest->getParsedInput());
110-
Assert::assertIsString($body);
111-
// Mock response to be equal to body of request
112-
$this->mockServer->setDefaultResponse(new MockWebServerResponse(
113-
$body
114-
));
139+
$parsedInput = $this->getParsedInputFromRequest($lastRequest);
140+
if ($this->hasNestedPayload($parsedInput)) {
141+
$body = json_encode($parsedInput);
142+
Assert::assertIsString($body);
143+
$this->response = new Response(200, [], $body);
144+
}
115145
parent::theResponseShouldBeAJsonArrayWithTheFollowingMandatoryValues($table);
116146
}
117147
}

features/test.feature

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,29 @@ Feature: Test this extension
111111
| key | value |
112112
| data | [{"foo":"<FIELD_FOO>"}] |
113113

114+
Scenario: Test placeholder decode keeps numeric types in complex payload
115+
When set the response to:
116+
"""
117+
{
118+
"primaryId": 639,
119+
"secondaryId": 690
120+
}
121+
"""
122+
And sending "POST" to "/"
123+
And fetch field "(PRIMARY_ID)primaryId" from previous JSON response
124+
And fetch field "(SECONDARY_ID)secondaryId" from previous JSON response
125+
And sending "PATCH" to "/"
126+
| items | [{"kind":"entry","metrics":{"index":1,"size":350,"offset":166},"primaryId":<PRIMARY_ID>,"secondaryId":<SECONDARY_ID>}] |
127+
Then the response should be a JSON array with the following mandatory values
128+
| key | value |
129+
| (jq).items[0].metrics.index | 1 |
130+
| (jq).items[0].metrics.size | 350 |
131+
| (jq).items[0].primaryId | 639 |
132+
| (jq).items[0].secondaryId | 690 |
133+
| (jq).items[0].metrics.index | (jq)type == "number" |
134+
| (jq).items[0].primaryId | (jq)type == "number" |
135+
| (jq).items[0].secondaryId | (jq)type == "number" |
136+
114137
Scenario: Test initial state with string
115138
When set the response to:
116139
"""

src/NextcloudApiContext.php

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ public function sendRequest(string $verb, string $url, $body = null, array $head
213213

214214
try {
215215
list($fullUrl, $options) = $this->beforeRequest($fullUrl, $options);
216+
$options = $this->normalizePayloadForRequest($verb, $options);
216217
$this->requestOptions = $options;
217218
$this->response = $client->{$verb}($fullUrl, $options);
218219
} catch (ClientException $ex) {
@@ -222,6 +223,42 @@ public function sendRequest(string $verb, string $url, $body = null, array $head
222223
}
223224
}
224225

226+
private function normalizePayloadForRequest(string $verb, array $options): array {
227+
if (empty($options['form_params'])) {
228+
return $options;
229+
}
230+
231+
$writeVerbs = ['post', 'put', 'patch'];
232+
if (!in_array(strtolower($verb), $writeVerbs, true)) {
233+
return $options;
234+
}
235+
236+
$hasComplexPayload = false;
237+
foreach ($options['form_params'] as $value) {
238+
if (is_array($value) || $value instanceof \stdClass) {
239+
$hasComplexPayload = true;
240+
break;
241+
}
242+
}
243+
244+
if (!$hasComplexPayload) {
245+
return $options;
246+
}
247+
248+
$encoded = json_encode($options['form_params']);
249+
Assert::assertIsString($encoded);
250+
$decoded = json_decode($encoded, true);
251+
Assert::assertIsArray($decoded);
252+
253+
$options['json'] = $decoded;
254+
unset($options['form_params']);
255+
if (!isset($options['headers']['Content-Type'])) {
256+
$options['headers']['Content-Type'] = 'application/json';
257+
}
258+
259+
return $options;
260+
}
261+
225262
#[Given('/^set the custom http header "([^"]*)" with "([^"]*)" as value to next request$/')]
226263
public function setTheCustomHttpHeaderAsValueToNextRequest(string $header, string $value):void {
227264
if (empty($value)) {
@@ -239,11 +276,15 @@ protected function beforeRequest(string $fullUrl, array $options): array {
239276

240277
protected function decodeIfIsJsonString(array $list): array {
241278
foreach ($list as $key => $value) {
242-
if ($this->isJson($value)) {
243-
$list[$key] = json_decode($value);
279+
if (!is_string($value)) {
280+
continue;
244281
}
245282
if (str_starts_with($value, '(string)')) {
246283
$list[$key] = substr($value, strlen('(string)'));
284+
continue;
285+
}
286+
if ($this->isJson($value)) {
287+
$list[$key] = json_decode($value);
247288
}
248289
}
249290
return $list;
@@ -447,6 +488,7 @@ public function setAppConfig(string $appId, TableNode $formData): void {
447488
protected function parseFormParams(array $options): array {
448489
if (!empty($options['form_params'])) {
449490
$this->parseTextRcursive($options['form_params']);
491+
$options['form_params'] = $this->decodeIfIsJsonString($options['form_params']);
450492
}
451493
return $options;
452494
}
@@ -489,12 +531,17 @@ public static function runCommand(string $command): array {
489531
if ($owner === false) {
490532
throw new \Exception('Could not retrieve owner information for UID ' . $fileOwnerUid);
491533
}
492-
$fullCommand = 'php ' . $console . ' ' . $command;
493-
if (!empty(self::$environments)) {
494-
$fullCommand = http_build_query(self::$environments, '', ' ') . ' ' . $fullCommand;
495-
}
534+
$baseCommand = 'php ' . $console . ' ' . $command;
535+
$environmentPrefix = !empty(self::$environments)
536+
? http_build_query(self::$environments, '', ' ')
537+
: '';
538+
496539
if (posix_getuid() !== $owner['uid']) {
497-
$fullCommand = 'runuser -u ' . $owner['name'] . ' -- ' . $fullCommand;
540+
$fullCommand = 'runuser -u ' . $owner['name'] . ' -- '
541+
. ($environmentPrefix !== '' ? 'env ' . $environmentPrefix . ' ' : '')
542+
. $baseCommand;
543+
} else {
544+
$fullCommand = ($environmentPrefix !== '' ? $environmentPrefix . ' ' : '') . $baseCommand;
498545
}
499546
$fullCommand .= ' 2>&1';
500547
return self::runBashCommand($fullCommand);
@@ -544,7 +591,7 @@ private static function runBashCommand(string $command): array {
544591

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

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

0 commit comments

Comments
 (0)