package service import ( "database/sql" "errors" "strings" "testing" "time" "cmautobuy/admin/model" ) // validProfile 返回一份合法的上报内容,测试里按需改某一项。 func validProfile() ClientProfileRequest { var p ClientProfileRequest p.Client.Name = "办公室-01" p.SupportedTypes = []string{"collect", "purchase"} p.Device.Address = "192.168.0.173:5555" p.Device.Platform = "android" p.Device.PddPackage = "com.xunmeng.pinduoduo" p.Capabilities.PurchaseMode = "dry_run" p.Capabilities.SchemaVersions = []int{1} return p } func TestProfileValidate_合法内容通过(t *testing.T) { if err := validProfile().Validate(); err != nil { t.Errorf("合法内容不该报错: %v", err) } } func TestProfileValidate_各种非法情况(t *testing.T) { cases := []struct { name string mutate func(*ClientProfileRequest) }{ {"名称超过 50 字", func(p *ClientProfileRequest) { p.Client.Name = strings.Repeat("测", 51) }}, {"任务类型为空", func(p *ClientProfileRequest) { p.SupportedTypes = nil }}, {"任务类型有非法值", func(p *ClientProfileRequest) { p.SupportedTypes = []string{"collect", "什么鬼"} }}, {"平台不是 android", func(p *ClientProfileRequest) { p.Device.Platform = "ios" }}, {"执行模式为空", func(p *ClientProfileRequest) { p.Capabilities.PurchaseMode = "" }}, {"执行模式非法", func(p *ClientProfileRequest) { p.Capabilities.PurchaseMode = "whatever" }}, {"结构版本为空", func(p *ClientProfileRequest) { p.Capabilities.SchemaVersions = nil }}, {"结构版本含非正整数", func(p *ClientProfileRequest) { p.Capabilities.SchemaVersions = []int{1, 0} }}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { p := validProfile() tc.mutate(&p) err := p.Validate() if !errors.Is(err, ErrInvalidProfile) { t.Fatalf("期望 ErrInvalidProfile,实际 %v", err) } // 错误信息要说清是哪个字段,光说"参数无效"排查不了 if !strings.Contains(err.Error(), ".") && !strings.Contains(err.Error(), "_") { t.Errorf("错误信息应指明具体字段,实际 %q", err.Error()) } }) } } // 名称按**字符数**算,不是字节数。50 个汉字是 150 字节, // 按字节判会把它误判成超长。 func TestProfileValidate_名称按字符数不是字节数(t *testing.T) { p := validProfile() p.Client.Name = strings.Repeat("测", 50) // 正好 50 个字,150 字节 if err := p.Validate(); err != nil { t.Errorf("50 个汉字应该合法,实际 %v", err) } } func TestProfileValidate_名称可以为空(t *testing.T) { p := validProfile() p.Client.Name = "" if err := p.Validate(); err != nil { t.Errorf("名称可以为空,实际 %v", err) } } func TestProfileValidate_设备平台可以不填(t *testing.T) { p := validProfile() p.Device.Platform = "" if err := p.Validate(); err != nil { t.Errorf("平台可以不填,实际 %v", err) } } // ── 显式登记 ─────────────────────────────────────────── func TestRegisterClientProfile_新建与幂等(t *testing.T) { db := newTestDB(t) at, err := RegisterClientProfile(db, "CLIENT-123", validProfile()) if err != nil { t.Fatalf("登记失败: %v", err) } if _, ok := parseTime(at); !ok { t.Errorf("registered_at 应是合法 ISO 8601,实际 %q", at) } // 重复登记:幂等,不产生第二条记录 for i := 0; i < 3; i++ { if _, err := RegisterClientProfile(db, "CLIENT-123", validProfile()); err != nil { t.Fatalf("第 %d 次重复登记失败: %v", i+2, err) } } views, _ := ListClientViews(db, "", time.Minute) if len(views) != 1 { t.Errorf("重复登记应保持 1 条记录,实际 %d", len(views)) } } func TestRegisterClientProfile_能力字段非法返回校验错误(t *testing.T) { db := newTestDB(t) p := validProfile() p.Capabilities.PurchaseMode = "非法" if _, err := RegisterClientProfile(db, "CLIENT-123", p); !errors.Is(err, ErrInvalidProfile) { t.Errorf("期望 ErrInvalidProfile,实际 %v", err) } views, _ := ListClientViews(db, "", time.Minute) if len(views) != 0 { t.Error("校验不过时不该写库") } } func TestRegisterClientProfile_缺少设备号(t *testing.T) { db := newTestDB(t) if _, err := RegisterClientProfile(db, "", validProfile()); err == nil { t.Error("缺少 client_id 应该报错") } } // ── 这是本工单最要紧的一条:登记绝不能碰任务 ─────────── func TestRegisterClientProfile_不产生任何任务副作用(t *testing.T) { db := newTestDB(t) insertTask(t, db, "TASK-A", "CLIENT-123") before := snapshotTask(t, db, "TASK-A") var claimsBefore int db.QueryRow(`SELECT COUNT(*) FROM task_claims`).Scan(&claimsBefore) // 登记好几次 for i := 0; i < 3; i++ { if _, err := RegisterClientProfile(db, "CLIENT-123", validProfile()); err != nil { t.Fatalf("登记失败: %v", err) } } after := snapshotTask(t, db, "TASK-A") if before != after { t.Errorf("登记改动了任务,这违反 #12 的核心约束:\n登记前 %+v\n登记后 %+v", before, after) } var claimsAfter int db.QueryRow(`SELECT COUNT(*) FROM task_claims`).Scan(&claimsAfter) if claimsBefore != claimsAfter { t.Errorf("登记产生了领取历史,条数 %d -> %d", claimsBefore, claimsAfter) } // 任务还在原地等着被领 task, err := ClaimNextTask(db, "CLIENT-123", []string{"purchase"}) if err != nil { t.Fatalf("领取失败: %v", err) } if task == nil { t.Error("登记之后任务应该还能被领到——说明登记没有把它领走") } } // taskSnapshot 记录任务上所有会被"领取"改动的字段。 type taskSnapshot struct { Status string AssignedClient string ClaimedAt string UpdatedAt string } func snapshotTask(t *testing.T, db *sql.DB, taskID string) taskSnapshot { t.Helper() var s taskSnapshot var assigned, claimedAt sql.NullString err := db.QueryRow( `SELECT status, assigned_client, claimed_at, updated_at FROM tasks WHERE task_id = ?`, taskID, ).Scan(&s.Status, &assigned, &claimedAt, &s.UpdatedAt) if err != nil { t.Fatalf("读取任务快照失败: %v", err) } s.AssignedClient = assigned.String s.ClaimedAt = claimedAt.String return s } // parseTime 判断字符串是不是合法的带时区 ISO 8601。 func parseTime(s string) (time.Time, bool) { return model.ParseISO(s) }