// subscription_test.go:多订阅容灾 / TTL 缓存 / stale-on-error 单测(httptest 环回,无外网)。 // 安全断言:失败摘要不含 URL。 package proxymanager import ( "context" "fmt" "net/http" "net/http/httptest" "strings" "testing" "time" ) func TestSubFetchOK(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { fmt.Fprint(w, sampleSub()) })) defer srv.Close() c := newSubscriptionCache([]string{srv.URL}) text, stale, err := c.Get(context.Background()) if err != nil || stale { t.Fatalf("Get err=%v stale=%v", err, stale) } if !strings.Contains(string(text), "圣何塞") { t.Fatal("正文不符") } // TTL 内二次 Get 不发请求(fetch 次数不变) text2, stale2, _ := c.Get(context.Background()) if stale2 || string(text2) != string(text) { t.Fatal("TTL 内应命中缓存") } } func TestSubMultiURLFailover(t *testing.T) { // 订阅 1 挂、订阅 2 活 → 容灾取 2 srv2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { fmt.Fprint(w, sampleSub()) })) defer srv2.Close() c := newSubscriptionCache([]string{"http://127.0.0.1:1/never", srv2.URL}) text, stale, err := c.Get(context.Background()) if err != nil || stale { t.Fatalf("容灾失败: err=%v stale=%v", err, stale) } if len(text) == 0 { t.Fatal("正文为空") } } func TestSubAllFailStaleOnError(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { fmt.Fprint(w, sampleSub()) })) c := newSubscriptionCache([]string{srv.URL}) if _, _, err := c.Get(context.Background()); err != nil { t.Fatalf("首次拉取失败: %v", err) } // 强制过 TTL,且源已死 → stale-on-error c.mu.Lock() c.fetched = time.Now().Add(-subTTLCache - time.Second) c.mu.Unlock() srv.Close() text, stale, err := c.Get(context.Background()) if err != nil { t.Fatalf("stale-on-error 不应报错: %v", err) } if !stale { t.Fatal("应标记 stale") } if !strings.Contains(string(text), "圣何塞") { t.Fatal("stale 正文应保留") } if !c.Stale() { t.Fatal("Stale() 应为 true") } } func TestSubAllFailNoCacheFailClosed(t *testing.T) { c := newSubscriptionCache([]string{"http://127.0.0.1:1/never"}) _, _, err := c.Get(context.Background()) if err == nil { t.Fatal("全部失败且无缓存应 fail-closed 报错") } if !strings.Contains(err.Error(), "fail-closed") { t.Errorf("错误应标明 fail-closed: %v", err) } // 脱敏断言:错误摘要不含任何 URL for _, e := range c.LastErrors() { if strings.Contains(e, "http://") || strings.Contains(e, "127.0.0.1") { t.Errorf("失败摘要泄漏 URL: %q", e) } } } func TestSubRejectsNonHTTP(t *testing.T) { c := newSubscriptionCache([]string{"file:///etc/passwd"}) _, _, err := c.Get(context.Background()) if err == nil { t.Fatal("非 http(s) URL 应拒绝") } } func TestSubCappedRead(t *testing.T) { // 8MB 上限:超限拒绝 big := strings.Repeat("a", subMaxBytes+10) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { fmt.Fprint(w, big) })) defer srv.Close() c := newSubscriptionCache([]string{srv.URL}) _, _, err := c.Get(context.Background()) if err == nil || !strings.Contains(err.Error(), "8MB") { t.Fatalf("超 8MB 应拒绝, err=%v", err) } } func TestSubURLNeverInError(t *testing.T) { secretURL := "http://127.0.0.1:1/path?token=SECRET_TOKEN_XYZ" c := newSubscriptionCache([]string{secretURL}) _, _, err := c.Get(context.Background()) if err == nil { t.Fatal("应失败") } if strings.Contains(err.Error(), "SECRET_TOKEN_XYZ") { t.Fatalf("错误信息泄漏订阅 URL: %v", err) } if !c.Stale() && len(c.LastErrors()) == 0 { t.Fatal("应有失败摘要记录") } for _, e := range c.LastErrors() { if strings.Contains(e, "SECRET_TOKEN_XYZ") || strings.Contains(e, "http") { t.Errorf("摘要泄漏 URL: %q", e) } } } func TestSubFetchViaBootstrapProxy(t *testing.T) { // 直连目标不可达;请求必须打到 bootstrap 代理才拿得到正文。 proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { fmt.Fprint(w, sampleSub()) })) defer proxy.Close() t.Setenv("BROWSER_SERVER_SUB_FETCH_PROXY", proxy.URL) c := newSubscriptionCache([]string{"http://127.0.0.1:1/never-direct"}) text, stale, err := c.Get(context.Background()) if err != nil || stale { t.Fatalf("经 bootstrap 应成功: err=%v stale=%v", err, stale) } if !strings.Contains(string(text), "圣何塞") { t.Fatal("正文不符") } } func TestSubClientEmptyProxyDirect(t *testing.T) { t.Setenv("BROWSER_SERVER_SUB_FETCH_PROXY", "") c := newSubClient() tr, ok := c.hc.Transport.(*http.Transport) if !ok { t.Fatal("Transport 类型不符") } if tr.Proxy != nil { t.Fatal("缺省应直连(Proxy 未挂)") } } func TestSubTTLCacheWindow(t *testing.T) { var hits int srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { hits++ fmt.Fprint(w, sampleSub()) })) defer srv.Close() c := newSubscriptionCache([]string{srv.URL}) for i := 0; i < 5; i++ { if _, _, err := c.Get(context.Background()); err != nil { t.Fatal(err) } } if hits != 1 { t.Errorf("TTL 内 5 次 Get 应只拉 1 次, hits=%d", hits) } // 强制过期后再拉 c.mu.Lock() c.fetched = time.Now().Add(-subTTLCache - time.Second) c.mu.Unlock() if _, _, err := c.Get(context.Background()); err != nil { t.Fatal(err) } if hits != 2 { t.Errorf("过期后应重拉, hits=%d", hits) } }