Files
affiliate_dash/backend/internal/service/rock_baseurl_test.go
T
yml2213 80e2bbf875 fix(rock): 上游地址改为以数据库配置为准,并消除 baseURL 数据竞争
GetConfig 从不把 rock_configs.base_url 回写到客户端,main.go 又硬编码了
https://0666.pro,导致后台改上游地址「保存那一刻生效、进程重启后失效」,
数据库里的值被静默忽略且无任何提示。

- 新增 model.RockDefaultBaseURL 统一默认值来源
- 新增 RockService.ApplyConfigToClient,启动时把 DB 地址同步到客户端
- UpdateConfig 改用 client.SetBaseURL 立即生效,不再直接写字段
- RockClient.baseURL 原为裸字段,被 HTTP 请求线程与库存巡检 goroutine 并发
  读写(数据竞争),改为 sync.RWMutex 保护并新增 BaseURL/SetBaseURL
- ClaimTokens/RefreshToken/RedeemSkin 的 Origin/Referer 跟随配置地址
- 空值回落默认地址,末尾斜杠自动裁剪

为保持该提交可编译,同一次改动把 rock_service.go(1177 行)按域拆分为
8 个文件(最大 221 行):rock_service / rock_bindlink / rock_stock /
rock_assign / rock_bindcheck / rock_redeem / rock_recover / rock_snapshot。
35 个声明块全部有归属,无遗漏无重复;迁移文件 012 受 checksum 保护故未改动。

验证:TestRockServiceBaseURLFromConfig 覆盖「改地址→立即生效→重启→仍生效」;
并发用例在 -race 下通过;全量测试通过。
2026-10-06 19:42:58 +08:00

124 lines
4.0 KiB
Go

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
}