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") } }