package service import ( "context" "errors" "testing" "fashionapi/internal/dto" "fashionapi/internal/model" "fashionapi/internal/pkg/imgurl" "fashionapi/internal/repository" ) // fakeGroupRepo 只实现分组相关的仓储方法,其余由嵌入的接口兜底(测试中不会调到未实现的方法)。 type fakeGroupRepo struct { repository.ReviewRepository images []model.StreetSnapImage detail *model.StreetSnap attachFn func(snapID, mainID uint32, imgIDs []uint32) error } func (f *fakeGroupRepo) StreetRecordDetail(_ context.Context, _ uint32) (*model.StreetSnap, error) { return f.detail, nil } func (f *fakeGroupRepo) StreetRecordImages(_ context.Context, _ uint32) ([]model.StreetSnapImage, error) { return f.images, nil } func (f *fakeGroupRepo) AttachStreetSnapImages(_ context.Context, snapID, mainID uint32, imgIDs []uint32) error { if f.attachFn != nil { return f.attachFn(snapID, mainID, imgIDs) } return nil } func (f *fakeGroupRepo) DetachStreetSnapImage(_ context.Context, _ uint32, _ uint32) error { if f.attachFn != nil { return f.attachFn(0, 0, nil) } return nil } var _ repository.ReviewRepository = (*fakeGroupRepo)(nil) func newGroupService(f *fakeGroupRepo) ReviewService { return NewReviewService(f, nil, imgurl.New("", "", ""), nil) } // TestStreetGroupDetailExposesGrouping 详情必须输出可折叠分组:主图各成一组,副图归入主图。 func TestStreetGroupDetailExposesGrouping(t *testing.T) { repo := &fakeGroupRepo{ detail: &model.StreetSnap{ID: 7, Title: "T", Year: 2026, City: "Beijing", Status: model.StatusPending}, images: []model.StreetSnapImage{ {ID: 1, Image: "main.jpg", Name: "main", SortOrder: 1, IsDetail: 0, ParentImageID: 0}, {ID: 2, Image: "det.jpg", Name: "det", SortOrder: 2, IsDetail: 1, ParentImageID: 1}, {ID: 3, Image: "solo.jpg", Name: "solo", SortOrder: 3, IsDetail: 0, ParentImageID: 0}, }, } svc := newGroupService(repo) v, err := svc.RecordDetail(context.Background(), dto.IngestKindStreet, 7) if err != nil { t.Fatalf("RecordDetail 失败: %v", err) } if len(v.Images) != 3 { t.Fatalf("Images 应保留扁平 3 张,实际 %d", len(v.Images)) } if len(v.Groups) != 2 { t.Fatalf("Groups 应为 2 组,实际 %d", len(v.Groups)) } if v.Groups[0].Image.ID != 1 || len(v.Groups[0].Details) != 1 { t.Fatalf("第 0 组应为 1 张主图 + 1 副图,实际 main=%d details=%d", v.Groups[0].Image.ID, len(v.Groups[0].Details)) } if v.Groups[0].Image.DetailCount != 1 { t.Fatalf("主图 DetailCount 应为 1,实际 %d", v.Groups[0].Image.DetailCount) } if v.Groups[0].Details[0].ID != 2 { t.Fatalf("副图应为 id=2,实际 %d", v.Groups[0].Details[0].ID) } if v.Groups[1].Image.ID != 3 { t.Fatalf("第 1 组主图应为 id=3,实际 %d", v.Groups[1].Image.ID) } } // TestStreetGroupAttachDelegates 并入必须透传参数到仓储。 func TestStreetGroupAttachDelegates(t *testing.T) { var got struct { snapID uint32 mainID uint32 imgIDs []uint32 } repo := &fakeGroupRepo{attachFn: func(snapID, mainID uint32, imgIDs []uint32) error { got.snapID, got.mainID, got.imgIDs = snapID, mainID, imgIDs return nil }} svc := newGroupService(repo) if err := svc.AttachImages(context.Background(), dto.IngestKindStreet, 7, 1, []uint32{2, 3}); err != nil { t.Fatalf("AttachImages 失败: %v", err) } if got.snapID != 7 || got.mainID != 1 || len(got.imgIDs) != 2 { t.Fatalf("参数透传错误: snapID=%d mainID=%d imgIDs=%v", got.snapID, got.mainID, got.imgIDs) } } // TestStreetGroupNonStreetRejected 走秀模块不允许主副图分组操作。 func TestStreetGroupNonStreetRejected(t *testing.T) { repo := &fakeGroupRepo{} svc := newGroupService(repo) if err := svc.AttachImages(context.Background(), dto.IngestKindRunway, 7, 1, []uint32{2}); !errors.Is(err, ErrInvalidImageGroup) { t.Fatalf("走秀并入应返回 ErrInvalidImageGroup,实际 %v", err) } } // TestStreetGroupPreventCrossRecord 仓储拦截跨记录主图后,service 翻成中文用户错误。 func TestStreetGroupPreventCrossRecord(t *testing.T) { repo := &fakeGroupRepo{attachFn: func(_, _ uint32, _ []uint32) error { return repository.ErrInvalidImageGroup }} svc := newGroupService(repo) err := svc.AttachImages(context.Background(), dto.IngestKindStreet, 7, 1, []uint32{2, 3}) if err == nil { t.Fatal("跨记录应报错") } if err.Error() != "主图无效:它必须属于本条记录、未被删除,且自身是主图" { t.Fatalf("错误文案不符: %q", err.Error()) } }