Clone переносил все привязки photo_objects, включая роли edit*, хотя правка принадлежит фото, чьи правки её породили. Теперь копия делит только обычные варианты и показывается в исходном виде. - EditRoles() и IsEditRole() в edit_render.go — единый список ролей; - фильтр edit-ролей в Store.Clone и SQLiteStore.Clone; - миграции 000016 (Postgres) и 000008 (SQLite) убирают привязки у копий, созданных до фикса; - контрактная проверка копирования, сервисные и e2e-тесты.
147 lines
4.3 KiB
Go
147 lines
4.3 KiB
Go
package photos
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestCopySharesStorageObjects(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
svc, cl := newTestService(t, pool)
|
|
user := createUser(t, ctx, pool)
|
|
albumA := createAlbum(t, ctx, pool, user)
|
|
albumB := createAlbum(t, ctx, pool, user)
|
|
|
|
p, err := svc.Upload(ctx, UploadInput{
|
|
AlbumID: albumA, UserID: user, Filename: "p.png", MimeType: "image/png", Data: makePNG(t),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("upload: %v", err)
|
|
}
|
|
t.Cleanup(func() { cleanupPhoto(t, ctx, pool, cl, p.ID) })
|
|
|
|
w := NewWorker(svc, time.Second, 10, time.Minute, 2)
|
|
w.processVariantJobs(ctx)
|
|
|
|
srcIDs, err := svc.store.PhotoObjectIDs(ctx, p.ID)
|
|
if err != nil || len(srcIDs) != 3 {
|
|
t.Fatalf("source objects = %v err=%v", srcIDs, err)
|
|
}
|
|
|
|
clones, err := svc.CopyBatch(ctx, []string{p.ID}, user, albumB)
|
|
if err != nil || len(clones) != 1 {
|
|
t.Fatalf("copy: %v len=%d", err, len(clones))
|
|
}
|
|
clone := clones[0]
|
|
t.Cleanup(func() { cleanupPhoto(t, ctx, pool, cl, clone.ID) })
|
|
|
|
cloneIDs, err := svc.store.PhotoObjectIDs(ctx, clone.ID)
|
|
if err != nil || len(cloneIDs) != 3 {
|
|
t.Fatalf("clone objects = %v err=%v", cloneIDs, err)
|
|
}
|
|
if !sameSet(srcIDs, cloneIDs) {
|
|
t.Fatalf("clone does not share objects: src=%v clone=%v", srcIDs, cloneIDs)
|
|
}
|
|
for _, id := range cloneIDs {
|
|
if n, err := svc.store.ObjectRefCount(ctx, id); err != nil || n != 2 {
|
|
t.Fatalf("object %s ref count = %d err=%v, want 2", id, n, err)
|
|
}
|
|
}
|
|
|
|
// Move the source into the same album; the storage key and shared objects
|
|
// must be untouched.
|
|
beforeKey := p.StorageKey
|
|
moved, err := svc.MoveBatch(ctx, []string{p.ID}, user, albumB)
|
|
if err != nil || moved != 1 {
|
|
t.Fatalf("move: %v moved=%d", err, moved)
|
|
}
|
|
after, err := svc.store.GetByID(ctx, p.ID, "")
|
|
if err != nil {
|
|
t.Fatalf("get after move: %v", err)
|
|
}
|
|
if after.AlbumID != albumB {
|
|
t.Fatalf("photo not moved: album=%q", after.AlbumID)
|
|
}
|
|
if after.StorageKey != beforeKey {
|
|
t.Fatalf("move changed storage key: %q -> %q", beforeKey, after.StorageKey)
|
|
}
|
|
for _, id := range cloneIDs {
|
|
if n, err := svc.store.ObjectRefCount(ctx, id); err != nil || n != 2 {
|
|
t.Fatalf("object %s ref count after move = %d err=%v, want 2", id, n, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCopyDoesNotShareEditedVersion(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
svc, cl, _ := newRecordingServiceFS(t, pool)
|
|
user := createUser(t, ctx, pool)
|
|
albumA := createAlbum(t, ctx, pool, user)
|
|
albumB := createAlbum(t, ctx, pool, user)
|
|
|
|
p, err := svc.Upload(ctx, UploadInput{
|
|
AlbumID: albumA, UserID: user, Filename: "p.png", MimeType: "image/png", Data: makePNG(t),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("upload: %v", err)
|
|
}
|
|
t.Cleanup(func() { cleanupPhoto(t, ctx, pool, cl, p.ID) })
|
|
|
|
w := NewWorker(svc, time.Second, 10, time.Minute, 2)
|
|
w.processVariantJobs(ctx)
|
|
|
|
// The edit belongs to the photo: the parameters live in its metadata and
|
|
// the background render binds the edited version to it.
|
|
if _, err := svc.EditPhoto(ctx, p.ID, user, EditParams{Light: LightParams{Exposure: 0.5}}); err != nil {
|
|
t.Fatalf("edit photo: %v", err)
|
|
}
|
|
w.processEditJobs(ctx)
|
|
|
|
src, err := svc.store.GetByID(ctx, p.ID, "")
|
|
if err != nil {
|
|
t.Fatalf("get source: %v", err)
|
|
}
|
|
if src.EditThumbKey == "" || src.EditPreviewKey == "" {
|
|
t.Fatalf("source has no edited version: %+v", src)
|
|
}
|
|
|
|
clones, err := svc.CopyBatch(ctx, []string{p.ID}, user, albumB)
|
|
if err != nil || len(clones) != 1 {
|
|
t.Fatalf("copy: %v len=%d", err, len(clones))
|
|
}
|
|
clone := clones[0]
|
|
t.Cleanup(func() { cleanupPhoto(t, ctx, pool, cl, clone.ID) })
|
|
|
|
// The copy shares the base variants with the source.
|
|
if clone.ThumbKey != src.ThumbKey || clone.MediumKey != src.MediumKey || clone.PreviewKey != src.PreviewKey {
|
|
t.Fatalf("clone does not share base variants: src=%+v clone=%+v", src, clone)
|
|
}
|
|
// But it never inherits the edited version or its parameters.
|
|
if clone.EditKey != "" || clone.EditThumbKey != "" || clone.EditMediumKey != "" || clone.EditPreviewKey != "" {
|
|
t.Fatalf("clone shares the edited version: %+v", clone)
|
|
}
|
|
if len(clone.EditParams) != 0 {
|
|
t.Fatalf("clone inherited edit params: %+v", clone.EditParams)
|
|
}
|
|
}
|
|
|
|
func sameSet(a, b []string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
m := make(map[string]int, len(a))
|
|
for _, v := range a {
|
|
m[v]++
|
|
}
|
|
for _, v := range b {
|
|
m[v]--
|
|
if m[v] < 0 {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|