Browse Source

Merge pull request #4 from mpociot/master

Update from base
Shalvah A 6 years ago
parent
commit
ec045162a8

+ 10 - 1
CHANGELOG.md

@@ -2,7 +2,7 @@
 All notable changes to this project will be documented in this file.
 
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
-and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). If you make a pull request to this project, please update this changelog.
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## Unreleased
 ### Added
@@ -13,6 +13,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Removed
 
+
+## [2.1.5] - 123h September, 2018
+### Fixed
+- Parse JSON responses from `@transformer` tag for DIngo router (https://github.com/mpociot/laravel-apidoc-generator/pull/323)
+
+## [2.1.4] - 12th September, 2018
+### Fixed
+- Parse JSON responses from  `@response` and `@transformer` tags correctly (https://github.com/mpociot/laravel-apidoc-generator/pull/321)
+
 ## [2.1.3] - 11th September, 2018
 ### Fixed
 - Parse `@response` tags regardless of HTTP method (https://github.com/mpociot/laravel-apidoc-generator/pull/318)

+ 1 - 1
CONTRIBUTING.md

@@ -44,7 +44,7 @@ If the project maintainer has any additional requirements, you will find them li
 
 - **Add tests!** - Your patch won't be accepted if it doesn't have tests.
 
-- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. Add your changes to the `CHANGELOG.md` under the "Unreleased" section.
+- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date.
 
 - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option.
 

+ 10 - 41
src/Mpociot/ApiDoc/Commands/GenerateDocumentation.php

@@ -3,15 +3,16 @@
 namespace Mpociot\ApiDoc\Commands;
 
 use ReflectionClass;
+use Illuminate\Routing\Route;
 use Illuminate\Console\Command;
 use Mpociot\Reflection\DocBlock;
 use Illuminate\Support\Collection;
-use Illuminate\Support\Facades\Route;
 use Mpociot\Documentarian\Documentarian;
 use Mpociot\ApiDoc\Postman\CollectionWriter;
 use Mpociot\ApiDoc\Generators\DingoGenerator;
 use Mpociot\ApiDoc\Generators\LaravelGenerator;
 use Mpociot\ApiDoc\Generators\AbstractGenerator;
+use Illuminate\Support\Facades\Route as RouteFacade;
 
 class GenerateDocumentation extends Command
 {
@@ -91,7 +92,7 @@ class GenerateDocumentation extends Command
         if ($this->option('router') === 'laravel') {
             foreach ($routeDomains as $routeDomain) {
                 foreach ($routePrefixes as $routePrefix) {
-                    $parsedRoutes += $this->processLaravelRoutes($generator, $allowedRoutes, $routeDomain, $routePrefix, $middleware);
+                    $parsedRoutes += $this->processRoutes($generator, $allowedRoutes, $routeDomain, $routePrefix, $middleware);
                 }
             }
         } else {
@@ -215,6 +216,7 @@ class GenerateDocumentation extends Command
         if (empty($bindings)) {
             return [];
         }
+
         $bindings = explode('|', $bindings);
         $resultBindings = [];
         foreach ($bindings as $binding) {
@@ -250,9 +252,9 @@ class GenerateDocumentation extends Command
     private function getRoutes()
     {
         if ($this->option('router') === 'laravel') {
-            return Route::getRoutes();
+            return RouteFacade::getRoutes();
         } else {
-            return app('Dingo\Api\Routing\Router')->getRoutes()[$this->option('routePrefix')];
+            return app('Dingo\Api\Routing\Router')->getRoutes();
         }
     }
 
@@ -264,13 +266,14 @@ class GenerateDocumentation extends Command
      *
      * @return array
      */
-    private function processLaravelRoutes(AbstractGenerator $generator, $allowedRoutes, $routeDomain, $routePrefix, $middleware)
+    private function processRoutes(AbstractGenerator $generator, array $allowedRoutes, $routeDomain, $routePrefix, $middleware)
     {
-        $withResponse = $this->option('noResponseCalls') === false;
+        $withResponse = $this->option('noResponseCalls') == false;
         $routes = $this->getRoutes();
         $bindings = $this->getBindings();
         $parsedRoutes = [];
         foreach ($routes as $route) {
+            /** @var Route $route */
             if (in_array($route->getName(), $allowedRoutes)
                 || (str_is($routeDomain, $generator->getDomain($route))
                     && str_is($routePrefix, $generator->getUri($route)))
@@ -288,46 +291,12 @@ class GenerateDocumentation extends Command
         return $parsedRoutes;
     }
 
-    /**
-     * @param AbstractGenerator $generator
-     * @param $allowedRoutes
-     * @param $routeDomain
-     * @param $routePrefix
-     *
-     * @return array
-     */
-    private function processDingoRoutes(AbstractGenerator $generator, $allowedRoutes, $routeDomain, $routePrefix, $middleware)
-    {
-        $withResponse = $this->option('noResponseCalls') === false;
-        $routes = $this->getRoutes();
-        $bindings = $this->getBindings();
-        $parsedRoutes = [];
-        foreach ($routes as $route) {
-            if (empty($allowedRoutes)
-                // TODO extract this into a method
-                || in_array($route->getName(), $allowedRoutes)
-                || (str_is($routeDomain, $generator->getDomain($route))
-                    && str_is($routePrefix, $generator->getUri($route)))
-                || in_array($middleware, $route->middleware())
-               ) {
-                if ($this->isValidRoute($route) && $this->isRouteVisibleForDocumentation($route->getAction()['uses'])) {
-                    $parsedRoutes[] = $generator->processRoute($route, $bindings, $this->option('header'), $withResponse && in_array('GET', $generator->getMethods($route)));
-                    $this->info('Processed route: ['.implode(',', $generator->getMethods($route)).'] '.$route->uri());
-                } else {
-                    $this->warn('Skipping route: ['.implode(',', $generator->getMethods($route)).'] '.$route->uri());
-                }
-            }
-        }
-
-        return $parsedRoutes;
-    }
-
     /**
      * @param $route
      *
      * @return bool
      */
-    private function isValidRoute($route)
+    private function isValidRoute(Route $route)
     {
         return ! is_callable($route->getAction()['uses']) && ! is_null($route->getAction()['uses']);
     }

+ 160 - 22
src/Mpociot/ApiDoc/Generators/AbstractGenerator.php

@@ -6,8 +6,12 @@ use Faker\Factory;
 use ReflectionClass;
 use Illuminate\Support\Arr;
 use Illuminate\Support\Str;
+use League\Fractal\Manager;
+use Illuminate\Routing\Route;
 use Mpociot\Reflection\DocBlock;
+use League\Fractal\Resource\Item;
 use Mpociot\Reflection\DocBlock\Tag;
+use League\Fractal\Resource\Collection;
 use Illuminate\Support\Facades\Validator;
 use Illuminate\Foundation\Http\FormRequest;
 use Mpociot\ApiDoc\Parsers\RuleDescriptionParser as Description;
@@ -16,25 +20,34 @@ use Illuminate\Contracts\Validation\Factory as ValidationFactory;
 abstract class AbstractGenerator
 {
     /**
-     * @param $route
+     * @param Route $route
      *
      * @return mixed
      */
-    abstract public function getDomain($route);
+    public function getDomain(Route $route)
+    {
+        return $route->domain();
+    }
 
     /**
-     * @param $route
+     * @param Route $route
      *
      * @return mixed
      */
-    abstract public function getUri($route);
+    public function getUri(Route $route)
+    {
+        return $route->uri();
+    }
 
     /**
-     * @param $route
+     * @param Route $route
      *
      * @return mixed
      */
-    abstract public function getMethods($route);
+    public function getMethods(Route $route)
+    {
+        return array_diff($route->methods(), ['HEAD']);
+    }
 
     /**
      * @param  \Illuminate\Routing\Route $route
@@ -55,14 +68,12 @@ abstract class AbstractGenerator
         $headers[] = "HTTP_HOST: {$routeDomain}";
         $headers[] = "SERVER_NAME: {$routeDomain}";
 
-        $content = '';
         $response = null;
         $docblockResponse = $this->getDocblockResponse($routeDescription['tags']);
         if ($docblockResponse) {
             // we have a response from the docblock ( @response )
             $response = $docblockResponse;
             $showresponse = true;
-            $content = $response->getContent();
         }
         if (! $response) {
             $transformerResponse = $this->getTransformerResponse($routeDescription['tags']);
@@ -70,22 +81,18 @@ abstract class AbstractGenerator
                 // we have a transformer response from the docblock ( @transformer || @transformercollection )
                 $response = $transformerResponse;
                 $showresponse = true;
-                $content = $response->getContent();
             }
         }
         if (! $response && $withResponse) {
             try {
                 $response = $this->getRouteResponse($route, $bindings, $headers);
-                if ($response->headers->get('Content-Type') === 'application/json') {
-                    $content = json_decode($response->getContent(), JSON_PRETTY_PRINT);
-                } else {
-                    $content = $response->getContent();
-                }
             } catch (\Exception $e) {
-                dump("Couldn't get response for route: ".implode(',', $this->getMethods($route)).'] '.$route->uri().'', $e);
+                echo "Couldn't get response for route: ".implode(',', $this->getMethods($route)).$route->uri().']: '.$e->getMessage()."\n";
             }
         }
 
+        $content = $this->getResponseContent($response);
+
         return $this->getParameters([
             'id' => md5($this->getUri($route).':'.implode($this->getMethods($route))),
             'resource' => $routeGroup,
@@ -152,7 +159,7 @@ abstract class AbstractGenerator
                 'description' => [],
             ];
             foreach ($ruleset as $rule) {
-                $this->parseRule($rule, $attribute, $attributeData, $routeData['id']);
+                $this->parseRule($rule, $attribute, $attributeData, $routeData['id'], $routeData);
             }
             $routeData['parameters'][$attribute] = $attributeData;
         }
@@ -356,7 +363,7 @@ abstract class AbstractGenerator
      *
      * @return void
      */
-    protected function parseRule($rule, $ruleName, &$attributeData, $seed)
+    protected function parseRule($rule, $ruleName, &$attributeData, $seed, $routeData)
     {
         $faker = Factory::create();
         $faker->seed(crc32($seed));
@@ -376,8 +383,17 @@ abstract class AbstractGenerator
                 break;
             case 'after':
                 $attributeData['type'] = 'date';
-                $attributeData['description'][] = Description::parse($rule)->with(date(DATE_RFC850, strtotime($parameters[0])))->getDescription();
-                $attributeData['value'] = date(DATE_RFC850, strtotime('+1 day', strtotime($parameters[0])));
+                $format = isset($attributeData['format']) ? $attributeData['format'] : DATE_RFC850;
+
+                if (strtotime($parameters[0]) === false) {
+                    // the `after` date refers to another parameter in the request
+                    $paramName = $parameters[0];
+                    $attributeData['description'][] = Description::parse($rule)->with($paramName)->getDescription();
+                    $attributeData['value'] = date($format, strtotime('+1 day', strtotime($routeData['parameters'][$paramName]['value'])));
+                } else {
+                    $attributeData['description'][] = Description::parse($rule)->with(date($format, strtotime($parameters[0])))->getDescription();
+                    $attributeData['value'] = date($format, strtotime('+1 day', strtotime($parameters[0])));
+                }
                 break;
             case 'alpha':
                 $attributeData['description'][] = Description::parse($rule)->getDescription();
@@ -418,13 +434,23 @@ abstract class AbstractGenerator
                 break;
             case 'before':
                 $attributeData['type'] = 'date';
-                $attributeData['description'][] = Description::parse($rule)->with(date(DATE_RFC850, strtotime($parameters[0])))->getDescription();
-                $attributeData['value'] = date(DATE_RFC850, strtotime('-1 day', strtotime($parameters[0])));
+                $format = isset($attributeData['format']) ? $attributeData['format'] : DATE_RFC850;
+
+                if (strtotime($parameters[0]) === false) {
+                    // the `before` date refers to another parameter in the request
+                    $paramName = $parameters[0];
+                    $attributeData['description'][] = Description::parse($rule)->with($paramName)->getDescription();
+                    $attributeData['value'] = date($format, strtotime('-1 day', strtotime($routeData['parameters'][$paramName]['value'])));
+                } else {
+                    $attributeData['description'][] = Description::parse($rule)->with(date($format, strtotime($parameters[0])))->getDescription();
+                    $attributeData['value'] = date($format, strtotime('-1 day', strtotime($parameters[0])));
+                }
                 break;
             case 'date_format':
                 $attributeData['type'] = 'date';
                 $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription();
-                $attributeData['value'] = date($parameters[0]);
+                $attributeData['format'] = $parameters[0];
+                $attributeData['value'] = date($attributeData['format']);
                 break;
             case 'different':
                 $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription();
@@ -646,4 +672,116 @@ abstract class AbstractGenerator
                 return $rule;
         }
     }
+
+    /**
+     * @param $response
+     *
+     * @return mixed
+     */
+    private function getResponseContent($response)
+    {
+        if (empty($response)) {
+            return '';
+        }
+        if ($response->headers->get('Content-Type') === 'application/json') {
+            $content = json_decode($response->getContent(), JSON_PRETTY_PRINT);
+        } else {
+            $content = $response->getContent();
+        }
+
+        return $content;
+    }
+
+    /**
+     * Get a response from the transformer tags.
+     *
+     * @param array $tags
+     *
+     * @return mixed
+     */
+    protected function getTransformerResponse($tags)
+    {
+        try {
+            $transFormerTags = array_filter($tags, function ($tag) {
+                if (! ($tag instanceof Tag)) {
+                    return false;
+                }
+
+                return \in_array(\strtolower($tag->getName()), ['transformer', 'transformercollection']);
+            });
+            if (empty($transFormerTags)) {
+                // we didn't have any of the tags so goodbye
+                return false;
+            }
+
+            $modelTag = array_first(array_filter($tags, function ($tag) {
+                if (! ($tag instanceof Tag)) {
+                    return false;
+                }
+
+                return \in_array(\strtolower($tag->getName()), ['transformermodel']);
+            }));
+            $tag = \array_first($transFormerTags);
+            $transformer = $tag->getContent();
+            if (! \class_exists($transformer)) {
+                // if we can't find the transformer we can't generate a response
+                return;
+            }
+            $demoData = [];
+
+            $reflection = new ReflectionClass($transformer);
+            $method = $reflection->getMethod('transform');
+            $parameter = \array_first($method->getParameters());
+            $type = null;
+            if ($modelTag) {
+                $type = $modelTag->getContent();
+            }
+            if (version_compare(PHP_VERSION, '7.0.0') >= 0 && \is_null($type)) {
+                // we can only get the type with reflection for PHP 7
+                if ($parameter->hasType() &&
+                    ! $parameter->getType()->isBuiltin() &&
+                    \class_exists((string) $parameter->getType())) {
+                    //we have a type
+                    $type = (string) $parameter->getType();
+                }
+            }
+            if ($type) {
+                // we have a class so we try to create an instance
+                $demoData = new $type;
+                try {
+                    // try a factory
+                    $demoData = \factory($type)->make();
+                } catch (\Exception $e) {
+                    if ($demoData instanceof \Illuminate\Database\Eloquent\Model) {
+                        // we can't use a factory but can try to get one from the database
+                        try {
+                            // check if we can find one
+                            $newDemoData = $type::first();
+                            if ($newDemoData) {
+                                $demoData = $newDemoData;
+                            }
+                        } catch (\Exception $e) {
+                            // do nothing
+                        }
+                    }
+                }
+            }
+
+            $fractal = new Manager();
+            $resource = [];
+            if ($tag->getName() == 'transformer') {
+                // just one
+                $resource = new Item($demoData, new $transformer);
+            }
+            if ($tag->getName() == 'transformercollection') {
+                // a collection
+                $resource = new Collection([$demoData, $demoData], new $transformer);
+            }
+
+            return \response($fractal->createData($resource)->toJson());
+        } catch (\Exception $e) {
+            // it isn't possible to parse the transformer
+            return;
+        }
+    }
 }

+ 0 - 24
src/Mpociot/ApiDoc/Generators/DingoGenerator.php

@@ -30,28 +30,4 @@ class DingoGenerator extends AbstractGenerator
 
         return call_user_func_array([$dispatcher, strtolower($method)], [$uri]);
     }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function getDomain($route)
-    {
-        return $route->domain();
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function getUri($route)
-    {
-        return $route->uri();
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function getMethods($route)
-    {
-        return array_diff($route->getMethods(), ['HEAD']);
-    }
 }

+ 2 - 110
src/Mpociot/ApiDoc/Generators/LaravelGenerator.php

@@ -2,14 +2,9 @@
 
 namespace Mpociot\ApiDoc\Generators;
 
-use ReflectionClass;
-use League\Fractal\Manager;
 use Illuminate\Routing\Route;
-use League\Fractal\Resource\Item;
 use Illuminate\Support\Facades\App;
-use Mpociot\Reflection\DocBlock\Tag;
 use Illuminate\Support\Facades\Request;
-use League\Fractal\Resource\Collection;
 
 class LaravelGenerator extends AbstractGenerator
 {
@@ -18,17 +13,7 @@ class LaravelGenerator extends AbstractGenerator
      *
      * @return mixed
      */
-    public function getDomain($route)
-    {
-        return $route->domain();
-    }
-
-    /**
-     * @param Route $route
-     *
-     * @return mixed
-     */
-    public function getUri($route)
+    public function getUri(Route $route)
     {
         if (version_compare(app()->version(), '5.4', '<')) {
             return $route->getUri();
@@ -42,7 +27,7 @@ class LaravelGenerator extends AbstractGenerator
      *
      * @return mixed
      */
-    public function getMethods($route)
+    public function getMethods(Route $route)
     {
         if (version_compare(app()->version(), '5.4', '<')) {
             $methods = $route->getMethods();
@@ -97,97 +82,4 @@ class LaravelGenerator extends AbstractGenerator
 
         return $response;
     }
-
-    /**
-     * Get a response from the transformer tags.
-     *
-     * @param array $tags
-     *
-     * @return mixed
-     */
-    protected function getTransformerResponse($tags)
-    {
-        try {
-            $transFormerTags = array_filter($tags, function ($tag) {
-                if (! ($tag instanceof Tag)) {
-                    return false;
-                }
-
-                return \in_array(\strtolower($tag->getName()), ['transformer', 'transformercollection']);
-            });
-            if (empty($transFormerTags)) {
-                // we didn't have any of the tags so goodbye
-                return false;
-            }
-
-            $modelTag = array_first(array_filter($tags, function ($tag) {
-                if (! ($tag instanceof Tag)) {
-                    return false;
-                }
-
-                return \in_array(\strtolower($tag->getName()), ['transformermodel']);
-            }));
-            $tag = \array_first($transFormerTags);
-            $transformer = $tag->getContent();
-            if (! \class_exists($transformer)) {
-                // if we can't find the transformer we can't generate a response
-                return;
-            }
-            $demoData = [];
-
-            $reflection = new ReflectionClass($transformer);
-            $method = $reflection->getMethod('transform');
-            $parameter = \array_first($method->getParameters());
-            $type = null;
-            if ($modelTag) {
-                $type = $modelTag->getContent();
-            }
-            if (version_compare(PHP_VERSION, '7.0.0') >= 0 && \is_null($type)) {
-                // we can only get the type with reflection for PHP 7
-                if ($parameter->hasType() &&
-                ! $parameter->getType()->isBuiltin() &&
-                \class_exists((string) $parameter->getType())) {
-                    //we have a type
-                    $type = (string) $parameter->getType();
-                }
-            }
-            if ($type) {
-                // we have a class so we try to create an instance
-                $demoData = new $type;
-                try {
-                    // try a factory
-                    $demoData = \factory($type)->make();
-                } catch (\Exception $e) {
-                    if ($demoData instanceof \Illuminate\Database\Eloquent\Model) {
-                        // we can't use a factory but can try to get one from the database
-                        try {
-                            // check if we can find one
-                            $newDemoData = $type::first();
-                            if ($newDemoData) {
-                                $demoData = $newDemoData;
-                            }
-                        } catch (\Exception $e) {
-                            // do nothing
-                        }
-                    }
-                }
-            }
-
-            $fractal = new Manager();
-            $resource = [];
-            if ($tag->getName() == 'transformer') {
-                // just one
-                $resource = new Item($demoData, new $transformer);
-            }
-            if ($tag->getName() == 'transformercollection') {
-                // a collection
-                $resource = new Collection([$demoData, $demoData], new $transformer);
-            }
-
-            return \response($fractal->createData($resource)->toJson());
-        } catch (\Exception $e) {
-            // it isn't possible to parse the transformer
-            return;
-        }
-    }
 }

+ 1 - 1
tests/ApiDocGeneratorTest.php

@@ -355,7 +355,7 @@ class ApiDocGeneratorTest extends TestCase
         $this->assertTrue(is_array($parsed));
         $this->assertArrayHasKey('showresponse', $parsed);
         $this->assertTrue($parsed['showresponse']);
-        $this->assertJsonStringEqualsJsonString(json_decode($parsed['response'], true), '{ "data": []}');
+        $this->assertJsonStringEqualsJsonString($parsed['response'], '{ "data": []}');
     }
 
     public function testCanParseTransformerTag()

+ 59 - 13
tests/GenerateDocumentationTest.php

@@ -2,7 +2,8 @@
 
 namespace Mpociot\ApiDoc\Tests;
 
-use Illuminate\Routing\Route;
+use RecursiveIteratorIterator;
+use RecursiveDirectoryIterator;
 use Orchestra\Testbench\TestCase;
 use Illuminate\Contracts\Console\Kernel;
 use Dingo\Api\Provider\LaravelServiceProvider;
@@ -32,7 +33,20 @@ class GenerateDocumentationTest extends TestCase
 
     public function tearDown()
     {
-        exec('rm -rf '.__DIR__.'/../public/docs');
+        // delete the generated docs - compatible cross-platform
+        $dir = __DIR__.'/../public/docs';
+        if (is_dir($dir)) {
+            $files = new RecursiveIteratorIterator(
+                new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
+                RecursiveIteratorIterator::CHILD_FIRST
+            );
+
+            foreach ($files as $fileinfo) {
+                $todo = ($fileinfo->isDir() ? 'rmdir' : 'unlink');
+                $todo($fileinfo->getRealPath());
+            }
+            rmdir($dir);
+        }
     }
 
     /**
@@ -48,7 +62,7 @@ class GenerateDocumentationTest extends TestCase
         ];
     }
 
-    public function testConsoleCommandNeedsAPrefixOrRoute()
+    public function testConsoleCommandNeedsPrefixesOrDomainsOrRoutes()
     {
         $output = $this->artisan('api:generate');
         $this->assertEquals('You must provide either a route prefix, a route domain, a route or a middleware to generate the documentation.'.PHP_EOL, $output);
@@ -108,9 +122,9 @@ class GenerateDocumentationTest extends TestCase
         $output = $this->artisan('api:generate', [
             '--routePrefix' => 'api/*',
         ]);
-        $generatedMarkdown = file_get_contents(__DIR__.'/../public/docs/source/index.md');
-        $fixtureMarkdown = file_get_contents(__DIR__.'/Fixtures/resource_index.md');
-        $this->assertSame($generatedMarkdown, $fixtureMarkdown);
+        $fixtureMarkdown = __DIR__.'/Fixtures/resource_index.md';
+        $gneratedMarkdown = __DIR__.'/../public/docs/source/index.md';
+        $this->assertFilesHaveSameContent($fixtureMarkdown, $gneratedMarkdown);
     }
 
     public function testGeneratedMarkdownFileIsCorrect()
@@ -122,11 +136,11 @@ class GenerateDocumentationTest extends TestCase
             '--routePrefix' => 'api/*',
         ]);
 
-        $generatedMarkdown = file_get_contents(__DIR__.'/../public/docs/source/index.md');
-        $compareMarkdown = file_get_contents(__DIR__.'/../public/docs/source/.compare.md');
-        $fixtureMarkdown = file_get_contents(__DIR__.'/Fixtures/index.md');
-        $this->assertSame($generatedMarkdown, $fixtureMarkdown);
-        $this->assertSame($compareMarkdown, $fixtureMarkdown);
+        $generatedMarkdown = __DIR__.'/../public/docs/source/index.md';
+        $compareMarkdown = __DIR__.'/../public/docs/source/.compare.md';
+        $fixtureMarkdown = __DIR__.'/Fixtures/index.md';
+        $this->assertFilesHaveSameContent($fixtureMarkdown, $generatedMarkdown);
+        $this->assertFilesHaveSameContent($fixtureMarkdown, $compareMarkdown);
     }
 
     public function testAddsBindingsToGetRouteRules()
@@ -171,8 +185,8 @@ class GenerateDocumentationTest extends TestCase
             ],
         ]);
 
-        $generatedMarkdown = file_get_contents(__DIR__.'/../public/docs/source/index.md');
-        $this->assertContains('"authorization": [
+        $generatedMarkdown = $this->getFileContents(__DIR__.'/../public/docs/source/index.md');
+        $this->assertContainsRaw('"authorization": [
         "customAuthToken"
     ],
     "x-custom-header": [
@@ -204,4 +218,36 @@ class GenerateDocumentationTest extends TestCase
 
         return $this->app[Kernel::class]->output();
     }
+
+    private function assertFilesHaveSameContent($pathToExpected, $pathToActual)
+    {
+        $actual = $this->getFileContents($pathToActual);
+        $expected = $this->getFileContents($pathToExpected);
+        $this->assertSame($expected, $actual);
+    }
+
+    /**
+     * Get the contents of a file in a cross-platform-compatible way.
+     *
+     * @param $path
+     *
+     * @return string
+     */
+    private function getFileContents($path)
+    {
+        return str_replace("\r\n", "\n", file_get_contents($path));
+    }
+
+    /**
+     * Assert that a string contains another string, ignoring all whitespace.
+     *
+     * @param $needle
+     * @param $haystack
+     */
+    private function assertContainsRaw($needle, $haystack)
+    {
+        $haystack = preg_replace('/\s/', '', $haystack);
+        $needle = preg_replace('/\s/', '', $needle);
+        $this->assertContains($needle, $haystack);
+    }
 }