mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-02 20:26:36 +08:00
120 lines
4.3 KiB
Go
120 lines
4.3 KiB
Go
package reader
|
||
|
||
import (
|
||
"compress/gzip"
|
||
"encoding/json"
|
||
"fmt"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// 本文件:规则层必须拿到「解压后」的响应体。
|
||
//
|
||
// 回归背景:HTTPHeaderPresets 曾显式写 `Accept-Encoding: gzip, deflate, br`。
|
||
// net/http 只在调用方「没有」设置该头时才会自动补 gzip 并透明解压,显式设置会
|
||
// 让它原样交出压缩字节。于是凡是走了压缩的上游(静态 config.json、CDN 页面等)
|
||
// 都会把 gzip 字节喂给书源的 JSON.parse,报
|
||
// SyntaxError: invalid character '\x1f' looking for beginning of value
|
||
// 表现出来就是「获取最新配置失败:SyntaxError: Unexpected end of JSON input」,
|
||
// 并且书源会误判为线路故障,逐条切换全部线路后仍然失败。
|
||
|
||
// gzipJSONServer 返回一个「客户端支持压缩时才压缩」的站点,模拟真实 CDN。
|
||
func gzipJSONServer(t *testing.T, payload string) (*httptest.Server, *string) {
|
||
t.Helper()
|
||
var seenAcceptEncoding string
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
seenAcceptEncoding = r.Header.Get("Accept-Encoding")
|
||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||
if strings.Contains(seenAcceptEncoding, "gzip") {
|
||
w.Header().Set("Content-Encoding", "gzip")
|
||
gz := gzip.NewWriter(w)
|
||
_, _ = gz.Write([]byte(payload))
|
||
_ = gz.Close()
|
||
return
|
||
}
|
||
_, _ = w.Write([]byte(payload))
|
||
}))
|
||
return srv, &seenAcceptEncoding
|
||
}
|
||
|
||
// readConfigSourceJSON 构造一个用 java.ajax 取配置并 JSON.parse 的书源。
|
||
func readConfigSourceJSON(t *testing.T, serverURL, target string, sourceHeader string) string {
|
||
t.Helper()
|
||
loginJS := fmt.Sprintf(`function readConfig() {
|
||
let data = java.ajax(%q);
|
||
let js = JSON.parse(String(data));
|
||
java.longToast('配置版本=' + js.version);
|
||
}`, target)
|
||
src := map[string]any{
|
||
"bookSourceUrl": serverURL,
|
||
"bookSourceName": "压缩测试源",
|
||
"loginUrl": loginJS,
|
||
"loginUi": `[{"name":"读配置","type":"button","action":"readConfig()"}]`,
|
||
}
|
||
if sourceHeader != "" {
|
||
src["header"] = sourceHeader
|
||
}
|
||
out, err := json.Marshal(src)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return string(out)
|
||
}
|
||
|
||
// TestReaderJSSeesDecompressedBody 上游压缩的响应必须先解压再进规则层。
|
||
func TestReaderJSSeesDecompressedBody(t *testing.T) {
|
||
const payload = `{"version":"20260926"}`
|
||
srv, seenAE := gzipJSONServer(t, payload)
|
||
defer srv.Close()
|
||
|
||
svc, _ := newLoginTestService(t)
|
||
sourceID := prepareLoginSource(t, svc, readConfigSourceJSON(t,
|
||
srv.URL, srv.URL+"/static/source_config/config.json", ""))
|
||
|
||
res, err := svc.RunLoginAction(t.Context(), readerTestUserID, sourceID, "readConfig()", nil)
|
||
if err != nil {
|
||
t.Fatalf("动作执行失败: %v", err)
|
||
}
|
||
if !res.OK {
|
||
t.Fatalf("动作未成功: %+v", res)
|
||
}
|
||
joined := strings.Join(res.Toasts, "\n")
|
||
if !strings.Contains(joined, "配置版本=20260926") {
|
||
t.Fatalf("规则层拿到的不是解压后的 JSON(压缩字节泄漏到 JSON.parse): %v", res.Toasts)
|
||
}
|
||
// 必须仍然协商压缩,否则等于用「不压缩」回避问题,真实站点该压还是压
|
||
if !strings.Contains(*seenAE, "gzip") {
|
||
t.Fatalf("请求应携带 gzip(由 net/http 自动添加),实际 %q", *seenAE)
|
||
}
|
||
}
|
||
|
||
// TestReaderStripsAcceptEncodingFromSourceHeader 书源 header 字段里的
|
||
// Accept-Encoding 也必须被清掉:显式设置会让 net/http 放弃解压,
|
||
// 而且一旦服务端选了 brotli,我们用标准库根本解不出来。
|
||
func TestReaderStripsAcceptEncodingFromSourceHeader(t *testing.T) {
|
||
const payload = `{"version":"20260926"}`
|
||
srv, seenAE := gzipJSONServer(t, payload)
|
||
defer srv.Close()
|
||
|
||
svc, _ := newLoginTestService(t)
|
||
sourceID := prepareLoginSource(t, svc, readConfigSourceJSON(t,
|
||
srv.URL, srv.URL+"/static/source_config/config.json",
|
||
`{"Accept-Encoding":"br, gzip"}`))
|
||
|
||
res, err := svc.RunLoginAction(t.Context(), readerTestUserID, sourceID, "readConfig()", nil)
|
||
if err != nil {
|
||
t.Fatalf("动作执行失败: %v", err)
|
||
}
|
||
if !res.OK {
|
||
t.Fatalf("动作未成功: %+v", res)
|
||
}
|
||
if !strings.Contains(strings.Join(res.Toasts, "\n"), "配置版本=20260926") {
|
||
t.Fatalf("规则层未拿到解压后的 JSON: %v", res.Toasts)
|
||
}
|
||
if strings.Contains(*seenAE, "br") {
|
||
t.Fatalf("书源 header 里的 Accept-Encoding 未被清理(服务端可能回 brotli): %q", *seenAE)
|
||
}
|
||
}
|