package customer_binding import ( "context" "fmt" "testing" "gorm.io/gorm" "github.com/break/junhong_cmp_fiber/internal/model" ) // ---- 测试工具 ---- // bindKey 构建 mock 中使用的查找键 func bindKey(customerID uint, key string) string { return fmt.Sprintf("%d:%s", customerID, key) } // ---- mock 实现 ---- type mockCardReader struct { cards map[uint]*model.IotCard } func (m *mockCardReader) GetByID(_ context.Context, id uint) (*model.IotCard, error) { if c, ok := m.cards[id]; ok { return c, nil } return nil, gorm.ErrRecordNotFound } type mockDeviceReader struct { devices map[uint]*model.Device } func (m *mockDeviceReader) GetByID(_ context.Context, id uint) (*model.Device, error) { if d, ok := m.devices[id]; ok { return d, nil } return nil, gorm.ErrRecordNotFound } // mockPCDOps 模拟 tb_personal_customer_device 操作 type mockPCDOps struct { // bindings["customerID:virtualNo"] = true 表示有效(status=1)绑定 bindings map[string]bool created []*model.PersonalCustomerDevice counts map[string]int64 // virtualNo → count(用于首绑判断) allRecords []*model.PersonalCustomerDevice // GetByDeviceNo 和 UpdateStatus/UpdateVirtualNo 使用 } func (m *mockPCDOps) ExistsByCustomerAndDevice(_ context.Context, customerID uint, deviceNo string) (bool, error) { return m.bindings[bindKey(customerID, deviceNo)], nil } func (m *mockPCDOps) Create(_ context.Context, record *model.PersonalCustomerDevice) error { m.created = append(m.created, record) return nil } func (m *mockPCDOps) CountByVirtualNo(_ context.Context, virtualNo string) (int64, error) { if m.counts != nil { return m.counts[virtualNo], nil } return 0, nil } func (m *mockPCDOps) GetByDeviceNo(_ context.Context, deviceNo string) ([]*model.PersonalCustomerDevice, error) { var result []*model.PersonalCustomerDevice for _, r := range m.allRecords { if r.VirtualNo == deviceNo { result = append(result, r) } } return result, nil } func (m *mockPCDOps) UpdateStatus(_ context.Context, id uint, status int) error { for _, r := range m.allRecords { if r.ID == id { r.Status = status return nil } } return nil } func (m *mockPCDOps) UpdateVirtualNo(_ context.Context, id uint, newVirtualNo string) error { for _, r := range m.allRecords { if r.ID == id { r.VirtualNo = newVirtualNo return nil } } return nil } // mockPCIOps 模拟 tb_personal_customer_iccid 操作 type mockPCIOps struct { bindings map[string]bool created []*model.PersonalCustomerICCID counts map[string]int64 // iccid → count allRecords []*model.PersonalCustomerICCID // GetByICCID 和 UpdateStatus 使用 } func (m *mockPCIOps) ExistsByCustomerAndICCID(_ context.Context, customerID uint, iccid string) (bool, error) { return m.bindings[bindKey(customerID, iccid)], nil } func (m *mockPCIOps) Create(_ context.Context, record *model.PersonalCustomerICCID) error { m.created = append(m.created, record) return nil } func (m *mockPCIOps) CountByICCID(_ context.Context, iccid string) (int64, error) { if m.counts != nil { return m.counts[iccid], nil } return 0, nil } func (m *mockPCIOps) GetByICCID(_ context.Context, iccid string) ([]*model.PersonalCustomerICCID, error) { var result []*model.PersonalCustomerICCID for _, r := range m.allRecords { if r.ICCID == iccid { result = append(result, r) } } return result, nil } func (m *mockPCIOps) UpdateStatus(_ context.Context, id uint, status int) error { for _, r := range m.allRecords { if r.ID == id { r.Status = status return nil } } return nil } // ---- 测试 Service 构造器 ---- // soldCalls 记录 markAsSold 调用 type soldCalls struct { items []string } func (s *soldCalls) mark(_ context.Context, _ *gorm.DB, assetType string, _ uint) error { s.items = append(s.items, assetType) return nil } func newTestService(cards cardReader, devices deviceReader, pcd pcdOps, pci pciOps) (*Service, *soldCalls) { sold := &soldCalls{} return &Service{ cards: cards, devices: devices, readCard: func(ctx context.Context, _ *gorm.DB, id uint) (*model.IotCard, error) { return cards.GetByID(ctx, id) }, readDevice: func(ctx context.Context, _ *gorm.DB, id uint) (*model.Device, error) { return devices.GetByID(ctx, id) }, makePCD: func(_ *gorm.DB) pcdOps { return pcd }, makePCI: func(_ *gorm.DB) pciOps { return pci }, markAsSold: sold.mark, }, sold } // ---- OwnsAsset 测试 ---- // 验证:有虚拟号卡 + 客户有有效绑定 → true(tracer bullet) func TestOwnsAsset_有虚拟号卡_有效绑定_返回true(t *testing.T) { card := &model.IotCard{VirtualNo: "VN001"} card.ID = 1 pcd := &mockPCDOps{ bindings: map[string]bool{bindKey(10, "VN001"): true}, } svc, _ := newTestService( &mockCardReader{cards: map[uint]*model.IotCard{1: card}}, &mockDeviceReader{}, pcd, &mockPCIOps{}, ) owned, err := svc.OwnsAsset(context.Background(), 10, "iot_card", 1) if err != nil { t.Fatalf("期望无错误,实际: %v", err) } if !owned { t.Fatal("期望返回 true,实际返回 false") } } // 验证:有虚拟号卡 + status=0 的绑定 → false(修复安全缺口) func TestOwnsAsset_有虚拟号卡_禁用绑定_返回false(t *testing.T) { card := &model.IotCard{VirtualNo: "VN002"} card.ID = 2 pcd := &mockPCDOps{ // status=0 的绑定:在 ExistsByCustomerAndDevice 中会过滤掉(bindings 中不存在) bindings: map[string]bool{}, } svc, _ := newTestService( &mockCardReader{cards: map[uint]*model.IotCard{2: card}}, &mockDeviceReader{}, pcd, &mockPCIOps{}, ) owned, err := svc.OwnsAsset(context.Background(), 10, "iot_card", 2) if err != nil { t.Fatalf("期望无错误,实际: %v", err) } if owned { t.Fatal("期望返回 false(status=0),实际返回 true") } } // 验证:有虚拟号卡 + 无绑定 → false func TestOwnsAsset_有虚拟号卡_无绑定_返回false(t *testing.T) { card := &model.IotCard{VirtualNo: "VN003"} card.ID = 3 svc, _ := newTestService( &mockCardReader{cards: map[uint]*model.IotCard{3: card}}, &mockDeviceReader{}, &mockPCDOps{bindings: map[string]bool{}}, &mockPCIOps{}, ) owned, err := svc.OwnsAsset(context.Background(), 10, "iot_card", 3) if err != nil { t.Fatalf("期望无错误,实际: %v", err) } if owned { t.Fatal("期望返回 false(无绑定),实际返回 true") } } // 验证:assetType="card"(来自 assetService.Resolve)等价于 "iot_card" func TestOwnsAsset_assetType_card_等价iot_card(t *testing.T) { card := &model.IotCard{VirtualNo: "VN_CARD"} card.ID = 9 pcd := &mockPCDOps{ bindings: map[string]bool{bindKey(10, "VN_CARD"): true}, } svc, _ := newTestService( &mockCardReader{cards: map[uint]*model.IotCard{9: card}}, &mockDeviceReader{}, pcd, &mockPCIOps{}, ) owned, err := svc.OwnsAsset(context.Background(), 10, "card", 9) if err != nil { t.Fatalf("期望无错误,实际: %v", err) } if !owned { t.Fatal("期望 card 类型等价 iot_card 返回 true,实际 false") } } // 验证:设备资产 + 有效绑定 → true func TestOwnsAsset_设备_有效绑定_返回true(t *testing.T) { device := &model.Device{VirtualNo: "DEV001"} device.ID = 5 pcd := &mockPCDOps{ bindings: map[string]bool{bindKey(10, "DEV001"): true}, } svc, _ := newTestService( &mockCardReader{}, &mockDeviceReader{devices: map[uint]*model.Device{5: device}}, pcd, &mockPCIOps{}, ) owned, err := svc.OwnsAsset(context.Background(), 10, "device", 5) if err != nil { t.Fatalf("期望无错误,实际: %v", err) } if !owned { t.Fatal("期望返回 true,实际返回 false") } } // ---- Bind 测试 ---- // 验证:首次绑定有虚拟号卡 → 写入 pcd 记录 + 触发 markAssetAsSold func TestBind_有虚拟号卡_首次绑定_创建记录并标记已售(t *testing.T) { card := &model.IotCard{VirtualNo: "VN010"} card.ID = 10 pcd := &mockPCDOps{ bindings: map[string]bool{}, counts: map[string]int64{"VN010": 0}, // 首次绑定 } svc, sold := newTestService( &mockCardReader{cards: map[uint]*model.IotCard{10: card}}, &mockDeviceReader{}, pcd, &mockPCIOps{}, ) err := svc.Bind(context.Background(), nil, 20, "iot_card", 10) if err != nil { t.Fatalf("期望无错误,实际: %v", err) } if len(pcd.created) != 1 { t.Fatalf("期望创建 1 条 pcd 记录,实际: %d", len(pcd.created)) } if pcd.created[0].VirtualNo != "VN010" { t.Errorf("期望 VirtualNo=VN010,实际: %s", pcd.created[0].VirtualNo) } if pcd.created[0].CustomerID != 20 { t.Errorf("期望 CustomerID=20,实际: %d", pcd.created[0].CustomerID) } if len(sold.items) != 1 || sold.items[0] != "iot_card" { t.Errorf("期望触发 markAssetAsSold(iot_card),实际: %v", sold.items) } } // 验证:重复绑定 → 不创建新记录 func TestBind_有虚拟号卡_已有绑定_不重复创建(t *testing.T) { card := &model.IotCard{VirtualNo: "VN011"} card.ID = 11 pcd := &mockPCDOps{ bindings: map[string]bool{bindKey(20, "VN011"): true}, counts: map[string]int64{"VN011": 1}, } svc, _ := newTestService( &mockCardReader{cards: map[uint]*model.IotCard{11: card}}, &mockDeviceReader{}, pcd, &mockPCIOps{}, ) err := svc.Bind(context.Background(), nil, 20, "iot_card", 11) if err != nil { t.Fatalf("期望无错误,实际: %v", err) } if len(pcd.created) != 0 { t.Fatalf("期望不创建新记录,实际创建了 %d 条", len(pcd.created)) } } // 验证:非首次绑定(已有其他客户绑定)→ 创建记录但不触发 markAssetAsSold func TestBind_有虚拟号卡_非首次绑定_创建记录不标记已售(t *testing.T) { card := &model.IotCard{VirtualNo: "VN012"} card.ID = 12 pcd := &mockPCDOps{ bindings: map[string]bool{}, counts: map[string]int64{"VN012": 1}, // 已有其他绑定 } svc, sold := newTestService( &mockCardReader{cards: map[uint]*model.IotCard{12: card}}, &mockDeviceReader{}, pcd, &mockPCIOps{}, ) err := svc.Bind(context.Background(), nil, 30, "iot_card", 12) if err != nil { t.Fatalf("期望无错误,实际: %v", err) } if len(pcd.created) != 1 { t.Fatalf("期望创建 1 条记录,实际: %d", len(pcd.created)) } if len(sold.items) != 0 { t.Errorf("期望不触发 markAssetAsSold,实际触发了: %v", sold.items) } } // ---- Issue 02: 无虚拟号卡 pci 路径测试 ---- // 验证:无虚拟号卡 + 客户有有效 PCI 绑定 → true func TestOwnsAsset_无虚拟号卡_有效PCI绑定_返回true(t *testing.T) { card := &model.IotCard{ICCID: "89860000000000000001"} // VirtualNo 为空 card.ID = 20 pci := &mockPCIOps{ bindings: map[string]bool{bindKey(10, "89860000000000000001"): true}, } svc, _ := newTestService( &mockCardReader{cards: map[uint]*model.IotCard{20: card}}, &mockDeviceReader{}, &mockPCDOps{bindings: map[string]bool{}}, pci, ) owned, err := svc.OwnsAsset(context.Background(), 10, "iot_card", 20) if err != nil { t.Fatalf("期望无错误,实际: %v", err) } if !owned { t.Fatal("期望返回 true,实际返回 false") } } // 验证:无虚拟号卡 + 无 PCI 绑定 → false func TestOwnsAsset_无虚拟号卡_无PCI绑定_返回false(t *testing.T) { card := &model.IotCard{ICCID: "89860000000000000002"} // VirtualNo 为空 card.ID = 21 svc, _ := newTestService( &mockCardReader{cards: map[uint]*model.IotCard{21: card}}, &mockDeviceReader{}, &mockPCDOps{bindings: map[string]bool{}}, &mockPCIOps{bindings: map[string]bool{}}, ) owned, err := svc.OwnsAsset(context.Background(), 10, "iot_card", 21) if err != nil { t.Fatalf("期望无错误,实际: %v", err) } if owned { t.Fatal("期望返回 false(无 PCI 绑定),实际返回 true") } } // 验证:无虚拟号卡首次绑定 → 写入 pci 记录 + 触发 markAssetAsSold func TestBind_无虚拟号卡_首次绑定_创建PCI记录并标记已售(t *testing.T) { card := &model.IotCard{ICCID: "89860000000000000010"} // VirtualNo 为空 card.ID = 30 pci := &mockPCIOps{ bindings: map[string]bool{}, counts: map[string]int64{"89860000000000000010": 0}, // 首次绑定 } svc, sold := newTestService( &mockCardReader{cards: map[uint]*model.IotCard{30: card}}, &mockDeviceReader{}, &mockPCDOps{bindings: map[string]bool{}}, pci, ) err := svc.Bind(context.Background(), nil, 50, "iot_card", 30) if err != nil { t.Fatalf("期望无错误,实际: %v", err) } if len(pci.created) != 1 { t.Fatalf("期望创建 1 条 pci 记录,实际: %d", len(pci.created)) } if pci.created[0].ICCID != "89860000000000000010" { t.Errorf("期望 ICCID=89860000000000000010,实际: %s", pci.created[0].ICCID) } if pci.created[0].CustomerID != 50 { t.Errorf("期望 CustomerID=50,实际: %d", pci.created[0].CustomerID) } if len(sold.items) != 1 || sold.items[0] != "iot_card" { t.Errorf("期望触发 markAssetAsSold(iot_card),实际: %v", sold.items) } } // 验证:无虚拟号卡已有绑定 → 不重复创建 func TestBind_无虚拟号卡_已有绑定_不重复创建(t *testing.T) { card := &model.IotCard{ICCID: "89860000000000000011"} // VirtualNo 为空 card.ID = 31 pci := &mockPCIOps{ bindings: map[string]bool{bindKey(50, "89860000000000000011"): true}, counts: map[string]int64{"89860000000000000011": 1}, } svc, _ := newTestService( &mockCardReader{cards: map[uint]*model.IotCard{31: card}}, &mockDeviceReader{}, &mockPCDOps{bindings: map[string]bool{}}, pci, ) err := svc.Bind(context.Background(), nil, 50, "iot_card", 31) if err != nil { t.Fatalf("期望无错误,实际: %v", err) } if len(pci.created) != 0 { t.Fatalf("期望不创建新记录,实际创建了 %d 条", len(pci.created)) } } // ---- Issue 03: Migrate 迁移测试 ---- // newMockPCDRecord 构建一条带 ID 的 pcd 记录(用于迁移测试) func newMockPCDRecord(id, customerID uint, virtualNo string, status int) *model.PersonalCustomerDevice { r := &model.PersonalCustomerDevice{ CustomerID: customerID, VirtualNo: virtualNo, Status: status, } r.ID = id return r } // newMockPCIRecord 构建一条带 ID 的 pci 记录(用于迁移测试) func newMockPCIRecord(id, customerID uint, iccid string, status int) *model.PersonalCustomerICCID { r := &model.PersonalCustomerICCID{ CustomerID: customerID, ICCID: iccid, Status: status, } r.ID = id return r } // 验证:旧卡有虚拟号 + pcd 有绑定 + 新卡有虚拟号 → pcd.virtual_no 更新为新卡虚拟号 func TestMigrate_有虚拟号旧卡_有绑定_换有虚拟号新卡_更新VirtualNo(t *testing.T) { oldCard := &model.IotCard{VirtualNo: "OLD_VN"} oldCard.ID = 100 newCard := &model.IotCard{VirtualNo: "NEW_VN"} newCard.ID = 101 existing := newMockPCDRecord(1, 50, "OLD_VN", 1) pcd := &mockPCDOps{ bindings: map[string]bool{}, allRecords: []*model.PersonalCustomerDevice{existing}, } svc, _ := newTestService( &mockCardReader{cards: map[uint]*model.IotCard{100: oldCard, 101: newCard}}, &mockDeviceReader{}, pcd, &mockPCIOps{}, ) err := svc.Migrate(context.Background(), nil, "iot_card", 100, "iot_card", 101) if err != nil { t.Fatalf("期望无错误,实际: %v", err) } if existing.VirtualNo != "NEW_VN" { t.Errorf("期望 VirtualNo 更新为 NEW_VN,实际: %s", existing.VirtualNo) } } // 验证:旧卡有虚拟号 + pcd 有绑定 + 新卡无虚拟号 → 旧 pcd status=0,新 pci 创建 func TestMigrate_有虚拟号旧卡_有绑定_换无虚拟号新卡_迁移到PCI(t *testing.T) { oldCard := &model.IotCard{VirtualNo: "OLD_VN2"} oldCard.ID = 110 newCard := &model.IotCard{ICCID: "89860000000000000099"} // 无虚拟号 newCard.ID = 111 existing := newMockPCDRecord(2, 60, "OLD_VN2", 1) pcd := &mockPCDOps{ allRecords: []*model.PersonalCustomerDevice{existing}, } pci := &mockPCIOps{} svc, _ := newTestService( &mockCardReader{cards: map[uint]*model.IotCard{110: oldCard, 111: newCard}}, &mockDeviceReader{}, pcd, pci, ) err := svc.Migrate(context.Background(), nil, "iot_card", 110, "iot_card", 111) if err != nil { t.Fatalf("期望无错误,实际: %v", err) } if existing.Status != 0 { t.Errorf("期望旧 pcd 记录 status=0,实际: %d", existing.Status) } if len(pci.created) != 1 { t.Fatalf("期望创建 1 条 pci 记录,实际: %d", len(pci.created)) } if pci.created[0].CustomerID != 60 { t.Errorf("期望 pci CustomerID=60,实际: %d", pci.created[0].CustomerID) } if pci.created[0].ICCID != "89860000000000000099" { t.Errorf("期望 pci ICCID=89860000000000000099,实际: %s", pci.created[0].ICCID) } } // 验证:旧卡有虚拟号 + pcd 无绑定 + 新卡无虚拟号 → 跳过,无写入 func TestMigrate_有虚拟号旧卡_无绑定_换无虚拟号新卡_跳过(t *testing.T) { oldCard := &model.IotCard{VirtualNo: "OLD_VN3"} oldCard.ID = 120 newCard := &model.IotCard{ICCID: "89860000000000000088"} // 无虚拟号 newCard.ID = 121 pcd := &mockPCDOps{allRecords: []*model.PersonalCustomerDevice{}} // 空 pci := &mockPCIOps{} svc, _ := newTestService( &mockCardReader{cards: map[uint]*model.IotCard{120: oldCard, 121: newCard}}, &mockDeviceReader{}, pcd, pci, ) err := svc.Migrate(context.Background(), nil, "iot_card", 120, "iot_card", 121) if err != nil { t.Fatalf("期望无错误,实际: %v", err) } if len(pci.created) != 0 { t.Fatalf("期望无写入,实际创建了 %d 条 pci 记录", len(pci.created)) } } // 验证:旧卡无虚拟号 + pci 有绑定 + 新卡有虚拟号 → 旧 pci status=0,新 pcd 创建 func TestMigrate_无虚拟号旧卡_有绑定_换有虚拟号新卡_迁移到PCD(t *testing.T) { oldCard := &model.IotCard{ICCID: "89860000000000000077"} // 无虚拟号 oldCard.ID = 130 newCard := &model.IotCard{VirtualNo: "NEW_VN3"} newCard.ID = 131 existingPCI := newMockPCIRecord(3, 70, "89860000000000000077", 1) pci := &mockPCIOps{allRecords: []*model.PersonalCustomerICCID{existingPCI}} pcd := &mockPCDOps{} svc, _ := newTestService( &mockCardReader{cards: map[uint]*model.IotCard{130: oldCard, 131: newCard}}, &mockDeviceReader{}, pcd, pci, ) err := svc.Migrate(context.Background(), nil, "iot_card", 130, "iot_card", 131) if err != nil { t.Fatalf("期望无错误,实际: %v", err) } if existingPCI.Status != 0 { t.Errorf("期望旧 pci 记录 status=0,实际: %d", existingPCI.Status) } if len(pcd.created) != 1 { t.Fatalf("期望创建 1 条 pcd 记录,实际: %d", len(pcd.created)) } if pcd.created[0].CustomerID != 70 { t.Errorf("期望 pcd CustomerID=70,实际: %d", pcd.created[0].CustomerID) } if pcd.created[0].VirtualNo != "NEW_VN3" { t.Errorf("期望 pcd VirtualNo=NEW_VN3,实际: %s", pcd.created[0].VirtualNo) } } // 验证:旧卡无虚拟号 + pci 有绑定 + 新卡也无虚拟号 → 旧 pci status=0,新 pci 创建新 ICCID func TestMigrate_无虚拟号旧卡_有绑定_换无虚拟号新卡_迁移PCI(t *testing.T) { oldCard := &model.IotCard{ICCID: "89860000000000000066"} // 无虚拟号 oldCard.ID = 140 newCard := &model.IotCard{ICCID: "89860000000000000055"} // 无虚拟号 newCard.ID = 141 existingPCI := newMockPCIRecord(4, 80, "89860000000000000066", 1) pci := &mockPCIOps{allRecords: []*model.PersonalCustomerICCID{existingPCI}} pcd := &mockPCDOps{} svc, _ := newTestService( &mockCardReader{cards: map[uint]*model.IotCard{140: oldCard, 141: newCard}}, &mockDeviceReader{}, pcd, pci, ) err := svc.Migrate(context.Background(), nil, "iot_card", 140, "iot_card", 141) if err != nil { t.Fatalf("期望无错误,实际: %v", err) } if existingPCI.Status != 0 { t.Errorf("期望旧 pci 记录 status=0,实际: %d", existingPCI.Status) } if len(pci.created) != 1 { t.Fatalf("期望创建 1 条新 pci 记录,实际: %d", len(pci.created)) } if pci.created[0].CustomerID != 80 { t.Errorf("期望 pci CustomerID=80,实际: %d", pci.created[0].CustomerID) } if pci.created[0].ICCID != "89860000000000000055" { t.Errorf("期望 pci ICCID=89860000000000000055,实际: %s", pci.created[0].ICCID) } } // 验证:无虚拟号卡非首次绑定(已有其他客户)→ 创建记录但不触发 markAssetAsSold func TestBind_无虚拟号卡_非首次绑定_创建记录不标记已售(t *testing.T) { card := &model.IotCard{ICCID: "89860000000000000012"} // VirtualNo 为空 card.ID = 32 pci := &mockPCIOps{ bindings: map[string]bool{}, counts: map[string]int64{"89860000000000000012": 1}, // 已有其他客户绑定 } svc, sold := newTestService( &mockCardReader{cards: map[uint]*model.IotCard{32: card}}, &mockDeviceReader{}, &mockPCDOps{bindings: map[string]bool{}}, pci, ) err := svc.Bind(context.Background(), nil, 60, "iot_card", 32) if err != nil { t.Fatalf("期望无错误,实际: %v", err) } if len(pci.created) != 1 { t.Fatalf("期望创建 1 条记录,实际: %d", len(pci.created)) } if len(sold.items) != 0 { t.Errorf("期望不触发 markAssetAsSold,实际触发了: %v", sold.items) } }