mini_api_contract_test.php 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. <?php
  2. /** Checks route coverage, authentication, references and examples. No database access. */
  3. require dirname(__DIR__) . '/vendor/autoload.php';
  4. require __DIR__ . '/support/MiniApiContract.php';
  5. \FastRoute\simpleDispatcher(function ($collector) {
  6. \Webman\Route::setCollector($collector);
  7. require dirname(__DIR__) . '/routes/mini_api.php';
  8. });
  9. $contract = new MiniApiContract();
  10. $spec = $contract->document;
  11. $checks = 0;
  12. function check(bool $ok, string $message): void
  13. {
  14. global $checks;
  15. $checks++;
  16. if (!$ok) {
  17. throw new RuntimeException($message);
  18. }
  19. }
  20. check($spec['openapi'] === '3.0.3', 'OpenAPI version');
  21. check($spec['security'] === [['MiniBearer' => []]], 'default bearer security');
  22. $walk = function (array $node) use (&$walk, $contract) {
  23. if (isset($node['$ref'])) {
  24. check((bool) $contract->resolve($node['$ref']), 'local reference resolves');
  25. }
  26. foreach ($node as $child) {
  27. if (is_array($child)) {
  28. $walk($child);
  29. }
  30. }
  31. };
  32. $walk($spec);
  33. $actual = [];
  34. foreach (\Webman\Route::getRoutes() as $route) {
  35. $path = $route->getPath();
  36. if (!str_starts_with($path, '/api/mini/v1/')) {
  37. continue;
  38. }
  39. $callback = $route->getCallback();
  40. if (is_array($callback) && $callback[1] === 'methodNotAllowed') {
  41. continue;
  42. }
  43. $path = substr($path, strlen('/api/mini/v1'));
  44. $variants = [$path];
  45. if (preg_match('/\{(?:section|action):([^}]+)\}/', $path, $match)) {
  46. $variants = array_map(fn ($option) => str_replace($match[0], $option, $path), explode('|', $match[1]));
  47. }
  48. foreach ($variants as $variant) {
  49. $variant = preg_replace('/\{(\w+):[^}]+\}/', '{$1}', $variant);
  50. foreach ($route->getMethods() as $method) {
  51. $method = strtolower($method);
  52. $actual[] = $method . ' ' . $variant;
  53. $operation = $spec['paths'][$variant][$method] ?? null;
  54. check(is_array($operation), 'documented route: ' . $method . ' ' . $variant);
  55. $authenticated = in_array(\app\miniapi\middleware\Authenticate::class, $route->getMiddleware(), true);
  56. check((($operation['security'] ?? $spec['security']) !== []) === $authenticated, 'route security: ' . $variant);
  57. }
  58. }
  59. }
  60. $documented = [];
  61. $operationIds = [];
  62. foreach ($spec['paths'] as $path => $methods) {
  63. foreach ($methods as $method => $operation) {
  64. $documented[] = $method . ' ' . $path;
  65. check(!in_array($operation['operationId'], $operationIds, true), 'unique operationId');
  66. $operationIds[] = $operation['operationId'];
  67. preg_match_all('/\{(\w+)\}/', $path, $matches);
  68. $parameters = [];
  69. foreach ($operation['parameters'] as $parameter) {
  70. if ($parameter['in'] === 'path') {
  71. check($parameter['required'] === true, 'path parameter required');
  72. $parameters[] = $parameter['name'];
  73. }
  74. }
  75. sort($parameters);
  76. sort($matches[1]);
  77. check($parameters === $matches[1], 'path parameter coverage: ' . $path);
  78. $content = $operation['requestBody']['content']['application/json'] ?? [];
  79. if (isset($content['example'])) {
  80. $contract->validate($content['schema'], json_decode(json_encode($content['example'])), $path . ' example');
  81. check(true, 'request example matches schema');
  82. }
  83. }
  84. }
  85. sort($actual);
  86. sort($documented);
  87. check($actual === $documented, 'no missing or invented business routes');
  88. check(isset($spec['paths']['/enterprises/{enterprise}/checkouts/{id}/pay']['post']['responses']['200']), 'balance payment success is documented');
  89. check(isset($spec['paths']['/enterprises/{enterprise}/checkouts/{id}/pay']['post']['responses']['503']), 'unconfigured WeChat payment failure is documented');
  90. check(!isset($spec['paths']['/agents/{agent}/withdrawals']['post']['responses']['200']), 'no fictional withdrawal success');
  91. check(isset($spec['paths']['/photos/{id}']['get']['responses']['200']['content']['image/jpeg']), 'private photos documented as binary');
  92. $contract->validate($contract->resolve('#/components/schemas/Error'), json_decode(json_encode($spec['components']['responses']['Error']['content']['application/json']['example'])));
  93. echo 'PASS: ' . $checks . ' contract checks; ' . count($actual) . " business operations documented.\n";