onesvm-browser-server/server/internal/proxymanager/subscription_test.go
chii eb972dfa93 feat: 落地 browser-server 控制面并打通 mgr1 海外订阅
单二进制三角色 + Dock 适配器 + Swarm stack 达到可部署态;mgr1 实测订阅经 central-proxy bootstrap,探活 alive=41/52。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 15:05:12 +08:00

193 lines
5.5 KiB
Go
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.

// 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)
}
}