Files
cmsp/internal/logx/logx_test.go
T
QiuSWandClaude Opus 5 2cefa67001 feat: 初始化 Wails v2 + Vue3 骨架与 SQLite 存储 (#4)
建立可运行、可构建的 Go 骨架,供后续 R1~R5 在其上叠加。

后端:
- main.go / app.go:Wails v2 窗口与 14 个前端可调方法。Wails 专有 API
  只出现在这两个文件,internal/ 对 Wails 零依赖,便于将来迁移 v3
- internal/config:config.yaml 的读写、校验、脱敏。保存时按固定模板
  渲染以保留注释,不用 yaml.Marshal
- internal/store:SQLite(modernc.org/sqlite,无需 CGO)。建表迁移、
  商品仓储、KV。重复同步不会覆盖本地下载与上传状态
- internal/logx:9 条脱敏规则,覆盖淘宝 token、Cookie、Authorization、
  密码和图片 base64;并发安全的内存日志缓冲

前端(Vue 3 + Naive UI,按已确认原型 v4):
- Sidebar:176px 可折叠,仅商品列表与参数设置两项,底部是登录状态的
  唯一显示位置
- ProductListView:两行工具栏(四项筛选 + 重置搜索 / 四个操作按钮)、
  商品表格、分页
- SettingsView:四张卡片,路径用文件与目录选择框
- LogWindow:运行日志子窗口,按级别过滤、自动滚动、导出

配置改用 YAML:分 huohanhan / taobao / download 三段。taobao 段刻意
没有账号密码字段,并有单元测试挡住后续加入。

构建相关:
- .gitignore 只忽略 build/bin/,不忽略 build/。Wails 把 build/ 当源码
  目录(图标、manifest),忽略掉会让别人克隆后构建失败
- .gitignore 忽略 config.yaml、payloads/ 和 *.har(含真实手机号与凭据)

未实现的下载数据、下载视频、上传数据三个方法返回明确的「还没实现」,
不静默失败。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LbdtsD3ohhSMy3KPoCgARq
2026-09-02 15:59:54 +08:00

183 lines
4.4 KiB
Go

package logx
import (
"strings"
"sync"
"testing"
)
// 这是本包最重要的测试:敏感内容绝不能出现在日志里。
// 每新增一条脱敏规则,都要在这里补一个用例。
func TestMaskHidesSecrets(t *testing.T) {
cases := []struct {
name string
in string
// leak 是绝对不能出现在结果里的片段
leak string
}{
{
name: "淘宝 MTOP token",
in: "cookie 里 _m_h5_tk=abc123def456_1699999999999 已读取",
leak: "abc123def456",
},
{
name: "淘宝 token enc",
in: "_m_h5_tk_enc=zzz999yyy888",
leak: "zzz999yyy888",
},
{
name: "淘宝 tb_token",
in: "_tb_token_=e3b0c44298fc; path=/",
leak: "e3b0c44298fc",
},
{
name: "Authorization 头",
in: "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.fake.token",
leak: "eyJhbGciOiJIUzI1NiJ9",
},
{
name: "Cookie 请求头",
in: "Cookie: a=1; b=2; secret=3",
leak: "secret=3",
},
{
name: "JSON 里的密码",
in: `{"account":"13500000000","password":"不该出现的密码"}`,
leak: "不该出现的密码",
},
{
name: "等号形式的密码",
in: "password=hunter2&next=1",
leak: "hunter2",
},
{
name: "中文冒号密码",
in: "登录失败,密码:明文密码值",
leak: "明文密码值",
},
{
name: "图片 base64",
in: "上传 data:image/jpeg;base64,AAAABBBBCCCCDDDD 完成",
leak: "AAAABBBBCCCCDDDD",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := Mask(tc.in)
if strings.Contains(got, tc.leak) {
t.Fatalf("脱敏后仍然泄漏了 %q\n原文:%s\n结果:%s", tc.leak, tc.in, got)
}
if !strings.Contains(got, "***") {
t.Fatalf("应当出现 *** 占位符,结果是:%s", got)
}
})
}
}
// 普通文本不应该被误伤。
func TestMaskKeepsNormalText(t *testing.T) {
in := "[3/6] 商品 40571188442 图搜返回 48 个同款,取前 20 个"
if got := Mask(in); got != in {
t.Fatalf("普通文本不应被改动\n原文:%s\n结果:%s", in, got)
}
}
// 写日志时也必须脱敏,不能只有直接调用 Mask 才生效。
func TestLoggerMasksOnWrite(t *testing.T) {
l := New(10)
l.Info("登录请求 password=真实密码")
entries := l.Entries()
if len(entries) != 1 {
t.Fatalf("应当有 1 条日志,实际 %d 条", len(entries))
}
if strings.Contains(entries[0].Message, "真实密码") {
t.Fatalf("日志里泄漏了密码:%s", entries[0].Message)
}
}
// 超过上限时要丢掉最旧的,防止长时间任务把内存吃光。
func TestLoggerRespectsLimit(t *testing.T) {
l := New(3)
for i := 0; i < 10; i++ {
l.Info("第 %d 条", i)
}
entries := l.Entries()
if len(entries) != 3 {
t.Fatalf("应当只保留 3 条,实际 %d 条", len(entries))
}
// 保留的应当是最后 3 条。
if !strings.Contains(entries[0].Message, "第 7 条") {
t.Fatalf("应当保留最新的日志,第一条却是:%s", entries[0].Message)
}
}
// 下载任务会有多个协程同时写日志,不能崩。
func TestLoggerIsConcurrentSafe(t *testing.T) {
l := New(1000)
var wg sync.WaitGroup
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
for j := 0; j < 50; j++ {
l.Info("协程 %d 第 %d 条", n, j)
}
}(i)
}
wg.Wait()
if len(l.Entries()) != 1000 {
t.Fatalf("应当有 1000 条日志,实际 %d 条", len(l.Entries()))
}
}
func TestLoggerLevels(t *testing.T) {
l := New(10)
l.Info("信息")
l.Success("成功")
l.Warn("警告")
l.Error("失败")
want := []Level{LevelInfo, LevelSuccess, LevelWarn, LevelError}
entries := l.Entries()
for i, level := range want {
if entries[i].Level != level {
t.Fatalf("第 %d 条级别应当是 %s,实际 %s", i, level, entries[i].Level)
}
}
}
func TestLoggerClearAndText(t *testing.T) {
l := New(10)
l.Info("第一条")
l.Info("第二条")
text := l.Text()
if !strings.Contains(text, "第一条") || !strings.Contains(text, "第二条") {
t.Fatalf("导出文本应当包含全部日志,实际:%s", text)
}
l.Clear()
if len(l.Entries()) != 0 {
t.Fatalf("清空后不应还有日志")
}
}
// 日志写入后要能推给前端。
func TestLoggerHandler(t *testing.T) {
l := New(10)
var got []Entry
l.SetHandler(func(e Entry) { got = append(got, e) })
l.Info("一条日志")
if len(got) != 1 {
t.Fatalf("回调应当被调用 1 次,实际 %d 次", len(got))
}
if got[0].Message != "一条日志" {
t.Fatalf("回调收到的内容不对:%s", got[0].Message)
}
}