216 lines
6.4 KiB
Go
216 lines
6.4 KiB
Go
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)
|
||
|
|
}
|