package e2e import ( "bytes" "context" "path/filepath" "testing" "time" "github.com/disintegration/imaging" "github.com/gen2brain/webp" "galo/internal/albums" "galo/internal/photos" ) // TestEffectsEdit covers the effects round trip: the parameters are stored in // the photo metadata, the background worker renders the edited version, the // saved parameters carry the normalized effects, a reset removes everything, // and re-rendering the same edit (including grain) is deterministic. func TestEffectsEdit(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, "effects@test.local", "", "password123", "E") if err != nil { t.Fatalf("register: %v", err) } album, err := st.albums.Create(ctx, user.ID, albums.CreateInput{Title: "Effects"}) if err != nil { t.Fatalf("create album: %v", err) } p, err := st.photos.Upload(ctx, photos.UploadInput{ AlbumID: album.ID, UserID: user.ID, Filename: "e.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) params := photos.EditParams{Effects: photos.EffectsParams{ Texture: 25, Clarity: 20, Dehaze: 15, Vignette: photos.VignetteParams{ Mode: photos.VignetteColor, Amount: 40, Midpoint: 60, Feather: 40, Color: "#ff8800", }, Grain: photos.GrainParams{Amount: 35, Size: 30, Roughness: 60}, }} edited, err := st.photos.EditPhoto(ctx, p.ID, user.ID, params) if err != nil { t.Fatalf("edit: %v", err) } if edited.EditParams == nil { t.Fatal("edit returned no parameters") } if _, ok := edited.EditParams["effects"]; !ok { t.Fatalf("effects params not returned: %+v", edited.EditParams) } w.ProcessOnce(ctx) got, err := st.photos.Get(ctx, p.ID, user.ID) if err != nil { t.Fatalf("get: %v", err) } if got.EditPreviewURL == "" { t.Fatalf("no edited preview url: %+v", got) } if got.EditPreviewKey == "" { t.Fatalf("no edited preview key: %+v", got) } effects, ok := got.EditParams["effects"].(map[string]any) if !ok { t.Fatalf("effects params not stored: %+v", got.EditParams) } vignette, ok := effects["vignette"].(map[string]any) if !ok { t.Fatalf("vignette params not stored: %+v", effects) } if vignette["mode"] != photos.VignetteColor { t.Fatalf("vignette mode = %v, want %q", vignette["mode"], photos.VignetteColor) } if vignette["color"] != "#ff8800" { t.Fatalf("vignette color = %v, want #ff8800", vignette["color"]) } if vignette["midpoint"] != float64(60) { t.Fatalf("vignette midpoint = %v, want 60", vignette["midpoint"]) } grain, ok := effects["grain"].(map[string]any) if !ok { t.Fatalf("grain params not stored: %+v", effects) } if grain["size"] != float64(30) || grain["roughness"] != float64(60) { t.Fatalf("grain params = %+v, want size 30 roughness 60", grain) } first := previewPixels(t, st, got.EditPreviewKey) reset, err := st.photos.ResetEdit(ctx, p.ID, user.ID) if err != nil { t.Fatalf("reset: %v", err) } if len(reset.EditParams) != 0 { t.Fatalf("reset kept params: %+v", reset.EditParams) } if reset.EditURL != "" || reset.EditPreviewURL != "" || reset.EditPreviewKey != "" { t.Fatalf("reset kept the edited version: %+v", reset) } // The same parameters and the same photo id must render the same grain. if _, err := st.photos.EditPhoto(ctx, p.ID, user.ID, params); err != nil { t.Fatalf("edit again: %v", err) } w.ProcessOnce(ctx) again, err := st.photos.Get(ctx, p.ID, user.ID) if err != nil { t.Fatalf("get after second edit: %v", err) } if again.EditPreviewURL == "" || again.EditPreviewKey == "" { t.Fatalf("no edited preview after second edit: %+v", again) } second := previewPixels(t, st, again.EditPreviewKey) if !bytes.Equal(first, second) { t.Fatal("repeated render with the same grain parameters is not deterministic") } } // previewPixels decodes an edited preview object into its pixel bytes. func previewPixels(t *testing.T, st *stack, key string) []uint8 { t.Helper() data, err := st.storage.Get(context.Background(), key) if err != nil { t.Fatalf("read preview object: %v", err) } img, err := webp.Decode(bytes.NewReader(data)) if err != nil { t.Fatalf("decode preview: %v", err) } return imaging.Clone(img).Pix }