package service import ( "context" "net/http" "net/http/httptest" "testing" "affiliate_dash/internal/model" ) // TestRockServiceBaseURLFromConfig 锁定「后台配置的上游地址必须真正生效,并且能跨进程重启」。 // // 回归背景:main.go 曾把 baseURL 硬编码进 NewRockClient,而 GetConfig 从 // 不把 rock_configs.base_url 回写到客户端。结果是后台改了地址在保存那一刻生效, // 进程一重启就悄悄退回 0666.pro。 func TestRockServiceBaseURLFromConfig(t *testing.T) { db := newRockTestDB(t) ctx := context.Background() client := NewRockClient(model.RockDefaultBaseURL) svc := NewRockService(db, client) // 首次读取会初始化配置,默认地址应与常量一致。 cfg, err := svc.GetConfig(ctx) if err != nil { t.Fatalf("读取 Rock 配置失败: %v", err) } if cfg.BaseURL != model.RockDefaultBaseURL { t.Fatalf("初始 base_url 期望 %s,实际 %s", model.RockDefaultBaseURL, cfg.BaseURL) } // 后台改地址后必须立即生效,不需要重启。 custom := "https://rock-mirror.example.com" if _, err := svc.UpdateConfig(ctx, UpdateRockConfigReq{BaseURL: &custom}); err != nil { t.Fatalf("更新 Rock 配置失败: %v", err) } if got := client.BaseURL(); got != custom { t.Fatalf("UpdateConfig 未立即生效:期望 %s,实际 %s", custom, got) } // 模拟进程重启:新的客户端 + 新的 service,读同一个数据库。 restarted := NewRockClient(model.RockDefaultBaseURL) restartedSvc := NewRockService(db, restarted) if got := restarted.BaseURL(); got != model.RockDefaultBaseURL { t.Fatalf("同步前应保持构造时传入的地址,实际 %s", got) } if err := restartedSvc.ApplyConfigToClient(ctx); err != nil { t.Fatalf("启动同步 Rock 配置失败: %v", err) } if got := restarted.BaseURL(); got != custom { t.Fatalf("重启后未采用数据库中的上游地址:期望 %s,实际 %s", custom, got) } } func TestRockClientSetBaseURL(t *testing.T) { cases := []struct { name string input string want string }{ {"空值回落到默认地址", "", model.RockDefaultBaseURL}, {"纯空白回落到默认地址", " ", model.RockDefaultBaseURL}, {"裁剪末尾斜杠", "https://a.example.com/", "https://a.example.com"}, {"裁剪多余空白与斜杠", " https://b.example.com// ", "https://b.example.com"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { client := NewRockClient(tc.input) if got := client.BaseURL(); got != tc.want { t.Fatalf("期望 %q,实际 %q", tc.want, got) } }) } } // TestRockClientOriginHeaderFollowsBaseURL 确认 Origin / Referer 跟随配置地址, // 而不是始终写死上游域名。 func TestRockClientOriginHeaderFollowsBaseURL(t *testing.T) { var gotOrigin, gotReferer string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotOrigin = r.Header.Get("Origin") gotReferer = r.Header.Get("Referer") w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"tokens":[{"id":1,"redeem_code":"ABC"}]}`)) })) defer server.Close() client := NewRockClient(server.URL) if _, err := client.ClaimTokens(context.Background(), "bearer", model.RockColorBlack, 1); err != nil { t.Fatalf("claim 失败: %v", err) } if gotOrigin != server.URL { t.Fatalf("Origin 期望 %s,实际 %s", server.URL, gotOrigin) } if want := server.URL + "/"; gotReferer != want { t.Fatalf("Referer 期望 %s,实际 %s", want, gotReferer) } } // TestRockClientBaseURLConcurrentAccess 在 -race 下验证读写 baseURL 无数据竞争。 func TestRockClientBaseURLConcurrentAccess(t *testing.T) { client := NewRockClient(model.RockDefaultBaseURL) done := make(chan struct{}) go func() { defer close(done) for i := 0; i < 200; i++ { client.SetBaseURL("https://rock-mirror.example.com") client.SetBaseURL(model.RockDefaultBaseURL) } }() for i := 0; i < 200; i++ { if got := client.BaseURL(); got == "" { t.Error("BaseURL 不应为空") break } } <-done }