Clone переносил все привязки photo_objects, включая роли edit*, хотя правка принадлежит фото, чьи правки её породили. Теперь копия делит только обычные варианты и показывается в исходном виде. - EditRoles() и IsEditRole() в edit_render.go — единый список ролей; - фильтр edit-ролей в Store.Clone и SQLiteStore.Clone; - миграции 000016 (Postgres) и 000008 (SQLite) убирают привязки у копий, созданных до фикса; - контрактная проверка копирования, сервисные и e2e-тесты.
165 lines
5.4 KiB
Go
165 lines
5.4 KiB
Go
package e2e
|
|
|
|
import (
|
|
"context"
|
|
"io/fs"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
assets "galo"
|
|
"galo/internal/albums"
|
|
"galo/internal/photos"
|
|
)
|
|
|
|
// A copy of an edited photo must not inherit the edited version: the edit
|
|
// belongs to the photo whose edits produced it.
|
|
func TestCopyDoesNotShareEditedVersion(t *testing.T) {
|
|
ctx := context.Background()
|
|
st, closeDB := openStack(t, filepath.Join(t.TempDir(), "galo.db"), t.TempDir())
|
|
defer closeDB()
|
|
|
|
user, _, err := st.auth.Register(ctx, "copy@test.local", "", "password123", "C")
|
|
if err != nil {
|
|
t.Fatalf("register: %v", err)
|
|
}
|
|
album, err := st.albums.Create(ctx, user.ID, albums.CreateInput{Title: "Source"})
|
|
if err != nil {
|
|
t.Fatalf("create album: %v", err)
|
|
}
|
|
target, err := st.albums.Create(ctx, user.ID, albums.CreateInput{Title: "Target"})
|
|
if err != nil {
|
|
t.Fatalf("create target album: %v", err)
|
|
}
|
|
|
|
p, err := st.photos.Upload(ctx, photos.UploadInput{AlbumID: album.ID, UserID: user.ID, Filename: "a.png", Data: makePNG(t)})
|
|
if err != nil {
|
|
t.Fatalf("upload: %v", err)
|
|
}
|
|
|
|
w := photos.NewWorker(st.photos, time.Second, 10, time.Minute, 2)
|
|
w.ProcessOnce(ctx)
|
|
|
|
if _, err := st.photos.EditPhoto(ctx, p.ID, user.ID, photos.EditParams{Light: photos.LightParams{Exposure: 0.5}}); err != nil {
|
|
t.Fatalf("edit photo: %v", err)
|
|
}
|
|
w.ProcessOnce(ctx)
|
|
|
|
source, err := st.photos.Get(ctx, p.ID, user.ID)
|
|
if err != nil {
|
|
t.Fatalf("get source: %v", err)
|
|
}
|
|
if source.EditThumbURL == "" || source.EditPreviewURL == "" {
|
|
t.Fatalf("source has no edited version: %+v", source)
|
|
}
|
|
|
|
clones, err := st.photos.CopyBatch(ctx, []string{p.ID}, user.ID, target.ID)
|
|
if err != nil || len(clones) != 1 {
|
|
t.Fatalf("copy: %v len=%d", err, len(clones))
|
|
}
|
|
clone := clones[0]
|
|
|
|
// The copy shares the base variants with the source.
|
|
if clone.ThumbURL != source.ThumbURL || clone.PreviewURL != source.PreviewURL {
|
|
t.Fatalf("clone does not share base variants: src=%+v clone=%+v", source, clone)
|
|
}
|
|
// But it shows the original picture and carries no edit parameters.
|
|
if clone.EditURL != "" || clone.EditThumbURL != "" || clone.EditMediumURL != "" || clone.EditPreviewURL != "" {
|
|
t.Fatalf("clone shares the edited version: %+v", clone)
|
|
}
|
|
if len(clone.EditParams) != 0 {
|
|
t.Fatalf("clone inherited edit params: %+v", clone.EditParams)
|
|
}
|
|
}
|
|
|
|
// The migration drops the edit bindings that copies carried before the fix,
|
|
// while the edited version of the photo that owns them stays untouched.
|
|
func TestMigrationDropsEditBindingsOfCopies(t *testing.T) {
|
|
ctx := context.Background()
|
|
st, closeDB := openStack(t, filepath.Join(t.TempDir(), "galo.db"), t.TempDir())
|
|
defer closeDB()
|
|
|
|
user, _, err := st.auth.Register(ctx, "mig@test.local", "", "password123", "M")
|
|
if err != nil {
|
|
t.Fatalf("register: %v", err)
|
|
}
|
|
album, err := st.albums.Create(ctx, user.ID, albums.CreateInput{Title: "Source"})
|
|
if err != nil {
|
|
t.Fatalf("create album: %v", err)
|
|
}
|
|
target, err := st.albums.Create(ctx, user.ID, albums.CreateInput{Title: "Target"})
|
|
if err != nil {
|
|
t.Fatalf("create target album: %v", err)
|
|
}
|
|
|
|
p, err := st.photos.Upload(ctx, photos.UploadInput{AlbumID: album.ID, UserID: user.ID, Filename: "a.png", Data: makePNG(t)})
|
|
if err != nil {
|
|
t.Fatalf("upload: %v", err)
|
|
}
|
|
w := photos.NewWorker(st.photos, time.Second, 10, time.Minute, 2)
|
|
w.ProcessOnce(ctx)
|
|
if _, err := st.photos.EditPhoto(ctx, p.ID, user.ID, photos.EditParams{Light: photos.LightParams{Exposure: 0.5}}); err != nil {
|
|
t.Fatalf("edit photo: %v", err)
|
|
}
|
|
w.ProcessOnce(ctx)
|
|
|
|
var editObjectID string
|
|
if err := st.db.QueryRowContext(ctx,
|
|
`SELECT object_id FROM photo_objects WHERE photo_id = $1 AND role = 'edit_thumb'`, p.ID).Scan(&editObjectID); err != nil {
|
|
t.Fatalf("edited thumb object: %v", err)
|
|
}
|
|
|
|
clones, err := st.photos.CopyBatch(ctx, []string{p.ID}, user.ID, target.ID)
|
|
if err != nil || len(clones) != 1 {
|
|
t.Fatalf("copy: %v len=%d", err, len(clones))
|
|
}
|
|
clone := clones[0]
|
|
|
|
// Reproduce the state left by the old code: the copy carries the edited
|
|
// version of the source photo.
|
|
if _, err := st.db.ExecContext(ctx,
|
|
`INSERT INTO photo_objects (photo_id, object_id, role) VALUES ($1, $2, 'edit_thumb')`,
|
|
clone.ID, editObjectID); err != nil {
|
|
t.Fatalf("bind edited version to the copy: %v", err)
|
|
}
|
|
|
|
migFS, err := fs.Sub(assets.Migrations, "migrations/sqlite")
|
|
if err != nil {
|
|
t.Fatalf("migrations fs: %v", err)
|
|
}
|
|
up, err := fs.ReadFile(migFS, "000008_photo_edit_copy_bindings.up.sql")
|
|
if err != nil {
|
|
t.Fatalf("read migration: %v", err)
|
|
}
|
|
if _, err := st.db.ExecContext(ctx, string(up)); err != nil {
|
|
t.Fatalf("apply migration: %v", err)
|
|
}
|
|
|
|
var n int
|
|
if err := st.db.QueryRowContext(ctx,
|
|
`SELECT count(*) FROM photo_objects WHERE photo_id = $1 AND role LIKE 'edit%'`, clone.ID).Scan(&n); err != nil {
|
|
t.Fatalf("count copy edit bindings: %v", err)
|
|
}
|
|
if n != 0 {
|
|
t.Fatalf("copy still carries %d edit bindings", n)
|
|
}
|
|
// The photo that owns the edited version keeps it.
|
|
if err := st.db.QueryRowContext(ctx,
|
|
`SELECT count(*) FROM photo_objects WHERE photo_id = $1 AND role = 'edit_thumb'`, p.ID).Scan(&n); err != nil {
|
|
t.Fatalf("count source edit bindings: %v", err)
|
|
}
|
|
if n != 1 {
|
|
t.Fatalf("source edit bindings = %d, want 1", n)
|
|
}
|
|
// A bound object is never garbage, so the edited version stays alive.
|
|
claimed, err := st.repos.Photos.ClaimGarbage(ctx, 10)
|
|
if err != nil {
|
|
t.Fatalf("claim garbage: %v", err)
|
|
}
|
|
for _, o := range claimed {
|
|
if o.ID == editObjectID {
|
|
t.Fatalf("edited version of the owner was claimed as garbage")
|
|
}
|
|
}
|
|
}
|