4
0

OpenApiTestCase.php 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. <?php declare(strict_types=1);
  2. /**
  3. * @license Apache 2.0
  4. */
  5. namespace OpenApiTests;
  6. use Closure;
  7. use DirectoryIterator;
  8. use Exception;
  9. use PHPUnit\Framework\TestCase;
  10. use stdClass;
  11. use OpenApi\Analyser;
  12. use OpenApi\Annotations\AbstractAnnotation;
  13. use OpenApi\Annotations\OpenApi;
  14. use OpenApi\Annotations\Info;
  15. use OpenApi\Annotations\PathItem;
  16. use OpenApi\Context;
  17. use OpenApi\Logger;
  18. class OpenApiTestCase extends TestCase
  19. {
  20. protected $countExceptions = 0;
  21. /**
  22. * @var array
  23. */
  24. private $expectedLogMessages;
  25. /**
  26. * @var Closure
  27. */
  28. private $originalLogger;
  29. /**
  30. * @param string $expectedFile File containing the excepted json.
  31. * @param OpenApi $actualOpenApi
  32. * @param string $message
  33. */
  34. public function assertOpenApiEqualsFile($expectedFile, $actualOpenApi, $message = '')
  35. {
  36. $expected = json_decode(file_get_contents($expectedFile));
  37. $error = json_last_error();
  38. if ($error !== JSON_ERROR_NONE) {
  39. $this->fail('File: "'.$expectedFile.'" doesn\'t contain valid json, error '.$error);
  40. }
  41. $json = json_encode($actualOpenApi);
  42. if ($json === false) {
  43. $this->fail('Failed to encode openapi object');
  44. }
  45. $actual = json_decode($json);
  46. $expectedJson = json_encode($this->sorted($expected, $expectedFile), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
  47. $actualJson = json_encode($this->sorted($actual, 'OpenApi'), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
  48. $this->assertEquals($expectedJson, $actualJson, $message);
  49. }
  50. public function assertOpenApiLog($expectedEntry, $expectedType, $message = '')
  51. {
  52. $this->expectedLogMessages[] = function ($actualEntry, $actualType) use ($expectedEntry, $expectedType, $message) {
  53. $this->assertSame($expectedEntry, $actualEntry, $message);
  54. $this->assertSame($expectedType, $actualType, $message);
  55. };
  56. }
  57. public function assertOpenApiLogType($expectedType, $message = '')
  58. {
  59. $this->expectedLogMessages[] = function ($entry, $actualType) use ($expectedType, $message) {
  60. $this->assertSame($expectedType, $actualType, $message);
  61. };
  62. }
  63. public function assertOpenApiLogEntry($expectedEntry, $message = '')
  64. {
  65. $this->expectedLogMessages[] = function ($actualEntry, $type) use ($expectedEntry, $message) {
  66. $this->assertSame($expectedEntry, $actualEntry, $message);
  67. };
  68. }
  69. public function assertOpenApiLogEntryStartsWith($entryPrefix, $message = '')
  70. {
  71. $this->expectedLogMessages[] = function ($entry, $type) use ($entryPrefix, $message) {
  72. if ($entry instanceof Exception) {
  73. $entry = $entry->getMessage();
  74. }
  75. $this->assertStringStartsWith($entryPrefix, $entry, $message);
  76. };
  77. }
  78. protected function setUp(): void
  79. {
  80. $this->expectedLogMessages = [];
  81. $this->originalLogger = Logger::getInstance()->log;
  82. Logger::getInstance()->log = function ($entry, $type) {
  83. if (count($this->expectedLogMessages)) {
  84. $assertion = array_shift($this->expectedLogMessages);
  85. $assertion($entry, $type);
  86. } else {
  87. $map = [
  88. E_USER_NOTICE => 'notice',
  89. E_USER_WARNING => 'warning',
  90. ];
  91. if (isset($map[$type])) {
  92. $this->fail('Unexpected \OpenApi\Logger::'.$map[$type].'("'.$entry.'")');
  93. } else {
  94. $this->fail('Unexpected \OpenApi\Logger->getInstance()->log("'.$entry.'",'.$type.')');
  95. }
  96. }
  97. };
  98. parent::setUp();
  99. }
  100. protected function tearDown(): void
  101. {
  102. $this->assertCount($this->countExceptions, $this->expectedLogMessages, count($this->expectedLogMessages).' OpenApi\Logger messages were not triggered');
  103. Logger::getInstance()->log = $this->originalLogger;
  104. parent::tearDown();
  105. }
  106. /**
  107. *
  108. * @param string $comment Contents of a comment block
  109. *
  110. * @return AbstractAnnotation[]
  111. */
  112. protected function parseComment($comment)
  113. {
  114. $analyser = new Analyser();
  115. $context = Context::detect(1);
  116. return $analyser->fromComment("<?php\n/**\n * ".implode("\n * ", explode("\n", $comment))."\n*/", $context);
  117. }
  118. /**
  119. * Create a OpenApi object with Info.
  120. * (So it will pass validation.)
  121. */
  122. protected function createOpenApiWithInfo()
  123. {
  124. $openapi = new OpenApi(
  125. [
  126. 'info' => new Info(
  127. [
  128. 'title' => 'swagger-php Test-API',
  129. 'version' => 'test',
  130. '_context' => new Context(['unittest' => true]),
  131. ]
  132. ),
  133. 'paths' => [
  134. new PathItem(['path' => '/test'])
  135. ],
  136. '_context' => new Context(['unittest' => true]),
  137. ]
  138. );
  139. return $openapi;
  140. }
  141. /**
  142. * Sorts the object to improve matching and debugging the differences.
  143. * Used by assertOpenApiEqualsFile
  144. *
  145. * @param stdClass $object
  146. * @param string $origin
  147. *
  148. * @return stdClass The sorted object
  149. */
  150. protected function sorted(stdClass $object, $origin = 'unknown')
  151. {
  152. static $sortMap = null;
  153. if ($sortMap === null) {
  154. $sortMap = [
  155. // property -> algorithm
  156. 'parameters' => function ($a, $b) {
  157. return strcasecmp($a->name, $b->name);
  158. },
  159. // 'responses' => function ($a, $b) {
  160. // return strcasecmp($a->name, $b->name);
  161. // },
  162. 'headers' => function ($a, $b) {
  163. return strcasecmp($a->header, $b->header);
  164. },
  165. 'tags' => function ($a, $b) {
  166. return strcasecmp($a->name, $b->name);
  167. },
  168. 'allOf' => function ($a, $b) {
  169. return strcasecmp(implode(',', array_keys(get_object_vars($a))), implode(',', array_keys(get_object_vars($b))));
  170. },
  171. 'security' => function ($a, $b) {
  172. return strcasecmp(implode(',', array_keys(get_object_vars($a))), implode(',', array_keys(get_object_vars($b))));
  173. },
  174. ];
  175. }
  176. $data = unserialize(serialize((array)$object));
  177. ksort($data);
  178. foreach ($data as $property => $value) {
  179. if (is_object($value)) {
  180. $data[$property] = $this->sorted($value, $origin.'->'.$property);
  181. } elseif (is_array($value)) {
  182. if (count($value) > 1) {
  183. if (gettype($value[0]) === 'string') {
  184. $sortFn = 'strcasecmp';
  185. } else {
  186. $sortFn = isset($sortMap[$property]) ? $sortMap[$property] : null;
  187. }
  188. if ($sortFn) {
  189. usort($value, $sortFn);
  190. $data[$property] = $value;
  191. } else {
  192. echo 'no sort for '.$origin.'->'.$property."\n";
  193. die;
  194. }
  195. }
  196. foreach ($value as $i => $element) {
  197. if (is_object($element)) {
  198. $data[$property][$i] = $this->sorted($element, $origin.'->'.$property.'['.$i.']');
  199. }
  200. }
  201. }
  202. }
  203. return (object)$data;
  204. }
  205. public function allAnnotations()
  206. {
  207. $data = [];
  208. $dir = new DirectoryIterator(__DIR__.'/../src/Annotations');
  209. foreach ($dir as $entry) {
  210. if ($entry->isFile() === false) {
  211. continue;
  212. }
  213. $class = substr($entry->getFilename(), 0, -4);
  214. if (in_array($class, ['AbstractAnnotation','Operation'])) {
  215. continue; // skip abstract classes
  216. }
  217. $data[] = ['OpenApi\\Annotations\\'.$class];
  218. }
  219. return $data;
  220. }
  221. }