fix(photos): копия фото не наследует отредактированную версию

Clone переносил все привязки photo_objects, включая роли edit*, хотя
правка принадлежит фото, чьи правки её породили. Теперь копия делит
только обычные варианты и показывается в исходном виде.

- EditRoles() и IsEditRole() в edit_render.go — единый список ролей;
- фильтр edit-ролей в Store.Clone и SQLiteStore.Clone;
- миграции 000016 (Postgres) и 000008 (SQLite) убирают привязки у
  копий, созданных до фикса;
- контрактная проверка копирования, сервисные и e2e-тесты.
This commit is contained in:
iu
2026-10-21 21:01:33 +03:00
parent 759e78ff79
commit 8dd8cb6f0c
12 changed files with 344 additions and 6 deletions
@@ -49,6 +49,7 @@ func Run(t *testing.T, r Repos) {
{"Social", testSocial},
{"Photos", testPhotos},
{"Objects", testObjects},
{"Clone", testClone},
{"UserAvatar", testUserAvatar},
{"VariantJobKinds", testVariantJobKinds},
{"Settings", testSettings},
@@ -709,6 +710,56 @@ func testObjects(t *testing.T, ctx context.Context, r Repos) {
}
}
func testClone(t *testing.T, ctx context.Context, r Repos) {
owner := seedUser(t, ctx, r, "Owner")
a := seedAlbum(t, ctx, r, owner.ID)
target := seedAlbum(t, ctx, r, owner.ID)
p := seedPhoto(t, ctx, r, owner.ID, a.ID)
// The photo carries the base variants and an edited version.
thumb := &photos.Object{ID: newID(), StorageKey: "objects/" + newID(), MimeType: "image/webp", State: photos.ObjectPending}
if err := r.Photos.CreateObject(ctx, thumb); err != nil {
t.Fatalf("create thumb: %v", err)
}
if err := r.Photos.ActivateObject(ctx, thumb.ID); err != nil {
t.Fatalf("activate thumb: %v", err)
}
if err := r.Photos.BindObject(ctx, p.ID, thumb.ID, photos.RoleThumb); err != nil {
t.Fatalf("bind thumb: %v", err)
}
edited := &photos.Object{ID: newID(), StorageKey: "objects/" + newID(), MimeType: "image/webp", State: photos.ObjectPending}
if err := r.Photos.CreateObject(ctx, edited); err != nil {
t.Fatalf("create edited object: %v", err)
}
if err := r.Photos.ActivateObject(ctx, edited.ID); err != nil {
t.Fatalf("activate edited object: %v", err)
}
if err := r.Photos.BindObject(ctx, p.ID, edited.ID, photos.RoleEdit); err != nil {
t.Fatalf("bind edited version: %v", err)
}
cloneID := newID()
if err := r.Photos.Clone(ctx, p, target.ID, owner.ID, cloneID); err != nil {
t.Fatalf("clone: %v", err)
}
cloneObjects, err := r.Photos.PhotoObjectIDs(ctx, cloneID)
if err != nil {
t.Fatalf("clone objects: %v", err)
}
// The copy shares the base variants.
if !contains(cloneObjects, thumb.ID) {
t.Fatalf("clone objects = %+v, want thumb %s", cloneObjects, thumb.ID)
}
// The edited version belongs to the photo whose edits produced it.
if contains(cloneObjects, edited.ID) {
t.Fatalf("clone shares the edited version: %+v", cloneObjects)
}
if refs, _ := r.Photos.ObjectRefCount(ctx, edited.ID); refs != 1 {
t.Fatalf("edited object refs = %d, want 1", refs)
}
}
func testUserAvatar(t *testing.T, ctx context.Context, r Repos) {
owner := seedUser(t, ctx, r, "Owner")