GenerateDocumentationTest.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. <?php
  2. namespace Knuckles\Scribe\Tests;
  3. use Illuminate\Support\Facades\App;
  4. use Illuminate\Support\Facades\Config;
  5. use Illuminate\Support\Facades\Route as RouteFacade;
  6. use Illuminate\Support\Str;
  7. use Knuckles\Scribe\ScribeServiceProvider;
  8. use Knuckles\Scribe\Tests\Fixtures\TestController;
  9. use Knuckles\Scribe\Tests\Fixtures\TestGroupController;
  10. use Knuckles\Scribe\Tests\Fixtures\TestPartialResourceController;
  11. use Knuckles\Scribe\Tests\Fixtures\TestResourceController;
  12. use Knuckles\Scribe\Tests\Fixtures\TestUser;
  13. use Knuckles\Scribe\Tools\Utils;
  14. use Orchestra\Testbench\TestCase;
  15. use ReflectionException;
  16. class GenerateDocumentationTest extends TestCase
  17. {
  18. use TestHelpers;
  19. protected function setUp(): void
  20. {
  21. parent::setUp();
  22. $factory = app(\Illuminate\Database\Eloquent\Factory::class);
  23. $factory->define(TestUser::class, function () {
  24. return [
  25. 'id' => 4,
  26. 'first_name' => 'Tested',
  27. 'last_name' => 'Again',
  28. 'email' => 'a@b.com',
  29. ];
  30. });
  31. }
  32. public function tearDown(): void
  33. {
  34. Utils::deleteDirectoryAndContents('/public/docs');
  35. Utils::deleteDirectoryAndContents('/resources/docs');
  36. }
  37. /**
  38. * @param \Illuminate\Foundation\Application $app
  39. *
  40. * @return array
  41. */
  42. protected function getPackageProviders($app)
  43. {
  44. return [
  45. ScribeServiceProvider::class,
  46. ];
  47. }
  48. /** @test */
  49. public function can_process_traditional_laravel_route_syntax()
  50. {
  51. RouteFacade::get('/api/test', TestController::class . '@withEndpointDescription');
  52. config(['scribe.routes.0.match.prefixes' => ['api/*']]);
  53. $output = $this->artisan('scribe:generate');
  54. $this->assertStringContainsString('Processed route: [GET] api/test', $output);
  55. }
  56. /** @test */
  57. public function can_process_closure_routes()
  58. {
  59. RouteFacade::get('/api/closure', function () {
  60. return 'hi';
  61. });
  62. config(['scribe.routes.0.match.prefixes' => ['api/*']]);
  63. $output = $this->artisan('scribe:generate');
  64. $this->assertStringContainsString('Processed route: [GET] api/closure', $output);
  65. }
  66. /**
  67. * @group dingo
  68. * @test
  69. */
  70. public function can_process_routes_on_dingo()
  71. {
  72. $api = app(\Dingo\Api\Routing\Router::class);
  73. $api->version('v1', function ($api) {
  74. $api->get('/closure', function () {
  75. return 'foo';
  76. });
  77. $api->get('/test', TestController::class . '@withEndpointDescription');
  78. });
  79. config(['scribe.router' => 'dingo']);
  80. config(['scribe.routes.0.match.prefixes' => ['*']]);
  81. config(['scribe.routes.0.match.versions' => ['v1']]);
  82. $output = $this->artisan('scribe:generate');
  83. $this->assertStringContainsString('Processed route: [GET] closure', $output);
  84. $this->assertStringContainsString('Processed route: [GET] test', $output);
  85. }
  86. /** @test */
  87. public function can_process_callable_tuple_syntax()
  88. {
  89. RouteFacade::get('/api/array/test', [TestController::class, 'withEndpointDescription']);
  90. config(['scribe.routes.0.match.prefixes' => ['api/*']]);
  91. $output = $this->artisan('scribe:generate');
  92. $this->assertStringContainsString('Processed route: [GET] api/array/test', $output);
  93. }
  94. /** @test */
  95. public function can_skip_single_routes()
  96. {
  97. RouteFacade::get('/api/skip', TestController::class . '@skip');
  98. RouteFacade::get('/api/test', TestController::class . '@withEndpointDescription');
  99. config(['scribe.routes.0.match.prefixes' => ['api/*']]);
  100. $output = $this->artisan('scribe:generate');
  101. $this->assertStringContainsString('Skipping route: [GET] api/skip', $output);
  102. $this->assertStringContainsString('Processed route: [GET] api/test', $output);
  103. }
  104. /** @test */
  105. public function can_skip_non_existent_response_files()
  106. {
  107. RouteFacade::get('/api/non-existent', TestController::class . '@withNonExistentResponseFile');
  108. config(['scribe.routes.0.match.prefixes' => ['api/*']]);
  109. $output = $this->artisan('scribe:generate');
  110. $this->assertStringContainsString('Skipping route: [GET] api/non-existent', $output);
  111. $this->assertStringContainsString('@responseFile i-do-not-exist.json does not exist', $output);
  112. }
  113. /** @test */
  114. public function can_parse_resource_routes()
  115. {
  116. RouteFacade::resource('/api/users', TestResourceController::class);
  117. config(['scribe.routes.0.match.prefixes' => ['api/*']]);
  118. config([
  119. 'scribe.routes.0.apply.headers' => [
  120. 'Accept' => 'application/json',
  121. ],
  122. ]);
  123. $output = $this->artisan('scribe:generate');
  124. $this->assertStringContainsString('Processed route: [GET] api/users', $output);
  125. $this->assertStringContainsString('Processed route: [GET] api/users/create', $output);
  126. $this->assertStringContainsString('Processed route: [GET] api/users/{user}', $output);
  127. $this->assertStringContainsString('Processed route: [GET] api/users/{user}/edit', $output);
  128. $this->assertStringContainsString('Processed route: [POST] api/users', $output);
  129. $this->assertStringContainsString('Processed route: [PUT,PATCH] api/users/{user}', $output);
  130. $this->assertStringContainsString('Processed route: [DELETE] api/users/{user}', $output);
  131. }
  132. /** @test */
  133. public function can_parse_partial_resource_routes()
  134. {
  135. RouteFacade::resource('/api/users', TestResourceController::class)
  136. ->only(['index', 'store']);
  137. config(['scribe.routes.0.match.prefixes' => ['api/*']]);
  138. config([
  139. 'scribe.routes.0.apply.headers' => [
  140. 'Accept' => 'application/json',
  141. ],
  142. ]);
  143. $output = $this->artisan('scribe:generate');
  144. $this->assertStringContainsString('Processed route: [GET] api/users', $output);
  145. $this->assertStringContainsString('Processed route: [POST] api/users', $output);
  146. $this->assertStringNotContainsString('Processed route: [PUT,PATCH] api/users/{user}', $output);
  147. $this->assertStringNotContainsString('Processed route: [DELETE] api/users/{user}', $output);
  148. RouteFacade::apiResource('/api/users', TestResourceController::class)
  149. ->only(['index', 'store']);
  150. $output = $this->artisan('scribe:generate');
  151. $this->assertStringContainsString('Processed route: [GET] api/users', $output);
  152. $this->assertStringContainsString('Processed route: [POST] api/users', $output);
  153. $this->assertStringNotContainsString('Processed route: [PUT,PATCH] api/users/{user}', $output);
  154. $this->assertStringNotContainsString('Processed route: [DELETE] api/users/{user}', $output);
  155. }
  156. /** @test */
  157. public function supports_partial_resource_controller()
  158. {
  159. RouteFacade::resource('/api/users', TestPartialResourceController::class);
  160. config(['scribe.routes.0.prefixes' => ['api/*']]);
  161. $output = $this->artisan('scribe:generate');
  162. $this->assertStringContainsString('Processed route: [GET] api/users', $output);
  163. $this->assertStringContainsString('Processed route: [PUT,PATCH] api/users/{user}', $output);
  164. }
  165. /** @test */
  166. public function generated_postman_collection_file_is_correct()
  167. {
  168. RouteFacade::get('/api/withDescription', [TestController::class, 'withEndpointDescription']);
  169. RouteFacade::get('/api/withResponseTag', TestController::class . '@withResponseTag');
  170. RouteFacade::post('/api/withBodyParameters', TestController::class . '@withBodyParameters');
  171. RouteFacade::get('/api/withQueryParameters', TestController::class . '@withQueryParameters');
  172. RouteFacade::get('/api/withAuthTag', TestController::class . '@withAuthenticatedTag');
  173. RouteFacade::get('/api/withEloquentApiResource', [TestController::class, 'withEloquentApiResource']);
  174. RouteFacade::get('/api/withEloquentApiResourceCollectionClass', [TestController::class, 'withEloquentApiResourceCollectionClass']);
  175. RouteFacade::post('/api/withMultipleResponseTagsAndStatusCode', [TestController::class, 'withMultipleResponseTagsAndStatusCode']);
  176. RouteFacade::get('/api/echoesUrlParameters/{param}-{param2}/{param3?}', [TestController::class, 'echoesUrlParameters']);
  177. // We want to have the same values for params each time
  178. config(['scribe.faker_seed' => 1234]);
  179. config(['scribe.routes.0.match.prefixes' => ['api/*']]);
  180. config([
  181. 'scribe.routes.0.apply.headers' => [
  182. 'Authorization' => 'customAuthToken',
  183. 'Custom-Header' => 'NotSoCustom',
  184. 'Accept' => 'application/json',
  185. 'Content-Type' => 'application/json',
  186. ],
  187. ]);
  188. $this->artisan('scribe:generate');
  189. $generatedCollection = json_decode(file_get_contents(__DIR__ . '/../public/docs/collection.json'), true);
  190. // The Postman ID varies from call to call; erase it to make the test data reproducible.
  191. $generatedCollection['info']['_postman_id'] = '';
  192. $fixtureCollection = json_decode(file_get_contents(__DIR__ . '/Fixtures/collection.json'), true);
  193. $this->assertEquals($fixtureCollection, $generatedCollection);
  194. }
  195. /** @test */
  196. public function generated_postman_collection_domain_is_correct()
  197. {
  198. $domain = 'http://somedomain.test';
  199. RouteFacade::get('/api/test', TestController::class . '@withEndpointDescription');
  200. config(['scribe.base_url' => $domain]);
  201. config(['scribe.routes.0.match.prefixes' => ['api/*']]);
  202. $this->artisan('scribe:generate');
  203. $generatedCollection = json_decode(file_get_contents(__DIR__ . '/../public/docs/collection.json'));
  204. $endpointUrl = $generatedCollection->item[0]->item[0]->request->url->host;
  205. $this->assertTrue(Str::startsWith($endpointUrl, 'somedomain.test'));
  206. }
  207. /** @test */
  208. public function generated_postman_collection_can_have_custom_url()
  209. {
  210. Config::set('scribe.base_url', 'http://yourapp.app');
  211. RouteFacade::get('/api/test', TestController::class . '@withEndpointDescription');
  212. RouteFacade::post('/api/responseTag', TestController::class . '@withResponseTag');
  213. config(['scribe.routes.0.match.prefixes' => ['api/*']]);
  214. $this->artisan('scribe:generate');
  215. $generatedCollection = json_decode(file_get_contents(__DIR__ . '/../public/docs/collection.json'), true);
  216. // The Postman ID varies from call to call; erase it to make the test data reproducible.
  217. $generatedCollection['info']['_postman_id'] = '';
  218. $fixtureCollection = json_decode(file_get_contents(__DIR__ . '/Fixtures/collection_custom_url.json'), true);
  219. $this->assertEquals($fixtureCollection, $generatedCollection);
  220. }
  221. /** @test */
  222. public function generated_postman_collection_can_have_secure_url()
  223. {
  224. Config::set('scribe.base_url', 'https://yourapp.app');
  225. RouteFacade::get('/api/test', TestController::class . '@withEndpointDescription');
  226. RouteFacade::post('/api/responseTag', TestController::class . '@withResponseTag');
  227. config(['scribe.routes.0.match.prefixes' => ['api/*']]);
  228. $this->artisan('scribe:generate');
  229. $generatedCollection = json_decode(file_get_contents(__DIR__ . '/../public/docs/collection.json'), true);
  230. // The Postman ID varies from call to call; erase it to make the test data reproducible.
  231. $generatedCollection['info']['_postman_id'] = '';
  232. $fixtureCollection = json_decode(file_get_contents(__DIR__ . '/Fixtures/collection_with_secure_url.json'), true);
  233. $this->assertEquals($fixtureCollection, $generatedCollection);
  234. }
  235. /** @test */
  236. public function generated_postman_collection_can_append_custom_http_headers()
  237. {
  238. RouteFacade::get('/api/headers', TestController::class . '@checkCustomHeaders');
  239. config(['scribe.routes.0.match.prefixes' => ['api/*']]);
  240. config([
  241. 'scribe.routes.0.apply.headers' => [
  242. 'Authorization' => 'customAuthToken',
  243. 'Custom-Header' => 'NotSoCustom',
  244. ],
  245. ]);
  246. $this->artisan('scribe:generate');
  247. $generatedCollection = json_decode(file_get_contents(__DIR__ . '/../public/docs/collection.json'), true);
  248. // The Postman ID varies from call to call; erase it to make the test data reproducible.
  249. $generatedCollection['info']['_postman_id'] = '';
  250. $fixtureCollection = json_decode(file_get_contents(__DIR__ . '/Fixtures/collection_with_custom_headers.json'), true);
  251. $this->assertEquals($fixtureCollection, $generatedCollection);
  252. }
  253. /** @test */
  254. public function generated_postman_collection_can_have_query_parameters()
  255. {
  256. RouteFacade::get('/api/withQueryParameters', TestController::class . '@withQueryParameters');
  257. // We want to have the same values for params each time
  258. config(['scribe.faker_seed' => 1234]);
  259. config(['scribe.routes.0.match.prefixes' => ['api/*']]);
  260. $this->artisan('scribe:generate');
  261. $generatedCollection = json_decode(file_get_contents(__DIR__ . '/../public/docs/collection.json'), true);
  262. // The Postman ID varies from call to call; erase it to make the test data reproducible.
  263. $generatedCollection['info']['_postman_id'] = '';
  264. $fixtureCollection = json_decode(file_get_contents(__DIR__ . '/Fixtures/collection_with_query_parameters.json'), true);
  265. $this->assertEquals($fixtureCollection, $generatedCollection);
  266. }
  267. /** @test */
  268. public function generated_postman_collection_can_add_body_parameters()
  269. {
  270. RouteFacade::get('/api/withBodyParameters', TestController::class . '@withBodyParameters');
  271. // We want to have the same values for params each time
  272. config(['scribe.faker_seed' => 1234]);
  273. config(['scribe.routes.0.match.prefixes' => ['api/*']]);
  274. $this->artisan('scribe:generate');
  275. $generatedCollection = json_decode(file_get_contents(__DIR__ . '/../public/docs/collection.json'), true);
  276. // The Postman ID varies from call to call; erase it to make the test data reproducible.
  277. $generatedCollection['info']['_postman_id'] = '';
  278. $fixtureCollection = json_decode(file_get_contents(__DIR__ . '/Fixtures/collection_with_body_parameters.json'), true);
  279. $this->assertEquals($fixtureCollection, $generatedCollection);
  280. }
  281. /** @test */
  282. public function can_append_custom_http_headers()
  283. {
  284. RouteFacade::get('/api/headers', TestController::class . '@checkCustomHeaders');
  285. config(['scribe.routes.0.match.prefixes' => ['api/*']]);
  286. config([
  287. 'scribe.routes.0.apply.headers' => [
  288. 'Authorization' => 'customAuthToken',
  289. 'Custom-Header' => 'NotSoCustom',
  290. ],
  291. ]);
  292. $this->artisan('scribe:generate');
  293. $generatedMarkdown = $this->getFileContents(__DIR__ . '/../resources/docs/source/groups/0-group-a.md');
  294. $this->assertContainsIgnoringWhitespace('"Authorization": "customAuthToken","Custom-Header":"NotSoCustom"', $generatedMarkdown);
  295. }
  296. /** @test */
  297. public function can_parse_utf8_response()
  298. {
  299. RouteFacade::get('/api/utf8', TestController::class . '@withUtf8ResponseTag');
  300. config(['scribe.routes.0.prefixes' => ['api/*']]);
  301. $this->artisan('scribe:generate');
  302. $generatedMarkdown = file_get_contents(__DIR__ . '/../resources/docs/source/groups/0-group-a.md');
  303. $this->assertStringContainsString('Лорем ипсум долор сит амет', $generatedMarkdown);
  304. }
  305. /** @test */
  306. public function sorts_group_naturally()
  307. {
  308. RouteFacade::get('/api/action1', TestGroupController::class . '@action1');
  309. RouteFacade::get('/api/action1b', TestGroupController::class . '@action1b');
  310. RouteFacade::get('/api/action2', TestGroupController::class . '@action2');
  311. RouteFacade::get('/api/action10', TestGroupController::class . '@action10');
  312. config(['scribe.routes.0.prefixes' => ['api/*']]);
  313. $this->artisan('scribe:generate');
  314. $this->assertFileExists(__DIR__ . '/../resources/docs/source/groups/0-1-group-1.md');
  315. $this->assertFileExists(__DIR__ . '/../resources/docs/source/groups/1-2-group-2.md');
  316. $this->assertFileExists(__DIR__ . '/../resources/docs/source/groups/2-10-group-10.md');
  317. }
  318. }