246 lines
7.7 KiB
Go
246 lines
7.7 KiB
Go
package e2e
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"galo/internal/albums"
|
|
"galo/internal/app"
|
|
"galo/internal/auth"
|
|
"galo/internal/config"
|
|
"galo/internal/photos"
|
|
)
|
|
|
|
func TestPermanentDeleteAlbumlessPhoto(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, "albumless@test.local", "", "password123", "A")
|
|
if err != nil {
|
|
t.Fatalf("register: %v", err)
|
|
}
|
|
album, err := st.albums.Create(ctx, user.ID, albums.CreateInput{Title: "Doomed"})
|
|
if err != nil {
|
|
t.Fatalf("create album: %v", err)
|
|
}
|
|
p, err := st.photos.Upload(ctx, photos.UploadInput{AlbumID: album.ID, UserID: user.ID, Filename: "a.png", Data: makePNG(t)})
|
|
if err != nil {
|
|
t.Fatalf("upload: %v", err)
|
|
}
|
|
|
|
var objectID string
|
|
if err := st.db.QueryRowContext(ctx,
|
|
`SELECT object_id FROM photo_objects WHERE photo_id = $1 AND role = 'original'`, p.ID).Scan(&objectID); err != nil {
|
|
t.Fatalf("original object: %v", err)
|
|
}
|
|
|
|
// Deleting the album moves its photo to the trash with no album binding.
|
|
if err := st.albums.Delete(ctx, album.ID, user.ID); err != nil {
|
|
t.Fatalf("delete album: %v", err)
|
|
}
|
|
var albumID sql.NullString
|
|
if err := st.db.QueryRowContext(ctx, `SELECT album_id FROM photos WHERE id = $1`, p.ID).Scan(&albumID); err != nil {
|
|
t.Fatalf("read album_id: %v", err)
|
|
}
|
|
if albumID.Valid {
|
|
t.Fatalf("photo still bound to album %q after album deletion", albumID.String)
|
|
}
|
|
|
|
if err := st.photos.PermanentDelete(ctx, p.ID, user.ID); err != nil {
|
|
t.Fatalf("permanent delete albumless photo: %v", err)
|
|
}
|
|
var n int
|
|
if err := st.db.QueryRowContext(ctx, `SELECT count(*) FROM photos WHERE id = $1`, p.ID).Scan(&n); err != nil {
|
|
t.Fatalf("count photos: %v", err)
|
|
}
|
|
if n != 0 {
|
|
t.Fatalf("photo row survived permanent delete")
|
|
}
|
|
var state string
|
|
if err := st.db.QueryRowContext(ctx, `SELECT state FROM objects WHERE id = $1`, objectID).Scan(&state); err != nil {
|
|
t.Fatalf("object row missing after delete: %v", err)
|
|
}
|
|
if state != photos.ObjectDeleting {
|
|
t.Fatalf("object state = %q, want %q", state, photos.ObjectDeleting)
|
|
}
|
|
}
|
|
|
|
func TestEmptyTrashSkipsForeignPhotos(t *testing.T) {
|
|
ctx := context.Background()
|
|
st, closeDB := openStack(t, filepath.Join(t.TempDir(), "galo.db"), t.TempDir())
|
|
defer closeDB()
|
|
|
|
owner, _, err := st.auth.Register(ctx, "owner@test.local", "", "password123", "O")
|
|
if err != nil {
|
|
t.Fatalf("register owner: %v", err)
|
|
}
|
|
member, _, err := st.auth.Register(ctx, "member@test.local", "", "password123", "M")
|
|
if err != nil {
|
|
t.Fatalf("register member: %v", err)
|
|
}
|
|
album, err := st.albums.Create(ctx, owner.ID, albums.CreateInput{Title: "Shared"})
|
|
if err != nil {
|
|
t.Fatalf("create album: %v", err)
|
|
}
|
|
if _, err := st.db.ExecContext(ctx,
|
|
`INSERT INTO album_members (album_id, user_id, role) VALUES ($1, $2, 'member')`, album.ID, member.ID); err != nil {
|
|
t.Fatalf("add member: %v", err)
|
|
}
|
|
|
|
ownerPhoto, err := st.photos.Upload(ctx, photos.UploadInput{AlbumID: album.ID, UserID: owner.ID, Filename: "o.png", Data: makePNG(t)})
|
|
if err != nil {
|
|
t.Fatalf("upload owner photo: %v", err)
|
|
}
|
|
memberPhoto, err := st.photos.Upload(ctx, photos.UploadInput{AlbumID: album.ID, UserID: member.ID, Filename: "m.png", Data: makePNG(t)})
|
|
if err != nil {
|
|
t.Fatalf("upload member photo: %v", err)
|
|
}
|
|
if err := st.photos.Delete(ctx, ownerPhoto.ID, owner.ID); err != nil {
|
|
t.Fatalf("trash owner photo: %v", err)
|
|
}
|
|
if err := st.photos.Delete(ctx, memberPhoto.ID, member.ID); err != nil {
|
|
t.Fatalf("trash member photo: %v", err)
|
|
}
|
|
|
|
if n, _ := st.photos.TrashCount(ctx, member.ID); n != 2 {
|
|
t.Fatalf("member trash count = %d, want 2", n)
|
|
}
|
|
|
|
deleted, err := st.photos.EmptyTrash(ctx, member.ID)
|
|
if err != nil {
|
|
t.Fatalf("empty trash: %v", err)
|
|
}
|
|
if deleted != 1 {
|
|
t.Fatalf("deleted = %d, want 1 (only the member's own photo)", deleted)
|
|
}
|
|
|
|
var n int
|
|
if err := st.db.QueryRowContext(ctx, `SELECT count(*) FROM photos WHERE id = $1`, memberPhoto.ID).Scan(&n); err != nil {
|
|
t.Fatalf("count member photo: %v", err)
|
|
}
|
|
if n != 0 {
|
|
t.Fatalf("member's own photo survived empty trash")
|
|
}
|
|
if err := st.db.QueryRowContext(ctx, `SELECT count(*) FROM photos WHERE id = $1`, ownerPhoto.ID).Scan(&n); err != nil {
|
|
t.Fatalf("count owner photo: %v", err)
|
|
}
|
|
if n != 1 {
|
|
t.Fatalf("foreign photo was removed by emptier")
|
|
}
|
|
if c, _ := st.photos.TrashCount(ctx, member.ID); c != 1 {
|
|
t.Fatalf("trash count after empty = %d, want 1 (the foreign photo)", c)
|
|
}
|
|
|
|
// Nothing the caller may delete remains, so a second pass deletes nothing.
|
|
if deleted, err := st.photos.EmptyTrash(ctx, member.ID); err != nil || deleted != 0 {
|
|
t.Fatalf("second empty = %d, err=%v; want 0, nil", deleted, err)
|
|
}
|
|
}
|
|
|
|
func TestListTrashIdentitiesNotCapped(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, "many@test.local", "", "password123", "M")
|
|
if err != nil {
|
|
t.Fatalf("register: %v", err)
|
|
}
|
|
|
|
tx, err := st.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
t.Fatalf("begin: %v", err)
|
|
}
|
|
for i := 0; i < 205; i++ {
|
|
if _, err := tx.ExecContext(ctx,
|
|
`INSERT INTO photos (id, uploader_id, deleted_at) VALUES ($1, $2, $3)`,
|
|
auth.NewID(), user.ID, "2026-01-01T00:00:00Z"); err != nil {
|
|
t.Fatalf("insert trashed photo %d: %v", i, err)
|
|
}
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
t.Fatalf("commit: %v", err)
|
|
}
|
|
|
|
items, err := st.repos.Photos.ListTrashIdentities(ctx, user.ID)
|
|
if err != nil {
|
|
t.Fatalf("list trash identities: %v", err)
|
|
}
|
|
if len(items) != 205 {
|
|
t.Fatalf("got %d identities, want 205 (display cap leaked into purge)", len(items))
|
|
}
|
|
}
|
|
|
|
func TestEmptyTrashReturnsZeroWhenEmpty(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, "empty@test.local", "", "password123", "E")
|
|
if err != nil {
|
|
t.Fatalf("register: %v", err)
|
|
}
|
|
deleted, err := st.photos.EmptyTrash(ctx, user.ID)
|
|
if err != nil {
|
|
t.Fatalf("empty trash: %v", err)
|
|
}
|
|
if deleted != 0 {
|
|
t.Fatalf("deleted = %d, want 0", deleted)
|
|
}
|
|
}
|
|
|
|
func TestEmptyTrashHTTP(t *testing.T) {
|
|
ctx := context.Background()
|
|
st, closeDB := openStack(t, filepath.Join(t.TempDir(), "galo.db"), t.TempDir())
|
|
defer closeDB()
|
|
|
|
user, pair, err := st.auth.Register(ctx, "http@test.local", "", "password123", "H")
|
|
if err != nil {
|
|
t.Fatalf("register: %v", err)
|
|
}
|
|
album, err := st.albums.Create(ctx, user.ID, albums.CreateInput{Title: "HTTP"})
|
|
if err != nil {
|
|
t.Fatalf("create album: %v", err)
|
|
}
|
|
p, err := st.photos.Upload(ctx, photos.UploadInput{AlbumID: album.ID, UserID: user.ID, Filename: "a.png", Data: makePNG(t)})
|
|
if err != nil {
|
|
t.Fatalf("upload: %v", err)
|
|
}
|
|
if err := st.photos.Delete(ctx, p.ID, user.ID); err != nil {
|
|
t.Fatalf("trash photo: %v", err)
|
|
}
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
srv := app.New(&config.Config{}, st.repos, st.db.PingContext, st.tm, st.storage, st.hub)
|
|
router := srv.Router()
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/trash", nil)
|
|
req.Header.Set("Authorization", "Bearer "+pair.AccessToken)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
var body struct {
|
|
Deleted int `json:"deleted"`
|
|
}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if body.Deleted != 1 {
|
|
t.Fatalf("deleted = %d, want 1", body.Deleted)
|
|
}
|
|
if n, _ := st.photos.TrashCount(ctx, user.ID); n != 0 {
|
|
t.Fatalf("trash count after empty = %d, want 0", n)
|
|
}
|
|
}
|