diff --git a/backend/internal/app/photo_checksum_test.go b/backend/internal/app/photo_checksum_test.go index 8b8bfb6..d9ef67a 100644 --- a/backend/internal/app/photo_checksum_test.go +++ b/backend/internal/app/photo_checksum_test.go @@ -2,7 +2,9 @@ package app import ( "context" + "crypto/sha256" "encoding/json" + "fmt" "net/http" "testing" @@ -70,3 +72,36 @@ func TestPhotoResponsesExposeOriginalChecksum(t *testing.T) { t.Fatalf("list original_checksum = %+v, want one photo with sha256-photo-a", list.Photos) } } + +// The upload response is photo data too: it must carry the stored original's +// checksum, which a synchronization client uses to confirm what the server +// actually accepted before it drops an earlier version of the same element. +func TestUploadResponseExposesOriginalChecksum(t *testing.T) { + ctx := context.Background() + r, token, uid, repos := newAvatarServer(t) + + album := &models.Album{ID: auth.NewID(), OwnerID: uid, Title: "A"} + if err := repos.Albums.Create(ctx, album); err != nil { + t.Fatalf("create album: %v", err) + } + if err := repos.Members.Add(ctx, album.ID, uid, "editor", nil); err != nil { + t.Fatalf("add owner member: %v", err) + } + + img := makeTestPNG(t) + body, contentType := multipartFile(t, "file", "c.png", img) + rec := doAuthed(r, http.MethodPost, "/api/albums/"+album.ID+"/photos", body, contentType, token) + if rec.Code != http.StatusCreated { + t.Fatalf("upload = %d body=%s", rec.Code, rec.Body.String()) + } + var out struct { + Photos []models.Photo `json:"photos"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil { + t.Fatalf("decode upload: %v", err) + } + want := fmt.Sprintf("%x", sha256.Sum256(img)) + if len(out.Photos) != 1 || out.Photos[0].OriginalChecksum != want { + t.Fatalf("upload original_checksum = %+v, want %q", out.Photos, want) + } +} diff --git a/backend/internal/photos/service.go b/backend/internal/photos/service.go index 35a24b7..87986ed 100644 --- a/backend/internal/photos/service.go +++ b/backend/internal/photos/service.go @@ -216,6 +216,9 @@ func (s *Service) Upload(ctx context.Context, in UploadInput) (*models.Photo, er Height: height, Checksum: hex.EncodeToString(hasher.Sum(nil)), } + // The response and the published event carry the checksum the object ledger + // holds, so a client can match its local file against the stored bytes. + p.OriginalChecksum = obj.Checksum // The metadata layer is the ledger of record: the row is written first, // then the bytes are stored, then the object is activated.