Files
ds-galo/backend/internal/app/photo_checksum_test.go
iu 97f8f4e768 fix(backend): отдавать контрольную сумму оригинала в ответе на загрузку
Ответ на загрузку и публикуемое событие теперь несут хеш сохранённого
объекта, чтобы клиент синхронизации мог сверить принятые байты со своим
файлом и только после этого удалять прежнюю версию элемента.
2026-10-20 20:32:20 +03:00

108 lines
3.5 KiB
Go

package app
import (
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"net/http"
"testing"
"galo/internal/auth"
"galo/internal/models"
"galo/internal/photos"
)
// The original object's content checksum is exposed on photo data so clients can
// match a local file against a server photo and avoid re-uploading it.
func TestPhotoResponsesExposeOriginalChecksum(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)
}
obj := &photos.Object{
ID: auth.NewID(),
StorageKey: "objects/" + auth.NewID(),
MimeType: "image/jpeg",
Checksum: "sha256-photo-a",
State: photos.ObjectPending,
}
p := &models.Photo{ID: auth.NewID(), AlbumID: album.ID, UploaderID: uid, OriginalFilename: "c.jpg"}
if err := repos.Photos.CreateWithOriginal(ctx, p, obj); err != nil {
t.Fatalf("create photo: %v", err)
}
if err := repos.Photos.ActivateObject(ctx, obj.ID); err != nil {
t.Fatalf("activate photo object: %v", err)
}
// Single photo.
rec := doAuthed(r, http.MethodGet, "/api/photos/"+p.ID, nil, "", token)
if rec.Code != http.StatusOK {
t.Fatalf("get photo = %d body=%s", rec.Code, rec.Body.String())
}
var one struct {
Photo models.Photo `json:"photo"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &one); err != nil {
t.Fatalf("decode photo: %v", err)
}
if one.Photo.OriginalChecksum != "sha256-photo-a" {
t.Fatalf("photo original_checksum = %q, want sha256-photo-a", one.Photo.OriginalChecksum)
}
// Album listing.
rec = doAuthed(r, http.MethodGet, "/api/albums/"+album.ID+"/photos", nil, "", token)
if rec.Code != http.StatusOK {
t.Fatalf("list photos = %d body=%s", rec.Code, rec.Body.String())
}
var list struct {
Photos []models.Photo `json:"photos"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &list); err != nil {
t.Fatalf("decode list: %v", err)
}
if len(list.Photos) != 1 || list.Photos[0].OriginalChecksum != "sha256-photo-a" {
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)
}
}