load(); \support\App::loadAllConfig(['route']); use app\miniapi\model\MiniUser; use app\miniapi\model\MiniSession; use app\miniapi\service\AuthService; use support\Redis; if (config('mini_api.environment') !== 'local' || !config('mini_protection.enabled')) { throw new RuntimeException('Run only against the local enabled protection service'); } $checks = 0; function check(bool $ok, string $label): void { global $checks; $checks++; if (!$ok) { throw new RuntimeException($label); } } function http(string $path, string $token, array $headers = []): array { $responseHeaders = []; $curl = curl_init('http://127.0.0.1:8787/api/mini/v1' . $path); curl_setopt_array($curl, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5, CURLOPT_HTTPHEADER => array_merge(['Authorization: Bearer ' . $token, 'Origin: http://127.0.0.1:8787'], $headers), CURLOPT_HEADERFUNCTION => static function ($handle, $line) use (&$responseHeaders) { if (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower($name)] = trim($value); } return strlen($line); }, ]); $raw = curl_exec($curl); $status = curl_getinfo($curl, CURLINFO_HTTP_CODE); curl_close($curl); return [$status, json_decode((string) $raw, true), $responseHeaders]; } $auth = new AuthService(); $identity = ['appid' => 'protection-http-test', 'openid' => bin2hex(random_bytes(16))]; $one = $auth->issue($identity); $two = $auth->issue($identity); $redis = Redis::connection(config('mini_protection.connection')); $accountHash = hash('sha256', $one['user_id']); $endpointKey = config('mini_protection.prefix') . 'account_endpoint:' . $accountHash . ':' . hash('sha256', 'GET /api/mini/v1/session'); try { check($one['identities'] === [], 'Fixture has no business permissions'); [$status] = http('/me', $one['access_token']); check($status === 200, 'Normal authenticated read works'); $limit = config('mini_protection.limits.account_endpoint')[0]; // Arrange only this disposable account near its endpoint threshold; avoid load bursts. $redis->setex($endpointKey, 30, $limit - 1); [$status] = http('/session', $one['access_token']); check($status === 200, 'Last allowed request succeeds'); [$status, $body, $headers] = http('/session?keyword=changed', $two['access_token']); check($status === 429 && $body['code'] === 42902, 'New token and query cannot evade account endpoint quota'); check((int) ($headers['retry-after'] ?? 0) > 0, 'HTTP Retry-After returned'); check(str_contains($headers['access-control-expose-headers'] ?? '', 'Retry-After'), 'H5 can read retry header'); check(isset($body['request_id']) && !str_contains(json_encode($body), $two['access_token']), 'Correlated response contains no token'); $redis->expire($endpointKey, 0); [$status] = http('/session', $two['access_token']); check($status === 200, 'Request recovers after quota expiry'); [$status, $body] = http('/packages?page=101&page_size=100', $one['access_token']); check($status === 422 && $body['code'] === 42201, 'Deep pagination rejected over HTTP'); [$status] = http('/enterprises/49/home', $one['access_token']); check($status === 403, 'Existing resource permissions remain enforced'); $auth->logout($two['access_token']); [$status] = http('/session', $two['access_token']); check($status === 401, 'Revoked session stays invalid'); } finally { // Only exact keys for this generated fixture account may be removed. foreach (['GET /api/mini/v1/session', 'GET /api/mini/v1/me', 'GET /api/mini/v1/enterprises/{id}/home'] as $endpoint) { $key = config('mini_protection.prefix') . 'account_endpoint:' . $accountHash . ':' . hash('sha256', $endpoint); $redis->del($key, $key . ':notice'); } foreach (['account_requests', 'account_list_rows'] as $rule) { $key = config('mini_protection.prefix') . $rule . ':' . $accountHash; $redis->del($key, $key . ':notice'); } (new MiniUser())->getConnection()->transaction(function () use ($one, $identity) { MiniSession::query()->where('user_id', $one['user_id'])->delete(); MiniUser::query()->where('id', $one['user_id'])->where($identity)->delete(); }); } echo "PASS: $checks real HTTP protection checks; temporary account removed.\n";