package core import ( "encoding/json" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" ) // 凭据静默续期(照参考 refresh.ts / cline.ts / gemini-oauth.ts)。 // // ⚠️ 修的是一个**比不做更坏**的缺陷:`account_refresh` 此前只重同步源、一个换 // token 的请求都不发,却恒返回 `ok:true` ⇒ `refresh_creds` 打出「凭据已续期 N 个」 // 而实际什么都没续。日志在说谎,排查时会去看网络、看上游,而真因是根本没有 // 这段代码。 // // 判据必须**看到真请求** —— 否则就还是在验一个恒真的返回值。 func TestAccountRefreshSendsRealRequest(t *testing.T) { var hits int32 var gotBody, gotPath string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { atomic.AddInt32(&hits, 1) gotPath = r.URL.Path b := make([]byte, 1024) n, _ := r.Body.Read(b) gotBody = string(b[:n]) w.Header().Set("Content-Type", "application/json") // 照参考:Cline 响应套 {success,data} 信封,字段是**驼峰** accessToken _, _ = w.Write([]byte(`{"success":true,"data":{"accessToken":"NEW-TOKEN-CAMEL","refreshToken":"NEW-REFRESH"}}`)) })) defer srv.Close() _, ps := poolCore(t) // 注入一个测试平台(夹具绝不写进生产源码) if _, err := ps.Call("channels", "__inject_refresh_platform_probe", map[string]interface{}{ "id": "rfptest", "refresh_url": srv.URL + "/api/v1/auth/refresh", "style": "cline", "credential": `{"access_token":"OLD","refresh_token":"RT1"}`, }); err != nil { t.Fatalf("注入平台: %v", err) } out, err := ps.Call("channels", "account_refresh", map[string]interface{}{ "platform": "rfptest", "account_id": "a1", }) if err != nil { t.Fatalf("account_refresh: %v", err) } j, _ := out.(map[string]interface{})["json"].(map[string]interface{}) // ① 必须真的发出了请求(这是本判据的核心 —— 原实现 hits=0) if n := atomic.LoadInt32(&hits); n != 1 { t.Fatalf("必须真发一次续期请求,实得 %d 次(原实现是 0 次却报成功)", n) } if !strings.Contains(gotPath, "/api/v1/auth/refresh") { t.Errorf("请求路径不对: %s", gotPath) } // ② 请求体必须是**驼峰** refreshToken/grantType(照参考 cline.ts) var sent map[string]interface{} if err := json.Unmarshal([]byte(gotBody), &sent); err != nil { t.Fatalf("请求体不是 JSON(Cline 要 JSON body): %q", gotBody) } if _, ok := sent["refreshToken"]; !ok { t.Errorf("请求体必须是驼峰 refreshToken(不是 refresh_token),实得 %v", sent) } if sent["grantType"] != "refresh_token" { t.Errorf("grantType 应为 refresh_token,实得 %v", sent["grantType"]) } // ③ 必须报 refreshed=true,且**新 token 真的落进凭据** if j["refreshed"] != true { t.Errorf("应报 refreshed=true,实得 %v(message=%v)", j["refreshed"], j["message"]) } if j["ok"] != true { t.Errorf("应报 ok=true,实得 %v", j["ok"]) } rd, _ := ps.Call("channels", "__read_cred_probe", map[string]interface{}{ "ref": "channels:rfptest", }) rj, _ := rd.(map[string]interface{})["json"].(map[string]interface{}) cred, _ := rj["credential"].(string) if !strings.Contains(cred, "NEW-TOKEN-CAMEL") { t.Errorf("新 token 必须写回凭据,实得 %q", cred) } // ④ 轮换的 refresh_token 也要回写(Google 场景;Cline 也返回了新的) if !strings.Contains(cred, "NEW-REFRESH") { t.Errorf("轮换的 refresh_token 必须回写,实得 %q", cred) } } // 失败时**必须如实回错误**,不能像原实现那样恒 ok:true。 func TestAccountRefreshReportsFailure(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(401) _, _ = w.Write([]byte(`{"error":"invalid_grant","error_description":"refresh token expired"}`)) })) defer srv.Close() _, ps := poolCore(t) if _, err := ps.Call("channels", "__inject_refresh_platform_probe", map[string]interface{}{ "id": "rfpfail", "refresh_url": srv.URL + "/refresh", "style": "google", "credential": `{"access_token":"OLD","refresh_token":"RT1"}`, }); err != nil { t.Fatalf("注入: %v", err) } out, _ := ps.Call("channels", "account_refresh", map[string]interface{}{ "platform": "rfpfail", "account_id": "a1", }) j, _ := out.(map[string]interface{})["json"].(map[string]interface{}) // ⚠️ 原实现在这里恒 ok:true/refreshed 隐含成功 —— 这就是日志说谎的机制 if j["refreshed"] == true { t.Error("续期失败时绝不能报 refreshed=true") } msg, _ := j["message"].(string) if !strings.Contains(msg, "401") { t.Errorf("失败原因应含 HTTP 状态(可诊断),实得 %q", msg) } // 无 refresh_token 时必须明确说"需重新登录",而不是笼统的失败 if _, err := ps.Call("channels", "__inject_refresh_platform_probe", map[string]interface{}{ "id": "rfpnort", "refresh_url": srv.URL + "/refresh", "style": "google", "credential": `{"access_token":"OLD"}`, }); err != nil { t.Fatalf("注入: %v", err) } out2, _ := ps.Call("channels", "account_refresh", map[string]interface{}{ "platform": "rfpnort", "account_id": "a1", }) j2, _ := out2.(map[string]interface{})["json"].(map[string]interface{}) m2, _ := j2["message"].(string) if !strings.Contains(m2, "重新登录") { t.Errorf("无 refresh_token 时应提示需重新登录,实得 %q", m2) } } // expires_in 是**相对秒数**,必须换算成绝对时刻 —— 否则每轮都判定"快过期"再续一遍。 func TestRefreshWritesBackAbsoluteExpiry(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"access_token":"NEW","refresh_token":"R2","expires_in":7200}`)) })) defer srv.Close() _, ps := poolCore(t) if _, err := ps.Call("channels", "__inject_refresh_platform_probe", map[string]interface{}{ "id": "rfpexp", "refresh_url": srv.URL + "/token", "style": "google", "credential": `{"access_token":"OLD","refresh_token":"RT1"}`, }); err != nil { t.Fatalf("注入: %v", err) } if _, err := ps.Call("channels", "account_refresh", map[string]interface{}{ "platform": "rfpexp", "account_id": "a1", }); err != nil { t.Fatalf("account_refresh: %v", err) } rd, _ := ps.Call("channels", "__read_cred_probe", map[string]interface{}{ "ref": "channels:rfpexp", }) rj, _ := rd.(map[string]interface{})["json"].(map[string]interface{}) cred, _ := rj["credential"].(string) var m map[string]interface{} _ = json.Unmarshal([]byte(cred), &m) ea, _ := m["expires_at"].(string) if ea == "" { t.Fatal("expires_in 必须换算成绝对 expires_at 写回(否则每轮都重续)") } // 必须是**绝对**时刻(10 位秒级),不是相对秒数 7200 if ea == "7200" { t.Error("expires_at 写成了相对秒数 —— 必须换算成绝对时刻") } if len(ea) < 9 { t.Errorf("expires_at 看着不像绝对时刻: %q", ea) } // 索引的 expiresAt 也必须回写(照 expiry-sync:否则出现"显示已过期但能发消息"分叉)。 // ⚠️ 必须通过**账号列表**端点读索引 —— __read_cred_probe 只读保险库, // 用它验索引会永远看到零,把"没验到"误判成"没写"。 al, _ := ps.Call("channels", "account_list", map[string]interface{}{"platform": "rfpexp"}) aj, _ := al.(map[string]interface{})["json"].(map[string]interface{}) accounts := asArrInterface(aj["accounts"]) if len(accounts) == 0 { t.Fatal("账号列表为空") } first, _ := accounts[0].(map[string]interface{}) idx, _ := first["expiresAt"].(float64) if idx == 0 { t.Error("索引的 expiresAt 必须一并回写(expiry-sync 的分叉缺陷)") } // 且应当是**绝对**秒级时刻(不是相对 7200) if idx > 0 && idx < 1e9 { t.Errorf("索引 expiresAt 应换算成绝对时刻,实得 %.0f", idx) } }