mini_protection_http_test.php 4.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. <?php
  2. /** Real local HTTP integration, using an unprivileged temporary fixture user. */
  3. require dirname(__DIR__) . '/vendor/autoload.php';
  4. \Dotenv\Dotenv::createUnsafeMutable(dirname(__DIR__))->load();
  5. \support\App::loadAllConfig(['route']);
  6. use app\miniapi\model\MiniUser;
  7. use app\miniapi\model\MiniSession;
  8. use app\miniapi\service\AuthService;
  9. use support\Redis;
  10. if (config('mini_api.environment') !== 'local' || !config('mini_protection.enabled')) {
  11. throw new RuntimeException('Run only against the local enabled protection service');
  12. }
  13. $checks = 0;
  14. function check(bool $ok, string $label): void
  15. {
  16. global $checks;
  17. $checks++;
  18. if (!$ok) {
  19. throw new RuntimeException($label);
  20. }
  21. }
  22. function http(string $path, string $token, array $headers = []): array
  23. {
  24. $responseHeaders = [];
  25. $curl = curl_init('http://127.0.0.1:8787/api/mini/v1' . $path);
  26. curl_setopt_array($curl, [
  27. CURLOPT_RETURNTRANSFER => true,
  28. CURLOPT_TIMEOUT => 5,
  29. CURLOPT_HTTPHEADER => array_merge(['Authorization: Bearer ' . $token, 'Origin: http://127.0.0.1:8787'], $headers),
  30. CURLOPT_HEADERFUNCTION => static function ($handle, $line) use (&$responseHeaders) {
  31. if (str_contains($line, ':')) {
  32. [$name, $value] = explode(':', $line, 2);
  33. $responseHeaders[strtolower($name)] = trim($value);
  34. }
  35. return strlen($line);
  36. },
  37. ]);
  38. $raw = curl_exec($curl);
  39. $status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
  40. curl_close($curl);
  41. return [$status, json_decode((string) $raw, true), $responseHeaders];
  42. }
  43. $auth = new AuthService();
  44. $identity = ['appid' => 'protection-http-test', 'openid' => bin2hex(random_bytes(16))];
  45. $one = $auth->issue($identity);
  46. $two = $auth->issue($identity);
  47. $redis = Redis::connection(config('mini_protection.connection'));
  48. $accountHash = hash('sha256', $one['user_id']);
  49. $endpointKey = config('mini_protection.prefix') . 'account_endpoint:' . $accountHash . ':' . hash('sha256', 'GET /api/mini/v1/session');
  50. try {
  51. check($one['identities'] === [], 'Fixture has no business permissions');
  52. [$status] = http('/me', $one['access_token']);
  53. check($status === 200, 'Normal authenticated read works');
  54. $limit = config('mini_protection.limits.account_endpoint')[0];
  55. // Arrange only this disposable account near its endpoint threshold; avoid load bursts.
  56. $redis->setex($endpointKey, 30, $limit - 1);
  57. [$status] = http('/session', $one['access_token']);
  58. check($status === 200, 'Last allowed request succeeds');
  59. [$status, $body, $headers] = http('/session?keyword=changed', $two['access_token']);
  60. check($status === 429 && $body['code'] === 42902, 'New token and query cannot evade account endpoint quota');
  61. check((int) ($headers['retry-after'] ?? 0) > 0, 'HTTP Retry-After returned');
  62. check(str_contains($headers['access-control-expose-headers'] ?? '', 'Retry-After'), 'H5 can read retry header');
  63. check(isset($body['request_id']) && !str_contains(json_encode($body), $two['access_token']), 'Correlated response contains no token');
  64. $redis->expire($endpointKey, 0);
  65. [$status] = http('/session', $two['access_token']);
  66. check($status === 200, 'Request recovers after quota expiry');
  67. [$status, $body] = http('/packages?page=101&page_size=100', $one['access_token']);
  68. check($status === 422 && $body['code'] === 42201, 'Deep pagination rejected over HTTP');
  69. [$status] = http('/enterprises/49/home', $one['access_token']);
  70. check($status === 403, 'Existing resource permissions remain enforced');
  71. $auth->logout($two['access_token']);
  72. [$status] = http('/session', $two['access_token']);
  73. check($status === 401, 'Revoked session stays invalid');
  74. } finally {
  75. // Only exact keys for this generated fixture account may be removed.
  76. foreach (['GET /api/mini/v1/session', 'GET /api/mini/v1/me', 'GET /api/mini/v1/enterprises/{id}/home'] as $endpoint) {
  77. $key = config('mini_protection.prefix') . 'account_endpoint:' . $accountHash . ':' . hash('sha256', $endpoint);
  78. $redis->del($key, $key . ':notice');
  79. }
  80. foreach (['account_requests', 'account_list_rows'] as $rule) {
  81. $key = config('mini_protection.prefix') . $rule . ':' . $accountHash;
  82. $redis->del($key, $key . ':notice');
  83. }
  84. (new MiniUser())->getConnection()->transaction(function () use ($one, $identity) {
  85. MiniSession::query()->where('user_id', $one['user_id'])->delete();
  86. MiniUser::query()->where('id', $one['user_id'])->where($identity)->delete();
  87. });
  88. }
  89. echo "PASS: $checks real HTTP protection checks; temporary account removed.\n";