Files
backend_v2/internal/repository/entity_key_unique_integration_test.go

116 lines
5.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//go:build integration
// 集成测试:实体键上的部分唯一索引必须阻止「同一实体两行」,
// 同时不阻止「实体键相同但已软删」的行(与 RunwayEntityState 的 is_deleted = 0 口径一致)。
package repository
import (
"context"
"testing"
"fashionapi/internal/model"
)
// TestRunwayEntityKeyUnique 同一实体键的第二行应被拒绝;软删后可以再插入。
func TestRunwayEntityKeyUnique(t *testing.T) {
db := testDB(t)
applyMigration(t, db, "2026-09-22-01-single-table-publish.sql")
applyMigration(t, db, "2026-09-22-03-entity-key-unique.sql")
repo := NewIngestRepository(db)
ctx := context.Background()
const season = "SS95"
rw := newRunwayForIngest(1, 1, season, "rtw", "uniq-a.jpg")
id, err := repo.CreateRunwayWithImages(ctx, rw, newRunwayImages("uniq-a.jpg"))
if err != nil {
t.Fatalf("首行应能插入: %v", err)
}
t.Cleanup(func() {
db.Exec("DELETE FROM brand_runway_images WHERE runway_id = ?", id)
db.Exec("DELETE FROM brand_runways WHERE id = ?", id)
})
dup := newRunwayForIngest(1, 1, season, "rtw", "uniq-b.jpg")
if _, err := repo.CreateRunwayWithImages(ctx, dup, newRunwayImages("uniq-b.jpg")); err == nil {
t.Fatalf("同一实体键的第二行应被唯一索引拒绝,实际插入成功")
}
// 软删首行后,实体键不再占用(与 is_deleted = 0 的部分索引口径一致)。
if err := db.Model(&model.BrandRunway{}).Where("id = ?", id).Update("is_deleted", 1).Error; err != nil {
t.Fatalf("软删失败: %v", err)
}
again := newRunwayForIngest(1, 1, season, "rtw", "uniq-c.jpg")
newID, err := repo.CreateRunwayWithImages(ctx, again, newRunwayImages("uniq-c.jpg"))
if err != nil {
t.Fatalf("软删后应可再插入同实体键: %v", err)
}
t.Cleanup(func() {
db.Exec("DELETE FROM brand_runway_images WHERE runway_id = ?", newID)
db.Exec("DELETE FROM brand_runways WHERE id = ?", newID)
})
}
// TestStreetEntityKeyUnique 街拍实体键 = city + year + title(2026-09-22-04 细化后)。
//
// 为什么键里要有 title:同城同年可以有多个专题(开发库实例 London 2027 Day 2 / Day 3),
// title 是数据里唯一能区分它们的字段。因此:
// ① 同城同年**同标题**的第二行必须被拒(实体键仍具备唯一性);
// ② 同城同年**不同标题**的第二行必须允许(对应「Day 2 / Day 3 分成两个文章」)。
func TestStreetEntityKeyUnique(t *testing.T) {
db := testDB(t)
applyMigration(t, db, "2026-09-22-01-single-table-publish.sql")
applyMigration(t, db, "2026-09-22-03-entity-key-unique.sql")
// 04 是替换索引:先 DROP 掉 03 建的 (city, year),再按 (city, year, title) 重建同名索引。
// 两个都执行才真实还原「迁移链走完」的库结构,也才能验证旧键确实不再生效。
applyMigration(t, db, "2026-09-22-04-street-entity-key-title.sql")
repo := NewIngestRepository(db)
ctx := context.Background()
const city = "UniqTestCity"
const year = 1994
const titleDay2 = "UniqTest Spring 1994 Day 2"
const titleDay3 = "UniqTest Spring 1994 Day 3"
snap := &model.StreetSnap{JobID: 1, Title: titleDay2, Year: year, City: city, Status: model.StatusPending}
id, err := repo.CreateStreetSnapWithImages(ctx, snap, []model.StreetSnapImage{{Image: "uniq-a.jpg", SortOrder: 1}})
if err != nil {
t.Fatalf("首行应能插入: %v", err)
}
t.Cleanup(func() {
db.Exec("DELETE FROM street_snap_images WHERE snap_id = ?", id)
db.Exec("DELETE FROM street_snaps WHERE id = ?", id)
})
// ① 同城同年同标题:实体键命中,唯一索引必须拒绝。
dup := &model.StreetSnap{JobID: 1, Title: titleDay2, Year: year, City: city, Status: model.StatusPending}
if _, err := repo.CreateStreetSnapWithImages(ctx, dup, []model.StreetSnapImage{{Image: "uniq-b.jpg", SortOrder: 1}}); err == nil {
t.Fatalf("同城同年同标题的第二行应被唯一索引拒绝,实际插入成功")
}
// ② 同城同年不同标题:不同专题,必须允许共存。
// 这一条是本任务的核心:若还原成 (city, year) 的旧键,这里会插入失败。
other := &model.StreetSnap{JobID: 1, Title: titleDay3, Year: year, City: city, Status: model.StatusPending}
otherID, err := repo.CreateStreetSnapWithImages(ctx, other, []model.StreetSnapImage{{Image: "uniq-c.jpg", SortOrder: 1}})
if err != nil {
t.Fatalf("同城同年不同标题应可插入,实际失败: %v", err)
}
t.Cleanup(func() {
db.Exec("DELETE FROM street_snap_images WHERE snap_id = ?", otherID)
db.Exec("DELETE FROM street_snaps WHERE id = ?", otherID)
})
// 软删首行后,实体键不再占用(与 is_deleted = 0 的部分索引口径一致)。
if err := db.Model(&model.StreetSnap{}).Where("id = ?", id).Update("is_deleted", 1).Error; err != nil {
t.Fatalf("软删失败: %v", err)
}
again := &model.StreetSnap{JobID: 1, Title: titleDay2, Year: year, City: city, Status: model.StatusPending}
newID, err := repo.CreateStreetSnapWithImages(ctx, again, []model.StreetSnapImage{{Image: "uniq-d.jpg", SortOrder: 1}})
if err != nil {
t.Fatalf("软删后应可再插入同实体键: %v", err)
}
t.Cleanup(func() {
db.Exec("DELETE FROM street_snap_images WHERE snap_id = ?", newID)
db.Exec("DELETE FROM street_snaps WHERE id = ?", newID)
})
}