package core import ( "testing" ) // account_adopt:把升级前的**非池**凭据接管进账号池。 // // 为什么需要它:v2.0.x 之前登录只写一条 `channels:`,引入账号池后凭据落在 // `channels::`。升级后老凭据仍在保险库、但池里没有账号引用它 —— // 表现为"账号消失了"(数据其实还在)。让用户重新登录是**可见的额外代价**。 // // ⚠️ 本判据同时钉住一条操作纪律:**接管必须走插件的保险库 API**(读出的是明文, // 写回时插件自己加密)。绝不要把保险库文件里的密文当明文喂给 API —— // 保险库读取时会解密,那样会把凭据**二次加密**(实测踩过:1971 → 2965 字符, // 该账号推理开始返回空)。 func TestAccountAdoptLegacyCredential(t *testing.T) { _, ps := poolCore(t) const legacy = `{"access_token":"LEGACY-TOK","refresh_token":"RT","nickname":"老账号"}` // 造一条"旧版"非池凭据(模拟升级前留下的) if _, err := ps.Call("channels", "__set_legacy_cred_probe", map[string]interface{}{ "platform": "buddy", "credential": legacy, }); err != nil { t.Fatalf("造旧凭据: %v", err) } // 接管进一个新池账号 m, err := ps.Call("channels", "account_adopt", map[string]interface{}{ "platform": "buddy", "account_id": "adopted1", }) if err != nil { t.Fatalf("account_adopt: %v", err) } w, _ := m.(map[string]interface{}) j, _ := w["json"].(map[string]interface{}) if j == nil || j["ok"] != true { t.Fatalf("接管失败: %#v", m) } // ① 池里必须出现该账号,且**有凭据** lm, _ := ps.Call("channels", "account_list", map[string]interface{}{"platform": "buddy"}) lj, _ := lm.(map[string]interface{})["json"].(map[string]interface{}) found := false for _, a := range asArrInterface(lj["accounts"]) { if am, ok := a.(map[string]interface{}); ok && am["id"] == "adopted1" { found = true if am["has_credential"] != true { t.Errorf("接管后应 has_credential=true,实得 %v", am["has_credential"]) } } } if !found { t.Fatalf("接管后账号未出现在池里: %#v", lj["accounts"]) } // ② 凭据必须是**原文**(未被二次加密)——这是本判据的核心 out, err := ps.Call("channels", "__read_cred_probe", map[string]interface{}{ "ref": "channels:buddy:adopted1", }) if err != nil { t.Fatalf("读凭据: %v", err) } rj, _ := out.(map[string]interface{})["json"].(map[string]interface{}) got, _ := rj["credential"].(string) if got != legacy { t.Errorf("接管后的凭据应与原文逐字节相同(不得二次加密)\n 期望: %s\n 实得: %s", legacy, got) } if rj["len"] != nil { if n, _ := rj["len"].(float64); int(n) != len(legacy) { t.Errorf("凭据长度 %v,应为 %d(长度变化说明被二次加密)", n, len(legacy)) } } // ③ 补齐了展示字段(昵称从凭据里取) for _, a := range asArrInterface(lj["accounts"]) { if am, ok := a.(map[string]interface{}); ok && am["id"] == "adopted1" { if am["nickname"] != "老账号" { t.Errorf("昵称应从凭据补齐为「老账号」,实得 %v", am["nickname"]) } } } // ④ 没有旧凭据时必须如实报错(不能假装成功) _, _ = ps.Call("channels", "__clear_all_creds_probe", nil) m2, _ := ps.Call("channels", "account_adopt", map[string]interface{}{ "platform": "buddy", "account_id": "adopted2", }) j2, _ := m2.(map[string]interface{})["json"].(map[string]interface{}) if j2["ok"] == true { t.Error("没有旧凭据时应如实失败,不能假装成功") } } // asArrInterface 宽松取数组(插件桥回传的可能是 []interface{})。 func asArrInterface(v interface{}) []interface{} { if a, ok := v.([]interface{}); ok { return a } return nil }