mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-02 12:26:36 +08:00
优化阅读功能
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user