From d582cc5dfb9162fef732c8c34e93205043b69b90 Mon Sep 17 00:00:00 2001 From: shalvah Date: Sat, 25 Jun 2022 04:09:52 +0200 Subject: [PATCH] Refactor tests/add tests for laravel type --- phpunit.xml | 3 +- src/Commands/GenerateDocumentation.php | 2 +- .../Responses/UseResponseFileTag.php | 23 +- src/Scribe.php | 24 + src/Tools/ErrorHandlingUtils.php | 30 +- src/Tools/Globals.php | 8 + src/Writing/Writer.php | 8 +- .../GenerateDocumentation/BehavioursTest.php | 462 ++---------------- tests/GenerateDocumentation/OutputTest.php | 315 ++++-------- .../Responses/UseResponseFileTagTest.php | 65 ++- .../UrlParameters/GetFromLaravelAPITest.php | 6 - tests/TestHelpers.php | 5 + tests/Unit/ValidationRuleParsingTest.php | 16 +- 13 files changed, 267 insertions(+), 700 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index 612cd95c..61978891 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -22,7 +22,8 @@ - tests/GenerateDocumentationTest.php + tests/GenerateDocumentation/OutputTest.php + tests/GenerateDocumentation/BehavioursTest.php tests/Strategies diff --git a/src/Commands/GenerateDocumentation.php b/src/Commands/GenerateDocumentation.php index d95f6395..02b87ed2 100644 --- a/src/Commands/GenerateDocumentation.php +++ b/src/Commands/GenerateDocumentation.php @@ -216,7 +216,7 @@ protected function upgradeConfigFileIfNeeded(): void } } } catch (\Throwable $e) { - $this->warn("Check failed wih error:"); + $this->warn("Check failed with error:"); e::dumpExceptionIfVerbose($e); $this->warn("This did not affect your docs. Please report this issue in the project repo: https://github.com/knuckleswtf/scribe"); } diff --git a/src/Extracting/Strategies/Responses/UseResponseFileTag.php b/src/Extracting/Strategies/Responses/UseResponseFileTag.php index d513ac65..0f071a69 100644 --- a/src/Extracting/Strategies/Responses/UseResponseFileTag.php +++ b/src/Extracting/Strategies/Responses/UseResponseFileTag.php @@ -52,15 +52,7 @@ public function getFileResponses(array $tags): ?array $status = $attributes['status'] ?: ($status ?: 200); $description = $attributes['scenario'] ? "$status, {$attributes['scenario']}" : "$status"; - if (!file_exists($filePath)) { - // Try Laravel storage folder - if (!file_exists(storage_path($filePath))) { - throw new \InvalidArgumentException("@responseFile {$filePath} does not exist"); - } - - $filePath = storage_path($filePath); - } - $content = file_get_contents($filePath, true); + $content = $this->getFileContents($filePath); if ($json) { $json = str_replace("'", '"', $json); $content = json_encode(array_merge(json_decode($content, true), json_decode($json, true))); @@ -76,4 +68,17 @@ public function getFileResponses(array $tags): ?array return $responses; } + + protected function getFileContents($filePath): string|false + { + if (!file_exists($filePath)) { + // Try Laravel storage folder + if (!file_exists(storage_path($filePath))) { + throw new \InvalidArgumentException("@responseFile {$filePath} does not exist"); + } + + $filePath = storage_path($filePath); + } + return file_get_contents($filePath, true); + } } diff --git a/src/Scribe.php b/src/Scribe.php index 367f92b6..3cb3f1d0 100644 --- a/src/Scribe.php +++ b/src/Scribe.php @@ -5,6 +5,7 @@ use Knuckles\Camel\Extraction\ExtractedEndpointData; use Knuckles\Scribe\Tools\Globals; use Symfony\Component\HttpFoundation\Request; +use Illuminate\Support\Collection; class Scribe { @@ -56,4 +57,27 @@ public static function instantiateFormRequestUsing(callable $callable) { Globals::$__instantiateFormRequestUsing = $callable; } + + /** + * Customise how Scribe orders your endpoint groups. + * Your callback will be given + * + * @param callable(string,\Illuminate\Routing\Route,\ReflectionFunctionAbstract): mixed $callable + */ + public static function orderGroupsUsing(callable $callable) + { + Globals::$__orderGroupsUsing = $callable; + } + + /** + * Customise how Scribe orders endpoints within a group. + * Your callback will be given a Laravel Collection of ExtractedEndpointData objects, + * and should return the sorted collection. + * + * @param callable(Collection): Collection $callable + */ + public static function orderEndpointsInGroupUsing(callable $callable) + { + Globals::$__orderEndpointsInGroupUsing = $callable; + } } \ No newline at end of file diff --git a/src/Tools/ErrorHandlingUtils.php b/src/Tools/ErrorHandlingUtils.php index 6b237e34..9ca91912 100644 --- a/src/Tools/ErrorHandlingUtils.php +++ b/src/Tools/ErrorHandlingUtils.php @@ -9,22 +9,26 @@ class ErrorHandlingUtils { public static function dumpExceptionIfVerbose(\Throwable $e, $completelySilent = false): void { + if ($completelySilent) { + return; + } + if (Globals::$shouldBeVerbose) { self::dumpException($e); - } else if (!$completelySilent) { - [$firstFrame, $secondFrame] = $e->getTrace(); - - try { - ['file' => $file, 'line' => $line] = $firstFrame; - } catch (\Exception $_) { - ['file' => $file, 'line' => $line] = $secondFrame; - } - $exceptionType = get_class($e); - $message = $e->getMessage(); - $message = "$exceptionType in $file at line $line: $message"; - ConsoleOutputUtils::error($message); - ConsoleOutputUtils::error('Run this again with the --verbose flag to see the full stack trace.'); + return; + } + [$firstFrame, $secondFrame] = $e->getTrace(); + + try { + ['file' => $file, 'line' => $line] = $firstFrame; + } catch (\Exception $_) { + ['file' => $file, 'line' => $line] = $secondFrame; } + $exceptionType = get_class($e); + $message = $e->getMessage(); + $message = "$exceptionType in $file at line $line: $message"; + ConsoleOutputUtils::error($message); + ConsoleOutputUtils::error('Run this again with the --verbose flag to see the full stack trace.'); } diff --git a/src/Tools/Globals.php b/src/Tools/Globals.php index 0bd27b8c..bf239552 100644 --- a/src/Tools/Globals.php +++ b/src/Tools/Globals.php @@ -8,9 +8,17 @@ class Globals public static bool $shouldBeVerbose = false; + /* + * Hooks, used by users to configure Scribe's behaviour. + */ + public static $__beforeResponseCall; public static $__afterGenerating; public static $__instantiateFormRequestUsing; + + public static $__orderEndpointsInGroupUsing; + + public static $__orderGroupsUsing; } diff --git a/src/Writing/Writer.php b/src/Writing/Writer.php index 0196d356..19ee68e3 100644 --- a/src/Writing/Writer.php +++ b/src/Writing/Writer.php @@ -83,7 +83,8 @@ protected function writePostmanCollection(array $groups): void } c::success("Wrote Postman collection to: {$collectionPath}"); - $this->generatedFiles['postman'] = realpath($collectionPath); + $this->generatedFiles['postman'] = realpath($collectionPath) + ?: Storage::disk('local')->path('scribe/collection.json'); } } @@ -102,7 +103,8 @@ protected function writeOpenAPISpec(array $parsedRoutes): void } c::success("Wrote OpenAPI specification to: {$specPath}"); - $this->generatedFiles['openapi'] = realpath($specPath); + $this->generatedFiles['openapi'] = realpath($specPath) + ?: Storage::disk('local')->path('scribe/openapi.yaml'); } } @@ -200,7 +202,7 @@ public function writeHtmlDocs(array $groupedEndpoints): void $outputPath = rtrim($this->laravelTypeOutputPath, '/') . '/'; c::success("Wrote Blade docs to: $outputPath"); $this->generatedFiles['blade'] = realpath("{$outputPath}index.blade.php"); - $assetsOutputPath = app()->get('path.public') . $this->laravelAssetsPath; + $assetsOutputPath = app()->get('path.public') . $this->laravelAssetsPath . '/'; c::success("Wrote Laravel assets to: " . realpath($assetsOutputPath)); } $this->generatedFiles['assets']['js'] = realpath("{$assetsOutputPath}js"); diff --git a/tests/GenerateDocumentation/BehavioursTest.php b/tests/GenerateDocumentation/BehavioursTest.php index 0335f924..999e955a 100644 --- a/tests/GenerateDocumentation/BehavioursTest.php +++ b/tests/GenerateDocumentation/BehavioursTest.php @@ -1,20 +1,20 @@ []]); + config(['scribe.routes.0.match.prefixes' => ['api/*']]); // Skip these ones for faster tests config(['scribe.openapi.enabled' => false]); config(['scribe.postman.enabled' => false]); @@ -45,25 +46,22 @@ public function tearDown(): void } /** @test */ - public function can_process_traditional_laravel_route_syntax() + public function can_process_traditional_laravel_route_syntax_and_callable_tuple_syntax() { RouteFacade::get('/api/test', [TestController::class, 'withEndpointDescription']); + RouteFacade::get('/api/array/test', [TestController::class, 'withEndpointDescription']); - config(['scribe.routes.0.match.prefixes' => ['api/*']]); - $output = $this->artisan('scribe:generate'); - - $this->assertStringContainsString('Processed route: [GET] api/test', $output); + $this->generateAndExpectConsoleOutput( + 'Processed route: [GET] api/test', + 'Processed route: [GET] api/array/test' + ); } /** @test */ - public function can_process_traditional_laravel_head_routes() + public function processes_head_routes_as_head_not_get() { RouteFacade::addRoute('HEAD', '/api/test', [TestController::class, 'withEndpointDescription']); - - config(['scribe.routes.0.match.prefixes' => ['api/*']]); - $output = $this->artisan('scribe:generate'); - - $this->assertStringContainsString('Processed route: [HEAD] api/test', $output); + $this->generateAndExpectConsoleOutput('Processed route: [HEAD] api/test'); } /** @@ -75,11 +73,7 @@ public function can_process_closure_routes() RouteFacade::get('/api/closure', function () { return 'hi'; }); - - config(['scribe.routes.0.match.prefixes' => ['api/*']]); - $output = $this->artisan('scribe:generate'); - - $this->assertStringContainsString('Processed route: [GET] api/closure', $output); + $this->generateAndExpectConsoleOutput('Processed route: [GET] api/closure'); } /** @@ -98,75 +92,58 @@ public function can_process_routes_on_dingo() config(['scribe.routes.0.match.prefixes' => ['*']]); config(['scribe.routes.0.match.versions' => ['v1']]); - $output = $this->artisan('scribe:generate'); - - $this->assertStringContainsString('Processed route: [GET] closure', $output); - $this->assertStringContainsString('Processed route: [GET] test', $output); - } - - /** @test */ - public function can_process_callable_tuple_syntax() - { - RouteFacade::get('/api/array/test', [TestController::class, 'withEndpointDescription']); - - config(['scribe.routes.0.match.prefixes' => ['api/*']]); - $output = $this->artisan('scribe:generate'); - $this->assertStringContainsString('Processed route: [GET] api/array/test', $output); + $this->generateAndExpectConsoleOutput( + 'Processed route: [GET] closure', + 'Processed route: [GET] test' + ); } /** @test */ public function calls_afterGenerating_hook() { - Scribe::afterGenerating(function (array $paths) { - $this->assertEquals( - [ - 'html' => realpath('public/docs/index.html'), - 'blade' => null, - 'postman' => realpath('public/docs/collection.json') ?: null, - 'openapi' => realpath('public/docs/openapi.yaml') ?: null, - 'assets' => [ - 'js' => realpath('public/docs/js'), - 'css' => realpath('public/docs/css'), - 'images' => realpath('public/docs/images'), - ] - ], $paths); + $paths = []; + Scribe::afterGenerating(function (array $outputPaths) use (&$paths) { + $paths = $outputPaths; }); + RouteFacade::get('/api/test', [TestController::class, 'withEndpointDescription']); - RouteFacade::get('/api/array/test', [TestController::class, 'withEndpointDescription']); - - config(['scribe.routes.0.match.prefixes' => ['api/*']]); - $output = $this->artisan('scribe:generate'); - - $this->assertStringContainsString('Processed route: [GET] api/array/test', $output); + $this->generate(); + + $this->assertEquals([ + 'html' => realpath('public/docs/index.html'), + 'blade' => null, + 'postman' => realpath('public/docs/collection.json') ?: null, + 'openapi' => realpath('public/docs/openapi.yaml') ?: null, + 'assets' => [ + 'js' => realpath('public/docs/js'), + 'css' => realpath('public/docs/css'), + 'images' => realpath('public/docs/images'), + ], + ], $paths); Scribe::afterGenerating(fn() => null); } /** @test */ - public function can_skip_methods_and_classes_with_hidefromapidocumentation_tag() + public function skips_methods_and_classes_with_hidefromapidocumentation_tag() { RouteFacade::get('/api/skip', [TestController::class, 'skip']); RouteFacade::get('/api/skipClass', TestIgnoreThisController::class . '@dummy'); RouteFacade::get('/api/test', [TestController::class, 'withEndpointDescription']); - config(['scribe.routes.0.match.prefixes' => ['api/*']]); - $output = $this->artisan('scribe:generate'); - - $this->assertStringContainsString('Skipping route: [GET] api/skip', $output); - $this->assertStringContainsString('Skipping route: [GET] api/skipClass', $output); - $this->assertStringContainsString('Processed route: [GET] api/test', $output); + $this->generateAndExpectConsoleOutput( + 'Skipping route: [GET] api/skip', + 'Skipping route: [GET] api/skipClass', + 'Processed route: [GET] api/test' + ); } /** @test */ public function warns_of_nonexistent_response_files() { RouteFacade::get('/api/non-existent', [TestController::class, 'withNonExistentResponseFile']); - - config(['scribe.routes.0.match.prefixes' => ['api/*']]); - $output = $this->artisan('scribe:generate'); - - $this->assertStringContainsString('@responseFile i-do-not-exist.json does not exist', $output); + $this->generateAndExpectConsoleOutput('@responseFile i-do-not-exist.json does not exist'); } /** @test */ @@ -175,24 +152,7 @@ public function can_parse_resource_routes() RouteFacade::resource('/api/users', TestResourceController::class) ->only(['index', 'store']); - config(['scribe.routes.0.match.prefixes' => ['api/*']]); - config([ - 'scribe.routes.0.apply.headers' => [ - 'Accept' => 'application/json', - ], - ]); - - $output = $this->artisan('scribe:generate'); - - $this->assertStringContainsString('Processed route: [GET] api/users', $output); - $this->assertStringContainsString('Processed route: [POST] api/users', $output); - - $this->assertStringNotContainsString('Processed route: [PUT,PATCH] api/users/{user}', $output); - $this->assertStringNotContainsString('Processed route: [DELETE] api/users/{user}', $output); - - RouteFacade::apiResource('/api/users', TestResourceController::class) - ->only(['index', 'store']); - $output = $this->artisan('scribe:generate'); + $output = $this->generate(); $this->assertStringContainsString('Processed route: [GET] api/users', $output); $this->assertStringContainsString('Processed route: [POST] api/users', $output); @@ -206,127 +166,10 @@ public function supports_partial_resource_controller() { RouteFacade::resource('/api/users', TestPartialResourceController::class); - config(['scribe.routes.0.match.prefixes' => ['api/*']]); - - $output = $this->artisan('scribe:generate'); - - $this->assertStringContainsString('Processed route: [GET] api/users', $output); - $this->assertStringContainsString('Processed route: [PUT,PATCH] api/users/{user}', $output); - - } - - /** @test */ - public function generated_postman_collection_file_is_correct() - { - RouteFacade::post('/api/withBodyParametersAsArray', [TestController::class, 'withBodyParametersAsArray']); - RouteFacade::post('/api/withFormDataParams', [TestController::class, 'withFormDataParams']); - RouteFacade::post('/api/withBodyParameters', [TestController::class, 'withBodyParameters']); - RouteFacade::get('/api/withQueryParameters', [TestController::class, 'withQueryParameters']); - RouteFacade::get('/api/withAuthTag', [TestController::class, 'withAuthenticatedTag']); - RouteFacade::get('/api/echoesUrlParameters/{param}/{param2}/{param3?}/{param4?}', [TestController::class, 'echoesUrlParameters']); - // We want to have the same values for params each time - config(['scribe.faker_seed' => 1234]); - config(['scribe.title' => 'GREAT API!']); - config(['scribe.auth.enabled' => true]); - config(['scribe.routes.0.match.prefixes' => ['api/*']]); - config(['scribe.postman.overrides' => [ - 'info.version' => '3.9.9', - ]]); - config([ - 'scribe.routes.0.apply.headers' => [ - 'Custom-Header' => 'NotSoCustom', - ], - ]); - config(['scribe.postman.enabled' => true]); - - $this->artisan('scribe:generate'); - - $generatedCollection = json_decode(file_get_contents(__DIR__ . '/../public/docs/collection.json'), true); - // The Postman ID varies from call to call; erase it to make the test data reproducible. - $generatedCollection['info']['_postman_id'] = ''; - $fixtureCollection = json_decode(file_get_contents(__DIR__ . '/Fixtures/collection.json'), true); - - $this->assertEquals($fixtureCollection, $generatedCollection); - } - - /** @test */ - public function generated_openapi_spec_file_is_correct() - { - RouteFacade::post('/api/withBodyParametersAsArray', [TestController::class, 'withBodyParametersAsArray']); - RouteFacade::post('/api/withFormDataParams', [TestController::class, 'withFormDataParams']); - RouteFacade::get('/api/withResponseTag', [TestController::class, 'withResponseTag']); - RouteFacade::get('/api/withQueryParameters', [TestController::class, 'withQueryParameters']); - RouteFacade::get('/api/withAuthTag', [TestController::class, 'withAuthenticatedTag']); - RouteFacade::get('/api/echoesUrlParameters/{param}/{param2}/{param3?}/{param4?}', [TestController::class, 'echoesUrlParameters']); - - // We want to have the same values for params each time - config(['scribe.faker_seed' => 1234]); - config(['scribe.openapi.enabled' => true]); - config(['scribe.openapi.overrides' => [ - 'info.version' => '3.9.9', - ]]); - config(['scribe.routes.0.match.prefixes' => ['api/*']]); - config([ - 'scribe.routes.0.apply.headers' => [ - 'Custom-Header' => 'NotSoCustom', - ], - ]); - - $this->artisan('scribe:generate'); - - $generatedSpec = Yaml::parseFile(__DIR__ . '/../public/docs/openapi.yaml'); - $fixtureSpec = Yaml::parseFile(__DIR__ . '/Fixtures/openapi.yaml'); - $this->assertEquals($fixtureSpec, $generatedSpec); - } - - /** @test */ - public function can_append_custom_http_headers() - { - RouteFacade::get('/api/headers', [TestController::class, 'checkCustomHeaders']); - - config(['scribe.routes.0.match.prefixes' => ['api/*']]); - config([ - 'scribe.routes.0.apply.headers' => [ - 'Authorization' => 'customAuthToken', - 'Custom-Header' => 'NotSoCustom', - ], - ]); - $this->artisan('scribe:generate'); - - $endpointDetails = Yaml::parseFile(__DIR__ . '/../.scribe/endpoints/00.yaml')['endpoints'][0]; - $this->assertEquals("customAuthToken", $endpointDetails['headers']["Authorization"]); - $this->assertEquals("NotSoCustom", $endpointDetails['headers']["Custom-Header"]); - } - - /** @test */ - public function can_parse_utf8_response() - { - RouteFacade::get('/api/utf8', [TestController::class, 'withUtf8ResponseTag']); - - config(['scribe.routes.0.match.prefixes' => ['api/*']]); - $this->artisan('scribe:generate'); - - $generatedHtml = file_get_contents('public/docs/index.html'); - $this->assertStringContainsString('Лорем ипсум долор сит амет', $generatedHtml); - } - - /** @test */ - public function sorts_group_naturally() - { - RouteFacade::get('/api/action1', TestGroupController::class . '@action1'); - RouteFacade::get('/api/action1b', TestGroupController::class . '@action1b'); - RouteFacade::get('/api/action2', TestGroupController::class . '@action2'); - RouteFacade::get('/api/action10', TestGroupController::class . '@action10'); - - config(['scribe.routes.0.match.prefixes' => ['api/*']]); - $this->artisan('scribe:generate'); - - $this->assertFileExists(__DIR__ . '/../.scribe/endpoints/00.yaml'); - $this->assertFileExists(__DIR__ . '/../.scribe/endpoints/01.yaml'); - $this->assertFileExists(__DIR__ . '/../.scribe/endpoints/02.yaml'); - $this->assertEquals('1. Group 1', Yaml::parseFile(__DIR__ . '/../.scribe/endpoints/00.yaml')['name']); - $this->assertEquals('2. Group 2', Yaml::parseFile(__DIR__ . '/../.scribe/endpoints/01.yaml')['name']); - $this->assertEquals('10. Group 10', Yaml::parseFile(__DIR__ . '/../.scribe/endpoints/02.yaml')['name']); + $this->generateAndExpectConsoleOutput( + 'Processed route: [GET] api/users', + 'Processed route: [PUT,PATCH] api/users/{user}' + ); } /** @test */ @@ -334,219 +177,22 @@ public function can_customise_static_output_path() { RouteFacade::get('/api/action1', TestGroupController::class . '@action1'); - config(['scribe.routes.0.match.prefixes' => ['*']]); config(['scribe.static.output_path' => 'static/docs']); - $this->artisan('scribe:generate'); + $this->assertFileDoesNotExist('static/docs/index.html'); + + $this->generate(); $this->assertFileExists('static/docs/index.html'); Utils::deleteDirectoryAndContents('static/'); } - /** @test */ - public function will_not_overwrite_manually_modified_content_unless_force_flag_is_set() + protected function generateAndExpectConsoleOutput(string ...$expectedOutput): void { - RouteFacade::get('/api/action1', [TestGroupController::class, 'action1']); - RouteFacade::get('/api/action1b', [TestGroupController::class, 'action1b']); - config(['scribe.routes.0.match.prefixes' => ['api/*']]); - config(['scribe.routes.0.apply.response_calls.methods' => []]); - - $this->artisan('scribe:generate'); - - $authFilePath = '.scribe/auth.md'; - $group1FilePath = '.scribe/endpoints/00.yaml'; - - $group = Yaml::parseFile($group1FilePath); - $this->assertEquals('api/action1', $group['endpoints'][0]['uri']); - $this->assertEquals([], $group['endpoints'][0]['urlParameters']); - $extraParam = [ - 'name' => 'a_param', - 'description' => 'A URL param.', - 'required' => true, - 'example' => 6, - 'type' => 'integer', - 'custom' => [], - ]; - $group['endpoints'][0]['urlParameters']['a_param'] = $extraParam; - file_put_contents($group1FilePath, Yaml::dump( - $group, 20, 2, - Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE | Yaml::DUMP_OBJECT_AS_MAP - )); - file_put_contents($authFilePath, 'Some other useful stuff.', FILE_APPEND); - - $this->artisan('scribe:generate'); - - $group = Yaml::parseFile($group1FilePath); - $this->assertEquals('api/action1', $group['endpoints'][0]['uri']); - $this->assertEquals(['a_param' => $extraParam], $group['endpoints'][0]['urlParameters']); - $this->assertStringContainsString('Some other useful stuff.', file_get_contents($authFilePath)); - - $this->artisan('scribe:generate', ['--force' => true]); - - $group = Yaml::parseFile($group1FilePath); - $this->assertEquals('api/action1', $group['endpoints'][0]['uri']); - $this->assertEquals([], $group['endpoints'][0]['urlParameters']); - $this->assertStringNotContainsString('Some other useful stuff.', file_get_contents($authFilePath)); - } + $output = $this->generate(); - /** @test */ - public function generates_correct_url_params_from_resource_routes_and_field_bindings() - { - if (version_compare($this->app->version(), '7.0.0', '<')) { - $this->markTestSkipped("Laravel < 7.x doesn't support field binding syntax."); - - return; + foreach ($expectedOutput as $expected) { + $this->assertStringContainsString($expected, $output); } - - RouteFacade::prefix('providers/{provider:slug}')->group(function () { - RouteFacade::resource('users.addresses', TestPartialResourceController::class)->parameters([ - 'addresses' => 'address:uuid', - ]); - }); - config(['scribe.routes.0.match.prefixes' => ['*']]); - config(['scribe.routes.0.apply.response_calls.methods' => []]); - - $this->artisan('scribe:generate'); - - $groupA = Yaml::parseFile('.scribe/endpoints/00.yaml'); - $this->assertEquals('providers/{provider_slug}/users/{user_id}/addresses', $groupA['endpoints'][0]['uri']); - $groupB = Yaml::parseFile('.scribe/endpoints/01.yaml'); - $this->assertEquals('providers/{provider_slug}/users/{user_id}/addresses/{uuid}', $groupB['endpoints'][0]['uri']); - } - - /** @test */ - public function will_generate_without_extracting_if_noExtraction_flag_is_set() - { - config(['scribe.routes.0.exclude' => ['*']]); - Utils::copyDirectory(__DIR__.'/Fixtures/.scribe', '.scribe'); - - $output = $this->artisan('scribe:generate', ['--no-extraction' => true]); - - $this->assertStringNotContainsString("Processing route", $output); - - $crawler = new Crawler(file_get_contents('public/docs/index.html')); - [$intro, $auth] = $crawler->filter('h1 + p')->getIterator(); - $this->assertEquals('Heyaa introduction!👋', trim($intro->firstChild->textContent)); - $this->assertEquals('This is just a test.', trim($auth->firstChild->textContent)); - $group = $crawler->filter('h1')->getNode(2); - $this->assertEquals('General', trim($group->textContent)); - $expectedEndpoint = $crawler->filter('h2'); - $this->assertCount(1, $expectedEndpoint); - $this->assertEquals("Healthcheck", $expectedEndpoint->text()); - } - - /** @test */ - public function merges_and_correctly_sorts_user_defined_endpoints() - { - RouteFacade::get('/api/action1', [TestGroupController::class, 'action1']); - RouteFacade::get('/api/action2', [TestGroupController::class, 'action2']); - config(['scribe.routes.0.match.prefixes' => ['api/*']]); - config(['scribe.routes.0.apply.response_calls.methods' => []]); - if (!is_dir('.scribe/endpoints')) - mkdir('.scribe/endpoints', 0777, true); - copy(__DIR__ . '/Fixtures/custom.0.yaml', '.scribe/endpoints/custom.0.yaml'); - - $this->artisan('scribe:generate'); - - $crawler = new Crawler(file_get_contents('public/docs/index.html')); - $headings = $crawler->filter('h1')->getIterator(); - // There should only be six headings — intro, auth and four groups - $this->assertCount(6, $headings); - [$_, $_, $group1, $group2, $group3, $group4] = $headings; - $this->assertEquals('1. Group 1', trim($group1->textContent)); - $this->assertEquals('5. Group 5', trim($group2->textContent)); - $this->assertEquals('4. Group 4', trim($group3->textContent)); - $this->assertEquals('2. Group 2', trim($group4->textContent)); - $expectedEndpoints = $crawler->filter('h2'); - $this->assertEquals(6, $expectedEndpoints->count()); - // Enforce the order of the endpoints - // Ideally, we should also check the groups they're under - $this->assertEquals("Some endpoint.", $expectedEndpoints->getNode(0)->textContent); - $this->assertEquals("User defined", $expectedEndpoints->getNode(1)->textContent); - $this->assertEquals("GET withBeforeGroup", $expectedEndpoints->getNode(2)->textContent); - $this->assertEquals("GET belongingToAnEarlierBeforeGroup", $expectedEndpoints->getNode(3)->textContent); - $this->assertEquals("GET withAfterGroup", $expectedEndpoints->getNode(4)->textContent); - $this->assertEquals("GET api/action2", $expectedEndpoints->getNode(5)->textContent); - } - - /** @test */ - public function respects_endpoints_and_group_sort_order() - { - RouteFacade::get('/api/action1', [TestGroupController::class, 'action1']); - RouteFacade::get('/api/action1b', [TestGroupController::class, 'action1b']); - RouteFacade::get('/api/action2', [TestGroupController::class, 'action2']); - config(['scribe.routes.0.match.prefixes' => ['api/*']]); - config(['scribe.routes.0.apply.response_calls.methods' => []]); - - $this->artisan('scribe:generate'); - - // First: verify the current order of the groups and endpoints - $crawler = new Crawler(file_get_contents('public/docs/index.html')); - $h1s = $crawler->filter('h1'); - $this->assertEquals('1. Group 1', trim($h1s->getNode(2)->textContent)); - $this->assertEquals('2. Group 2', trim($h1s->getNode(3)->textContent)); - $expectedEndpoints = $crawler->filter('h2'); - $this->assertEquals("Some endpoint.", $expectedEndpoints->getNode(0)->textContent); - $this->assertEquals("Another endpoint.", $expectedEndpoints->getNode(1)->textContent); - $this->assertEquals("GET api/action2", $expectedEndpoints->getNode(2)->textContent); - - // Now swap the endpoints - $group = Yaml::parseFile('.scribe/endpoints/00.yaml'); - $this->assertEquals('api/action1', $group['endpoints'][0]['uri']); - $this->assertEquals('api/action1b', $group['endpoints'][1]['uri']); - $action1 = $group['endpoints'][0]; - $group['endpoints'][0] = $group['endpoints'][1]; - $group['endpoints'][1] = $action1; - file_put_contents('.scribe/endpoints/00.yaml', Yaml::dump( - $group, 20, 2, - Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE | Yaml::DUMP_OBJECT_AS_MAP - )); - // And then the groups - rename('.scribe/endpoints/00.yaml', '.scribe/endpoints/temp.yaml'); - rename('.scribe/endpoints/01.yaml', '.scribe/endpoints/00.yaml'); - rename('.scribe/endpoints/temp.yaml', '.scribe/endpoints/1.yaml'); - - $this->artisan('scribe:generate'); - - $crawler = new Crawler(file_get_contents('public/docs/index.html')); - $h1s = $crawler->filter('h1'); - $this->assertEquals('2. Group 2', trim($h1s->getNode(2)->textContent)); - $this->assertEquals('1. Group 1', trim($h1s->getNode(3)->textContent)); - $expectedEndpoints = $crawler->filter('h2'); - $this->assertEquals("GET api/action2", $expectedEndpoints->getNode(0)->textContent); - $this->assertEquals("Another endpoint.", $expectedEndpoints->getNode(1)->textContent); - $this->assertEquals("Some endpoint.", $expectedEndpoints->getNode(2)->textContent); - } - - /** @test */ - public function will_auto_set_content_type_to_multipart_if_file_params_are_present() - { - /** - * @bodyParam param string required - */ - RouteFacade::post('no-file', fn() => null); - /** - * @bodyParam a_file file required - */ - RouteFacade::post('top-level-file', fn() => null); - /** - * @bodyParam data object - * @bodyParam data.thing string - * @bodyParam data.a_file file - */ - RouteFacade::post('nested-file', fn() => null); - config(['scribe.routes.0.match.prefixes' => ['*']]); - config(['scribe.routes.0.apply.response_calls.methods' => []]); - - $this->artisan('scribe:generate'); - - $group = Yaml::parseFile('.scribe/endpoints/00.yaml'); - $this->assertEquals('no-file', $group['endpoints'][0]['uri']); - $this->assertEquals('application/json', $group['endpoints'][0]['headers']['Content-Type']); - $this->assertEquals('top-level-file', $group['endpoints'][1]['uri']); - $this->assertEquals('multipart/form-data', $group['endpoints'][1]['headers']['Content-Type']); - $this->assertEquals('nested-file', $group['endpoints'][2]['uri']); - $this->assertEquals('multipart/form-data', $group['endpoints'][2]['headers']['Content-Type']); - } } diff --git a/tests/GenerateDocumentation/OutputTest.php b/tests/GenerateDocumentation/OutputTest.php index 0335f924..a02d3675 100644 --- a/tests/GenerateDocumentation/OutputTest.php +++ b/tests/GenerateDocumentation/OutputTest.php @@ -1,20 +1,21 @@ []]); + config(['scribe.routes.0.match.prefixes' => ['api/*']]); // Skip these ones for faster tests config(['scribe.openapi.enabled' => false]); config(['scribe.postman.enabled' => false]); + // We want to have the same values for params each time + config(['scribe.faker_seed' => 1234]); $factory = app(\Illuminate\Database\Eloquent\Factory::class); $factory->define(TestUser::class, function () { @@ -44,175 +48,40 @@ public function tearDown(): void Utils::deleteDirectoryAndContents('.scribe'); } - /** @test */ - public function can_process_traditional_laravel_route_syntax() - { - RouteFacade::get('/api/test', [TestController::class, 'withEndpointDescription']); - - config(['scribe.routes.0.match.prefixes' => ['api/*']]); - $output = $this->artisan('scribe:generate'); - - $this->assertStringContainsString('Processed route: [GET] api/test', $output); - } - - /** @test */ - public function can_process_traditional_laravel_head_routes() + protected function defineEnvironment($app) { - RouteFacade::addRoute('HEAD', '/api/test', [TestController::class, 'withEndpointDescription']); - - config(['scribe.routes.0.match.prefixes' => ['api/*']]); - $output = $this->artisan('scribe:generate'); - - $this->assertStringContainsString('Processed route: [HEAD] api/test', $output); - } - - /** - * @test - * @see https://github.com/knuckleswtf/scribe/issues/53 - */ - public function can_process_closure_routes() - { - RouteFacade::get('/api/closure', function () { - return 'hi'; - }); - - config(['scribe.routes.0.match.prefixes' => ['api/*']]); - $output = $this->artisan('scribe:generate'); - - $this->assertStringContainsString('Processed route: [GET] api/closure', $output); - } - - /** - * @group dingo - * @test - */ - public function can_process_routes_on_dingo() - { - $api = app(\Dingo\Api\Routing\Router::class); - $api->version('v1', function ($api) { - $api->get('/closure', function () { - return 'foo'; - }); - $api->get('/test', [TestController::class, 'withEndpointDescription']); - }); - - config(['scribe.routes.0.match.prefixes' => ['*']]); - config(['scribe.routes.0.match.versions' => ['v1']]); - $output = $this->artisan('scribe:generate'); - - $this->assertStringContainsString('Processed route: [GET] closure', $output); - $this->assertStringContainsString('Processed route: [GET] test', $output); - } - - /** @test */ - public function can_process_callable_tuple_syntax() - { - RouteFacade::get('/api/array/test', [TestController::class, 'withEndpointDescription']); - - config(['scribe.routes.0.match.prefixes' => ['api/*']]); - $output = $this->artisan('scribe:generate'); - - $this->assertStringContainsString('Processed route: [GET] api/array/test', $output); - } - - /** @test */ - public function calls_afterGenerating_hook() - { - Scribe::afterGenerating(function (array $paths) { - $this->assertEquals( - [ - 'html' => realpath('public/docs/index.html'), - 'blade' => null, - 'postman' => realpath('public/docs/collection.json') ?: null, - 'openapi' => realpath('public/docs/openapi.yaml') ?: null, - 'assets' => [ - 'js' => realpath('public/docs/js'), - 'css' => realpath('public/docs/css'), - 'images' => realpath('public/docs/images'), - ] - ], $paths); - }); - - RouteFacade::get('/api/array/test', [TestController::class, 'withEndpointDescription']); - - config(['scribe.routes.0.match.prefixes' => ['api/*']]); - $output = $this->artisan('scribe:generate'); - - $this->assertStringContainsString('Processed route: [GET] api/array/test', $output); - - Scribe::afterGenerating(fn() => null); - } - - /** @test */ - public function can_skip_methods_and_classes_with_hidefromapidocumentation_tag() - { - RouteFacade::get('/api/skip', [TestController::class, 'skip']); - RouteFacade::get('/api/skipClass', TestIgnoreThisController::class . '@dummy'); - RouteFacade::get('/api/test', [TestController::class, 'withEndpointDescription']); - - config(['scribe.routes.0.match.prefixes' => ['api/*']]); - $output = $this->artisan('scribe:generate'); - - $this->assertStringContainsString('Skipping route: [GET] api/skip', $output); - $this->assertStringContainsString('Skipping route: [GET] api/skipClass', $output); - $this->assertStringContainsString('Processed route: [GET] api/test', $output); - } - - /** @test */ - public function warns_of_nonexistent_response_files() - { - RouteFacade::get('/api/non-existent', [TestController::class, 'withNonExistentResponseFile']); - - config(['scribe.routes.0.match.prefixes' => ['api/*']]); - $output = $this->artisan('scribe:generate'); - - $this->assertStringContainsString('@responseFile i-do-not-exist.json does not exist', $output); - } - - /** @test */ - public function can_parse_resource_routes() - { - RouteFacade::resource('/api/users', TestResourceController::class) - ->only(['index', 'store']); - - config(['scribe.routes.0.match.prefixes' => ['api/*']]); - config([ - 'scribe.routes.0.apply.headers' => [ - 'Accept' => 'application/json', - ], + $app['config']->set('database.default', 'testbench'); + $app['config']->set('database.connections.testbench', [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '', ]); - - $output = $this->artisan('scribe:generate'); - - $this->assertStringContainsString('Processed route: [GET] api/users', $output); - $this->assertStringContainsString('Processed route: [POST] api/users', $output); - - $this->assertStringNotContainsString('Processed route: [PUT,PATCH] api/users/{user}', $output); - $this->assertStringNotContainsString('Processed route: [DELETE] api/users/{user}', $output); - - RouteFacade::apiResource('/api/users', TestResourceController::class) - ->only(['index', 'store']); - $output = $this->artisan('scribe:generate'); - - $this->assertStringContainsString('Processed route: [GET] api/users', $output); - $this->assertStringContainsString('Processed route: [POST] api/users', $output); - - $this->assertStringNotContainsString('Processed route: [PUT,PATCH] api/users/{user}', $output); - $this->assertStringNotContainsString('Processed route: [DELETE] api/users/{user}', $output); } /** @test */ - public function supports_partial_resource_controller() + public function generates_laravel_type_output() { - RouteFacade::resource('/api/users', TestPartialResourceController::class); - - config(['scribe.routes.0.match.prefixes' => ['api/*']]); + RouteFacade::post('/api/withBodyParametersAsArray', [TestController::class, 'withBodyParametersAsArray']); + RouteFacade::post('/api/withFormDataParams', [TestController::class, 'withFormDataParams']); + RouteFacade::post('/api/withBodyParameters', [TestController::class, 'withBodyParameters']); + RouteFacade::get('/api/withQueryParameters', [TestController::class, 'withQueryParameters']); + RouteFacade::get('/api/withAuthTag', [TestController::class, 'withAuthenticatedTag']); + RouteFacade::get('/api/echoesUrlParameters/{param}/{param2}/{param3?}/{param4?}', [TestController::class, 'echoesUrlParameters']); + config(['scribe.title' => 'GREAT API!']); + config(['scribe.auth.enabled' => true]); + config(['scribe.type' => 'laravel']); + config(['scribe.postman.enabled' => true]); + config(['scribe.openapi.enabled' => true]); - $output = $this->artisan('scribe:generate'); + $this->generate(); - $this->assertStringContainsString('Processed route: [GET] api/users', $output); - $this->assertStringContainsString('Processed route: [PUT,PATCH] api/users/{user}', $output); + $this->assertFileExists($this->postmanOutputPath(true)); + $this->assertFileExists($this->openapiOutputPath(true)); + $this->assertFileExists($this->bladeOutputPath()); + unlink($this->postmanOutputPath(true)); + unlink($this->openapiOutputPath(true)); + unlink($this->bladeOutputPath()); } /** @test */ @@ -224,11 +93,8 @@ public function generated_postman_collection_file_is_correct() RouteFacade::get('/api/withQueryParameters', [TestController::class, 'withQueryParameters']); RouteFacade::get('/api/withAuthTag', [TestController::class, 'withAuthenticatedTag']); RouteFacade::get('/api/echoesUrlParameters/{param}/{param2}/{param3?}/{param4?}', [TestController::class, 'echoesUrlParameters']); - // We want to have the same values for params each time - config(['scribe.faker_seed' => 1234]); config(['scribe.title' => 'GREAT API!']); config(['scribe.auth.enabled' => true]); - config(['scribe.routes.0.match.prefixes' => ['api/*']]); config(['scribe.postman.overrides' => [ 'info.version' => '3.9.9', ]]); @@ -239,12 +105,12 @@ public function generated_postman_collection_file_is_correct() ]); config(['scribe.postman.enabled' => true]); - $this->artisan('scribe:generate'); + $this->generate(); - $generatedCollection = json_decode(file_get_contents(__DIR__ . '/../public/docs/collection.json'), true); + $generatedCollection = json_decode(file_get_contents($this->postmanOutputPath()), true); // The Postman ID varies from call to call; erase it to make the test data reproducible. $generatedCollection['info']['_postman_id'] = ''; - $fixtureCollection = json_decode(file_get_contents(__DIR__ . '/Fixtures/collection.json'), true); + $fixtureCollection = json_decode(file_get_contents(__DIR__ . '/../Fixtures/collection.json'), true); $this->assertEquals($fixtureCollection, $generatedCollection); } @@ -259,23 +125,20 @@ public function generated_openapi_spec_file_is_correct() RouteFacade::get('/api/withAuthTag', [TestController::class, 'withAuthenticatedTag']); RouteFacade::get('/api/echoesUrlParameters/{param}/{param2}/{param3?}/{param4?}', [TestController::class, 'echoesUrlParameters']); - // We want to have the same values for params each time - config(['scribe.faker_seed' => 1234]); config(['scribe.openapi.enabled' => true]); config(['scribe.openapi.overrides' => [ 'info.version' => '3.9.9', ]]); - config(['scribe.routes.0.match.prefixes' => ['api/*']]); config([ 'scribe.routes.0.apply.headers' => [ 'Custom-Header' => 'NotSoCustom', ], ]); - $this->artisan('scribe:generate'); + $this->generate(); - $generatedSpec = Yaml::parseFile(__DIR__ . '/../public/docs/openapi.yaml'); - $fixtureSpec = Yaml::parseFile(__DIR__ . '/Fixtures/openapi.yaml'); + $generatedSpec = Yaml::parseFile($this->openapiOutputPath()); + $fixtureSpec = Yaml::parseFile(__DIR__ . '/../Fixtures/openapi.yaml'); $this->assertEquals($fixtureSpec, $generatedSpec); } @@ -283,17 +146,15 @@ public function generated_openapi_spec_file_is_correct() public function can_append_custom_http_headers() { RouteFacade::get('/api/headers', [TestController::class, 'checkCustomHeaders']); - - config(['scribe.routes.0.match.prefixes' => ['api/*']]); config([ 'scribe.routes.0.apply.headers' => [ 'Authorization' => 'customAuthToken', 'Custom-Header' => 'NotSoCustom', ], ]); - $this->artisan('scribe:generate'); + $this->generate(); - $endpointDetails = Yaml::parseFile(__DIR__ . '/../.scribe/endpoints/00.yaml')['endpoints'][0]; + $endpointDetails = Yaml::parseFile('.scribe/endpoints/00.yaml')['endpoints'][0]; $this->assertEquals("customAuthToken", $endpointDetails['headers']["Authorization"]); $this->assertEquals("NotSoCustom", $endpointDetails['headers']["Custom-Header"]); } @@ -303,10 +164,9 @@ public function can_parse_utf8_response() { RouteFacade::get('/api/utf8', [TestController::class, 'withUtf8ResponseTag']); - config(['scribe.routes.0.match.prefixes' => ['api/*']]); - $this->artisan('scribe:generate'); + $this->generate(); - $generatedHtml = file_get_contents('public/docs/index.html'); + $generatedHtml = file_get_contents($this->htmlOutputPath()); $this->assertStringContainsString('Лорем ипсум долор сит амет', $generatedHtml); } @@ -318,29 +178,11 @@ public function sorts_group_naturally() RouteFacade::get('/api/action2', TestGroupController::class . '@action2'); RouteFacade::get('/api/action10', TestGroupController::class . '@action10'); - config(['scribe.routes.0.match.prefixes' => ['api/*']]); - $this->artisan('scribe:generate'); - - $this->assertFileExists(__DIR__ . '/../.scribe/endpoints/00.yaml'); - $this->assertFileExists(__DIR__ . '/../.scribe/endpoints/01.yaml'); - $this->assertFileExists(__DIR__ . '/../.scribe/endpoints/02.yaml'); - $this->assertEquals('1. Group 1', Yaml::parseFile(__DIR__ . '/../.scribe/endpoints/00.yaml')['name']); - $this->assertEquals('2. Group 2', Yaml::parseFile(__DIR__ . '/../.scribe/endpoints/01.yaml')['name']); - $this->assertEquals('10. Group 10', Yaml::parseFile(__DIR__ . '/../.scribe/endpoints/02.yaml')['name']); - } + $this->generate(); - /** @test */ - public function can_customise_static_output_path() - { - RouteFacade::get('/api/action1', TestGroupController::class . '@action1'); - - config(['scribe.routes.0.match.prefixes' => ['*']]); - config(['scribe.static.output_path' => 'static/docs']); - $this->artisan('scribe:generate'); - - $this->assertFileExists('static/docs/index.html'); - - Utils::deleteDirectoryAndContents('static/'); + $this->assertEquals('1. Group 1', Yaml::parseFile('.scribe/endpoints/00.yaml')['name']); + $this->assertEquals('2. Group 2', Yaml::parseFile('.scribe/endpoints/01.yaml')['name']); + $this->assertEquals('10. Group 10', Yaml::parseFile('.scribe/endpoints/02.yaml')['name']); } /** @test */ @@ -348,10 +190,9 @@ public function will_not_overwrite_manually_modified_content_unless_force_flag_i { RouteFacade::get('/api/action1', [TestGroupController::class, 'action1']); RouteFacade::get('/api/action1b', [TestGroupController::class, 'action1b']); - config(['scribe.routes.0.match.prefixes' => ['api/*']]); config(['scribe.routes.0.apply.response_calls.methods' => []]); - $this->artisan('scribe:generate'); + $this->generate(); $authFilePath = '.scribe/auth.md'; $group1FilePath = '.scribe/endpoints/00.yaml'; @@ -374,14 +215,14 @@ public function will_not_overwrite_manually_modified_content_unless_force_flag_i )); file_put_contents($authFilePath, 'Some other useful stuff.', FILE_APPEND); - $this->artisan('scribe:generate'); + $this->generate(); $group = Yaml::parseFile($group1FilePath); $this->assertEquals('api/action1', $group['endpoints'][0]['uri']); $this->assertEquals(['a_param' => $extraParam], $group['endpoints'][0]['urlParameters']); $this->assertStringContainsString('Some other useful stuff.', file_get_contents($authFilePath)); - $this->artisan('scribe:generate', ['--force' => true]); + $this->generate(['--force' => true]); $group = Yaml::parseFile($group1FilePath); $this->assertEquals('api/action1', $group['endpoints'][0]['uri']); @@ -392,12 +233,6 @@ public function will_not_overwrite_manually_modified_content_unless_force_flag_i /** @test */ public function generates_correct_url_params_from_resource_routes_and_field_bindings() { - if (version_compare($this->app->version(), '7.0.0', '<')) { - $this->markTestSkipped("Laravel < 7.x doesn't support field binding syntax."); - - return; - } - RouteFacade::prefix('providers/{provider:slug}')->group(function () { RouteFacade::resource('users.addresses', TestPartialResourceController::class)->parameters([ 'addresses' => 'address:uuid', @@ -406,7 +241,7 @@ public function generates_correct_url_params_from_resource_routes_and_field_bind config(['scribe.routes.0.match.prefixes' => ['*']]); config(['scribe.routes.0.apply.response_calls.methods' => []]); - $this->artisan('scribe:generate'); + $this->generate(); $groupA = Yaml::parseFile('.scribe/endpoints/00.yaml'); $this->assertEquals('providers/{provider_slug}/users/{user_id}/addresses', $groupA['endpoints'][0]['uri']); @@ -415,16 +250,16 @@ public function generates_correct_url_params_from_resource_routes_and_field_bind } /** @test */ - public function will_generate_without_extracting_if_noExtraction_flag_is_set() + public function generates_from_camel_dir_if_noExtraction_flag_is_set() { config(['scribe.routes.0.exclude' => ['*']]); - Utils::copyDirectory(__DIR__.'/Fixtures/.scribe', '.scribe'); + Utils::copyDirectory(__DIR__.'/../Fixtures/.scribe', '.scribe'); - $output = $this->artisan('scribe:generate', ['--no-extraction' => true]); + $output = $this->generate(['--no-extraction' => true]); $this->assertStringNotContainsString("Processing route", $output); - $crawler = new Crawler(file_get_contents('public/docs/index.html')); + $crawler = new Crawler(file_get_contents($this->htmlOutputPath())); [$intro, $auth] = $crawler->filter('h1 + p')->getIterator(); $this->assertEquals('Heyaa introduction!👋', trim($intro->firstChild->textContent)); $this->assertEquals('This is just a test.', trim($auth->firstChild->textContent)); @@ -440,15 +275,14 @@ public function merges_and_correctly_sorts_user_defined_endpoints() { RouteFacade::get('/api/action1', [TestGroupController::class, 'action1']); RouteFacade::get('/api/action2', [TestGroupController::class, 'action2']); - config(['scribe.routes.0.match.prefixes' => ['api/*']]); config(['scribe.routes.0.apply.response_calls.methods' => []]); if (!is_dir('.scribe/endpoints')) mkdir('.scribe/endpoints', 0777, true); - copy(__DIR__ . '/Fixtures/custom.0.yaml', '.scribe/endpoints/custom.0.yaml'); + copy(__DIR__ . '/../Fixtures/custom.0.yaml', '.scribe/endpoints/custom.0.yaml'); - $this->artisan('scribe:generate'); + $this->generate(); - $crawler = new Crawler(file_get_contents('public/docs/index.html')); + $crawler = new Crawler(file_get_contents($this->htmlOutputPath())); $headings = $crawler->filter('h1')->getIterator(); // There should only be six headings — intro, auth and four groups $this->assertCount(6, $headings); @@ -475,13 +309,12 @@ public function respects_endpoints_and_group_sort_order() RouteFacade::get('/api/action1', [TestGroupController::class, 'action1']); RouteFacade::get('/api/action1b', [TestGroupController::class, 'action1b']); RouteFacade::get('/api/action2', [TestGroupController::class, 'action2']); - config(['scribe.routes.0.match.prefixes' => ['api/*']]); config(['scribe.routes.0.apply.response_calls.methods' => []]); - $this->artisan('scribe:generate'); + $this->generate(); // First: verify the current order of the groups and endpoints - $crawler = new Crawler(file_get_contents('public/docs/index.html')); + $crawler = new Crawler(file_get_contents($this->htmlOutputPath())); $h1s = $crawler->filter('h1'); $this->assertEquals('1. Group 1', trim($h1s->getNode(2)->textContent)); $this->assertEquals('2. Group 2', trim($h1s->getNode(3)->textContent)); @@ -506,9 +339,9 @@ public function respects_endpoints_and_group_sort_order() rename('.scribe/endpoints/01.yaml', '.scribe/endpoints/00.yaml'); rename('.scribe/endpoints/temp.yaml', '.scribe/endpoints/1.yaml'); - $this->artisan('scribe:generate'); + $this->generate(); - $crawler = new Crawler(file_get_contents('public/docs/index.html')); + $crawler = new Crawler(file_get_contents($this->htmlOutputPath())); $h1s = $crawler->filter('h1'); $this->assertEquals('2. Group 2', trim($h1s->getNode(2)->textContent)); $this->assertEquals('1. Group 1', trim($h1s->getNode(3)->textContent)); @@ -538,7 +371,7 @@ public function will_auto_set_content_type_to_multipart_if_file_params_are_prese config(['scribe.routes.0.match.prefixes' => ['*']]); config(['scribe.routes.0.apply.response_calls.methods' => []]); - $this->artisan('scribe:generate'); + $this->generate(); $group = Yaml::parseFile('.scribe/endpoints/00.yaml'); $this->assertEquals('no-file', $group['endpoints'][0]['uri']); @@ -549,4 +382,26 @@ public function will_auto_set_content_type_to_multipart_if_file_params_are_prese $this->assertEquals('multipart/form-data', $group['endpoints'][2]['headers']['Content-Type']); } + + protected function postmanOutputPath(bool $laravelType = false): string + { + return $laravelType + ? Storage::disk('local')->path('scribe/collection.json') : 'public/docs/collection.json'; + } + + protected function openapiOutputPath(bool $laravelType = false): string + { + return $laravelType + ? Storage::disk('local')->path('scribe/openapi.yaml') : 'public/docs/openapi.yaml'; + } + + protected function htmlOutputPath(): string + { + return 'public/docs/index.html'; + } + + protected function bladeOutputPath(): string + { + return 'resources/views/scribe/index.blade.php'; + } } diff --git a/tests/Strategies/Responses/UseResponseFileTagTest.php b/tests/Strategies/Responses/UseResponseFileTagTest.php index db4c285a..b53c0bf5 100644 --- a/tests/Strategies/Responses/UseResponseFileTagTest.php +++ b/tests/Strategies/Responses/UseResponseFileTagTest.php @@ -2,7 +2,9 @@ namespace Knuckles\Scribe\Tests\Strategies\Responses; +use Illuminate\Support\Facades\Route as RouteFacade; use Knuckles\Scribe\Extracting\Strategies\Responses\UseResponseFileTag; +use Knuckles\Scribe\Tests\Fixtures\TestController; use Knuckles\Scribe\Tools\DocumentationConfig; use Mpociot\Reflection\DocBlock\Tag; use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts; @@ -18,8 +20,8 @@ class UseResponseFileTagTest extends TestCase */ public function allows_multiple_responsefile_tags_for_multiple_statuses_and_scenarios(array $tags, array $expected) { - $filePath = __DIR__ . '/../../Fixtures/response_test.json'; - $filePath2 = __DIR__ . '/../../Fixtures/response_error_test.json'; + $filePath = __DIR__ . '/../../Fixtures/response_test.json'; + $filePath2 = __DIR__ . '/../../Fixtures/response_error_test.json'; $strategy = new UseResponseFileTag(new DocumentationConfig([])); $results = $strategy->getFileResponses($tags); @@ -36,24 +38,6 @@ public function allows_multiple_responsefile_tags_for_multiple_statuses_and_scen 'content' => file_get_contents($filePath2), ], ], $results); - - } - - /** @test */ - public function can_add_or_replace_key_value_pair_in_response_file() - { - $strategy = new UseResponseFileTag(new DocumentationConfig([])); - $tags = [ - new Tag('responseFile', 'tests/Fixtures/response_test.json {"message" : "Serendipity", "gender": "male"}'), - ]; - $results = $strategy->getFileResponses($tags); - - $this->assertArraySubset([ - [ - 'status' => 200, - 'content' => '{"id":5,"name":"Jessica Jones","gender":"male","message":"Serendipity"}', - ], - ], $results); } public function responseFileTags() @@ -94,4 +78,45 @@ public function responseFileTags() ], ]; } + + /** @test */ + public function can_add_or_replace_key_value_pair_in_response_file() + { + $strategy = new UseResponseFileTag(new DocumentationConfig([])); + $tags = [ + new Tag('responseFile', 'tests/Fixtures/response_test.json {"message" : "Serendipity", "gender": "male"}'), + ]; + $results = $strategy->getFileResponses($tags); + + $this->assertArraySubset([ + [ + 'status' => 200, + 'content' => '{"id":5,"name":"Jessica Jones","gender":"male","message":"Serendipity"}', + ], + ], $results); + } + + /** @test */ + public function supports_relative_or_absolute_paths() + { + $filePath = __DIR__ . '/../../Fixtures/response_test.json'; + $strategy = new UseResponseFileTag(new DocumentationConfig([])); + + $tags = [new Tag('responseFile', 'tests/Fixtures/response_test.json')]; + $this->assertArraySubset([ + [ + 'status' => 200, + 'content' => file_get_contents($filePath), + ], + ], $strategy->getFileResponses($tags)); + + + $tags = [new Tag('responseFile', realpath($filePath))]; + $this->assertArraySubset([ + [ + 'status' => 200, + 'content' => file_get_contents($filePath), + ], + ], $strategy->getFileResponses($tags)); + } } diff --git a/tests/Strategies/UrlParameters/GetFromLaravelAPITest.php b/tests/Strategies/UrlParameters/GetFromLaravelAPITest.php index 88062afd..ce25777f 100644 --- a/tests/Strategies/UrlParameters/GetFromLaravelAPITest.php +++ b/tests/Strategies/UrlParameters/GetFromLaravelAPITest.php @@ -103,12 +103,6 @@ public function __construct(array $parameters = []) /** @test */ public function can_infer_data_from_field_bindings() { - if (version_compare($this->app->version(), '7.0.0', '<')) { - $this->markTestSkipped("Laravel < 7.x doesn't support field binding syntax."); - - return; - } - $strategy = new GetFromLaravelAPI(new DocumentationConfig([])); $endpoint = new class extends ExtractedEndpointData { diff --git a/tests/TestHelpers.php b/tests/TestHelpers.php index 31d01c45..58910701 100644 --- a/tests/TestHelpers.php +++ b/tests/TestHelpers.php @@ -20,4 +20,9 @@ public function artisan($command, $parameters = []) return $kernel->output(); } + + protected function generate(array $flags = []): mixed + { + return $this->artisan('scribe:generate', $flags); + } } diff --git a/tests/Unit/ValidationRuleParsingTest.php b/tests/Unit/ValidationRuleParsingTest.php index f7115b4d..b9572221 100644 --- a/tests/Unit/ValidationRuleParsingTest.php +++ b/tests/Unit/ValidationRuleParsingTest.php @@ -320,13 +320,11 @@ public function supportedRules() [], ['description' => "The value and other_field must match."], ]; - if (version_compare(Application::VERSION, '7.0.0', '>=')) { - yield 'different' => [ - ['different_param' => 'string|different:other_field'], - [], - ['description' => "The value and other_field must be different."], - ]; - } + yield 'different' => [ + ['different_param' => 'string|different:other_field'], + [], + ['description' => "The value and other_field must be different."], + ]; yield 'after' => [ ['after_param' => 'after:2020-02-12'], [], @@ -373,7 +371,7 @@ public function supportedRules() [ 'type' => 'boolean', 'description' => 'Must be accepted.', - ] + ], ]; if (version_compare(Application::VERSION, '8.53', '>=')) { yield 'accepted_if' => [ @@ -382,7 +380,7 @@ public function supportedRules() [ 'type' => 'boolean', 'description' => "Must be accepted when another_field is a_value.", - ] + ], ]; } }