package browser // 本文件钉住「浏览器自己说的话」这条自由文本出口的两条约束 —— 既**有界**(600 rune + // 12 行)又**打码**(凭据形状不外泄),而且两者都**不许**伤到排障信息:普通原话必须仍然 // 看得见(这是 launch_test / adopt_test 既有的「失败要如实报」口径)。 // // 「删实现能打红」的落点:把 preview() 里的 chromeOutputPreview 调用退回原样直吐,本文件 // 的凭据断言(以及日志读面那条)会全红。 import ( "context" "errors" "log/slog" "strings" "testing" "time" "unicode/utf8" ) // TestChromeOutputPreviewRedactsCredentials 是这条出口的**核心断言**:浏览器把带凭据的 // 地址/请求头打进 stderr 时,出镜的预览里只有 [redacted],没有凭据本体;同时排障用的 // 普通原话一个字都不许少。 func TestChromeOutputPreviewRedactsCredentials(t *testing.T) { t.Parallel() const ( complaint = "ERROR:process_singleton.cc(343)] 参数被拒: --remote-allow-origins=*" credURL = "ERROR:navigation] 载入失败 https://example.com/cb?access_token=sk-live-ABCDEFGH12345" bearer = "WARNING:net] 请求头 Authorization: Bearer abcdefghij1234567890" ) var out chromeOutput for _, line := range []string{complaint, credURL, bearer} { if _, err := out.Write([]byte(line + "\n")); err != nil { t.Fatalf("chromeOutput.Write 永不返回错误: %v", err) } } got := out.preview() t.Logf("实测预览=%q", got) for _, secret := range []string{"sk-live-ABCDEFGH12345", "abcdefghij1234567890", "access_token="} { if strings.Contains(got, secret) { t.Errorf("凭据形状 %q 原样出镜了: %q", secret, got) } } if !strings.Contains(got, "[redacted]") { t.Errorf("凭据应被打成 [redacted](看得见的打码,不是静默删除): %q", got) } // 反面:打码不能顺手把「浏览器为什么拒绝」一起吃掉 —— 那正是这条出口存在的理由。 for _, want := range []string{complaint, "载入失败", "请求头 Authorization"} { if !strings.Contains(got, want) { t.Errorf("排障用的普通原话 %q 不该被洗掉: %q", want, got) } } } // TestChromeOutputPreviewIsBounded 钉住上限的两半:正文被截到 chromeOutputPreviewRunes // 并留 `…`;而「其实说了多少字节」这条**我们自己的**说明必须活着(上限只压正文,不吃 // 量级)。没有它,读者会把半截输出误当成完整输出。 func TestChromeOutputPreviewIsBounded(t *testing.T) { t.Parallel() t.Run("单行超长只留上限并标截断", func(t *testing.T) { t.Parallel() var out chromeOutput if _, err := out.Write([]byte(strings.Repeat("A", 4000) + "\n")); err != nil { t.Fatalf("Write: %v", err) } got := out.preview() if !strings.HasSuffix(got, "…") { t.Errorf("超上限必须留可见的截断标记: 长度=%d 尾部=%q", utf8.RuneCountInString(got), tailRunes(got, 8)) } if n := utf8.RuneCountInString(got); n != chromeOutputPreviewRunes+1 { t.Errorf("正文应正好截到 %d rune + 一个 `…`,实际 %d rune", chromeOutputPreviewRunes, n) } }) t.Run("字节截断时仍报总量", func(t *testing.T) { t.Parallel() var out chromeOutput // 5000 字节 > chromeOutputMaxBytes(4096)⇒ 缓存本身就被截,preview 必须点明总量。 if _, err := out.Write([]byte(strings.Repeat("B", 5000))); err != nil { t.Fatalf("Write: %v", err) } got := out.preview() if !strings.Contains(got, "已截断,共 5000 字节") { t.Errorf("字节被截时必须报出真实总量(不能拿留下的那部分当全部): %q", tailRunes(got, 40)) } }) } // TestDial_ChromeOutputOnFailureIsRedacted 走真实失败路径(假浏览器 exit 0 ⇒ // profileBusyHint + withChromeOutput),断言**拼接后的整条**(hint 那半 + 浏览器原话那半) // 在用户侧可见的错误串与失败日志里都没有凭据形状,且两半的信息都还在。 func TestDial_ChromeOutputOnFailureIsRedacted(t *testing.T) { t.Parallel() const ( complaint = "不支持的标记: --remote-allow-origins=*" credLine = "载入失败 https://example.com/cb?access_token=sk-live-ABCDEFGH12345" bearerLine = "请求头 Authorization: Bearer abcdefghij1234567890" ) log, buf := newCapturedLog(slog.LevelDebug) l := newFakeChrome(t, fakeBrowserScript{ Stderr: []string{complaint, credLine, bearerLine}, ExitCode: 0, }, 5*time.Second, log) _, stop, err := l.Dial(context.Background()) if stop != nil { t.Error("失败路径不该交出停止函数") } if err == nil { t.Fatal("进程 exit 0 却不写端口记录 ⇒ 必须报错") } if !errors.Is(err, ErrLaunch) { t.Fatalf("仍须包住 ErrLaunch: %v", err) } msg := err.Error() logs := buf.String() t.Logf("实测 error=%v\n实测日志=%s", err, logs) for _, secret := range []string{"sk-live-ABCDEFGH12345", "abcdefghij1234567890", "access_token="} { if strings.Contains(msg, secret) { t.Errorf("返回给调用方的错误里出现了凭据形状 %q(用户侧工具结果直接来自这条串): %v", secret, err) } if strings.Contains(logs, secret) { t.Errorf("失败日志里出现了凭据形状 %q(密钥不落日志):\n%s", secret, logs) } } if !strings.Contains(msg, "[redacted]") { t.Errorf("打码应当看得见([redacted]): %v", err) } // 两半都得活着:hint 那半(可操作建议 + profileDir 路径)不能被上限截掉, // 浏览器原话那半是这条出口存在的理由。 for _, want := range []string{ "绝不杀别人的浏览器", l.profileDir, complaint, "载入失败", } { if !strings.Contains(msg, want) { t.Errorf("整条错误里缺少 %q(过度打码/截断): %v", want, err) } } if !strings.Contains(logs, complaint) { t.Errorf("失败日志里丢了浏览器原话 %q(排障信息):\n%s", complaint, logs) } } // tailRunes 返回 s 的末 n 个 rune,供失败断言打印尾部而不是把 4000 字符灌进报告。 func tailRunes(s string, n int) string { r := []rune(s) if len(r) <= n { return s } return string(r[len(r)-n:]) }