package service import ( "context" "testing" "fashionapi/internal/dto" "fashionapi/internal/model" "fashionapi/internal/repository" ) // stubManualIngest 只实现建稿用到的两个方法(其余经嵌入接口转发,本测试不会调用)。 type stubManualIngest struct { repository.IngestRepository entityHit bool created bool gotRW *model.BrandRunway gotImgs []model.BrandRunwayImage } func (s *stubManualIngest) RunwayEntityState(_ context.Context, _ uint32, _, _ string) (uint32, string, bool, bool, error) { return 0, "", s.entityHit, false, nil } func (s *stubManualIngest) CreateRunwayWithImages(_ context.Context, rw *model.BrandRunway, imgs []model.BrandRunwayImage) (uint32, error) { s.created, s.gotRW, s.gotImgs = true, rw, imgs return 99, nil } // stubManualBrands 提供品牌精确匹配所需的一行。 type stubManualBrands struct { repository.BrandRepository name string } func (s *stubManualBrands) ListAdmin(_ context.Context, _ dto.BrandQuery, _ []uint32) ([]model.Brand, int64, error) { if s.name == "" { return []model.Brand{}, 0, nil } return []model.Brand{{ID: 7, NameEn: s.name}}, 1, nil } // stubManualUploader 记录上传的 key,用于断言 sha1 内容寻址与去重。 type stubManualUploader struct{ keys []string } func (s *stubManualUploader) Upload(_ context.Context, key string, _ []byte, _ string) (string, error) { s.keys = append(s.keys, key) return "https://cdn/" + key, nil } func (s *stubManualUploader) Enabled() bool { return true } func manualRunwayInput(images []ManualImage) ManualArticleInput { return ManualArticleInput{ Kind: manualKindRunway, BrandName: "Gucci", TitleEn: "Spring 2027 RTW", Year: 2027, Season: "spring", CollectionType: "rtw", Images: images, } } // 一张最小 JPEG 头,http.DetectContentType 会识别为 image/jpeg。 var fakeJPEG = append([]byte{0xFF, 0xD8, 0xFF, 0xE0}, []byte("manual-test-payload")...) // TestManualCreateRequiresImages 没有图片必须直接拒绝,且既不上传也不写库。 func TestManualCreateRequiresImages(t *testing.T) { up := &stubManualUploader{} ing := &stubManualIngest{} svc := NewManualArticleService(ing, &stubManualBrands{name: "Gucci"}, up, nil, nil, nil) if _, err := svc.Create(context.Background(), manualRunwayInput(nil)); err == nil { t.Fatal("没有图片应报错") } if len(up.keys) != 0 || ing.created { t.Fatalf("校验失败不应上传/写库,实际 uploads=%d created=%v", len(up.keys), ing.created) } } // TestManualCreateBrandNotFound 品牌名匹配不到时必须报错且不写库。 func TestManualCreateBrandNotFound(t *testing.T) { up := &stubManualUploader{} ing := &stubManualIngest{} svc := NewManualArticleService(ing, &stubManualBrands{name: "SomeOtherBrand"}, up, nil, nil, nil) in := manualRunwayInput([]ManualImage{{Filename: "a.jpg", Data: fakeJPEG}}) if _, err := svc.Create(context.Background(), in); err == nil { t.Fatal("品牌不存在应报错") } if ing.created || len(up.keys) != 0 { t.Fatalf("品牌不存在不应写库/上传,实际 created=%v uploads=%d", ing.created, len(up.keys)) } } // TestManualCreateRunwayUploadsAndInserts 正常提交:sha1 内容寻址、同内容去重、 // 封面取第一张、image_count 与实际行数一致。 func TestManualCreateRunwayUploadsAndInserts(t *testing.T) { up := &stubManualUploader{} ing := &stubManualIngest{} svc := NewManualArticleService(ing, &stubManualBrands{name: "gucci"}, up, nil, nil, nil) // 两张同内容(应去重成 1 张)+ 一张不同内容 → 共 2 张 other := append([]byte{0xFF, 0xD8, 0xFF, 0xE0}, []byte("another-payload")...) in := manualRunwayInput([]ManualImage{ {Filename: "a.jpg", Data: fakeJPEG}, {Filename: "a-copy.jpg", Data: fakeJPEG}, {Filename: "b.jpg", Data: other}, }) id, err := svc.Create(context.Background(), in) if err != nil { t.Fatalf("正常提交不应报错: %v", err) } if id != 99 { t.Fatalf("应返回仓储给的主键,实际 %d", id) } if len(up.keys) != 2 { t.Fatalf("同内容图片应去重为 2 张上传,实际 %d: %v", len(up.keys), up.keys) } for _, k := range up.keys { if len(k) < len("runway/")+40+len(".jpg") || k[:7] != "runway/" { t.Fatalf("key 应为 runway/.jpg,实际 %q", k) } } if !ing.created { t.Fatal("应写入正式表") } if len(ing.gotImgs) != 2 || ing.gotRW.ImageCount != 2 { t.Fatalf("图片行数与 image_count 都应等于 2,实际 rows=%d count=%d", len(ing.gotImgs), ing.gotRW.ImageCount) } if ing.gotRW.Cover != up.keys[0] { t.Fatalf("封面应取第一张,实际 cover=%q keys=%v", ing.gotRW.Cover, up.keys) } if ing.gotRW.Status != model.StatusPending { t.Fatalf("新建记录必须是 pending(走审核),实际 %s", ing.gotRW.Status) } if ing.gotRW.BrandID != 7 { t.Fatalf("品牌应按英文名匹配到 id=7(不区分大小写),实际 %d", ing.gotRW.BrandID) } }