//go:build integration // 集成测试:公开仓储方法必须读视图(读不到 pending),后台方法必须读基表(读得到 pending)。 package repository import ( "context" "errors" "strconv" "testing" "fashionapi/internal/dto" "fashionapi/internal/model" ) // TestPublicReadHidesUnpublishedRunway 走秀公开列表/详情/图片看不到 pending;后台 GetForEdit 看得到。 func TestPublicReadHidesUnpublishedRunway(t *testing.T) { db := testDB(t) applyMigration(t, db, "2026-09-22-01-single-table-publish.sql") ctx := context.Background() repo := NewArticleRepository(db) const year = 1997 id := seedRunway(t, db, model.StatusPending, year) idStr := strconv.FormatUint(uint64(id), 10) if _, err := repo.FindByID(ctx, idStr); !errors.Is(err, ErrNotFound) { t.Fatalf("pending 走秀详情应 ErrNotFound,实际 %v", err) } imgs, err := repo.ListImages(ctx, idStr) if err != nil { t.Fatalf("ListImages 出错: %v", err) } if len(imgs) != 0 { t.Fatalf("pending 走秀的图片不应公开,实际 %d 张", len(imgs)) } _, total, err := repo.List(ctx, dto.ArticleQuery{Year: year, Size: 20}) if err != nil { t.Fatalf("List 出错: %v", err) } if total != 0 { t.Fatalf("pending 走秀不应出现在公开列表,实际 total=%d", total) } // 后台读基表:必须能看到这条待审记录与其图片,否则审核页是空的。 rw, baseImgs, err := repo.GetForEdit(ctx, id) if err != nil { t.Fatalf("后台 GetForEdit 应读到 pending 行: %v", err) } if rw == nil || len(baseImgs) == 0 { t.Fatalf("后台应读到 pending 行与其图片,实际 rw=%v imgs=%d", rw != nil, len(baseImgs)) } // 后台详情通路:FindByIDBase 也必须读得到 pending(后台详情页据此渲染; // 若它读视图,后台列表里的待审行点进去就 404)。 baseRow, err := repo.FindByIDBase(ctx, idStr) if err != nil { t.Fatalf("后台 FindByIDBase 应读到 pending 行: %v", err) } if baseRow == nil || baseRow.ID != id { t.Fatalf("后台 FindByIDBase 返回不符: %+v", baseRow) } // 置为已发布后,公开侧才可见。 if err := db.Model(&model.BrandRunway{}).Where("id = ?", id).Update("status", model.StatusPublished).Error; err != nil { t.Fatalf("置为已发布失败: %v", err) } if _, err := repo.FindByID(ctx, idStr); err != nil { t.Fatalf("已发布走秀详情应可读: %v", err) } _, total, err = repo.List(ctx, dto.ArticleQuery{Year: year, Size: 20}) if err != nil { t.Fatalf("List 出错: %v", err) } if total != 1 { t.Fatalf("已发布走秀应出现在公开列表,实际 total=%d", total) } } // TestPublicReadHidesUnpublishedStreet 街拍公开列表/详情/图片看不到 pending;后台 GetForEdit 看得到。 func TestPublicReadHidesUnpublishedStreet(t *testing.T) { db := testDB(t) applyMigration(t, db, "2026-09-22-01-single-table-publish.sql") ctx := context.Background() repo := NewStreetSnapRepository(db) const year = 1996 id := seedStreetSnap(t, db, model.StatusPending, year) idStr := strconv.FormatUint(uint64(id), 10) if _, err := repo.FindByID(ctx, idStr); !errors.Is(err, ErrNotFound) { t.Fatalf("pending 街拍详情应 ErrNotFound,实际 %v", err) } imgs, err := repo.ListImages(ctx, idStr) if err != nil { t.Fatalf("ListImages 出错: %v", err) } if len(imgs) != 0 { t.Fatalf("pending 街拍的图片不应公开,实际 %d 张", len(imgs)) } _, total, err := repo.List(ctx, dto.StreetSnapQuery{City: "ViewTestCity", Size: 20}) if err != nil { t.Fatalf("List 出错: %v", err) } if total != 0 { t.Fatalf("pending 街拍不应出现在公开列表,实际 total=%d", total) } snap, baseImgs, err := repo.GetForEdit(ctx, id) if err != nil { t.Fatalf("后台 GetForEdit 应读到 pending 行: %v", err) } if snap == nil || len(baseImgs) == 0 { t.Fatalf("后台应读到 pending 行与其图片,实际 snap=%v imgs=%d", snap != nil, len(baseImgs)) } // 后台列表 ListAdmin 读基表:必须能看到这条待审记录,否则审核员无从下手。 _, adminTotal, err := repo.ListAdmin(ctx, dto.StreetSnapQuery{City: "ViewTestCity", Size: 20}) if err != nil { t.Fatalf("ListAdmin 出错: %v", err) } if adminTotal != 1 { t.Fatalf("后台列表应看到 pending 街拍,实际 total=%d", adminTotal) } // 后台详情通路:FindByIDBase 也必须读得到 pending(否则后台详情页 404)。 baseSnap, err := repo.FindByIDBase(ctx, idStr) if err != nil { t.Fatalf("后台 FindByIDBase 应读到 pending 行: %v", err) } if baseSnap == nil || baseSnap.ID != id { t.Fatalf("后台 FindByIDBase 返回不符: %+v", baseSnap) } if err := db.Model(&model.StreetSnap{}).Where("id = ?", id).Update("status", model.StatusPublished).Error; err != nil { t.Fatalf("置为已发布失败: %v", err) } _, total, err = repo.List(ctx, dto.StreetSnapQuery{City: "ViewTestCity", Size: 20}) if err != nil { t.Fatalf("List 出错: %v", err) } if total != 1 { t.Fatalf("已发布街拍应出现在公开列表,实际 total=%d", total) } } // hasBrandID 判断品牌列表里是否含指定 id(按 id 判定,避免受库里既有同名/同首字母品牌干扰)。 func hasBrandID(items []model.Brand, id uint32) bool { for _, it := range items { if it.ID == id { return true } } return false } // TestBrandIndexSplitsPublicAndAdmin 品牌 A-Z 索引的公开/后台分野: // 只有待审走秀的品牌不得出现在公开索引(否则未发布内容从索引泄露), // 但必须出现在后台列表(否则该品牌从后台「消失」,审核员无从下手)。 func TestBrandIndexSplitsPublicAndAdmin(t *testing.T) { db := testDB(t) applyMigration(t, db, "2026-09-22-01-single-table-publish.sql") ctx := context.Background() repo := NewBrandRepository(db) now := uint32(1_800_000_000) brand := model.Brand{NameEn: "ViewTestBrand", NameCn: "视图测试品牌", CreatedAt: now, UpdatedAt: now} if err := db.Create(&brand).Error; err != nil { t.Fatalf("插入品牌失败: %v", err) } rw := model.BrandRunway{ BrandID: brand.ID, TitleEn: "view-test-brand", Year: 1995, Status: model.StatusPending, CreatedAt: now, UpdatedAt: now, } if err := db.Create(&rw).Error; err != nil { t.Fatalf("插入走秀失败: %v", err) } t.Cleanup(func() { db.Exec("DELETE FROM brand_runways WHERE id = ?", rw.ID) db.Exec("DELETE FROM brands WHERE id = ?", brand.ID) }) // 字母桶 V + 大页,确保命中「必须有走秀档案」的子查询分支、且不会被分页截断。 q := dto.BrandQuery{Letter: "V", Page: 1, Size: 400} pubItems, _, err := repo.List(ctx, q, nil) if err != nil { t.Fatalf("公开 List 出错: %v", err) } if hasBrandID(pubItems, brand.ID) { t.Fatalf("只有待审走秀的品牌不应出现在公开索引,实际出现在结果里(id=%d)", brand.ID) } admItems, _, err := repo.ListAdmin(ctx, q, nil) if err != nil { t.Fatalf("后台 ListAdmin 出错: %v", err) } if !hasBrandID(admItems, brand.ID) { t.Fatalf("后台列表应看到只有待审走秀的品牌(id=%d)", brand.ID) } }