fix: Gate3 health plain text, refresh empty body, prune underscore chunks
test_api: assert /health returns plain text ok instead of JSON parse.
auth: RefreshRequest.refresh_token optional so SPA POST {} is not 422.
prune: allow leading underscore in asset names to avoid deleting _plugin-vue_export-helper.
Add run_test_api_on_server.sh and deployment follow-up changelog.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10,8 +10,9 @@ import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Leading `_` required for Vite chunks like `_plugin-vue_export-helper-*.js`
|
||||
ASSET_NAME_RE = re.compile(
|
||||
r"[A-Za-z0-9][A-Za-z0-9_.-]*\.(?:js|css|woff2?|ttf|eot)"
|
||||
r"[_A-Za-z0-9][A-Za-z0-9_.-]*\.(?:js|css|woff2?|ttf|eot)"
|
||||
)
|
||||
URL_RE = re.compile(r"""url\(["']?([^"')]+)["']?\)""")
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
# Run tests/test_api.py on the deploy host (reads NEXUS_TEST_* from .env).
|
||||
set -euo pipefail
|
||||
cd /www/wwwroot/api.synaglobal.vip
|
||||
export NEXUS_TEST_BASE="${NEXUS_TEST_BASE:-http://127.0.0.1:8600}"
|
||||
python3 tests/test_api.py
|
||||
@@ -0,0 +1,56 @@
|
||||
# Changelog — test_api 健康检查与生产 Gate 跟进
|
||||
|
||||
**日期**: 2026-06-01
|
||||
**类型**: 修复 / 运维跟进
|
||||
|
||||
## 摘要
|
||||
|
||||
- `tests/test_api.py`:`GET /health` 改为断言纯文本 `ok`(与 `server/api/health.py` 的 `PlainTextResponse` 一致),不再 `json.loads`。
|
||||
- 生产 `.env`:`NEXUS_TEST_ADMIN_PASSWORD` 须与数据库 `admin` 密码一致;原值 `Nexus@2026Test` 导致 Gate 3 登录 401。
|
||||
|
||||
## 动机
|
||||
|
||||
部署后 `python3 tests/test_api.py` 18/18 失败:健康检查因期望 JSON 解析失败;其余用例因未拿到 JWT 连锁失败。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `tests/test_api.py` — 新增 `test_plain_text()`,`/health` 用例改用
|
||||
- 生产 `/www/wwwroot/api.synaglobal.vip/.env` — `NEXUS_TEST_ADMIN_PASSWORD` 对齐真实管理员密码(运维操作,不入库)
|
||||
|
||||
## 前置条件
|
||||
|
||||
- `NEXUS_TEST_ADMIN_USER` / `NEXUS_TEST_ADMIN_PASSWORD` 与 MySQL `admins` 表一致
|
||||
- 后端监听 `NEXUS_TEST_BASE`(默认 `http://127.0.0.1:8600`)
|
||||
|
||||
## 迁移 / 重启
|
||||
|
||||
- 仅改测试脚本:无需重启;改 `.env` 后无需重启(测试进程读环境变量)
|
||||
- 前端 assets prune:务必先 `python deploy/prune_frontend_assets.py --dry-run`,确认 `would_remove` 合理后再实删
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
# 服务器
|
||||
cd /www/wwwroot/api.synaglobal.vip
|
||||
python3 tests/test_api.py
|
||||
# 预期:Results: N/N passed, 0 failed
|
||||
|
||||
curl -s http://127.0.0.1:8600/health # ok
|
||||
```
|
||||
|
||||
## 回滚
|
||||
|
||||
- 还原 `tests/test_api.py` 中 health 用例为旧版(不推荐,与 API 契约不符)
|
||||
- `.env` 测试密码改回前值仅当故意使用独立测试账号时
|
||||
|
||||
## 2026-06-01 晚间追加(前端部署)
|
||||
|
||||
- `vite build` + tar/scp 部署至生产 `web/app/`
|
||||
- **事故**:`prune_frontend_assets.py` 正则首字符不允许 `_`,误删 `_plugin-vue_export-helper-*.js` → 全站白屏;已 scp 恢复并修复正则 `[_A-Za-z0-9]...`
|
||||
- **后端**:`RefreshRequest.refresh_token` 改为 Optional,空 body `{}` 不再 422
|
||||
- **浏览器验证**:`#/servers` 勾选 2 台后出现「已选择 2 台服务器」及批量按钮(含 `normalizeServerIds` 的 `ServersPage-Vzm34hgn.js`)
|
||||
|
||||
## 关联
|
||||
|
||||
- `docs/changelog/2026-06-01-bug-remediation.md`
|
||||
- `deploy/prune_frontend_assets.py`(Rolldown 闭包引用扫描;须匹配 `_plugin-vue_export-helper` 等 chunk)
|
||||
+4
-2
@@ -74,7 +74,9 @@ class LoginRequest(BaseModel):
|
||||
|
||||
|
||||
class RefreshRequest(BaseModel):
|
||||
refresh_token: str = Field(..., min_length=1)
|
||||
"""Body optional — refresh token is read from HttpOnly cookie first."""
|
||||
|
||||
refresh_token: Optional[str] = Field(default=None, min_length=1)
|
||||
|
||||
|
||||
class LogoutRequest(BaseModel):
|
||||
@@ -152,7 +154,7 @@ async def login(
|
||||
async def refresh_token(
|
||||
request: Request,
|
||||
response: Response,
|
||||
payload: Optional[RefreshRequest] = None,
|
||||
payload: RefreshRequest | None = None,
|
||||
service: AuthService = Depends(get_auth_service),
|
||||
):
|
||||
"""Exchange refresh token for new access token (with rotation + version-based reuse detection)
|
||||
|
||||
+26
-2
@@ -120,15 +120,39 @@ def test(name: str, method: str, path: str, body=None, expect_code=200, headers=
|
||||
return None
|
||||
|
||||
|
||||
def test_plain_text(name: str, method: str, path: str, expect_body: str, expect_code=200):
|
||||
"""Health and other probes that return plain text, not JSON."""
|
||||
global PASS, FAIL
|
||||
url = f"{BASE}{path}"
|
||||
req = urllib.request.Request(url, method=method)
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, timeout=10)
|
||||
code = resp.getcode()
|
||||
body = resp.read().decode().strip()
|
||||
if code == expect_code and body == expect_body:
|
||||
PASS += 1
|
||||
print(f" [PASS] {name}")
|
||||
return body
|
||||
FAIL += 1
|
||||
print(f" [FAIL] {name}: expected {expect_code} body={expect_body!r}, got {code} body={body!r}")
|
||||
except urllib.error.HTTPError as e:
|
||||
FAIL += 1
|
||||
print(f" [FAIL] {name}: HTTP {e.code} — {e.read().decode()[:200]}")
|
||||
except Exception as e:
|
||||
FAIL += 1
|
||||
print(f" [FAIL] {name}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
# ================================================================
|
||||
print("=" * 50)
|
||||
print("Nexus API End-to-End Tests")
|
||||
print(f" Target: {BASE}")
|
||||
print("=" * 50)
|
||||
|
||||
# --- Health (no auth) ---
|
||||
# --- Health (no auth) — plain text per health.py (Shell guardian) ---
|
||||
print("\n[1] Health")
|
||||
test("GET /health", "GET", "/health")
|
||||
test_plain_text("GET /health", "GET", "/health", expect_body="ok")
|
||||
|
||||
# --- Auth: Login ---
|
||||
print("\n[2] Auth")
|
||||
|
||||
Reference in New Issue
Block a user