OpenAPISpecWriterTest.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. <?php
  2. namespace Knuckles\Scribe\Tests\Unit;
  3. use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
  4. use Faker\Factory;
  5. use Knuckles\Scribe\Tools\DocumentationConfig;
  6. use Knuckles\Scribe\Writing\OpenAPISpecWriter;
  7. use Orchestra\Testbench\TestCase;
  8. /**
  9. * See https://swagger.io/specification/
  10. */
  11. class OpenAPISpecWriterTest extends TestCase
  12. {
  13. use ArraySubsetAsserts;
  14. protected $config = [
  15. 'title' => 'My Testy Testes API',
  16. 'description' => 'All about testy testes.',
  17. 'base_url' => 'http://api.api.dev',
  18. ];
  19. /** @test */
  20. public function follows_correct_spec_structure()
  21. {
  22. $fakeRoute1 = $this->createMockRouteData();
  23. $fakeRoute2 = $this->createMockRouteData();
  24. $groupedEndpoints = collect([$fakeRoute1, $fakeRoute2])->groupBy('metadata.groupName');
  25. $writer = new OpenAPISpecWriter(new DocumentationConfig($this->config));
  26. $results = $writer->generateSpecContent($groupedEndpoints);
  27. $this->assertEquals(OpenAPISpecWriter::VERSION, $results['openapi']);
  28. $this->assertEquals($this->config['title'], $results['info']['title']);
  29. $this->assertEquals($this->config['description'], $results['info']['description']);
  30. $this->assertNotEmpty($results['info']['version']);
  31. $this->assertEquals($this->config['base_url'], $results['servers'][0]['url']);
  32. $this->assertIsArray($results['paths']);
  33. $this->assertGreaterThan(0, count($results['paths']));
  34. }
  35. /** @test */
  36. public function adds_endpoints_correctly_as_operations_under_paths()
  37. {
  38. $fakeRoute1 = $this->createMockRouteData(['uri' => 'path1', 'methods' => ['GET']]);
  39. $fakeRoute2 = $this->createMockRouteData(['uri' => 'path1', 'methods' => ['POST']]);
  40. $fakeRoute3 = $this->createMockRouteData(['uri' => 'path1/path2']);
  41. $groupedEndpoints = collect([$fakeRoute1, $fakeRoute2, $fakeRoute3])->groupBy('metadata.groupName');
  42. $writer = new OpenAPISpecWriter(new DocumentationConfig($this->config));
  43. $results = $writer->generateSpecContent($groupedEndpoints);
  44. $this->assertIsArray($results['paths']);
  45. $this->assertCount(2, $results['paths']);
  46. $this->assertCount(2, $results['paths']['/path1']);
  47. $this->assertCount(1, $results['paths']['/path1/path2']);
  48. $this->assertArrayHasKey('get', $results['paths']['/path1']);
  49. $this->assertArrayHasKey('post', $results['paths']['/path1']);
  50. $this->assertArrayHasKey(strtolower($fakeRoute3['methods'][0]), $results['paths']['/path1/path2']);
  51. collect([$fakeRoute1, $fakeRoute2, $fakeRoute3])->each(function ($endpoint) use ($results) {
  52. $method = strtolower($endpoint['methods'][0]);
  53. $this->assertEquals([$endpoint['metadata']['groupName']], $results['paths']['/' . $endpoint['uri']][$method]['tags']);
  54. $this->assertEquals($endpoint['metadata']['title'], $results['paths']['/' . $endpoint['uri']][$method]['summary']);
  55. $this->assertEquals($endpoint['metadata']['description'], $results['paths']['/' . $endpoint['uri']][$method]['description']);
  56. });
  57. }
  58. /** @test */
  59. public function adds_authentication_details_correctly_as_security_info()
  60. {
  61. $fakeRoute1 = $this->createMockRouteData(['uri' => 'path1', 'methods' => ['GET'], 'metadata.authenticated' => true]);
  62. $fakeRoute2 = $this->createMockRouteData(['uri' => 'path1', 'methods' => ['POST'], 'metadata.authenticated' => false]);
  63. $groupedEndpoints = collect([$fakeRoute1, $fakeRoute2])->groupBy('metadata.groupName');
  64. $config = array_merge($this->config, ['auth' => ['enabled' => true, 'in' => 'bearer']]);
  65. $writer = new OpenAPISpecWriter(new DocumentationConfig($config));
  66. $results = $writer->generateSpecContent($groupedEndpoints);
  67. $this->assertCount(1, $results['components']['securitySchemes']);
  68. $this->assertArrayHasKey('default', $results['components']['securitySchemes']);
  69. $this->assertEquals('http', $results['components']['securitySchemes']['default']['type']);
  70. $this->assertEquals('bearer', $results['components']['securitySchemes']['default']['scheme']);
  71. $this->assertCount(1, $results['security']);
  72. $this->assertCount(1, $results['security'][0]);
  73. $this->assertArrayHasKey('default', $results['security'][0]);
  74. $this->assertArrayNotHasKey('security', $results['paths']['/path1']['get']);
  75. $this->assertArrayHasKey('security', $results['paths']['/path1']['post']);
  76. $this->assertCount(0, $results['paths']['/path1']['post']['security']);
  77. // Next try: auth with a query parameter
  78. $config = array_merge($this->config, ['auth' => ['enabled' => true, 'in' => 'query', 'name' => 'token']]);
  79. $writer = new OpenAPISpecWriter(new DocumentationConfig($config));
  80. $results = $writer->generateSpecContent($groupedEndpoints);
  81. $this->assertCount(1, $results['components']['securitySchemes']);
  82. $this->assertArrayHasKey('default', $results['components']['securitySchemes']);
  83. $this->assertEquals('apiKey', $results['components']['securitySchemes']['default']['type']);
  84. $this->assertEquals($config['auth']['name'], $results['components']['securitySchemes']['default']['name']);
  85. $this->assertEquals('query', $results['components']['securitySchemes']['default']['in']);
  86. $this->assertCount(1, $results['security']);
  87. $this->assertCount(1, $results['security'][0]);
  88. $this->assertArrayHasKey('default', $results['security'][0]);
  89. $this->assertArrayNotHasKey('security', $results['paths']['/path1']['get']);
  90. $this->assertArrayHasKey('security', $results['paths']['/path1']['post']);
  91. $this->assertCount(0, $results['paths']['/path1']['post']['security']);
  92. }
  93. /** @test */
  94. public function adds_url_parameters_correctly_as_parameters_on_path_item_object()
  95. {
  96. $fakeRoute1 = $this->createMockRouteData([
  97. 'methods' => ['POST'],
  98. 'uri' => 'path1/{param}/{optionalParam?}',
  99. 'urlParameters.param' => [
  100. 'description' => 'Something',
  101. 'required' => true,
  102. 'value' => '56',
  103. ],
  104. 'urlParameters.optionalParam' => [
  105. 'description' => 'Another',
  106. 'required' => false,
  107. 'value' => '69',
  108. ],
  109. ]);
  110. $fakeRoute2 = $this->createMockRouteData(['uri' => 'path1', 'methods' => ['POST']]);
  111. $groupedEndpoints = collect([$fakeRoute1, $fakeRoute2])->groupBy('metadata.groupName');
  112. $writer = new OpenAPISpecWriter(new DocumentationConfig($this->config));
  113. $results = $writer->generateSpecContent($groupedEndpoints);
  114. $this->assertArrayNotHasKey('parameters', $results['paths']['/path1']);
  115. $this->assertCount(2, $results['paths']['/path1/{param}/{optionalParam}']['parameters']);
  116. $this->assertEquals([
  117. 'in' => 'path',
  118. 'required' => true,
  119. 'name' => 'param',
  120. 'description' => 'Something',
  121. 'example' => '56',
  122. 'schema' => ['type' => 'string'],
  123. ], $results['paths']['/path1/{param}/{optionalParam}']['parameters'][0]);
  124. $this->assertEquals([
  125. 'in' => 'path',
  126. 'required' => true,
  127. 'name' => 'optionalParam',
  128. 'description' => 'Optional parameter. Another',
  129. 'examples' => [
  130. 'omitted' => ['summary' => 'When the value is omitted', 'value' => ''],
  131. 'present' => [
  132. 'summary' => 'When the value is present', 'value' => '69'],
  133. ],
  134. 'schema' => ['type' => 'string'],
  135. ], $results['paths']['/path1/{param}/{optionalParam}']['parameters'][1]);
  136. }
  137. /** @test */
  138. public function adds_headers_correctly_as_parameters_on_operation_object()
  139. {
  140. $fakeRoute1 = $this->createMockRouteData(['methods' => ['POST'], 'uri' => 'path1', 'headers.Extra-Header' => 'Some-Value']);
  141. $fakeRoute2 = $this->createMockRouteData(['uri' => 'path1', 'methods' => ['GET'], 'headers' => []]);
  142. $groupedEndpoints = collect([$fakeRoute1, $fakeRoute2])->groupBy('metadata.groupName');
  143. $writer = new OpenAPISpecWriter(new DocumentationConfig($this->config));
  144. $results = $writer->generateSpecContent($groupedEndpoints);
  145. $this->assertEquals([], $results['paths']['/path1']['get']['parameters']);
  146. $this->assertCount(2, $results['paths']['/path1']['post']['parameters']);
  147. $this->assertEquals([
  148. 'in' => 'header',
  149. 'name' => 'Content-Type',
  150. 'description' => '',
  151. 'example' => 'application/json',
  152. 'schema' => ['type' => 'string'],
  153. ], $results['paths']['/path1']['post']['parameters'][0]);
  154. $this->assertEquals([
  155. 'in' => 'header',
  156. 'name' => 'Extra-Header',
  157. 'description' => '',
  158. 'example' => 'Some-Value',
  159. 'schema' => ['type' => 'string'],
  160. ], $results['paths']['/path1']['post']['parameters'][1]);
  161. }
  162. /** @test */
  163. public function adds_query_parameters_correctly_as_parameters_on_operation_object()
  164. {
  165. $fakeRoute1 = $this->createMockRouteData([
  166. 'methods' => ['GET'],
  167. 'uri' => '/path1',
  168. 'headers' => [], // Emptying headers so it doesn't interfere with parameters object
  169. 'queryParameters' => [
  170. 'param' => [
  171. 'description' => 'A query param',
  172. 'required' => false,
  173. 'value' => 'hahoho',
  174. ],
  175. ],
  176. ]);
  177. $fakeRoute2 = $this->createMockRouteData(['queryParameters' => [], 'headers' => [], 'methods' => ['POST'], 'uri' => '/path1',]);
  178. $groupedEndpoints = collect([$fakeRoute1, $fakeRoute2])->groupBy('metadata.groupName');
  179. $writer = new OpenAPISpecWriter(new DocumentationConfig($this->config));
  180. $results = $writer->generateSpecContent($groupedEndpoints);
  181. $this->assertEquals([], $results['paths']['/path1']['post']['parameters']);
  182. $this->assertArrayHasKey('parameters', $results['paths']['/path1']['get']);
  183. $this->assertCount(1, $results['paths']['/path1']['get']['parameters']);
  184. $this->assertEquals([
  185. 'in' => 'query',
  186. 'required' => false,
  187. 'name' => 'param',
  188. 'description' => 'A query param',
  189. 'example' => 'hahoho',
  190. 'schema' => ['type' => 'string'],
  191. ], $results['paths']['/path1']['get']['parameters'][0]);
  192. }
  193. /** @test */
  194. public function adds_body_parameters_correctly_as_requestBody_on_operation_object()
  195. {
  196. $fakeRoute1 = $this->createMockRouteData([
  197. 'methods' => ['POST'],
  198. 'uri' => '/path1',
  199. 'bodyParameters' => [
  200. 'stringParam' => [
  201. 'description' => 'String param',
  202. 'required' => false,
  203. 'value' => 'hahoho',
  204. 'type' => 'string',
  205. ],
  206. 'integerParam' => [
  207. 'description' => 'Integer param',
  208. 'required' => true,
  209. 'value' => 99,
  210. 'type' => 'integer',
  211. ],
  212. 'booleanParam' => [
  213. 'description' => 'Boolean param',
  214. 'required' => true,
  215. 'value' => false,
  216. 'type' => 'boolean',
  217. ],
  218. ],
  219. ]);
  220. $fakeRoute2 = $this->createMockRouteData(['methods' => ['GET'], 'uri' => '/path1']);
  221. $fakeRoute3 = $this->createMockRouteData([
  222. 'methods' => ['PUT'],
  223. 'uri' => '/path2',
  224. 'bodyParameters' => [
  225. 'fileParam' => [
  226. 'description' => 'File param',
  227. 'required' => false,
  228. 'value' => null,
  229. 'type' => 'file',
  230. ],
  231. 'numberParam' => [
  232. 'description' => 'Number param',
  233. 'required' => false,
  234. 'value' => 186.9,
  235. 'type' => 'float',
  236. ],
  237. ],
  238. ]);
  239. $groupedEndpoints = collect([$fakeRoute1, $fakeRoute2, $fakeRoute3])->groupBy('metadata.groupName');
  240. $writer = new OpenAPISpecWriter(new DocumentationConfig($this->config));
  241. $results = $writer->generateSpecContent($groupedEndpoints);
  242. $this->assertArrayNotHasKey('requestBody', $results['paths']['/path1']['get']);
  243. $this->assertArrayHasKey('requestBody', $results['paths']['/path1']['post']);
  244. $this->assertEquals([
  245. 'required' => true,
  246. 'content' => [
  247. 'application/json' => [
  248. 'schema' => [
  249. 'type' => 'object',
  250. 'properties' => [
  251. 'stringParam' => [
  252. 'description' => 'String param',
  253. 'example' => 'hahoho',
  254. 'type' => 'string',
  255. ],
  256. 'booleanParam' => [
  257. 'description' => 'Boolean param',
  258. 'example' => false,
  259. 'type' => 'boolean',
  260. ],
  261. 'integerParam' => [
  262. 'description' => 'Integer param',
  263. 'example' => 99,
  264. 'type' => 'integer',
  265. ],
  266. ],
  267. 'required' => [
  268. 'integerParam',
  269. 'booleanParam',
  270. ],
  271. ],
  272. ],
  273. ],
  274. ], $results['paths']['/path1']['post']['requestBody']);
  275. $this->assertEquals([
  276. 'required' => false,
  277. 'content' => [
  278. 'multipart/form-data' => [
  279. 'schema' => [
  280. 'type' => 'object',
  281. 'properties' => [
  282. 'fileParam' => [
  283. 'description' => 'File param',
  284. 'type' => 'string',
  285. 'format' => 'binary',
  286. ],
  287. 'numberParam' => [
  288. 'description' => 'Number param',
  289. 'example' => 186.9,
  290. 'type' => 'number',
  291. ],
  292. ],
  293. ],
  294. ],
  295. ],
  296. ], $results['paths']['/path2']['put']['requestBody']);
  297. }
  298. /** @test */
  299. public function adds_responses_correctly_as_responses_on_operation_object()
  300. {
  301. $fakeRoute1 = $this->createMockRouteData([
  302. 'methods' => ['POST'],
  303. 'uri' => '/path1',
  304. 'responses' => [
  305. [
  306. 'status' => '204',
  307. 'description' => 'Successfully updated.',
  308. 'content' => '{"this": "should be ignored"}',
  309. ],
  310. [
  311. 'status' => '201',
  312. 'description' => '',
  313. 'content' => '{"this": "shouldn\'t be ignored", "and this": "too"}',
  314. ],
  315. ],
  316. 'responseFields' => [
  317. 'and this' => [
  318. 'type' => 'string',
  319. 'description' => 'Parameter description, ha!',
  320. ],
  321. ],
  322. ]);
  323. $fakeRoute2 = $this->createMockRouteData([
  324. 'methods' => ['PUT'],
  325. 'uri' => '/path2',
  326. 'responses' => [
  327. [
  328. 'status' => '200',
  329. 'description' => '',
  330. 'content' => '<<binary>> The cropped image',
  331. ],
  332. ],
  333. ]);
  334. $groupedEndpoints = collect([$fakeRoute1, $fakeRoute2])->groupBy('metadata.groupName');
  335. $writer = new OpenAPISpecWriter(new DocumentationConfig($this->config));
  336. $results = $writer->generateSpecContent($groupedEndpoints);
  337. $this->assertCount(2, $results['paths']['/path1']['post']['responses']);
  338. $this->assertArraySubset([
  339. '204' => [
  340. 'description' => 'Successfully updated.',
  341. ],
  342. '201' => [
  343. 'content' => [
  344. 'application/json' => [
  345. 'schema' => [
  346. 'type' => 'object',
  347. 'properties' => [
  348. 'this' => [
  349. 'example' => "shouldn't be ignored",
  350. 'type' => 'string',
  351. ],
  352. 'and this' => [
  353. 'description' => 'Parameter description, ha!',
  354. 'example' => "too",
  355. 'type' => 'string',
  356. ],
  357. ],
  358. ],
  359. ],
  360. ],
  361. ],
  362. ], $results['paths']['/path1']['post']['responses']);
  363. $this->assertCount(1, $results['paths']['/path2']['put']['responses']);
  364. $this->assertEquals([
  365. '200' => [
  366. 'description' => 'The cropped image',
  367. 'content' => [
  368. 'application/octet-stream' => [
  369. 'schema' => [
  370. 'type' => 'string',
  371. 'format' => 'binary',
  372. ],
  373. ],
  374. ],
  375. ],
  376. ], $results['paths']['/path2']['put']['responses']);
  377. }
  378. protected function createMockRouteData(array $custom = [])
  379. {
  380. $faker = Factory::create();
  381. $data = [
  382. 'uri' => '/' . $faker->word,
  383. 'methods' => $faker->randomElements(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], 1),
  384. 'headers' => [
  385. 'Content-Type' => 'application/json',
  386. ],
  387. 'metadata' => [
  388. 'groupDescription' => '',
  389. 'groupName' => $faker->randomElement(['Endpoints', 'Group A', 'Group B']),
  390. 'title' => $faker->sentence,
  391. 'description' => $faker->randomElement([$faker->sentence, '']),
  392. 'authenticated' => $faker->boolean,
  393. ],
  394. 'urlParameters' => [], // Should be set by caller (along with custom path)
  395. 'queryParameters' => [],
  396. 'bodyParameters' => [],
  397. 'responses' => [
  398. [
  399. 'status' => 200,
  400. 'content' => '{"random": "json"}',
  401. 'description' => 'Okayy',
  402. ],
  403. ],
  404. 'responseFields' => [],
  405. ];
  406. foreach ($custom as $key => $value) {
  407. data_set($data, $key, $value);
  408. }
  409. return $data;
  410. }
  411. }