gcli2api / internal /auth /auth_test.go
a3216's picture
chore: 同步到上游 1.12.0-panel + 凭证同步/独立启动器/保活
6d60378 verified
Raw History Blame Contribute Delete
9.89 kB
package auth
import (
"io"
"log"
"os"
"path/filepath"
"strings"
"testing"
)
func TestParseNested(t *testing.T) {
raw := []byte(`{"auth":{"accessToken":"at","refreshToken":"rt","expiresAt":1753600000,"domain":""},"account":{"uid":"u1","enterpriseId":"e1","nickname":"n1"}}`)
sa, err := Parse(raw)
if err != nil {
t.Fatalf("nested parse err: %v", err)
}
if sa.AccessToken != "at" || sa.RefreshToken != "rt" || sa.ExpiresAt != 1753600000 {
t.Errorf("tokens: %+v", sa)
}
if sa.UID != "u1" || sa.EnterpriseID != "e1" || sa.Nickname != "n1" {
t.Errorf("account: %+v", sa)
}
}
func TestParseFlat(t *testing.T) {
raw := []byte(`{"accessToken":"at","refreshToken":"rt","expiresAt":1753600000,"uid":"u2","nickname":"n2"}`)
sa, err := Parse(raw)
if err != nil || sa.UID != "u2" || sa.AccessToken != "at" {
t.Fatalf("flat: %+v %v", sa, err)
}
}
func TestParseMissingToken(t *testing.T) {
if _, err := Parse([]byte(`{"uid":"u3"}`)); err == nil {
t.Fatal("want error for missing accessToken")
}
}
func TestSaveAtomicRoundtrip(t *testing.T) {
dir := t.TempDir()
fp := filepath.Join(dir, "workbuddy-u1.json")
a := &Auth{AccessToken: "at", RefreshToken: "rt", ExpiresAt: 1753600000,
UID: "u1", EnterpriseID: "e1", Nickname: "n1", FilePath: fp}
if err := a.SaveAtomic(); err != nil {
t.Fatalf("save: %v", err)
}
if _, err := os.Stat(fp + ".tmp"); !os.IsNotExist(err) {
t.Error("tmp file should not remain")
}
raw, err := os.ReadFile(fp)
if err != nil {
t.Fatalf("read: %v", err)
}
b, err := Parse(raw)
if err != nil {
t.Fatalf("reparse: %v", err)
}
if b.AccessToken != "at" || b.UID != "u1" || b.EnterpriseID != "e1" {
t.Errorf("roundtrip: %+v", b)
}
}
// TestLoadDirLoadsAllValid 不再按 region 过滤:所有可解析的 auth 文件都被加载,
// 解析失败的文件静默跳过。
func TestLoadDirLoadsAllValid(t *testing.T) {
dir := t.TempDir()
cn := `{"auth":{"accessToken":"at1","refreshToken":"r","expiresAt":1,"domain":""},"account":{"uid":"cn1"}}`
other := `{"auth":{"accessToken":"at2","refreshToken":"r","expiresAt":1,"domain":"example.com"},"account":{"uid":"u2"}}`
bad := `not json`
os.WriteFile(filepath.Join(dir, "workbuddy-cn1.json"), []byte(cn), 0o600)
os.WriteFile(filepath.Join(dir, "workbuddy-u2.json"), []byte(other), 0o600)
os.WriteFile(filepath.Join(dir, "workbuddy-bad.json"), []byte(bad), 0o600)
list, err := LoadDir(dir)
if err != nil {
t.Fatalf("load: %v", err)
}
if len(list) != 2 {
t.Fatalf("want 2 valid accounts, got %+v", list)
}
for _, a := range list {
if a.FilePath == "" {
t.Error("FilePath not set")
}
}
}
func TestNeedsRefresh(t *testing.T) {
a := &Auth{ExpiresAt: 0}
if !a.NeedsRefresh(0) {
t.Error("zero expiry should need refresh")
}
a.ExpiresAt = 9999999999
if a.NeedsRefresh(0) {
t.Error("far future should not need refresh")
}
}
// TestParseDeviceToken 嵌套形与扁平形 auth 文件的顶层 device_token 键均被解析。
func TestParseDeviceToken(t *testing.T) {
nested := []byte(`{"auth":{"accessToken":"at","refreshToken":"rt","expiresAt":1,"domain":""},"account":{"uid":"u1"},"device_token":"dev-tok-nested"}`)
sa, err := Parse(nested)
if err != nil {
t.Fatalf("nested parse: %v", err)
}
if sa.DeviceToken != "dev-tok-nested" {
t.Errorf("nested DeviceToken = %q want %q", sa.DeviceToken, "dev-tok-nested")
}
flat := []byte(`{"accessToken":"at","refreshToken":"rt","expiresAt":1,"uid":"u2","device_token":"dev-tok-flat"}`)
fa, err := Parse(flat)
if err != nil {
t.Fatalf("flat parse: %v", err)
}
if fa.DeviceToken != "dev-tok-flat" {
t.Errorf("flat DeviceToken = %q want %q", fa.DeviceToken, "dev-tok-flat")
}
}
// TestSaveAtomicPreservesDeviceToken SaveAtomic 写回后顶层 device_token 被保留并重新解析回来。
func TestSaveAtomicPreservesDeviceToken(t *testing.T) {
dir := t.TempDir()
fp := filepath.Join(dir, "workbuddy-dt.json")
a := &Auth{AccessToken: "at", RefreshToken: "rt", ExpiresAt: 1,
UID: "u1", DeviceToken: "persisted-tok", FilePath: fp}
if err := a.SaveAtomic(); err != nil {
t.Fatalf("save: %v", err)
}
raw, err := os.ReadFile(fp)
if err != nil {
t.Fatalf("read: %v", err)
}
b, err := Parse(raw)
if err != nil {
t.Fatalf("reparse: %v", err)
}
if b.DeviceToken != "persisted-tok" {
t.Errorf("roundtrip DeviceToken = %q want %q", b.DeviceToken, "persisted-tok")
}
}
// TestLoadDirBackfillsRealm 存量迁移:LoadDir 加载目录时对空 realm 的 auth 自动
// backfill + SaveAtomic;已有 realm 的保持原值(不被 domain 覆盖);文件全部带标识。
func TestLoadDirBackfillsRealm(t *testing.T) {
t.Parallel()
dir := t.TempDir()
fixtures := map[string]string{
"workbuddy-g1.json": `{"auth":{"accessToken":"at","refreshToken":"r","expiresAt":1,"domain":"www.workbuddy.ai"},"account":{"uid":"g1"}}`,
"workbuddy-c1.json": `{"auth":{"accessToken":"at","refreshToken":"r","expiresAt":1,"domain":""},"account":{"uid":"c1"}}`,
// 已有 realm 的不因 domain 变化被覆盖:global domain + 显式 cn → 保持 cn
"workbuddy-c2.json": `{"auth":{"accessToken":"at","refreshToken":"r","expiresAt":1,"domain":"www.workbuddy.ai","realm":"cn"},"account":{"uid":"c2"}}`,
}
for name, body := range fixtures {
if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o600); err != nil {
t.Fatal(err)
}
}
list, err := LoadDir(dir)
if err != nil {
t.Fatalf("load: %v", err)
}
if len(list) != 3 {
t.Fatalf("want 3 accounts, got %d", len(list))
}
want := map[string]string{"g1": "global", "c1": "cn", "c2": "cn"}
for _, a := range list {
// 内存态已补标识
if got := a.RealmStored(); got != want[a.UID] {
t.Errorf("uid=%s in-memory realm=%q want %q", a.UID, got, want[a.UID])
}
// 落盘文件也带 realm 键
raw, err := os.ReadFile(a.FilePath)
if err != nil {
t.Fatalf("read %s: %v", a.FilePath, err)
}
b, err := Parse(raw)
if err != nil {
t.Fatalf("reparse %s: %v", a.FilePath, err)
}
if got := b.RealmStored(); got != want[a.UID] {
t.Errorf("uid=%s on-disk realm=%q want %q", a.UID, got, want[a.UID])
}
}
}
// TestLoadDirBackfillWriteFailureDoesNotBlock 单个文件 backfill 落盘失败(tmp 预置目录
// 使 WriteFile 失败)不阻断启动:其他文件照常迁移,LoadDir 不向上抛错。
// (历史纯 CN auth 目录一次性迁移时,个别文件不可写不应让整个服务起不来。)
func TestLoadDirBackfillWriteFailureDoesNotBlock(t *testing.T) {
t.Parallel()
dir := t.TempDir()
good := `{"auth":{"accessToken":"at","refreshToken":"r","expiresAt":1,"domain":"www.workbuddy.ai"},"account":{"uid":"g1"}}`
if err := os.WriteFile(filepath.Join(dir, "workbuddy-g1.json"), []byte(good), 0o600); err != nil {
t.Fatal(err)
}
// 预置同名 .tmp 目录 → SaveAtomic 的 os.WriteFile(".tmp") 报 is a directory。
if err := os.Mkdir(filepath.Join(dir, "workbuddy-c1.json.tmp"), 0o700); err != nil {
t.Fatal(err)
}
bad := `{"auth":{"accessToken":"at","refreshToken":"r","expiresAt":1},"account":{"uid":"c1"}}`
if err := os.WriteFile(filepath.Join(dir, "workbuddy-c1.json"), []byte(bad), 0o600); err != nil {
t.Fatal(err)
}
list, err := LoadDir(dir)
if err != nil {
t.Fatalf("load err=%v want nil (write failure must not block startup)", err)
}
if len(list) != 2 {
t.Fatalf("want 2 accounts loaded, got %d", len(list))
}
// 好文件迁移成功
raw, _ := os.ReadFile(filepath.Join(dir, "workbuddy-g1.json"))
b, _ := Parse(raw)
if b.RealmStored() != "global" {
t.Errorf("good file realm=%q want global (migration should succeed)", b.RealmStored())
}
}
// TestLoadDirDuplicateUIDWarning 同 UID 双 realm auth 文件(概率近零的 EDGE):LoadDir
// 检测到重复 UID 时打 WARN(含两文件路径),且不改变加载行为——后载入者胜出(返回 1 个、
// 不 panic、realm 为后载入者值)。LoadDir 现在有额外 seenUID 副作用,逐字验证 WARN。
func TestLoadDirDuplicateUIDWarning(t *testing.T) {
dir := t.TempDir()
// 同一 UID u9 的两个文件:cn realm 文件按文件名排序在前(workbuddy-a-...),
// global realm 文件在后 → 后载入者(global)胜出。
cn := `{"auth":{"accessToken":"at1","refreshToken":"r","expiresAt":1,"domain":"www.codebuddy.cn"},"account":{"uid":"u9"}}`
gl := `{"auth":{"accessToken":"at2","refreshToken":"r","expiresAt":1,"domain":"www.workbuddy.ai"},"account":{"uid":"u9"}}`
if err := os.WriteFile(filepath.Join(dir, "workbuddy-a-cn.json"), []byte(cn), 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "workbuddy-z-global.json"), []byte(gl), 0o600); err != nil {
t.Fatal(err)
}
// 捕获 log 输出(本测试不 t.Parallel:log.SetOutput 是进程级全局,需串行)。
old := log.Writer()
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
log.SetOutput(w)
list, err := LoadDir(dir)
_ = w.Close()
raw, _ := io.ReadAll(r)
log.SetOutput(old)
if err != nil {
t.Fatalf("load err=%v", err)
}
// 行为稳定(不改加载结果):LoadDir 返回全部可解析文件(去重发生在 pool.SyncToDir
// 的 UID 键 upsert),不 panic。
if len(list) != 2 {
t.Fatalf("want 2 accounts loaded (dedup later in pool), got %d", len(list))
}
// WARN 已触发且含两文件路径。
if !strings.Contains(string(raw), "WARN: uid") ||
!strings.Contains(string(raw), "duplicated") ||
!strings.Contains(string(raw), "workbuddy-a-cn.json") ||
!strings.Contains(string(raw), "workbuddy-z-global.json") {
t.Errorf("expected WARN with both paths, got output: %s", string(raw))
}
}