bootstrap.php 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. <?php
  2. /**
  3. * 测试引导:加载生成的 Guzzle 客户端 + 注册/断言 helper。
  4. * 纯 PHP,无测试框架,不依赖 Laravel。
  5. */
  6. require_once __DIR__ . '/../client/vendor/autoload.php';
  7. use GuzzleHttp\Client;
  8. use \WikipaliApi\ApiException;
  9. use \WikipaliApi\Configuration;
  10. final class Fail extends RuntimeException
  11. {
  12. }
  13. final class Skip extends RuntimeException
  14. {
  15. }
  16. $GLOBALS['__tests'] = [];
  17. function test(string $name, callable $fn): void
  18. {
  19. $GLOBALS['__tests'][] = [$name, $fn];
  20. }
  21. function skip(string $reason): void
  22. {
  23. throw new Skip($reason);
  24. }
  25. function fail(string $message): void
  26. {
  27. throw new Fail($message);
  28. }
  29. function config(): array
  30. {
  31. return $GLOBALS['config'];
  32. }
  33. function fixtures(): array
  34. {
  35. return $GLOBALS['fixtures'];
  36. }
  37. /** 造一个已指向目标服务器的 Configuration;传 $token 时带 bearer。 */
  38. function makeConfig(?string $token = null): Configuration
  39. {
  40. $c = new Configuration();
  41. $c->setHost(config()['base_url']);
  42. if ($token !== null) {
  43. $c->setAccessToken($token);
  44. }
  45. return $c;
  46. }
  47. /** 实例化生成的 Api 类。$class 传完整 FQCN,如 \WikipaliApi\Api\HeartbeatApi::class */
  48. function api(string $class, ?string $token = null): object
  49. {
  50. // http_errors 保持默认 true:生成的客户端靠 RequestException→ApiException 这条链
  51. // 把非 2xx 的 body 塞进 ApiException(尤其 204-only 的 DELETE,它没有显式状态检查,
  52. // 关掉 http_errors 会让错误 body 丢失)。
  53. $http = new Client(['timeout' => config()['timeout']]);
  54. return new $class($http, makeConfig($token));
  55. }
  56. /**
  57. * 统一执行一次请求,返回 ['status'=>int, 'body'=>?array, 'headers'=>array]。
  58. * $fn 必须返回 WithHttpInfo 的三元组 [$content, $status, $headers]。
  59. * 非 2xx 时生成的客户端抛 ApiException,这里捕获并解出状态码与 body。
  60. */
  61. function request(callable $fn): array
  62. {
  63. try {
  64. [$content, $status, $headers] = $fn();
  65. return ['status' => $status, 'body' => normalize($content), 'headers' => $headers];
  66. } catch (ApiException $e) {
  67. $status = $e->getCode();
  68. // 网络层错误(DNS/连接/超时)时,生成客户端把异常包成 ApiException 但 code 不是
  69. // 合法 HTTP 状态(可能是 0,真实 curl 错误在 message 里);转成可读的 Fail。
  70. if ($status < 100 || $status > 599) {
  71. throw new Fail(sprintf('网络错误(服务器不可达):%s', $e->getMessage()));
  72. }
  73. $body = json_decode((string) $e->getResponseBody(), true);
  74. return ['status' => $status, 'body' => is_array($body) ? $body : null, 'headers' => $e->getResponseHeaders()];
  75. }
  76. }
  77. /**
  78. * 原始 HTTP 请求:绕过生成客户端的必填参数校验,用于「缺参/非法组合」这类负向用例。
  79. * $path 以 / 开头(如 /v3/reactions),base URL 已含 /api 前缀。
  80. */
  81. function raw(string $method, string $path, ?array $query = null, ?array $json = null, ?string $token = null): array
  82. {
  83. $http = new Client(['timeout' => config()['timeout'], 'http_errors' => false]);
  84. $opts = ['headers' => []];
  85. if ($query !== null) {
  86. $opts['query'] = $query;
  87. }
  88. if ($json !== null) {
  89. $opts['json'] = $json;
  90. }
  91. if ($token !== null) {
  92. $opts['headers']['Authorization'] = 'Bearer ' . $token;
  93. }
  94. try {
  95. $resp = $http->request($method, config()['base_url'] . $path, $opts);
  96. } catch (\GuzzleHttp\Exception\GuzzleException $e) {
  97. // 网络层错误(DNS 解析失败、连接拒绝、超时等)→ 转成 Fail,别让 Guzzle 异常
  98. // 一路冒成 fatal error。消息带上目标,方便定位是哪个服务器连不上。
  99. throw new Fail(sprintf('网络错误(%s %s%s):%s', strtoupper($method), config()['base_url'], $path, $e->getMessage()));
  100. }
  101. $body = json_decode((string) $resp->getBody(), true);
  102. return ['status' => $resp->getStatusCode(), 'body' => is_array($body) ? $body : null, 'headers' => $resp->getHeaders()];
  103. }
  104. function normalize(mixed $content): mixed
  105. {
  106. if ($content === null || is_array($content)) {
  107. return $content;
  108. }
  109. if (is_object($content)) {
  110. return json_decode((string) json_encode($content), true);
  111. }
  112. return $content;
  113. }
  114. /**
  115. * 调 v2 登录接口换 bearer token。
  116. * `POST /v2/sign-in`(username 可以是用户名或邮箱),成功返回 {ok,data:JWT},
  117. * 失败也是 JSON({ok:false,message})且 HTTP 可能 200 或 400,所以只看 ok 字段。
  118. */
  119. function login(string $username, string $password): string
  120. {
  121. $res = raw('POST', '/v2/sign-in', null, ['username' => $username, 'password' => $password]);
  122. if (($res['body']['ok'] ?? false) === true && is_string($res['body']['data'] ?? null) && $res['body']['data'] !== '') {
  123. return $res['body']['data'];
  124. }
  125. fail(sprintf('登录失败(HTTP %d):%s', $res['status'], $res['body']['message'] ?? '未知错误'));
  126. }
  127. /** 取当前 token;没有就 FAIL(需登录用例不跳过,缺 token 就是失败)。 */
  128. function required_token(): string
  129. {
  130. $token = config()['token'];
  131. if (! $token) {
  132. fail('缺少 token:请用 --token= 或 --username/--password(自动调 /v2/sign-in)');
  133. }
  134. return $token;
  135. }
  136. /* ---- 断言 ---- */
  137. function assert_status(int $expected, array $res, string $what): void
  138. {
  139. if ($res['status'] !== $expected) {
  140. fail(sprintf(
  141. '%s:期望 HTTP %d,实际 %d,body=%s',
  142. $what,
  143. $expected,
  144. $res['status'],
  145. substr((string) json_encode($res['body'], JSON_UNESCAPED_UNICODE), 0, 400)
  146. ));
  147. }
  148. }
  149. function assert_has_key(string $key, mixed $value, string $what): void
  150. {
  151. if (! is_array($value) || ! array_key_exists($key, $value)) {
  152. fail(sprintf('%s:响应缺少字段 `%s`,body=%s', $what, $key, substr((string) json_encode($value, JSON_UNESCAPED_UNICODE), 0, 400)));
  153. }
  154. }
  155. function assert_eq(mixed $expected, mixed $actual, string $what): void
  156. {
  157. if ($expected !== $actual) {
  158. fail(sprintf('%s:期望 %s,实际 %s', $what, var_export($expected, true), var_export($actual, true)));
  159. }
  160. }
  161. /** 断言失败响应是 RFC 9457 Problem Details,且 status 与 HTTP 码一致。 */
  162. function assert_problem(array $res, string $what): void
  163. {
  164. $b = $res['body'];
  165. if (! is_array($b) || ! isset($b['type'], $b['title'], $b['status'])) {
  166. fail(sprintf('%s:不是 Problem Details(缺 type/title/status),body=%s', $what, substr((string) json_encode($b, JSON_UNESCAPED_UNICODE), 0, 400)));
  167. }
  168. assert_eq($res['status'], (int) $b['status'], $what . ':Problem.status 与 HTTP 状态码不一致');
  169. }