Files
MeBox/internal/service/reader/http_encoding_test.go
T
2026-10-01 21:57:25 +08:00

120 lines
4.3 KiB
Go
Raw 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.
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)
}
}