Uploads skip per-gallery duplicates, checksums can be backfilled, and STORAGE_BACKEND plus sync tooling make switching storage backends safe.
136 lines
3.8 KiB
Go
136 lines
3.8 KiB
Go
package photosync
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.azzamo.net/Michilis/Spanglish/photo-api/internal/storage"
|
|
"git.azzamo.net/Michilis/Spanglish/photo-api/internal/store"
|
|
)
|
|
|
|
// Two local backends stand in for the real pair: copyObject only talks to the
|
|
// storage.Storage interface, so the S3 side needs no fake here.
|
|
func backends(t *testing.T) (src, dst storage.Storage, srcRoot string) {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
srcRoot = filepath.Join(dir, "src")
|
|
src, err := storage.NewLocal(srcRoot)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dst, err = storage.NewLocal(filepath.Join(dir, "dst"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return src, dst, srcRoot
|
|
}
|
|
|
|
func put(t *testing.T, s storage.Storage, key, body string) {
|
|
t.Helper()
|
|
if err := s.Put(context.Background(), key, strings.NewReader(body), int64(len(body)), "image/jpeg"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
const testKey = "galleries/g1/original/p1.jpg"
|
|
|
|
func testObject() store.PhotoObject {
|
|
return store.PhotoObject{PhotoID: "p1", GalleryID: "g1", Variant: "original", Key: testKey, ContentType: "image/jpeg"}
|
|
}
|
|
|
|
func TestCopyObject(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("copies to destination", func(t *testing.T) {
|
|
src, dst, _ := backends(t)
|
|
put(t, src, testKey, "hello-photo")
|
|
|
|
act, size, err := copyObject(ctx, src, dst, testObject(), Options{})
|
|
if err != nil || act != actionCopied || size != 11 {
|
|
t.Fatalf("got (%v, %d, %v), want (copied, 11, nil)", act, size, err)
|
|
}
|
|
if got, err := dst.Stat(ctx, testKey); err != nil || got != 11 {
|
|
t.Fatalf("destination stat: (%d, %v)", got, err)
|
|
}
|
|
})
|
|
|
|
t.Run("skips same-size object already present", func(t *testing.T) {
|
|
src, dst, _ := backends(t)
|
|
put(t, src, testKey, "hello-photo")
|
|
put(t, dst, testKey, "hello-photo")
|
|
|
|
act, _, err := copyObject(ctx, src, dst, testObject(), Options{})
|
|
if err != nil || act != actionSkipped {
|
|
t.Fatalf("got (%v, %v), want (skipped, nil)", act, err)
|
|
}
|
|
})
|
|
|
|
t.Run("overwrite re-copies", func(t *testing.T) {
|
|
src, dst, _ := backends(t)
|
|
put(t, src, testKey, "new-content")
|
|
put(t, dst, testKey, "old-content")
|
|
|
|
act, _, err := copyObject(ctx, src, dst, testObject(), Options{Overwrite: true})
|
|
if err != nil || act != actionCopied {
|
|
t.Fatalf("got (%v, %v), want (copied, nil)", act, err)
|
|
}
|
|
r, _, err := dst.Open(ctx, testKey)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer r.Close()
|
|
body, err := io.ReadAll(r)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(body) != "new-content" {
|
|
t.Fatalf("destination body = %q, want new-content", body)
|
|
}
|
|
})
|
|
|
|
t.Run("reports objects missing on the source", func(t *testing.T) {
|
|
src, dst, _ := backends(t)
|
|
|
|
act, _, err := copyObject(ctx, src, dst, testObject(), Options{})
|
|
if err != nil || act != actionMissing {
|
|
t.Fatalf("got (%v, %v), want (missing, nil)", act, err)
|
|
}
|
|
if _, err := dst.Stat(ctx, testKey); err != storage.ErrNotExist {
|
|
t.Fatalf("destination stat err = %v, want ErrNotExist", err)
|
|
}
|
|
})
|
|
|
|
t.Run("dry run writes nothing", func(t *testing.T) {
|
|
src, dst, _ := backends(t)
|
|
put(t, src, testKey, "hello-photo")
|
|
|
|
act, size, err := copyObject(ctx, src, dst, testObject(), Options{DryRun: true})
|
|
if err != nil || act != actionCopied || size != 11 {
|
|
t.Fatalf("got (%v, %d, %v), want (copied, 11, nil)", act, size, err)
|
|
}
|
|
if _, err := dst.Stat(ctx, testKey); err != storage.ErrNotExist {
|
|
t.Fatalf("destination stat err = %v, want ErrNotExist", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestParseDirection(t *testing.T) {
|
|
for in, want := range map[string]Direction{
|
|
"to-s3": ToS3,
|
|
"local-to-s3": ToS3,
|
|
"to-local": ToLocal,
|
|
"s3-to-local": ToLocal,
|
|
} {
|
|
got, err := ParseDirection(in)
|
|
if err != nil || got != want {
|
|
t.Errorf("ParseDirection(%q) = (%v, %v), want %v", in, got, err, want)
|
|
}
|
|
}
|
|
if _, err := ParseDirection("sideways"); err == nil {
|
|
t.Error("ParseDirection(\"sideways\") should fail")
|
|
}
|
|
}
|