OpenAPISpecWriterTest.php 47 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123
  1. <?php
  2. namespace Knuckles\Scribe\Tests\Unit;
  3. use Faker\Factory;
  4. use Illuminate\Support\Arr;
  5. use Knuckles\Camel\Camel;
  6. use Knuckles\Camel\Output\OutputEndpointData;
  7. use Knuckles\Scribe\Tests\BaseUnitTest;
  8. use Knuckles\Scribe\Tests\Fixtures\ComponentsOpenApiGenerator;
  9. use Knuckles\Scribe\Tests\Fixtures\TestOpenApiGenerator;
  10. use Knuckles\Scribe\Tools\DocumentationConfig;
  11. use Knuckles\Scribe\Writing\OpenAPISpecWriter;
  12. /**
  13. * See https://swagger.io/specification/
  14. */
  15. class OpenAPISpecWriterTest extends BaseUnitTest
  16. {
  17. protected $config = [
  18. 'title' => 'My Testy Testes API',
  19. 'description' => 'All about testy testes.',
  20. 'base_url' => 'http://api.api.dev',
  21. ];
  22. /** @test */
  23. public function follows_correct_spec_structure()
  24. {
  25. $endpointData1 = $this->createMockEndpointData();
  26. $endpointData2 = $this->createMockEndpointData();
  27. $groups = [$this->createGroup([$endpointData1, $endpointData2])];
  28. $results = $this->generate($groups);
  29. $this->assertEquals(OpenAPISpecWriter::SPEC_VERSION, $results['openapi']);
  30. $this->assertEquals($this->config['title'], $results['info']['title']);
  31. $this->assertEquals($this->config['description'], $results['info']['description']);
  32. $this->assertNotEmpty($results['info']['version']);
  33. $this->assertEquals($this->config['base_url'], $results['servers'][0]['url']);
  34. $this->assertIsArray($results['paths']);
  35. $this->assertGreaterThan(0, count($results['paths']));
  36. }
  37. /** @test */
  38. public function adds_endpoints_correctly_as_operations_under_paths()
  39. {
  40. $endpointData1 = $this->createMockEndpointData(['uri' => 'path1', 'httpMethods' => ['GET']]);
  41. $endpointData2 = $this->createMockEndpointData(['uri' => 'path1', 'httpMethods' => ['POST']]);
  42. $endpointData3 = $this->createMockEndpointData(['uri' => 'path1/path2']);
  43. $groups = [$this->createGroup([$endpointData1, $endpointData2, $endpointData3])];
  44. $results = $this->generate($groups);
  45. $this->assertIsArray($results['paths']);
  46. $this->assertCount(2, $results['paths']);
  47. $this->assertCount(2, $results['paths']['/path1']);
  48. $this->assertCount(1, $results['paths']['/path1/path2']);
  49. $this->assertArrayHasKey('get', $results['paths']['/path1']);
  50. $this->assertArrayHasKey('post', $results['paths']['/path1']);
  51. $this->assertArrayHasKey(strtolower($endpointData3->httpMethods[0]), $results['paths']['/path1/path2']);
  52. collect([$endpointData1, $endpointData2, $endpointData3])->each(function (OutputEndpointData $endpoint) use ($groups, $results) {
  53. $endpointSpec = $results['paths']['/' . $endpoint->uri][strtolower($endpoint->httpMethods[0])];
  54. $tags = $endpointSpec['tags'];
  55. $containingGroup = Arr::first($groups, function ($group) use ($endpoint) {
  56. return Camel::doesGroupContainEndpoint($group, $endpoint);
  57. });
  58. $this->assertEquals([$containingGroup['name']], $tags);
  59. $this->assertEquals($endpoint->metadata->title, $endpointSpec['summary']);
  60. $this->assertEquals($endpoint->metadata->description, $endpointSpec['description']);
  61. });
  62. }
  63. /** @test */
  64. public function adds_authentication_details_correctly_as_security_info()
  65. {
  66. $endpointData1 = $this->createMockEndpointData(['uri' => 'path1', 'httpMethods' => ['GET'], 'metadata.authenticated' => true]);
  67. $endpointData2 = $this->createMockEndpointData(['uri' => 'path1', 'httpMethods' => ['POST'], 'metadata.authenticated' => false]);
  68. $groups = [$this->createGroup([$endpointData1, $endpointData2])];
  69. $extraInfo = "When stuck trying to authenticate, have a coffee!";
  70. $config = array_merge($this->config, [
  71. 'auth' => [
  72. 'enabled' => true,
  73. 'in' => 'bearer',
  74. 'extra_info' => $extraInfo,
  75. ],
  76. ]);
  77. $writer = new OpenAPISpecWriter(new DocumentationConfig($config));
  78. $results = $writer->generateSpecContent($groups);
  79. $this->assertCount(1, $results['components']['securitySchemes']);
  80. $this->assertArrayHasKey('default', $results['components']['securitySchemes']);
  81. $this->assertEquals('http', $results['components']['securitySchemes']['default']['type']);
  82. $this->assertEquals('bearer', $results['components']['securitySchemes']['default']['scheme']);
  83. $this->assertEquals($extraInfo, $results['components']['securitySchemes']['default']['description']);
  84. $this->assertCount(1, $results['security']);
  85. $this->assertCount(1, $results['security'][0]);
  86. $this->assertArrayHasKey('default', $results['security'][0]);
  87. $this->assertArrayNotHasKey('security', $results['paths']['/path1']['get']);
  88. $this->assertArrayHasKey('security', $results['paths']['/path1']['post']);
  89. $this->assertCount(0, $results['paths']['/path1']['post']['security']);
  90. // Next try: auth with a query parameter
  91. $config = array_merge($this->config, [
  92. 'auth' => [
  93. 'enabled' => true,
  94. 'in' => 'query',
  95. 'name' => 'token',
  96. 'extra_info' => $extraInfo,
  97. ],
  98. ]);
  99. $writer = new OpenAPISpecWriter(new DocumentationConfig($config));
  100. $results = $writer->generateSpecContent($groups);
  101. $this->assertCount(1, $results['components']['securitySchemes']);
  102. $this->assertArrayHasKey('default', $results['components']['securitySchemes']);
  103. $this->assertEquals('apiKey', $results['components']['securitySchemes']['default']['type']);
  104. $this->assertEquals($extraInfo, $results['components']['securitySchemes']['default']['description']);
  105. $this->assertEquals($config['auth']['name'], $results['components']['securitySchemes']['default']['name']);
  106. $this->assertEquals('query', $results['components']['securitySchemes']['default']['in']);
  107. $this->assertCount(1, $results['security']);
  108. $this->assertCount(1, $results['security'][0]);
  109. $this->assertArrayHasKey('default', $results['security'][0]);
  110. $this->assertArrayNotHasKey('security', $results['paths']['/path1']['get']);
  111. $this->assertArrayHasKey('security', $results['paths']['/path1']['post']);
  112. $this->assertCount(0, $results['paths']['/path1']['post']['security']);
  113. }
  114. /** @test */
  115. public function adds_url_parameters_correctly_as_parameters_on_path_item_object()
  116. {
  117. $endpointData1 = $this->createMockEndpointData([
  118. 'httpMethods' => ['POST'],
  119. 'uri' => 'path1/{param}/{optionalParam?}',
  120. 'urlParameters.param' => [
  121. 'description' => 'Something',
  122. 'required' => true,
  123. 'example' => 56,
  124. 'type' => 'integer',
  125. 'name' => 'param',
  126. ],
  127. 'urlParameters.optionalParam' => [
  128. 'description' => 'Another',
  129. 'required' => false,
  130. 'example' => '69',
  131. 'type' => 'string',
  132. 'name' => 'optionalParam',
  133. ],
  134. ]);
  135. $endpointData2 = $this->createMockEndpointData(['uri' => 'path1', 'httpMethods' => ['POST']]);
  136. $groups = [$this->createGroup([$endpointData1, $endpointData2])];
  137. $results = $this->generate($groups);
  138. $this->assertArrayNotHasKey('parameters', $results['paths']['/path1']);
  139. $this->assertCount(2, $results['paths']['/path1/{param}/{optionalParam}']['parameters']);
  140. $this->assertEquals([
  141. 'in' => 'path',
  142. 'required' => true,
  143. 'name' => 'param',
  144. 'description' => 'Something',
  145. 'example' => 56,
  146. 'schema' => ['type' => 'integer'],
  147. ], $results['paths']['/path1/{param}/{optionalParam}']['parameters'][0]);
  148. $this->assertEquals([
  149. 'in' => 'path',
  150. 'required' => true,
  151. 'name' => 'optionalParam',
  152. 'description' => 'Optional parameter. Another',
  153. 'examples' => [
  154. 'omitted' => ['summary' => 'When the value is omitted', 'value' => ''],
  155. 'present' => [
  156. 'summary' => 'When the value is present', 'value' => '69'],
  157. ],
  158. 'schema' => ['type' => 'string'],
  159. ], $results['paths']['/path1/{param}/{optionalParam}']['parameters'][1]);
  160. }
  161. /** @test */
  162. public function adds_headers_correctly_as_parameters_on_operation_object()
  163. {
  164. $endpointData1 = $this->createMockEndpointData(['httpMethods' => ['POST'], 'uri' => 'path1', 'headers.Extra-Header' => 'Some-example']);
  165. $endpointData2 = $this->createMockEndpointData(['uri' => 'path1', 'httpMethods' => ['GET'], 'headers' => []]);
  166. $groups = [$this->createGroup([$endpointData1, $endpointData2])];
  167. $results = $this->generate($groups);
  168. $this->assertEquals([], $results['paths']['/path1']['get']['parameters']);
  169. $this->assertCount(1, $results['paths']['/path1']['post']['parameters']);
  170. $this->assertEquals([
  171. 'in' => 'header',
  172. 'name' => 'Extra-Header',
  173. 'description' => '',
  174. 'example' => 'Some-example',
  175. 'schema' => ['type' => 'string'],
  176. ], $results['paths']['/path1']['post']['parameters'][0]);
  177. }
  178. /** @test */
  179. public function adds_query_parameters_correctly_as_parameters_on_operation_object()
  180. {
  181. $endpointData1 = $this->createMockEndpointData([
  182. 'httpMethods' => ['GET'],
  183. 'uri' => '/path1',
  184. 'headers' => [], // Emptying headers so it doesn't interfere with parameters object
  185. 'queryParameters' => [
  186. 'param' => [
  187. 'description' => 'A query param',
  188. 'required' => false,
  189. 'example' => 'hahoho',
  190. 'type' => 'string',
  191. 'name' => 'param',
  192. 'nullable' => false
  193. ],
  194. ],
  195. ]);
  196. $endpointData2 = $this->createMockEndpointData(['headers' => [], 'httpMethods' => ['POST'], 'uri' => '/path1',]);
  197. $groups = [$this->createGroup([$endpointData1, $endpointData2])];
  198. $results = $this->generate($groups);
  199. $this->assertEquals([], $results['paths']['/path1']['post']['parameters']);
  200. $this->assertArrayHasKey('parameters', $results['paths']['/path1']['get']);
  201. $this->assertCount(1, $results['paths']['/path1']['get']['parameters']);
  202. $this->assertEquals([
  203. 'in' => 'query',
  204. 'required' => false,
  205. 'name' => 'param',
  206. 'description' => 'A query param',
  207. 'example' => 'hahoho',
  208. 'schema' => [
  209. 'type' => 'string',
  210. 'description' => 'A query param',
  211. 'example' => 'hahoho',
  212. 'nullable' => false
  213. ],
  214. ], $results['paths']['/path1']['get']['parameters'][0]);
  215. }
  216. /** @test */
  217. public function adds_body_parameters_correctly_as_requestBody_on_operation_object()
  218. {
  219. $endpointData1 = $this->createMockEndpointData([
  220. 'httpMethods' => ['POST'],
  221. 'uri' => '/path1',
  222. 'bodyParameters' => [
  223. 'stringParam' => [
  224. 'name' => 'stringParam',
  225. 'description' => 'String param',
  226. 'required' => false,
  227. 'example' => 'hahoho',
  228. 'type' => 'string',
  229. 'nullable' => false,
  230. ],
  231. 'integerParam' => [
  232. 'name' => 'integerParam',
  233. 'description' => 'Integer param',
  234. 'required' => true,
  235. 'example' => 99,
  236. 'type' => 'integer',
  237. 'nullable' => false,
  238. ],
  239. 'booleanParam' => [
  240. 'name' => 'booleanParam',
  241. 'description' => 'Boolean param',
  242. 'required' => true,
  243. 'example' => false,
  244. 'type' => 'boolean',
  245. 'nullable' => false,
  246. ],
  247. 'objectParam' => [
  248. 'name' => 'objectParam',
  249. 'description' => 'Object param',
  250. 'required' => false,
  251. 'example' => [],
  252. 'type' => 'object',
  253. 'nullable' => false,
  254. ],
  255. 'objectParam.field' => [
  256. 'name' => 'objectParam.field',
  257. 'description' => 'Object param field',
  258. 'required' => false,
  259. 'example' => 119.0,
  260. 'type' => 'number',
  261. 'nullable' => false,
  262. ],
  263. ],
  264. ]);
  265. $endpointData2 = $this->createMockEndpointData(['httpMethods' => ['GET'], 'uri' => '/path1']);
  266. $endpointData3 = $this->createMockEndpointData([
  267. 'httpMethods' => ['PUT'],
  268. 'uri' => '/path2',
  269. 'bodyParameters' => [
  270. 'fileParam' => [
  271. 'name' => 'fileParam',
  272. 'description' => 'File param',
  273. 'required' => false,
  274. 'example' => null,
  275. 'type' => 'file',
  276. ],
  277. 'numberArrayParam' => [
  278. 'name' => 'numberArrayParam',
  279. 'description' => 'Number array param',
  280. 'required' => false,
  281. 'example' => [186.9],
  282. 'type' => 'number[]',
  283. ],
  284. 'objectArrayParam' => [
  285. 'name' => 'objectArrayParam',
  286. 'description' => 'Object array param',
  287. 'required' => false,
  288. 'example' => [[]],
  289. 'type' => 'object[]',
  290. ],
  291. 'objectArrayParam[].field1' => [
  292. 'name' => 'objectArrayParam[].field1',
  293. 'description' => 'Object array param first field',
  294. 'required' => true,
  295. 'example' => ["hello"],
  296. 'type' => 'string[]',
  297. ],
  298. 'objectArrayParam[].field2' => [
  299. 'name' => 'objectArrayParam[].field2',
  300. 'description' => '',
  301. 'required' => false,
  302. 'example' => "hi",
  303. 'type' => 'string',
  304. ],
  305. ],
  306. ]);
  307. $groups = [$this->createGroup([$endpointData1, $endpointData2, $endpointData3])];
  308. $results = $this->generate($groups);
  309. $this->assertArrayNotHasKey('requestBody', $results['paths']['/path1']['get']);
  310. $this->assertArrayHasKey('requestBody', $results['paths']['/path1']['post']);
  311. $this->assertEquals([
  312. 'required' => true,
  313. 'content' => [
  314. 'application/json' => [
  315. 'schema' => [
  316. 'type' => 'object',
  317. 'properties' => [
  318. 'stringParam' => [
  319. 'description' => 'String param',
  320. 'example' => 'hahoho',
  321. 'type' => 'string',
  322. 'nullable' => false,
  323. ],
  324. 'booleanParam' => [
  325. 'description' => 'Boolean param',
  326. 'example' => false,
  327. 'type' => 'boolean',
  328. 'nullable' => false,
  329. ],
  330. 'integerParam' => [
  331. 'description' => 'Integer param',
  332. 'example' => 99,
  333. 'type' => 'integer',
  334. 'nullable' => false,
  335. ],
  336. 'objectParam' => [
  337. 'description' => 'Object param',
  338. 'example' => [],
  339. 'type' => 'object',
  340. 'nullable' => false,
  341. 'properties' => [
  342. 'field' => [
  343. 'description' => 'Object param field',
  344. 'example' => 119.0,
  345. 'type' => 'number',
  346. 'nullable' => false,
  347. ],
  348. ],
  349. ],
  350. ],
  351. 'required' => [
  352. 'integerParam',
  353. 'booleanParam',
  354. ],
  355. ],
  356. ],
  357. ],
  358. ], $results['paths']['/path1']['post']['requestBody']);
  359. $this->assertEquals([
  360. 'required' => false,
  361. 'content' => [
  362. 'multipart/form-data' => [
  363. 'schema' => [
  364. 'type' => 'object',
  365. 'properties' => [
  366. 'fileParam' => [
  367. 'description' => 'File param',
  368. 'type' => 'string',
  369. 'format' => 'binary',
  370. 'nullable' => false,
  371. ],
  372. 'numberArrayParam' => [
  373. 'description' => 'Number array param',
  374. 'example' => [186.9],
  375. 'type' => 'array',
  376. 'items' => [
  377. 'type' => 'number',
  378. ],
  379. ],
  380. 'objectArrayParam' => [
  381. 'description' => 'Object array param',
  382. 'example' => [[]],
  383. 'type' => 'array',
  384. 'items' => [
  385. 'type' => 'object',
  386. 'required' => ['field1'],
  387. 'properties' => [
  388. 'field1' => [
  389. 'type' => 'array',
  390. 'items' => [
  391. 'type' => 'string',
  392. ],
  393. 'description' => 'Object array param first field',
  394. 'example' => ["hello"],
  395. ],
  396. 'field2' => [
  397. 'type' => 'string',
  398. 'description' => '',
  399. 'example' => "hi",
  400. 'nullable' => false,
  401. ],
  402. ],
  403. ],
  404. ],
  405. ],
  406. ],
  407. ],
  408. ],
  409. ], $results['paths']['/path2']['put']['requestBody']);
  410. }
  411. /** @test */
  412. public function adds_responses_correctly_as_responses_on_operation_object()
  413. {
  414. $endpointData1 = $this->createMockEndpointData([
  415. 'httpMethods' => ['POST'],
  416. 'uri' => '/path1',
  417. 'responses' => [
  418. [
  419. 'status' => 204,
  420. 'description' => 'Successfully updated.',
  421. 'content' => '{"this": "should be ignored"}',
  422. ],
  423. [
  424. 'status' => 201,
  425. 'description' => '',
  426. 'content' => '{"this": "shouldn\'t be ignored", "and this": "too", "also this": "too", "sub level 0": { "sub level 1 key 1": "sl0_sl1k1", "sub level 1 key 2": [ { "sub level 2 key 1": "sl0_sl1k2_sl2k1", "sub level 2 key 2": { "sub level 3 key 1": "sl0_sl1k2_sl2k2_sl3k1" } } ], "sub level 1 key 3": { "sub level 2 key 1": "sl0_sl1k3_sl2k2", "sub level 2 key 2": { "sub level 3 key 1": "sl0_sl1k3_sl2k2_sl3k1", "sub level 3 key null": null, "sub level 3 key integer": 99 }, "sub level 2 key 3 required" : "sl0_sl1k3_sl2k3" } } }',
  427. ],
  428. ],
  429. 'responseFields' => [
  430. 'and this' => [
  431. 'name' => 'and this',
  432. 'type' => 'string',
  433. 'description' => 'Parameter description, ha!',
  434. ],
  435. 'also this' => [
  436. 'name' => 'also this',
  437. 'type' => 'string',
  438. 'description' => 'This response parameter is required.',
  439. 'required' => true,
  440. ],
  441. 'sub level 0.sub level 1 key 3.sub level 2 key 1' => [
  442. 'description' => 'This is a description of a nested object',
  443. ],
  444. 'sub level 0.sub level 1 key 3.sub level 2 key 3 required' => [
  445. 'description' => 'This is a description of a required nested object',
  446. 'required' => true,
  447. ],
  448. ],
  449. ]);
  450. $endpointData2 = $this->createMockEndpointData([
  451. 'httpMethods' => ['PUT'],
  452. 'uri' => '/path2',
  453. 'responses' => [
  454. [
  455. 'status' => 200,
  456. 'description' => '',
  457. 'content' => '<<binary>> The cropped image',
  458. ],
  459. ],
  460. ]);
  461. $groups = [$this->createGroup([$endpointData1, $endpointData2])];
  462. $results = $this->generate($groups);
  463. $this->assertCount(2, $results['paths']['/path1']['post']['responses']);
  464. $this->assertArraySubset([
  465. '204' => [
  466. 'description' => 'Successfully updated.',
  467. ],
  468. '201' => [
  469. 'content' => [
  470. 'application/json' => [
  471. 'schema' => [
  472. 'type' => 'object',
  473. 'properties' => [
  474. 'this' => [
  475. 'example' => "shouldn't be ignored",
  476. 'type' => 'string',
  477. ],
  478. 'and this' => [
  479. 'description' => 'Parameter description, ha!',
  480. 'example' => "too",
  481. 'type' => 'string',
  482. ],
  483. 'also this' => [
  484. 'description' => 'This response parameter is required.',
  485. 'example' => "too",
  486. 'type' => 'string',
  487. ],
  488. 'sub level 0' => [
  489. 'type' => 'object',
  490. 'properties' => [
  491. 'sub level 1 key 1' => [
  492. 'type' => 'string',
  493. 'example' => 'sl0_sl1k1'
  494. ],
  495. 'sub level 1 key 2' => [
  496. 'type' => 'array',
  497. 'example' => [
  498. [
  499. 'sub level 2 key 1' => 'sl0_sl1k2_sl2k1',
  500. 'sub level 2 key 2' => [
  501. 'sub level 3 key 1' => 'sl0_sl1k2_sl2k2_sl3k1'
  502. ]
  503. ]
  504. ],
  505. 'items' => [
  506. 'type' => 'object'
  507. ]
  508. ],
  509. 'sub level 1 key 3' => [
  510. 'type' => 'object',
  511. 'properties' => [
  512. 'sub level 2 key 1' => [
  513. 'type' => 'string',
  514. 'example' => 'sl0_sl1k3_sl2k2',
  515. 'description' => 'This is a description of a nested object'
  516. ],
  517. 'sub level 2 key 2' => [
  518. 'type' => 'object',
  519. 'properties' => [
  520. 'sub level 3 key 1' => [
  521. 'type' => 'string',
  522. 'example' => 'sl0_sl1k3_sl2k2_sl3k1'
  523. ],
  524. 'sub level 3 key null' => [
  525. 'type' => 'string',
  526. 'example' => null
  527. ],
  528. 'sub level 3 key integer' => [
  529. 'type' => 'integer',
  530. 'example' => 99
  531. ]
  532. ]
  533. ],
  534. 'sub level 2 key 3 required' => [
  535. 'type' => 'string',
  536. 'example' => 'sl0_sl1k3_sl2k3',
  537. 'description' => 'This is a description of a required nested object'
  538. ],
  539. ],
  540. 'required' => [
  541. 'sub level 2 key 3 required'
  542. ]
  543. ]
  544. ]
  545. ]
  546. ],
  547. 'required' => [
  548. 'also this'
  549. ]
  550. ],
  551. ],
  552. ],
  553. ],
  554. ], $results['paths']['/path1']['post']['responses']);
  555. $this->assertCount(1, $results['paths']['/path2']['put']['responses']);
  556. $this->assertEquals([
  557. '200' => [
  558. 'description' => 'The cropped image',
  559. 'content' => [
  560. 'application/octet-stream' => [
  561. 'schema' => [
  562. 'type' => 'string',
  563. 'format' => 'binary',
  564. ],
  565. ],
  566. ],
  567. ],
  568. ], $results['paths']['/path2']['put']['responses']);
  569. }
  570. /** @test */
  571. public function adds_required_fields_on_objects_wrapped_in_array()
  572. {
  573. $endpointData = $this->createMockEndpointData([
  574. 'httpMethods' => ['GEt'],
  575. 'uri' => '/path1',
  576. 'responses' => [
  577. [
  578. 'status' => 200,
  579. 'description' => 'List of entities',
  580. 'content' => '{"data":[{"name":"Resource name","uuid":"UUID","primary":true}]}',
  581. ],
  582. ],
  583. 'responseFields' => [
  584. 'data' => [
  585. 'name' => 'data',
  586. 'type' => 'array',
  587. 'description' => 'Data wrapper',
  588. ],
  589. 'data.name' => [
  590. 'name' => 'Resource name',
  591. 'type' => 'string',
  592. 'description' => 'Name of the resource object',
  593. 'required' => true,
  594. ],
  595. 'data.uuid' => [
  596. 'name' => 'Resource UUID',
  597. 'type' => 'string',
  598. 'description' => 'Unique ID for the resource',
  599. 'required' => true,
  600. ],
  601. 'data.primary' => [
  602. 'name' => 'Is primary',
  603. 'type' => 'bool',
  604. 'description' => 'Is primary resource',
  605. 'required' => true,
  606. ],
  607. ],
  608. ]);
  609. $groups = [$this->createGroup([$endpointData])];
  610. $results = $this->generate($groups);
  611. $this->assertArraySubset([
  612. '200' => [
  613. 'description' => 'List of entities',
  614. 'content' => [
  615. 'application/json' => [
  616. 'schema' => [
  617. 'type' => 'object',
  618. 'properties' => [
  619. 'data' => [
  620. 'type' => 'array',
  621. 'description' => 'Data wrapper',
  622. 'items' => [
  623. 'type' => 'object',
  624. 'properties' => [
  625. 'name' => [
  626. 'type' => 'string',
  627. 'description' => 'Name of the resource object',
  628. ],
  629. 'uuid' => [
  630. 'type' => 'string',
  631. 'description' => 'Unique ID for the resource',
  632. ],
  633. 'primary' => [
  634. 'type' => 'boolean',
  635. 'description' => 'Is primary resource',
  636. ],
  637. ],
  638. ],
  639. 'required' => [
  640. 'name',
  641. 'uuid',
  642. 'primary',
  643. ]
  644. ],
  645. ],
  646. ],
  647. ],
  648. ],
  649. ],
  650. ], $results['paths']['/path1']['get']['responses']);
  651. }
  652. /** @test */
  653. public function adds_multiple_responses_correctly_using_oneOf()
  654. {
  655. $endpointData1 = $this->createMockEndpointData([
  656. 'httpMethods' => ['POST'],
  657. 'uri' => '/path1',
  658. 'responses' => [
  659. [
  660. 'status' => 201,
  661. 'description' => 'This one',
  662. 'content' => '{"this": "one"}',
  663. ],
  664. [
  665. 'status' => 201,
  666. 'description' => 'No, that one.',
  667. 'content' => '{"that": "one"}',
  668. ],
  669. [
  670. 'status' => 200,
  671. 'description' => 'A separate one',
  672. 'content' => '{"the other": "one"}',
  673. ],
  674. ],
  675. ]);
  676. $groups = [$this->createGroup([$endpointData1])];
  677. $results = $this->generate($groups);
  678. $this->assertArraySubset([
  679. '200' => [
  680. 'description' => 'A separate one',
  681. 'content' => [
  682. 'application/json' => [
  683. 'schema' => [
  684. 'type' => 'object',
  685. 'properties' => [
  686. 'the other' => [
  687. 'example' => "one",
  688. 'type' => 'string',
  689. ],
  690. ],
  691. ],
  692. ],
  693. ],
  694. ],
  695. '201' => [
  696. 'description' => '',
  697. 'content' => [
  698. 'application/json' => [
  699. 'schema' => [
  700. 'oneOf' => [
  701. [
  702. 'type' => 'object',
  703. 'description' => 'This one',
  704. 'properties' => [
  705. 'this' => [
  706. 'example' => "one",
  707. 'type' => 'string',
  708. ],
  709. ],
  710. ],
  711. [
  712. 'type' => 'object',
  713. 'description' => 'No, that one.',
  714. 'properties' => [
  715. 'that' => [
  716. 'example' => "one",
  717. 'type' => 'string',
  718. ],
  719. ],
  720. ],
  721. ],
  722. ],
  723. ],
  724. ],
  725. ],
  726. ], $results['paths']['/path1']['post']['responses']);
  727. }
  728. /** @test */
  729. public function adds_more_than_two_answers_correctly_using_oneOf()
  730. {
  731. $endpointData1 = $this->createMockEndpointData([
  732. 'httpMethods' => ['POST'],
  733. 'uri' => '/path1',
  734. 'responses' => [
  735. [
  736. 'status' => 201,
  737. 'description' => 'This one',
  738. 'content' => '{"this": "one"}',
  739. ],
  740. [
  741. 'status' => 201,
  742. 'description' => 'No, that one.',
  743. 'content' => '{"that": "one"}',
  744. ],
  745. [
  746. 'status' => 201,
  747. 'description' => 'No, another one.',
  748. 'content' => '{"another": "one"}',
  749. ],
  750. [
  751. 'status' => 200,
  752. 'description' => 'A separate one',
  753. 'content' => '{"the other": "one"}',
  754. ],
  755. ],
  756. ]);
  757. $groups = [$this->createGroup([$endpointData1])];
  758. $results = $this->generate($groups);
  759. $this->assertArraySubset([
  760. '200' => [
  761. 'description' => 'A separate one',
  762. 'content' => [
  763. 'application/json' => [
  764. 'schema' => [
  765. 'type' => 'object',
  766. 'properties' => [
  767. 'the other' => [
  768. 'example' => "one",
  769. 'type' => 'string',
  770. ],
  771. ],
  772. ],
  773. ],
  774. ],
  775. ],
  776. '201' => [
  777. 'description' => '',
  778. 'content' => [
  779. 'application/json' => [
  780. 'schema' => [
  781. 'oneOf' => [
  782. [
  783. 'type' => 'object',
  784. 'description' => 'This one',
  785. 'properties' => [
  786. 'this' => [
  787. 'example' => "one",
  788. 'type' => 'string',
  789. ],
  790. ],
  791. ],
  792. [
  793. 'type' => 'object',
  794. 'description' => 'No, that one.',
  795. 'properties' => [
  796. 'that' => [
  797. 'example' => "one",
  798. 'type' => 'string',
  799. ],
  800. ],
  801. ],
  802. [
  803. 'type' => 'object',
  804. 'description' => 'No, another one.',
  805. 'properties' => [
  806. 'another' => [
  807. 'example' => "one",
  808. 'type' => 'string',
  809. ],
  810. ],
  811. ],
  812. ],
  813. ],
  814. ],
  815. ],
  816. ],
  817. ], $results['paths']['/path1']['post']['responses']);
  818. }
  819. /** @test */
  820. public function adds_enum_values_to_response_properties()
  821. {
  822. $endpointData = $this->createMockEndpointData([
  823. 'httpMethods' => ['GEt'],
  824. 'uri' => '/path1',
  825. 'responses' => [
  826. [
  827. 'status' => 200,
  828. 'description' => 'List of entities',
  829. 'content' => '{"data":[{"name":"Resource name","uuid":"UUID","primary":true}]}',
  830. ],
  831. ],
  832. 'responseFields' => [
  833. 'data' => [
  834. 'name' => 'data',
  835. 'type' => 'array',
  836. 'description' => 'Data wrapper',
  837. ],
  838. 'data.name' => [
  839. 'name' => 'Resource name',
  840. 'type' => 'string',
  841. 'description' => 'Name of the resource object',
  842. 'required' => true,
  843. ],
  844. 'data.uuid' => [
  845. 'name' => 'Resource UUID',
  846. 'type' => 'string',
  847. 'description' => 'Unique ID for the resource',
  848. 'required' => true,
  849. ],
  850. 'data.primary' => [
  851. 'name' => 'Is primary',
  852. 'type' => 'bool',
  853. 'description' => 'Is primary resource',
  854. 'required' => true,
  855. ],
  856. ],
  857. ]);
  858. $groups = [$this->createGroup([$endpointData])];
  859. $results = $this->generate($groups);
  860. $this->assertArraySubset([
  861. '200' => [
  862. 'description' => 'List of entities',
  863. 'content' => [
  864. 'application/json' => [
  865. 'schema' => [
  866. 'type' => 'object',
  867. 'properties' => [
  868. 'data' => [
  869. 'type' => 'array',
  870. 'description' => 'Data wrapper',
  871. 'items' => [
  872. 'type' => 'object',
  873. 'properties' => [
  874. 'name' => [
  875. 'type' => 'string',
  876. 'description' => 'Name of the resource object',
  877. ],
  878. 'uuid' => [
  879. 'type' => 'string',
  880. 'description' => 'Unique ID for the resource',
  881. ],
  882. 'primary' => [
  883. 'type' => 'boolean',
  884. 'description' => 'Is primary resource',
  885. ],
  886. ],
  887. ],
  888. 'required' => [
  889. 'name',
  890. 'uuid',
  891. 'primary',
  892. ]
  893. ],
  894. ],
  895. ],
  896. ],
  897. ],
  898. ],
  899. ], $results['paths']['/path1']['get']['responses']);
  900. }
  901. /** @test */
  902. public function lists_required_properties_in_request_body()
  903. {
  904. $endpointData = $this->createMockEndpointData([
  905. 'uri' => '/path',
  906. 'httpMethods' => ['POST'],
  907. 'bodyParameters' => [
  908. 'my_field' => [
  909. 'name' => 'my_field',
  910. 'description' => '',
  911. 'required' => true,
  912. 'example' => 'abc',
  913. 'type' => 'string',
  914. 'nullable' => false,
  915. ],
  916. 'other_field.nested_field' => [
  917. 'name' => 'nested_field',
  918. 'description' => '',
  919. 'required' => true,
  920. 'example' => 'abc',
  921. 'type' => 'string',
  922. 'nullable' => false,
  923. ],
  924. ],
  925. ]);
  926. $groups = [$this->createGroup([$endpointData])];
  927. $results = $this->generate($groups);
  928. $this->assertArraySubset([
  929. 'requestBody' => [
  930. 'content' => [
  931. 'application/json' => [
  932. 'schema' => [
  933. 'type' => 'object',
  934. 'properties' => [
  935. 'my_field' => [
  936. 'type' => 'string',
  937. ],
  938. 'other_field' => [
  939. 'type' => 'object',
  940. 'properties' => [
  941. 'nested_field' => [
  942. 'type' => 'string',
  943. ],
  944. ],
  945. 'required' => ['nested_field'],
  946. ],
  947. ],
  948. 'required' => ['my_field']
  949. ],
  950. ],
  951. ],
  952. ],
  953. ], $results['paths']['/path']['post']);
  954. }
  955. /** @test */
  956. public function can_extend_openapi_generator()
  957. {
  958. $endpointData1 = $this->createMockEndpointData([
  959. 'uri' => '/path',
  960. 'httpMethods' => ['POST'],
  961. 'custom' => ['permissions' => ['post:view']]
  962. ]);
  963. $groups = [$this->createGroup([$endpointData1])];
  964. $extraGenerator = TestOpenApiGenerator::class;
  965. $config = array_merge($this->config, [
  966. 'openapi' => [
  967. 'generators' => [
  968. $extraGenerator,
  969. ],
  970. ],
  971. ]);
  972. $writer = new OpenAPISpecWriter(new DocumentationConfig($config));
  973. $results = $writer->generateSpecContent($groups);
  974. $this->assertEquals([['default' => ['post:view']]], $results['paths']['/path']['post']['security']);
  975. }
  976. /** @test */
  977. public function can_extend_openapi_generator_parameters()
  978. {
  979. $endpointData1 = $this->createMockEndpointData([
  980. 'uri' => '/{slug}/path',
  981. 'httpMethods' => ['POST'],
  982. 'custom' => ['permissions' => ['post:view']],
  983. 'urlParameters.slug' => [
  984. 'description' => 'Something',
  985. 'required' => true,
  986. 'example' => 56,
  987. 'type' => 'integer',
  988. 'name' => 'slug',
  989. ],
  990. ]);
  991. $groups = [$this->createGroup([$endpointData1])];
  992. $extraGenerator = ComponentsOpenApiGenerator::class;
  993. $config = array_merge($this->config, [
  994. 'openapi' => [
  995. 'generators' => [
  996. $extraGenerator,
  997. ],
  998. ],
  999. ]);
  1000. $writer = new OpenAPISpecWriter(new DocumentationConfig($config));
  1001. $results = $writer->generateSpecContent($groups);
  1002. $actualParameters = $results['paths']['/{slug}/path']['parameters'];
  1003. $this->assertCount(1, $actualParameters);
  1004. $this->assertEquals(['$ref' => "#/components/parameters/slugParam"], $actualParameters[0]);
  1005. $this->assertEquals([
  1006. 'slugParam' => [
  1007. 'in' => 'path',
  1008. 'name' => 'slug',
  1009. 'description' => 'The slug of the organization.',
  1010. 'example' => 'acme-corp',
  1011. 'required' => true,
  1012. 'schema' => [
  1013. 'type' => 'string',
  1014. ],
  1015. ]
  1016. ], $results['components']['parameters']);
  1017. }
  1018. protected function createMockEndpointData(array $custom = []): OutputEndpointData
  1019. {
  1020. $faker = Factory::create();
  1021. $path = '/' . $faker->word();
  1022. $data = [
  1023. 'uri' => $path,
  1024. 'httpMethods' => $faker->randomElements(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], 1),
  1025. 'headers' => [
  1026. 'Content-Type' => 'application/json',
  1027. ],
  1028. 'metadata' => [
  1029. 'title' => $faker->sentence(),
  1030. 'description' => $faker->randomElement([$faker->sentence(), '']),
  1031. 'authenticated' => $faker->boolean(),
  1032. ],
  1033. 'urlParameters' => [], // Should be set by caller (along with custom path)
  1034. 'queryParameters' => [],
  1035. 'bodyParameters' => [],
  1036. 'responses' => [
  1037. [
  1038. 'status' => 200,
  1039. 'content' => '{"random": "json"}',
  1040. 'description' => 'Okayy',
  1041. ],
  1042. ],
  1043. 'responseFields' => [],
  1044. ];
  1045. foreach ($custom as $key => $value) {
  1046. data_set($data, $key, $value);
  1047. }
  1048. return OutputEndpointData::create($data);
  1049. }
  1050. protected function createGroup(array $endpoints)
  1051. {
  1052. $faker = Factory::create();
  1053. return [
  1054. 'description' => '',
  1055. 'name' => $faker->randomElement(['Endpoints', 'Group A', 'Group B']),
  1056. 'endpoints' => $endpoints,
  1057. ];
  1058. }
  1059. protected function generate(array $groups): array
  1060. {
  1061. $writer = new OpenAPISpecWriter(new DocumentationConfig($this->config));
  1062. return $writer->generateSpecContent($groups);
  1063. }
  1064. }