OpenAPISpecWriterTest.php 49 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172
  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_array_of_objects()
  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 generates_correctly_for_array_of_strings()
  654. {
  655. $endpointData = $this->createMockEndpointData([
  656. 'httpMethods' => ['GET'],
  657. 'uri' => '/path1',
  658. 'responses' => [
  659. [
  660. 'status' => 200,
  661. 'description' => 'List of entities',
  662. 'content' => '{"data":["Resource name"]}',
  663. ],
  664. ],
  665. 'responseFields' => [
  666. 'data' => [
  667. 'name' => 'data',
  668. 'type' => 'string[]',
  669. 'description' => 'Data wrapper',
  670. ],
  671. ],
  672. ]);
  673. $groups = [$this->createGroup([$endpointData])];
  674. $results = $this->generate($groups);
  675. $this->assertArraySubset([
  676. '200' => [
  677. 'description' => 'List of entities',
  678. 'content' => [
  679. 'application/json' => [
  680. 'schema' => [
  681. 'type' => 'object',
  682. 'properties' => [
  683. 'data' => [
  684. 'type' => 'array',
  685. 'description' => 'Data wrapper',
  686. 'items' => [
  687. 'type' => 'string',
  688. ],
  689. ],
  690. ],
  691. ],
  692. ],
  693. ],
  694. ],
  695. ], $results['paths']['/path1']['get']['responses']);
  696. }
  697. /** @test */
  698. public function adds_multiple_responses_correctly_using_oneOf()
  699. {
  700. $endpointData1 = $this->createMockEndpointData([
  701. 'httpMethods' => ['POST'],
  702. 'uri' => '/path1',
  703. 'responses' => [
  704. [
  705. 'status' => 201,
  706. 'description' => 'This one',
  707. 'content' => '{"this": "one"}',
  708. ],
  709. [
  710. 'status' => 201,
  711. 'description' => 'No, that one.',
  712. 'content' => '{"that": "one"}',
  713. ],
  714. [
  715. 'status' => 200,
  716. 'description' => 'A separate one',
  717. 'content' => '{"the other": "one"}',
  718. ],
  719. ],
  720. ]);
  721. $groups = [$this->createGroup([$endpointData1])];
  722. $results = $this->generate($groups);
  723. $this->assertArraySubset([
  724. '200' => [
  725. 'description' => 'A separate one',
  726. 'content' => [
  727. 'application/json' => [
  728. 'schema' => [
  729. 'type' => 'object',
  730. 'properties' => [
  731. 'the other' => [
  732. 'example' => "one",
  733. 'type' => 'string',
  734. ],
  735. ],
  736. ],
  737. ],
  738. ],
  739. ],
  740. '201' => [
  741. 'description' => '',
  742. 'content' => [
  743. 'application/json' => [
  744. 'schema' => [
  745. 'oneOf' => [
  746. [
  747. 'type' => 'object',
  748. 'description' => 'This one',
  749. 'properties' => [
  750. 'this' => [
  751. 'example' => "one",
  752. 'type' => 'string',
  753. ],
  754. ],
  755. ],
  756. [
  757. 'type' => 'object',
  758. 'description' => 'No, that one.',
  759. 'properties' => [
  760. 'that' => [
  761. 'example' => "one",
  762. 'type' => 'string',
  763. ],
  764. ],
  765. ],
  766. ],
  767. ],
  768. ],
  769. ],
  770. ],
  771. ], $results['paths']['/path1']['post']['responses']);
  772. }
  773. /** @test */
  774. public function adds_more_than_two_answers_correctly_using_oneOf()
  775. {
  776. $endpointData1 = $this->createMockEndpointData([
  777. 'httpMethods' => ['POST'],
  778. 'uri' => '/path1',
  779. 'responses' => [
  780. [
  781. 'status' => 201,
  782. 'description' => 'This one',
  783. 'content' => '{"this": "one"}',
  784. ],
  785. [
  786. 'status' => 201,
  787. 'description' => 'No, that one.',
  788. 'content' => '{"that": "one"}',
  789. ],
  790. [
  791. 'status' => 201,
  792. 'description' => 'No, another one.',
  793. 'content' => '{"another": "one"}',
  794. ],
  795. [
  796. 'status' => 200,
  797. 'description' => 'A separate one',
  798. 'content' => '{"the other": "one"}',
  799. ],
  800. ],
  801. ]);
  802. $groups = [$this->createGroup([$endpointData1])];
  803. $results = $this->generate($groups);
  804. $this->assertArraySubset([
  805. '200' => [
  806. 'description' => 'A separate one',
  807. 'content' => [
  808. 'application/json' => [
  809. 'schema' => [
  810. 'type' => 'object',
  811. 'properties' => [
  812. 'the other' => [
  813. 'example' => "one",
  814. 'type' => 'string',
  815. ],
  816. ],
  817. ],
  818. ],
  819. ],
  820. ],
  821. '201' => [
  822. 'description' => '',
  823. 'content' => [
  824. 'application/json' => [
  825. 'schema' => [
  826. 'oneOf' => [
  827. [
  828. 'type' => 'object',
  829. 'description' => 'This one',
  830. 'properties' => [
  831. 'this' => [
  832. 'example' => "one",
  833. 'type' => 'string',
  834. ],
  835. ],
  836. ],
  837. [
  838. 'type' => 'object',
  839. 'description' => 'No, that one.',
  840. 'properties' => [
  841. 'that' => [
  842. 'example' => "one",
  843. 'type' => 'string',
  844. ],
  845. ],
  846. ],
  847. [
  848. 'type' => 'object',
  849. 'description' => 'No, another one.',
  850. 'properties' => [
  851. 'another' => [
  852. 'example' => "one",
  853. 'type' => 'string',
  854. ],
  855. ],
  856. ],
  857. ],
  858. ],
  859. ],
  860. ],
  861. ],
  862. ], $results['paths']['/path1']['post']['responses']);
  863. }
  864. /** @test */
  865. public function adds_enum_values_to_response_properties()
  866. {
  867. $endpointData = $this->createMockEndpointData([
  868. 'httpMethods' => ['GEt'],
  869. 'uri' => '/path1',
  870. 'responses' => [
  871. [
  872. 'status' => 200,
  873. 'description' => 'List of entities',
  874. 'content' => '{"data":[{"name":"Resource name","uuid":"UUID","primary":true}]}',
  875. ],
  876. ],
  877. 'responseFields' => [
  878. 'data' => [
  879. 'name' => 'data',
  880. 'type' => 'array',
  881. 'description' => 'Data wrapper',
  882. ],
  883. 'data.name' => [
  884. 'name' => 'Resource name',
  885. 'type' => 'string',
  886. 'description' => 'Name of the resource object',
  887. 'required' => true,
  888. ],
  889. 'data.uuid' => [
  890. 'name' => 'Resource UUID',
  891. 'type' => 'string',
  892. 'description' => 'Unique ID for the resource',
  893. 'required' => true,
  894. ],
  895. 'data.primary' => [
  896. 'name' => 'Is primary',
  897. 'type' => 'bool',
  898. 'description' => 'Is primary resource',
  899. 'required' => true,
  900. ],
  901. ],
  902. ]);
  903. $groups = [$this->createGroup([$endpointData])];
  904. $results = $this->generate($groups);
  905. $this->assertArraySubset([
  906. '200' => [
  907. 'description' => 'List of entities',
  908. 'content' => [
  909. 'application/json' => [
  910. 'schema' => [
  911. 'type' => 'object',
  912. 'properties' => [
  913. 'data' => [
  914. 'type' => 'array',
  915. 'description' => 'Data wrapper',
  916. 'items' => [
  917. 'type' => 'object',
  918. 'properties' => [
  919. 'name' => [
  920. 'type' => 'string',
  921. 'description' => 'Name of the resource object',
  922. ],
  923. 'uuid' => [
  924. 'type' => 'string',
  925. 'description' => 'Unique ID for the resource',
  926. ],
  927. 'primary' => [
  928. 'type' => 'boolean',
  929. 'description' => 'Is primary resource',
  930. ],
  931. ],
  932. ],
  933. 'required' => [
  934. 'name',
  935. 'uuid',
  936. 'primary',
  937. ]
  938. ],
  939. ],
  940. ],
  941. ],
  942. ],
  943. ],
  944. ], $results['paths']['/path1']['get']['responses']);
  945. }
  946. /** @test */
  947. public function lists_required_properties_in_request_body()
  948. {
  949. $endpointData = $this->createMockEndpointData([
  950. 'uri' => '/path',
  951. 'httpMethods' => ['POST'],
  952. 'bodyParameters' => [
  953. 'my_field' => [
  954. 'name' => 'my_field',
  955. 'description' => '',
  956. 'required' => true,
  957. 'example' => 'abc',
  958. 'type' => 'string',
  959. 'nullable' => false,
  960. ],
  961. 'other_field.nested_field' => [
  962. 'name' => 'nested_field',
  963. 'description' => '',
  964. 'required' => true,
  965. 'example' => 'abc',
  966. 'type' => 'string',
  967. 'nullable' => false,
  968. ],
  969. ],
  970. ]);
  971. $groups = [$this->createGroup([$endpointData])];
  972. $results = $this->generate($groups);
  973. $this->assertArraySubset([
  974. 'requestBody' => [
  975. 'content' => [
  976. 'application/json' => [
  977. 'schema' => [
  978. 'type' => 'object',
  979. 'properties' => [
  980. 'my_field' => [
  981. 'type' => 'string',
  982. ],
  983. 'other_field' => [
  984. 'type' => 'object',
  985. 'properties' => [
  986. 'nested_field' => [
  987. 'type' => 'string',
  988. ],
  989. ],
  990. 'required' => ['nested_field'],
  991. ],
  992. ],
  993. 'required' => ['my_field']
  994. ],
  995. ],
  996. ],
  997. ],
  998. ], $results['paths']['/path']['post']);
  999. }
  1000. /** @test */
  1001. public function can_extend_openapi_generator()
  1002. {
  1003. $endpointData1 = $this->createMockEndpointData([
  1004. 'uri' => '/path',
  1005. 'httpMethods' => ['POST'],
  1006. 'custom' => ['permissions' => ['post:view']]
  1007. ]);
  1008. $groups = [$this->createGroup([$endpointData1])];
  1009. $extraGenerator = TestOpenApiGenerator::class;
  1010. $config = array_merge($this->config, [
  1011. 'openapi' => [
  1012. 'generators' => [
  1013. $extraGenerator,
  1014. ],
  1015. ],
  1016. ]);
  1017. $writer = new OpenAPISpecWriter(new DocumentationConfig($config));
  1018. $results = $writer->generateSpecContent($groups);
  1019. $this->assertEquals([['default' => ['post:view']]], $results['paths']['/path']['post']['security']);
  1020. }
  1021. /** @test */
  1022. public function can_extend_openapi_generator_parameters()
  1023. {
  1024. $endpointData1 = $this->createMockEndpointData([
  1025. 'uri' => '/{slug}/path',
  1026. 'httpMethods' => ['POST'],
  1027. 'custom' => ['permissions' => ['post:view']],
  1028. 'urlParameters.slug' => [
  1029. 'description' => 'Something',
  1030. 'required' => true,
  1031. 'example' => 56,
  1032. 'type' => 'integer',
  1033. 'name' => 'slug',
  1034. ],
  1035. ]);
  1036. $groups = [$this->createGroup([$endpointData1])];
  1037. $extraGenerator = ComponentsOpenApiGenerator::class;
  1038. $config = array_merge($this->config, [
  1039. 'openapi' => [
  1040. 'generators' => [
  1041. $extraGenerator,
  1042. ],
  1043. ],
  1044. ]);
  1045. $writer = new OpenAPISpecWriter(new DocumentationConfig($config));
  1046. $results = $writer->generateSpecContent($groups);
  1047. $actualParameters = $results['paths']['/{slug}/path']['parameters'];
  1048. $this->assertCount(1, $actualParameters);
  1049. $this->assertEquals(['$ref' => "#/components/parameters/slugParam"], $actualParameters[0]);
  1050. $this->assertEquals([
  1051. 'slugParam' => [
  1052. 'in' => 'path',
  1053. 'name' => 'slug',
  1054. 'description' => 'The slug of the organization.',
  1055. 'example' => 'acme-corp',
  1056. 'required' => true,
  1057. 'schema' => [
  1058. 'type' => 'string',
  1059. ],
  1060. ]
  1061. ], $results['components']['parameters']);
  1062. }
  1063. protected function createMockEndpointData(array $custom = []): OutputEndpointData
  1064. {
  1065. $faker = Factory::create();
  1066. $path = '/' . $faker->word();
  1067. $data = [
  1068. 'uri' => $path,
  1069. 'httpMethods' => $faker->randomElements(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], 1),
  1070. 'headers' => [
  1071. 'Content-Type' => 'application/json',
  1072. ],
  1073. 'metadata' => [
  1074. 'title' => $faker->sentence(),
  1075. 'description' => $faker->randomElement([$faker->sentence(), '']),
  1076. 'authenticated' => $faker->boolean(),
  1077. ],
  1078. 'urlParameters' => [], // Should be set by caller (along with custom path)
  1079. 'queryParameters' => [],
  1080. 'bodyParameters' => [],
  1081. 'responses' => [
  1082. [
  1083. 'status' => 200,
  1084. 'content' => '{"random": "json"}',
  1085. 'description' => 'Okayy',
  1086. ],
  1087. ],
  1088. 'responseFields' => [],
  1089. ];
  1090. foreach ($custom as $key => $value) {
  1091. data_set($data, $key, $value);
  1092. }
  1093. return OutputEndpointData::create($data);
  1094. }
  1095. protected function createGroup(array $endpoints)
  1096. {
  1097. $faker = Factory::create();
  1098. return [
  1099. 'description' => '',
  1100. 'name' => $faker->randomElement(['Endpoints', 'Group A', 'Group B']),
  1101. 'endpoints' => $endpoints,
  1102. ];
  1103. }
  1104. protected function generate(array $groups): array
  1105. {
  1106. $writer = new OpenAPISpecWriter(new DocumentationConfig($this->config));
  1107. return $writer->generateSpecContent($groups);
  1108. }
  1109. }