diff --git a/src/Writing/PostmanCollectionWriter.php b/src/Writing/PostmanCollectionWriter.php index ea386e0f..445595a5 100644 --- a/src/Writing/PostmanCollectionWriter.php +++ b/src/Writing/PostmanCollectionWriter.php @@ -318,8 +318,8 @@ protected function generateUrlObject(OutputEndpointData $endpointData): array } } else { $query[] = [ - 'key' => urlencode($name), - 'value' => $parameterData->example !== null ? urlencode($parameterData->example) : '', + 'key' => $name, + 'value' => $parameterData->example !== null ? (string) $parameterData->example : '', 'description' => strip_tags($parameterData->description), // Default query params to disabled if they aren't required and have empty values 'disabled' => ! $parameterData->required && empty($parameterData->example), @@ -329,9 +329,12 @@ protected function generateUrlObject(OutputEndpointData $endpointData): array $base['query'] = $query; - // Create raw url-parameter (Insomnia uses this on import) + // Create raw url-parameter (Insomnia uses this on import). + // Per Postman Collection v2.1, query[].key/value are stored raw and the client + // encodes them when sending. The raw URL string still needs encoding to be a + // syntactically valid URL, so we only encode it here. $queryString = collect($base['query'])->map(function ($queryParamData) { - return $queryParamData['key'].'='.$queryParamData['value']; + return rawurlencode($queryParamData['key']).'='.rawurlencode($queryParamData['value']); })->implode('&'); $base['raw'] = sprintf('%s/%s%s', $base['host'], $base['path'], $queryString ? "?{$queryString}" : null); @@ -344,7 +347,7 @@ protected function generateUrlObject(OutputEndpointData $endpointData): array return [ 'id' => $name, 'key' => $name, - 'value' => urlencode($parameter->example), + 'value' => $parameter->example, 'description' => $parameter->description, ]; })->values()->toArray(); diff --git a/tests/Fixtures/collection.json b/tests/Fixtures/collection.json index d8ad8b13..6451629e 100644 --- a/tests/Fixtures/collection.json +++ b/tests/Fixtures/collection.json @@ -170,12 +170,12 @@ }, { "key": "url_encoded", - "value": "%2B+%5B%5D%26%3D", + "value": "+ []&=", "description": "Used for testing that URL parameters will be URL-encoded where needed.", "disabled": false } ], - "raw": "{{baseUrl}}/api/withQueryParameters?location_id=architecto&user_id=me&page=4&filters=architecto&url_encoded=%2B+%5B%5D%26%3D" + "raw": "{{baseUrl}}/api/withQueryParameters?location_id=architecto&user_id=me&page=4&filters=architecto&url_encoded=%2B%20%5B%5D%26%3D" }, "method": "GET", "header": [ diff --git a/tests/Unit/PostmanCollectionWriterTest.php b/tests/Unit/PostmanCollectionWriterTest.php index 1a898045..82665410 100644 --- a/tests/Unit/PostmanCollectionWriterTest.php +++ b/tests/Unit/PostmanCollectionWriterTest.php @@ -207,6 +207,64 @@ public function query_parameters_are_disabled_with_no_value_when_not_required() ], $variableData); } + /** @test */ + public function query_parameter_keys_and_values_are_not_pre_url_encoded() + { + // Per Postman Collection v2.1 spec, query[].key/value are stored as raw strings + // and the Postman client encodes them when sending. Pre-encoding here causes + // a double-encode on send (e.g. `,` → `%2C` → `%252C` at the server). + $endpointData = $this->createMockEndpointData('fake/{id}'); + $endpointData->urlParameters['id'] = new Parameter([ + 'name' => 'id', + 'description' => '', + 'required' => true, + 'example' => 'foo bar', + ]); + $endpointData->queryParameters = [ + 'include' => new Parameter([ + 'name' => 'include', + 'type' => 'string', + 'description' => 'Comma-separated relationships', + 'required' => false, + 'example' => 'customer,reward', + ]), + 'filter[email]' => new Parameter([ + 'name' => 'filter[email]', + 'type' => 'string', + 'description' => 'Filter', + 'required' => false, + 'example' => 'test@example.com', + ]), + ]; + $endpointData->cleanQueryParameters = Extractor::cleanParams($endpointData->queryParameters); + + $endpoints = $this->createMockEndpointGroup([$endpointData]); + $collection = $this->generate(endpoints: [$endpoints]); + + $url = data_get($collection, 'item.0.item.0.request.url'); + + // Query objects keep raw values — Postman encodes on send. + $this->assertContains([ + 'key' => 'include', + 'value' => 'customer,reward', + 'description' => 'Comma-separated relationships', + 'disabled' => false, + ], $url['query']); + $this->assertContains([ + 'key' => 'filter[email]', + 'value' => 'test@example.com', + 'description' => 'Filter', + 'disabled' => false, + ], $url['query']); + + // URL variables also stored raw. + $this->assertSame('foo bar', $url['variable'][0]['value']); + + // Raw URL is encoded for HTTP validity. + $this->assertStringContainsString('include=customer%2Creward', $url['raw']); + $this->assertStringContainsString('filter%5Bemail%5D=test%40example.com', $url['raw']); + } + /** @test */ public function auth_info_is_added_correctly() {