Przeglądaj źródła

Merge pull request #666 from ScopeyNZ/feature/extended-postman-support

Expanding Postman collection support
Shalvah 5 lat temu
rodzic
commit
5d24402152

+ 6 - 0
config/apidoc.php

@@ -41,6 +41,12 @@ return [
          * The description for the exported Postman collection.
          */
         'description' => null,
+
+        /*
+         * The "Auth" section that should appear in the postman collection. See the schema docs for more information:
+         * https://schema.getpostman.com/json/collection/v2.0.0/docs/index.html
+         */
+        'auth' => null,
     ],
 
     /*

+ 1 - 1
phpstan.neon

@@ -5,7 +5,7 @@ parameters:
     ignoreErrors:
         - '#Call to an undefined static method Illuminate\\Support\\Facades\\Route::getRoutes().#'
         - '#Call to an undefined static method Illuminate\\Support\\Facades\\URL::forceRootUrl()#'
-        - '#Call to an undefined static method Illuminate\\Support\\Facades\\URL::forceScheme()#'
+        - '#Call to an undefined static method Illuminate\\Support\\Facades\\URL::formatRoot()#'
         - '#Cannot access offset .+ on Illuminate\\Contracts\\Foundation\\Application.#'
         - '#Call to an undefined method Illuminate\\Routing\\Route::versions().#'
         - '#(.*)NunoMaduro\\Collision(.*)#'

+ 134 - 40
src/Writing/PostmanCollectionWriter.php

@@ -19,6 +19,16 @@ class PostmanCollectionWriter
      */
     private $baseUrl;
 
+    /**
+     * @var string
+     */
+    private $protocol;
+
+    /**
+     * @var array|null
+     */
+    private $auth;
+
     /**
      * CollectionWriter constructor.
      *
@@ -27,16 +37,13 @@ class PostmanCollectionWriter
     public function __construct(Collection $routeGroups, $baseUrl)
     {
         $this->routeGroups = $routeGroups;
-        $this->baseUrl = $baseUrl;
+        $this->protocol = Str::startsWith($baseUrl, 'https') ? 'https' : 'http';
+        $this->baseUrl = URL::formatRoot('', $baseUrl);
+        $this->auth = config('apidoc.postman.auth');
     }
 
     public function getCollection()
     {
-        URL::forceRootUrl($this->baseUrl);
-        if (Str::startsWith($this->baseUrl, 'https://')) {
-            URL::forceScheme('https');
-        }
-
         $collection = [
             'variables' => [],
             'info' => [
@@ -45,46 +52,133 @@ class PostmanCollectionWriter
                 'description' => config('apidoc.postman.description') ?: '',
                 'schema' => 'https://schema.getpostman.com/json/collection/v2.0.0/collection.json',
             ],
-            'item' => $this->routeGroups->map(function ($routes, $groupName) {
+            'item' => $this->routeGroups->map(function (Collection $routes, $groupName) {
                 return [
                     'name' => $groupName,
-                    'description' => '',
-                    'item' => $routes->map(function ($route) {
-                        $mode = 'raw';
-
-                        return [
-                            'name' => $route['metadata']['title'] != '' ? $route['metadata']['title'] : url($route['uri']),
-                            'request' => [
-                                'url' => url($route['uri']).(collect($route['queryParameters'])->isEmpty()
-                                        ? ''
-                                        : ('?'.implode('&', collect($route['queryParameters'])->map(function ($parameter, $key) {
-                                            return urlencode($key).'='.urlencode($parameter['value'] ?? '');
-                                        })->all()))),
-                                'method' => $route['methods'][0],
-                                'header' => collect($route['headers'])
-                                    ->union([
-                                        'Accept' => 'application/json',
-                                    ])
-                                    ->map(function ($value, $header) {
-                                        return [
-                                            'key' => $header,
-                                            'value' => $value,
-                                        ];
-                                    })
-                                    ->values()->all(),
-                                'body' => [
-                                    'mode' => $mode,
-                                    $mode => json_encode($route['cleanBodyParameters'], JSON_PRETTY_PRINT),
-                                ],
-                                'description' => $route['metadata']['description'],
-                                'response' => [],
-                            ],
-                        ];
-                    })->toArray(),
+                    'description' => $routes->first()['metadata']['groupDescription'],
+                    'item' => $routes->map(\Closure::fromCallable([$this, 'generateEndpointItem']))->toArray(),
                 ];
             })->values()->toArray(),
         ];
 
+        if (! empty($this->auth)) {
+            $collection['auth'] = $this->auth;
+        }
+
         return json_encode($collection, JSON_PRETTY_PRINT);
     }
+
+    protected function generateEndpointItem($route)
+    {
+        $mode = 'raw';
+
+        $method = $route['methods'][0];
+
+        return [
+            'name' => $route['metadata']['title'] != '' ? $route['metadata']['title'] : $route['uri'],
+            'request' => [
+                'url' => $this->makeUrlData($route),
+                'method' => $method,
+                'header' => $this->resolveHeadersForRoute($route),
+                'body' => [
+                    'mode' => $mode,
+                    $mode => json_encode($route['cleanBodyParameters'], JSON_PRETTY_PRINT),
+                ],
+                'description' => $route['metadata']['description'] ?? null,
+                'response' => [],
+            ],
+        ];
+    }
+
+    protected function resolveHeadersForRoute($route)
+    {
+        $headers = collect($route['headers']);
+
+        // Exclude authentication headers if they're handled by Postman auth
+        $authHeader = $this->getAuthHeader();
+        if (! empty($authHeader)) {
+            $headers = $headers->except($authHeader);
+        }
+
+        return $headers
+            ->union([
+                'Accept' => 'application/json',
+            ])
+            ->map(function ($value, $header) {
+                return [
+                    'key' => $header,
+                    'value' => $value,
+                ];
+            })
+            ->values()
+            ->all();
+    }
+
+    protected function makeUrlData($route)
+    {
+        // URL Parameters are collected by the `UrlParameters` strategies, but only make sense if they're in the route
+        // definition. Filter out any URL parameters that don't appear in the URL.
+        $urlParams = collect($route['urlParameters'])->filter(function ($_, $key) use ($route) {
+            return Str::contains($route['uri'], '{'.$key.'}');
+        });
+
+        /** @var Collection $queryParams */
+        $base = [
+            'protocol' => $this->protocol,
+            'host' => $this->baseUrl,
+            // Substitute laravel/symfony query params ({example}) to Postman style, prefixed with a colon
+            'path' => preg_replace_callback('/\/{(\w+)\??}(?=\/|$)/', function ($matches) {
+                return '/:'.$matches[1];
+            }, $route['uri']),
+            'query' => collect($route['queryParameters'])->map(function ($parameter, $key) {
+                return [
+                    'key' => $key,
+                    'value' => urlencode($parameter['value']),
+                    'description' => $parameter['description'],
+                    // Default query params to disabled if they aren't required and have empty values
+                    'disabled' => ! $parameter['required'] && empty($parameter['value']),
+                ];
+            })->values()->toArray(),
+        ];
+
+        // If there aren't any url parameters described then return what we've got
+        /** @var $urlParams Collection */
+        if ($urlParams->isEmpty()) {
+            return $base;
+        }
+
+        $base['variable'] = $urlParams->map(function ($parameter, $key) {
+            return [
+                'id' => $key,
+                'key' => $key,
+                'value' => urlencode($parameter['value']),
+                'description' => $parameter['description'],
+            ];
+        })->values()->toArray();
+
+        return $base;
+    }
+
+    protected function getAuthHeader()
+    {
+        $auth = $this->auth;
+        if (empty($auth) || ! is_string($auth['type'] ?? null)) {
+            return null;
+        }
+
+        switch ($auth['type']) {
+            case 'bearer':
+                return 'Authorization';
+            case 'apikey':
+                $spec = $auth['apikey'];
+
+                if (isset($spec['in']) && $spec['in'] !== 'header') {
+                    return null;
+                }
+
+                return $spec['key'];
+            default:
+                return null;
+        }
+    }
 }

+ 5 - 1
src/Writing/Writer.php

@@ -223,7 +223,11 @@ class Writer
      */
     public function generatePostmanCollection(Collection $routes)
     {
-        $writer = new PostmanCollectionWriter($routes, $this->baseUrl);
+        /** @var PostmanCollectionWriter $writer */
+        $writer = app()->makeWith(
+            PostmanCollectionWriter::class,
+            ['routeGroups' => $routes, 'baseUrl' => $this->baseUrl]
+        );
 
         return $writer->getCollection();
     }

+ 113 - 16
tests/Fixtures/collection.json

@@ -14,7 +14,12 @@
                 {
                     "name": "Example title.",
                     "request": {
-                        "url": "http:\/\/localhost\/api\/withDescription",
+                        "url": {
+                            "protocol": "http",
+                            "host": "localhost",
+                            "path": "api\/withDescription",
+                            "query": []
+                        },
                         "method": "GET",
                         "header": [
                             {
@@ -43,9 +48,14 @@
                     }
                 },
                 {
-                    "name": "http:\/\/localhost\/api\/withResponseTag",
+                    "name": "api\/withResponseTag",
                     "request": {
-                        "url": "http:\/\/localhost\/api\/withResponseTag",
+                        "url": {
+                            "protocol": "http",
+                            "host": "localhost",
+                            "path": "api\/withResponseTag",
+                            "query": []
+                        },
                         "method": "GET",
                         "header": [
                             {
@@ -76,7 +86,12 @@
                 {
                     "name": "Endpoint with body parameters.",
                     "request": {
-                        "url": "http:\/\/localhost\/api\/withBodyParameters",
+                        "url": {
+                            "protocol": "http",
+                            "host": "localhost",
+                            "path": "api\/withBodyParameters",
+                            "query": []
+                        },
                         "method": "POST",
                         "header": [
                             {
@@ -105,9 +120,45 @@
                     }
                 },
                 {
-                    "name": "http:\/\/localhost\/api\/withQueryParameters",
+                    "name": "api\/withQueryParameters",
                     "request": {
-                        "url": "http:\/\/localhost\/api\/withQueryParameters?location_id=consequatur&user_id=me&page=4&filters=consequatur&url_encoded=%2B+%5B%5D%26%3D",
+                        "url": {
+                            "protocol": "http",
+                            "host": "localhost",
+                            "path": "api\/withQueryParameters",
+                            "query": [
+                                {
+                                    "key": "location_id",
+                                    "value": "consequatur",
+                                    "description": "The id of the location.",
+                                    "disabled": false
+                                },
+                                {
+                                    "key": "user_id",
+                                    "value": "me",
+                                    "description": "The id of the user.",
+                                    "disabled": false
+                                },
+                                {
+                                    "key": "page",
+                                    "value": "4",
+                                    "description": "The page number.",
+                                    "disabled": false
+                                },
+                                {
+                                    "key": "filters",
+                                    "value": "consequatur",
+                                    "description": "The filters.",
+                                    "disabled": false
+                                },
+                                {
+                                    "key": "url_encoded",
+                                    "value": "%2B+%5B%5D%26%3D",
+                                    "description": "Used for testing that URL parameters will be URL-encoded where needed.",
+                                    "disabled": false
+                                }
+                            ]
+                        },
                         "method": "GET",
                         "header": [
                             {
@@ -136,9 +187,14 @@
                     }
                 },
                 {
-                    "name": "http:\/\/localhost\/api\/withAuthTag",
+                    "name": "api\/withAuthTag",
                     "request": {
-                        "url": "http:\/\/localhost\/api\/withAuthTag",
+                        "url": {
+                            "protocol": "http",
+                            "host": "localhost",
+                            "path": "api\/withAuthTag",
+                            "query": []
+                        },
                         "method": "GET",
                         "header": [
                             {
@@ -167,9 +223,14 @@
                     }
                 },
                 {
-                    "name": "http:\/\/localhost\/api\/withEloquentApiResource",
+                    "name": "api\/withEloquentApiResource",
                     "request": {
-                        "url": "http:\/\/localhost\/api\/withEloquentApiResource",
+                        "url": {
+                            "protocol": "http",
+                            "host": "localhost",
+                            "path": "api\/withEloquentApiResource",
+                            "query": []
+                        },
                         "method": "GET",
                         "header": [
                             {
@@ -198,9 +259,14 @@
                     }
                 },
                 {
-                    "name": "http:\/\/localhost\/api\/withMultipleResponseTagsAndStatusCode",
+                    "name": "api\/withMultipleResponseTagsAndStatusCode",
                     "request": {
-                        "url": "http:\/\/localhost\/api\/withMultipleResponseTagsAndStatusCode",
+                        "url": {
+                            "protocol": "http",
+                            "host": "localhost",
+                            "path": "api\/withMultipleResponseTagsAndStatusCode",
+                            "query": []
+                        },
                         "method": "POST",
                         "header": [
                             {
@@ -235,9 +301,14 @@
             "description": "",
             "item": [
                 {
-                    "name": "http:\/\/localhost\/api\/withEloquentApiResourceCollectionClass",
+                    "name": "api\/withEloquentApiResourceCollectionClass",
                     "request": {
-                        "url": "http:\/\/localhost\/api\/withEloquentApiResourceCollectionClass",
+                        "url": {
+                            "protocol": "http",
+                            "host": "localhost",
+                            "path": "api\/withEloquentApiResourceCollectionClass",
+                            "query": []
+                        },
                         "method": "GET",
                         "header": [
                             {
@@ -266,9 +337,35 @@
                     }
                 },
                 {
-                    "name": "http:\/\/localhost\/api\/echoesUrlParameters\/{param}-{param2}\/{param3?}",
+                    "name": "api\/echoesUrlParameters\/{param}-{param2}\/{param3?}",
                     "request": {
-                        "url": "http:\/\/localhost\/api\/echoesUrlParameters\/{param}-{param2}\/{param3?}?something=consequatur",
+                        "url": {
+                            "protocol": "http",
+                            "host": "localhost",
+                            "path": "api\/echoesUrlParameters\/{param}-{param2}\/:param3",
+                            "query": [
+                                {
+                                    "key": "something",
+                                    "value": "consequatur",
+                                    "description": "",
+                                    "disabled": false
+                                }
+                            ],
+                            "variable": [
+                                {
+                                    "id": "param",
+                                    "key": "param",
+                                    "value": "4",
+                                    "description": ""
+                                },
+                                {
+                                    "id": "param2",
+                                    "key": "param2",
+                                    "value": "consequatur",
+                                    "description": ""
+                                }
+                            ]
+                        },
                         "method": "GET",
                         "header": [
                             {

+ 13 - 3
tests/Fixtures/collection_custom_url.json

@@ -14,7 +14,12 @@
                 {
                     "name": "Example title.",
                     "request": {
-                        "url": "http:\/\/yourapp.app\/api\/test",
+                        "url": {
+                            "protocol": "http",
+                            "host": "yourapp.app",
+                            "path": "api/test",
+                            "query": []
+                        },
                         "method": "GET",
                         "header": [
                             {
@@ -35,9 +40,14 @@
                     }
                 },
                 {
-                    "name": "http:\/\/yourapp.app\/api\/responseTag",
+                    "name": "api\/responseTag",
                     "request": {
-                        "url": "http:\/\/yourapp.app\/api\/responseTag",
+                        "url": {
+                            "protocol": "http",
+                            "host": "yourapp.app",
+                            "path": "api/responseTag",
+                            "query": []
+                        },
                         "method": "POST",
                         "header": [
                             {

+ 6 - 1
tests/Fixtures/collection_with_body_parameters.json

@@ -14,7 +14,12 @@
                 {
                     "name": "Endpoint with body parameters.",
                     "request": {
-                        "url": "http:\/\/localhost\/api\/withBodyParameters",
+                        "url": {
+                            "protocol": "http",
+                            "host": "localhost",
+                            "path": "api\/withBodyParameters",
+                            "query": []
+                        },
                         "method": "GET",
                         "header": [
                             {

+ 7 - 2
tests/Fixtures/collection_with_custom_headers.json

@@ -12,9 +12,14 @@
             "description": "",
             "item": [
                 {
-                    "name": "http:\/\/localhost\/api\/headers",
+                    "name": "api\/headers",
                     "request": {
-                        "url": "http:\/\/localhost\/api\/headers",
+                        "url": {
+                            "protocol": "http",
+                            "host": "localhost",
+                            "path": "api/headers",
+                            "query": []
+                        },
                         "method": "GET",
                         "header": [
                             {

+ 38 - 2
tests/Fixtures/collection_with_query_parameters.json

@@ -12,9 +12,45 @@
             "description": "",
             "item": [
                 {
-                    "name": "http:\/\/localhost\/api\/withQueryParameters",
+                    "name": "api\/withQueryParameters",
                     "request": {
-                        "url": "http:\/\/localhost\/api\/withQueryParameters?location_id=consequatur&user_id=me&page=4&filters=consequatur&url_encoded=%2B+%5B%5D%26%3D",
+                        "url": {
+                            "protocol": "http",
+                            "host": "localhost",
+                            "path": "api\/withQueryParameters",
+                            "query": [
+                                {
+                                    "key": "location_id",
+                                    "value": "consequatur",
+                                    "description": "The id of the location.",
+                                    "disabled": false
+                                },
+                                {
+                                    "key": "user_id",
+                                    "value": "me",
+                                    "description": "The id of the user.",
+                                    "disabled": false
+                                },
+                                {
+                                    "key": "page",
+                                    "value": "4",
+                                    "description": "The page number.",
+                                    "disabled": false
+                                },
+                                {
+                                    "key": "filters",
+                                    "value": "consequatur",
+                                    "description": "The filters.",
+                                    "disabled": false
+                                },
+                                {
+                                    "key": "url_encoded",
+                                    "value": "%2B+%5B%5D%26%3D",
+                                    "description": "Used for testing that URL parameters will be URL-encoded where needed.",
+                                    "disabled": false
+                                }
+                            ]
+                        },
                         "method": "GET",
                         "header": [
                             {

+ 13 - 3
tests/Fixtures/collection_with_secure_url.json

@@ -14,7 +14,12 @@
                 {
                     "name": "Example title.",
                     "request": {
-                        "url": "https:\/\/yourapp.app\/api\/test",
+                        "url": {
+                            "protocol": "https",
+                            "host": "yourapp.app",
+                            "path": "api/test",
+                            "query": []
+                        },
                         "method": "GET",
                         "header": [
                             {
@@ -35,9 +40,14 @@
                     }
                 },
                 {
-                    "name": "https:\/\/yourapp.app\/api\/responseTag",
+                    "name": "api\/responseTag",
                     "request": {
-                        "url": "https:\/\/yourapp.app\/api\/responseTag",
+                        "url": {
+                            "protocol": "https",
+                            "host": "yourapp.app",
+                            "path": "api/responseTag",
+                            "query": []
+                        },
                         "method": "POST",
                         "header": [
                             {

+ 2 - 2
tests/GenerateDocumentationTest.php

@@ -275,8 +275,8 @@ class GenerateDocumentationTest extends TestCase
         $this->artisan('apidoc:generate');
 
         $generatedCollection = json_decode(file_get_contents(__DIR__.'/../public/docs/collection.json'));
-        $endpointUrl = $generatedCollection->item[0]->item[0]->request->url;
-        $this->assertTrue(Str::startsWith($endpointUrl, $domain));
+        $endpointUrl = $generatedCollection->item[0]->item[0]->request->url->host;
+        $this->assertTrue(Str::startsWith($endpointUrl, 'somedomain.test'));
     }
 
     /** @test */

+ 313 - 0
tests/Unit/PostmanCollectionWriterTest.php

@@ -0,0 +1,313 @@
+<?php
+
+namespace Mpociot\ApiDoc\Tests\Unit;
+
+use Illuminate\Support\Collection;
+use Mpociot\ApiDoc\Writing\PostmanCollectionWriter;
+use Orchestra\Testbench\TestCase;
+
+class PostmanCollectionWriterTest extends TestCase
+{
+    public function testNameIsPresentInCollection()
+    {
+        \Config::set('apidoc.postman', [
+            'name' => 'Test collection',
+        ]);
+
+        $writer = new PostmanCollectionWriter(new Collection(), '');
+        $collection = $writer->getCollection();
+
+        $this->assertSame('Test collection', json_decode($collection)->info->name);
+    }
+
+    public function testFallbackCollectionNameIsUsed()
+    {
+        \Config::set('app.name', 'Fake App');
+
+        $writer = new PostmanCollectionWriter(new Collection(), '');
+        $collection = $writer->getCollection();
+
+        $this->assertSame('Fake App API', json_decode($collection)->info->name);
+    }
+
+    public function testDescriptionIsPresentInCollection()
+    {
+        \Config::set('apidoc.postman', [
+            'description' => 'A fake description',
+        ]);
+
+        $writer = new PostmanCollectionWriter(new Collection(), '');
+        $collection = $writer->getCollection();
+
+        $this->assertSame('A fake description', json_decode($collection)->info->description);
+    }
+
+    public function testAuthIsNotIncludedWhenNull()
+    {
+        $writer = new PostmanCollectionWriter(new Collection(), '');
+        $collection = $writer->getCollection();
+
+        $this->assertArrayNotHasKey('auth', json_decode($collection, true));
+    }
+
+    public function testAuthIsIncludedVerbatim()
+    {
+        $auth = [
+            'type' => 'test',
+            'test' => ['a' => 1],
+        ];
+        \Config::set('apidoc.postman', [
+            'auth' => $auth,
+        ]);
+
+        $writer = new PostmanCollectionWriter(new Collection(), '');
+        $collection = $writer->getCollection();
+
+        $this->assertSame($auth, json_decode($collection, true)['auth']);
+    }
+
+    public function testEndpointIsParsed()
+    {
+        $route = $this->createMockRouteData('some/path');
+
+        // Ensure method is set correctly for assertion later
+        $route['methods'] = ['GET'];
+
+        $collection = $this->createMockRouteGroup([$route], 'Group');
+
+        $writer = new PostmanCollectionWriter($collection, 'fake.localhost');
+        $collection = json_decode($writer->getCollection(), true);
+
+        $this->assertSame('Group', data_get($collection, 'item.0.name'), 'Group name exists');
+
+        $item = data_get($collection, 'item.0.item.0');
+        $this->assertSame('some/path', $item['name'], 'Name defaults to path');
+        $this->assertSame('http', data_get($item, 'request.url.protocol'), 'Protocol defaults to http');
+        $this->assertSame('fake.localhost', data_get($item, 'request.url.host'), 'Host uses what\'s given');
+        $this->assertSame('some/path', data_get($item, 'request.url.path'), 'Path is set correctly');
+        $this->assertEmpty(data_get($item, 'request.url.query'), 'Query parameters are empty');
+        $this->assertSame('GET', data_get($item, 'request.method'), 'Method is correctly resolved');
+        $this->assertContains([
+            'key' => 'Accept',
+            'value' => 'application/json',
+        ], data_get($item, 'request.header'), 'JSON Accept header is added');
+    }
+
+    public function testHttpsProtocolIsDetected()
+    {
+        $collection = $this->createMockRouteGroup([$this->createMockRouteData('fake')]);
+        $writer = new PostmanCollectionWriter($collection, 'https://fake.localhost');
+        $collection = json_decode($writer->getCollection(), true);
+
+        $this->assertSame('https', data_get($collection, 'item.0.item.0.request.url.protocol'));
+    }
+
+    public function testHeadersArePulledFromRoute()
+    {
+        $route = $this->createMockRouteData('some/path');
+
+        $route['headers'] = ['X-Fake' => 'Test'];
+
+        $collection = $this->createMockRouteGroup([$route], 'Group');
+        $writer = new PostmanCollectionWriter($collection, 'fake.localhost');
+        $collection = json_decode($writer->getCollection(), true);
+
+        $this->assertContains([
+            'key' => 'X-Fake',
+            'value' => 'Test',
+        ], data_get($collection, 'item.0.item.0.request.header'));
+    }
+
+    public function testUrlParametersAreConverted()
+    {
+        $collection = $this->createMockRouteGroup([$this->createMockRouteData('fake/{param}')]);
+        $writer = new PostmanCollectionWriter($collection, 'fake.localhost');
+        $collection = json_decode($writer->getCollection(), true);
+
+        $item = data_get($collection, 'item.0.item.0');
+        $this->assertSame('fake/{param}', $item['name'], 'Name defaults to path');
+        $this->assertSame('fake/:param', data_get($item, 'request.url.path'), 'Path is converted');
+    }
+
+    public function testUrlParamsResolveTheirDocumentation()
+    {
+        $fakeRoute = $this->createMockRouteData('fake/{param}');
+
+        $fakeRoute['urlParameters'] = ['param' => [
+            'description' => 'A test description for the test param',
+            'required' => true,
+            'value' => 'foobar',
+        ]];
+
+        $collection = $this->createMockRouteGroup([$fakeRoute]);
+        $writer = new PostmanCollectionWriter($collection, 'fake.localhost');
+        $collection = json_decode($writer->getCollection(), true);
+
+        $variableData = data_get($collection, 'item.0.item.0.request.url.variable');
+
+        $this->assertCount(1, $variableData);
+        $this->assertSame([
+            'id' => 'param',
+            'key' => 'param',
+            'value' => 'foobar',
+            'description' => 'A test description for the test param',
+        ], $variableData[0]);
+    }
+
+    public function testQueryParametersAreDocumented()
+    {
+        $fakeRoute = $this->createMockRouteData('fake/path');
+
+        $fakeRoute['queryParameters'] = ['limit' => [
+            'description' => 'A fake limit for my fake endpoint',
+            'required' => false,
+            'value' => 5,
+        ]];
+
+        $collection = $this->createMockRouteGroup([$fakeRoute]);
+        $writer = new PostmanCollectionWriter($collection, 'fake.localhost');
+        $collection = json_decode($writer->getCollection(), true);
+
+        $variableData = data_get($collection, 'item.0.item.0.request.url.query');
+
+        $this->assertCount(1, $variableData);
+        $this->assertSame([
+            'key' => 'limit',
+            'value' => '5',
+            'description' => 'A fake limit for my fake endpoint',
+            'disabled' => false,
+        ], $variableData[0]);
+    }
+
+    public function testUrlParametersAreNotIncludedIfMissingFromPath()
+    {
+        $fakeRoute = $this->createMockRouteData('fake/path');
+
+        $fakeRoute['urlParameters'] = ['limit' => [
+            'description' => 'A fake limit for my fake endpoint',
+            'required' => false,
+            'value' => 5,
+        ]];
+
+        $collection = $this->createMockRouteGroup([$fakeRoute]);
+        $writer = new PostmanCollectionWriter($collection, 'fake.localhost');
+        $collection = json_decode($writer->getCollection(), true);
+
+        $variableData = data_get($collection, 'item.0.item.0.request.url.query');
+
+        $this->assertCount(0, $variableData);
+    }
+
+    public function testQueryParametersAreDisabledWithNoValueWhenNotRequired()
+    {
+        $fakeRoute = $this->createMockRouteData('fake/path');
+        $fakeRoute['queryParameters'] = [
+            'required' => [
+                'description' => 'A required param with a null value',
+                'required' => true,
+                'value' => null,
+            ],
+            'not_required' => [
+                'description' => 'A not required param with a null value',
+                'required' => false,
+                'value' => null,
+            ],
+        ];
+
+        $collection = $this->createMockRouteGroup([$fakeRoute]);
+        $writer = new PostmanCollectionWriter($collection, 'fake.localhost');
+        $collection = json_decode($writer->getCollection(), true);
+
+        $variableData = data_get($collection, 'item.0.item.0.request.url.query');
+
+        $this->assertCount(2, $variableData);
+        $this->assertContains([
+            'key' => 'required',
+            'value' => null,
+            'description' => 'A required param with a null value',
+            'disabled' => false,
+        ], $variableData);
+        $this->assertContains([
+            'key' => 'not_required',
+            'value' => null,
+            'description' => 'A not required param with a null value',
+            'disabled' => true,
+        ], $variableData);
+    }
+
+    /**
+     * @dataProvider provideAuthConfigHeaderData
+     */
+    public function testAuthAutoExcludesHeaderDefinitions(array $authConfig, array $expectedRemovedHeaders)
+    {
+        \Config::set('apidoc.postman', [
+            'auth' => $authConfig,
+        ]);
+
+        $route = $this->createMockRouteData('some/path');
+        $route['headers'] = $expectedRemovedHeaders;
+        $collection = $this->createMockRouteGroup([$route], 'Group');
+        $writer = new PostmanCollectionWriter($collection, 'fake.localhost');
+        $collection = json_decode($writer->getCollection(), true);
+
+        foreach ($expectedRemovedHeaders as $key => $value) {
+            $this->assertNotContains(compact('key', 'value'), data_get($collection, 'item.0.item.0.request.header'));
+        }
+    }
+
+    public function provideAuthConfigHeaderData()
+    {
+        yield [
+            ['type' => 'bearer', 'bearer' => ['token' => 'Test']],
+            ['Authorization' => 'Bearer Test'],
+        ];
+
+        yield [
+            ['type' => 'apikey', 'apikey' => ['value' => 'Test', 'key' => 'X-Authorization']],
+            ['X-Authorization' => 'Test'],
+        ];
+    }
+
+    public function testApiKeyAuthIsIgnoredIfExplicitlyNotInHeader()
+    {
+        \Config::set('apidoc.postman', [
+            'auth' => ['type' => 'apikey', 'apikey' => [
+                'value' => 'Test',
+                'key' => 'X-Authorization',
+                'in' => 'notheader',
+            ]],
+        ]);
+
+        $route = $this->createMockRouteData('some/path');
+        $route['headers'] = ['X-Authorization' => 'Test'];
+        $collection = $this->createMockRouteGroup([$route], 'Group');
+        $writer = new PostmanCollectionWriter($collection, 'fake.localhost');
+        $collection = json_decode($writer->getCollection(), true);
+
+        $this->assertContains([
+            'key' => 'X-Authorization',
+            'value' => 'Test',
+        ], data_get($collection, 'item.0.item.0.request.header'));
+    }
+
+    protected function createMockRouteData($path, $title = '')
+    {
+        return [
+            'uri' => $path,
+            'methods' => ['GET'],
+            'headers' => [],
+            'metadata' => [
+                'groupDescription' => '',
+                'title' => $title,
+            ],
+            'queryParameters' => [],
+            'urlParameters' => [],
+            'cleanBodyParameters' => [],
+        ];
+    }
+
+    protected function createMockRouteGroup(array $routes, $groupName = 'Group')
+    {
+        return collect([$groupName => collect($routes)]);
+    }
+}