Files
ykxmz/_build/test_response_headers.py
xmz7891 c97d7e8961 修「去密码硬编码」的第三批机械替换伤
上一轮的批量替换又留下四处伤,全都表现成"测试红了/文件坏了",
根子都在替换脚本不看上下文。

== 1. import os 插到了 __future__ 前面 ==

fix_password_round2.py 给文件补 `import os` 时,直接插在第一个
import 之前。但那个文件是:

    '''
    from __future__ import annotations
    import ast

被插成了 `import os` 在 `from __future__` 之前 ——
Python 要求 __future__ 必须是第一条语句,于是整个文件语法错:

    SyntaxError: from __future__ imports must occur at the beginning

受影响:_build/test_response_headers.py

== 2. 把中文说明塞进了字符串字面量 ==

同一个脚本里:
    t = t.replace(OLD, "(从 RELAY_ADMIN_PASSWORD 环境变量读)")

凡 `"@<老密码>"` 这种形状(@ 在引号里)都被替换成字符串:

    PASS = "@(从 RELAY_ADMIN_PASSWORD 环境变量读)"
    ADMIN_PASS = "@(从 RELAY_ADMIN_PASSWORD 环境变量读)"

本该是 `(os.environ.get("RELAY_ADMIN_PASSWORD") or "").strip()`。

受影响:_build/test_response_headers.py、
        _build/split_site_admin2.py

== 3. 测试自己起的中转拿不到密码(第二批漏了一个文件)==

test_response_headers.py 也是自己起本地中转,然后用 PASS 登录。
PASS 从环境变量取,本地跑时是空的 -> 登录失败 -> 401
-> 表现成「登录拿到 cookie(没拿到)」,看起来像登录功能坏了。

和上一批同样的修法:没设环境变量就**生成一个本次运行专用的随机密码**,
同时通过 env 和 data/admin_password.txt 告诉那台测试中转。

== 4. 模块级调用写在函数定义之前 ==

test_relay_admin_login.py 里 `PASSWORD = _admin_password()`
在模块级第 29 行,而 `def _admin_password()` 在第 64 行 ——
执行时 NameError(pyflakes 也报 undefined name)。
把这个赋值挪到 helper 定义之后。

== 5. 顺手清掉一个语法就是坏的死脚本 ==

_build/poll60_patch.py 第 49 行有个未闭合字符串,从来跑不起来,
也没人引用 —— 它是一次性补丁,改完就该删。留着只会让静态检查
一直报错。已 git rm(历史里还能查到内容)。

== 6. verify_all.ps1 的 Show-Fails 会崩 ==

    $lines | Select-Object -Last 1 | ForEach-Object {
        Write-Host "  $($_.Trim())"
    }

子进程的 stderr 被塞进输出流时,$_ 是 ErrorRecord,没有 .Trim()
方法 -> 抛 "does not contain a method named 'Trim'",
反而把真正的失败信息盖住了。改成先转字符串再 Trim。

== 验证 ==

    网页响应头      12/12 通过
    管理页设备控制  23/23 通过
    管理页批删      24/24 通过
    管理页按钮      10/10 通过
    管理页网络体检  15/15 通过
    密码随机生成    11/11 通过
    全仓语法检查    0 个问题

教训再记一笔:**机械替换必须替换完就验证**,
而且兜底的 str.replace 一定要慎用 —— 它不看上下文,
"函数调用进引号" 和 "中文说明进字符串" 都是这么来的。
2026-10-07 21:04:59 +08:00

202 lines
7.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
r'''加一个"响应头"检查,并把这次的坑记下来。
踩的坑:
重做管理页时,_send() 的 content_type 默认是 application/json,
我调它的时候没传 text/html —— 结果浏览器把整页 HTML
当纯文本显示,用户看到的是一屏源码。
更糟的是**我当时没发现**:验证脚本只看了 HTTP 200 和字节数,
两个都对,就以为没问题了。**200 不等于页面能看。**
所以这个检查必须看响应头,不能只看状态码。
顺便也发现 HEAD 请求返回 501(没实现 do_HEAD),一起补了。
'''
from __future__ import annotations
import os
import ast
import json
import subprocess
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
PORT = 8944
PASS = (os.environ.get("RELAY_ADMIN_PASSWORD") or "").strip()
if not PASS:
# 本地跑的时候环境变量通常没设。这里生成一个**本次运行专用**的随机密码,
# 下面会同时通过 env 和 data/admin_password.txt 告诉这台测试中转 ——
# 两边就一致了。不能写死,写死又变成"密码进仓库"。
import secrets as _secrets
PASS = "t" + _secrets.token_hex(12)
def wait_port(seconds: float = 12.0) -> bool:
deadline = time.time() + seconds
while time.time() < deadline:
try:
with urllib.request.urlopen(
"http://127.0.0.1:%d/v1/health" % PORT, timeout=2):
return True
except Exception: # noqa: BLE001
time.sleep(0.5)
return False
def head(path: str, cookie: str = "") -> dict:
"""发一个 HEAD,取回响应头。"""
req = urllib.request.Request("http://127.0.0.1:%d%s" % (PORT, path),
method="HEAD")
if cookie:
req.add_header("Cookie", cookie)
try:
with urllib.request.urlopen(req, timeout=8) as resp:
return {"status": resp.status,
"ctype": resp.headers.get("Content-Type", ""),
"len": resp.headers.get("Content-Length", "")}
except urllib.error.HTTPError as exc:
return {"status": exc.code,
"ctype": exc.headers.get("Content-Type", ""), "len": ""}
except Exception as exc: # noqa: BLE001
return {"status": 0, "ctype": "", "len": str(exc)}
def login() -> str:
"""登录拿 cookie。
用 http.cookiejar 而不是手工读 Set-Cookie ——
/admin/login 会 302 跳转,urllib 默认跟着跳,
Set-Cookie 头在第一个响应上就被丢掉了,手工读永远是空。
cookiejar 会把中间那次的 cookie 存下来。
"""
import http.cookiejar
jar = http.cookiejar.CookieJar()
opener = urllib.request.build_opener(
urllib.request.HTTPCookieProcessor(jar))
body = ("username=xmz7891&password=" + PASS).encode()
req = urllib.request.Request(
"http://127.0.0.1:%d/admin/login" % PORT, data=body, method="POST")
req.add_header("Content-Type", "application/x-www-form-urlencoded")
try:
opener.open(req, timeout=8).read()
except Exception: # noqa: BLE001
pass
for cookie in jar:
if cookie.name == "lr_admin":
return "%s=%s" % (cookie.name, cookie.value)
return ""
def main() -> int:
env = dict(__import__("os").environ)
env["RELAY_PORT"] = str(PORT)
env["RELAY_ADMIN_PASSWORD"] = PASS
data = ROOT / "_build" / "_hdr_data"
data.mkdir(parents=True, exist_ok=True)
env["RELAY_DATA"] = str(data)
# 中转把管理密码放在 DATA_DIR/data/admin_password.txt,
# 没有这个文件就用内置默认值。为了让测试不依赖默认值,
# 这里显式写一份,保证登录能成。
auth_dir = data / "data"
auth_dir.mkdir(parents=True, exist_ok=True)
(auth_dir / "admin_password.txt").write_text(PASS, encoding="utf-8")
# /shot/ 路由是从 relay.py 所在目录找 site_shots/。
# 仓库里那张图在 _build/site_shots/,这里拷一份过去,
# 否则那条断言会因为"文件不存在"而误报。
shot_src = ROOT / "_build" / "site_shots" / "pc-main.png"
shot_dst = ROOT / "relay" / "site_shots" / "pc-main.png"
if shot_src.is_file():
shot_dst.parent.mkdir(parents=True, exist_ok=True)
if not shot_dst.is_file():
shot_dst.write_bytes(shot_src.read_bytes())
print(" 启动中转(端口 %d)" % PORT)
proc = subprocess.Popen(
[sys.executable, "relay.py"], cwd=str(ROOT / "relay"), env=env,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
passed = failed = 0
def check(name, ok, detail=""):
nonlocal passed, failed
if ok:
passed += 1
print(" [PASS] " + name)
else:
failed += 1
print(" [FAIL] " + name + " " + str(detail))
try:
if not wait_port():
print(" [失败] 中转没起来")
return 1
print()
print(" 一、HEAD 请求要能用(原来一律 501)")
for path in ("/", "/v1/health", "/favicon.ico"):
info = head(path)
check("HEAD %s 不是 501" % path, info["status"] != 501,
info["status"])
print()
print(" 二、Content-Type 必须是 text/html(否则浏览器显示源码)")
site = head("/")
check("官网 Content-Type 是 text/html",
"text/html" in site["ctype"], site["ctype"])
cookie = login()
check("登录拿到 cookie", bool(cookie), cookie or "(没拿到)")
admin = head("/zhongzhuanzhan", cookie)
check("管理页 Content-Type 是 text/html",
"text/html" in admin["ctype"],
admin["ctype"] or "(空)")
check("管理页不是 application/json",
"json" not in admin["ctype"].lower(), admin["ctype"])
print()
print(" 二b、HEAD 下载文件要报对长度(不能是 0)")
# 这条路原来漏了:HEAD 一个 APK 会返回 Content-Length: 0
# + text/plain,下载器据此认为文件是空的。
dldir = ROOT / "relay" / "downloads"
dldir.mkdir(parents=True, exist_ok=True)
probe = dldir / "probe-1.8.0.apk"
probe.write_bytes(b"PK\\x03\\x04" + b"x" * 4096)
info = head("/download/probe-1.8.0.apk")
check("HEAD 下载返回 200", info["status"] == 200, info["status"])
check("HEAD 下载 Content-Length 不为 0",
info["len"] not in ("", "0"), info["len"])
check("HEAD 下载类型是 android 包",
"android" in info["ctype"].lower()
or "octet-stream" in info["ctype"].lower(), info["ctype"])
print()
print(" 三、其它资源的类型")
icon = head("/favicon.ico")
check("favicon 是 image/*", "image" in icon["ctype"], icon["ctype"])
if shot_dst.is_file():
shot = head("/shot/pc-main.png")
check("截图是 image/*", "image" in shot["ctype"], shot["ctype"])
else:
print(" [跳过] 截图类型(本地没有 site_shots/pc-main.png)")
finally:
proc.terminate()
try:
proc.wait(timeout=8)
except subprocess.TimeoutExpired:
proc.kill()
print()
print("=" * 54)
print(" 通过 %d 项,失败 %d 项" % (passed, failed))
return 0 if failed == 0 else 1
if __name__ == "__main__":
sys.exit(main())