package cdp import ( "os" "path/filepath" "strings" "testing" ) // writeFile 建一个带内容的文件(自动建父目录)。 func writeFile(t *testing.T, path, content string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte(content), 0o600); err != nil { t.Fatal(err) } } // TestCheckProfileWarnings:三个最常见的坑都要被明确讲出来。 // // 这三条正是"配了 profile 却用不上登录态"的真实原因: // 1. 指到了 profile 本体(Default/ 里面)而不是 user-data-dir; // 2. 指到了 Chrome 默认目录(Chrome 136+ 禁止在其上开远程调试,且日常 Chrome 占着); // 3. 那份 profile 正被一个运行中的 Chrome 占用。 func TestCheckProfileWarnings(t *testing.T) { t.Run("目录不存在", func(t *testing.T) { warns := CheckProfile(filepath.Join(t.TempDir(), "nope")) if len(warns) != 1 || !strings.Contains(warns[0], "不存在") { t.Fatalf("应提示目录不存在,实际 %v", warns) } }) t.Run("指向 profile 本体", func(t *testing.T) { dir := t.TempDir() writeFile(t, filepath.Join(dir, "Cookies"), "x") warns := CheckProfile(dir) if !hasSubstr(warns, "父目录") { t.Fatalf("应提示 --user-data-dir 要指向父目录,实际 %v", warns) } }) t.Run("指向 Chrome 默认目录", func(t *testing.T) { cands := defaultUserDataDirCandidates() if len(cands) == 0 { t.Skip("当前平台没有已知的默认目录") } // 直接对候选路径做判断(纯字符串逻辑,不需要它真的存在)。 if !IsChromeDefaultDir(cands[0]) { t.Fatalf("IsChromeDefaultDir(%q) 应为 true", cands[0]) } if IsChromeDefaultDir(filepath.Join(t.TempDir(), "somecopy")) { t.Error("非默认目录不该被判为默认目录") } }) t.Run("被运行中的 Chrome 占用", func(t *testing.T) { dir := t.TempDir() writeFile(t, filepath.Join(dir, "Local State"), "{}") writeFile(t, filepath.Join(dir, "SingletonLock"), "") warns := CheckProfile(dir) if !hasSubstr(warns, "SingletonLock") { t.Fatalf("应提示 profile 被占用,实际 %v", warns) } }) } func hasSubstr(list []string, sub string) bool { for _, s := range list { if strings.Contains(s, sub) { return true } } return false } // TestCopyProfile:复制要**保留登录态与扩展、跳过缓存、不带单实例锁**。 func TestCopyProfile(t *testing.T) { src := t.TempDir() dst := filepath.Join(t.TempDir(), "copy") // 要保留的:登录态与扩展。 writeFile(t, filepath.Join(src, "Local State"), "{}") writeFile(t, filepath.Join(src, "Default", "Cookies"), "cookies-db") writeFile(t, filepath.Join(src, "Default", "Preferences"), "{}") writeFile(t, filepath.Join(src, "Default", "Extensions", "abc", "manifest.json"), "{}") // 要跳过的:缓存(体积大且与登录态无关)。 writeFile(t, filepath.Join(src, "Default", "Cache", "big.bin"), strings.Repeat("x", 4096)) writeFile(t, filepath.Join(src, "Default", "Code Cache", "big.bin"), strings.Repeat("y", 4096)) writeFile(t, filepath.Join(src, "Default", "Service Worker", "s.bin"), "z") n, err := CopyProfile(src, dst) if err != nil { t.Fatalf("复制失败: %v", err) } if n == 0 { t.Error("复制字节数为 0") } for _, want := range []string{ filepath.Join("Default", "Cookies"), filepath.Join("Default", "Preferences"), filepath.Join("Default", "Extensions", "abc", "manifest.json"), "Local State", } { if _, err := os.Stat(filepath.Join(dst, want)); err != nil { t.Errorf("应保留 %s:%v", want, err) } } for _, skip := range []string{ filepath.Join("Default", "Cache"), filepath.Join("Default", "Code Cache"), filepath.Join("Default", "Service Worker"), } { if _, err := os.Stat(filepath.Join(dst, skip)); err == nil { t.Errorf("缓存目录应被跳过:%s", skip) } } if !LooksLikeBrowserProfile(dst) { t.Error("复制结果应当被识别为浏览器 profile") } } // TestCopyProfileRefusesRunningChrome:源被占用(SingletonLock)时必须拒绝—— // 运行中复制的 profile 可能不一致,而且锁文件带过去会让 Chrome 犯迷糊。 func TestCopyProfileRefusesRunningChrome(t *testing.T) { src := t.TempDir() writeFile(t, filepath.Join(src, "Local State"), "{}") writeFile(t, filepath.Join(src, "SingletonLock"), "") if _, err := CopyProfile(src, filepath.Join(t.TempDir(), "copy")); err == nil { t.Fatal("源被占用时应拒绝复制") } else if !strings.Contains(err.Error(), "退出 Chrome") { t.Errorf("错误应提示先退出 Chrome:%v", err) } } // TestCopyProfileNoSingletonInDst:目标里不该出现单实例锁,否则 Chrome 会以为另有一个实例在跑。 func TestCopyProfileNoSingletonInDst(t *testing.T) { src := t.TempDir() writeFile(t, filepath.Join(src, "Local State"), "{}") writeFile(t, filepath.Join(src, "Default", "Cookies"), "c") dst := filepath.Join(t.TempDir(), "copy") if _, err := CopyProfile(src, dst); err != nil { t.Fatal(err) } for _, f := range []string{"SingletonLock", "SingletonSocket", "SingletonCookie"} { if _, err := os.Lstat(filepath.Join(dst, f)); err == nil { t.Errorf("目标不该有 %s", f) } } } // TestCopyProfileRejectsProfileBodyAsSource:源指向 profile 本体时拒绝并说清该指哪。 func TestCopyProfileRejectsProfileBodyAsSource(t *testing.T) { src := t.TempDir() writeFile(t, filepath.Join(src, "Cookies"), "x") if _, err := CopyProfile(src, filepath.Join(t.TempDir(), "copy")); err == nil { t.Fatal("源是 profile 本体时应拒绝") } } // TestPersistentProfileDir:默认目录必须是**固定**路径。 // // 旧的实现用 os.MkdirTemp——每次都给出一个新目录,于是每次进程启动都像一台从没 // 登录过的新机器(登录一次也留不住)。这里断言的关键性质就是"两次调用同一个路径"。 func TestPersistentProfileDir(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) dir, err := PersistentProfileDir() if err != nil { t.Fatal(err) } want := filepath.Join(home, ".autogo", "browser-profile") if dir != want { t.Errorf("默认 profile = %q, want %q", dir, want) } again, err := PersistentProfileDir() if err != nil { t.Fatal(err) } if again != dir { t.Errorf("两次调用应返回同一路径(临时目录才会每次不同):%q vs %q", dir, again) } }