refactor(10.2-01): nest framework packages under modules
- Move remaining beach packages and embedded admin assets\n- Rewrite framework, example, build, and gate paths
This commit is contained in:
303
modules/lagoon/attach/lifecycle_test.go
Normal file
303
modules/lagoon/attach/lifecycle_test.go
Normal file
@@ -0,0 +1,303 @@
|
||||
package attach_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/lagoon"
|
||||
"git.golem15.com/golem15/summercms/modules/lagoon/attach"
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/modules/postgres"
|
||||
"gocloud.dev/blob"
|
||||
"gocloud.dev/blob/memblob"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type lifecycleOwner struct {
|
||||
ID uint `gorm:"column:id;primaryKey"`
|
||||
Name string `gorm:"column:name"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at"`
|
||||
}
|
||||
|
||||
func (lifecycleOwner) TableName() string { return "attach_lifecycle_owners" }
|
||||
|
||||
func (lifecycleOwner) MorphName() string {
|
||||
return `Golem15\Fonoteka\Models\Album`
|
||||
}
|
||||
|
||||
func TestFileLifecycle(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("requires testcontainers postgres")
|
||||
}
|
||||
ctx := t.Context()
|
||||
gdb := attachGorm(t)
|
||||
if err := lagoon.Migrate(gdb, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := gdb.Exec(`
|
||||
CREATE TABLE attach_lifecycle_owners (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
deleted_at TIMESTAMPTZ
|
||||
)`).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
bucket := memblob.OpenBucket(nil)
|
||||
t.Cleanup(func() { _ = bucket.Close() })
|
||||
|
||||
owner := lifecycleOwner{Name: "album"}
|
||||
if err := gdb.Create(&owner).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
isPublic := true
|
||||
file := attach.File{
|
||||
DiskName: "abc123xyz.jpg",
|
||||
FileName: "cover.jpg",
|
||||
FileSize: 12,
|
||||
ContentType: "image/jpeg",
|
||||
Field: "photos",
|
||||
AttachmentID: strconv.FormatUint(uint64(owner.ID), 10),
|
||||
AttachmentType: owner.MorphName(),
|
||||
IsPublic: &isPublic,
|
||||
}
|
||||
if err := gdb.Create(&file).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
origKey := attach.BlobKey(file.DiskName)
|
||||
thumbKey := attach.PartitionDirectory(file.DiskName) + attach.ThumbFilename(file.ID, 200, 200, 0, 0, "crop", "jpg")
|
||||
if err := bucket.WriteAll(ctx, origKey, []byte("original"), &blob.WriterOptions{ContentType: "image/jpeg"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := bucket.WriteAll(ctx, thumbKey, []byte("thumb"), &blob.WriterOptions{ContentType: "image/jpeg"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := gdb.Delete(&owner).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var softOwner lifecycleOwner
|
||||
if err := gdb.Unscoped().First(&softOwner, owner.ID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !softOwner.DeletedAt.Valid {
|
||||
t.Fatal("soft-delete must set deleted_at")
|
||||
}
|
||||
assertFileRow(t, gdb, file.ID, true)
|
||||
assertBlob(t, ctx, bucket, origKey, true)
|
||||
assertBlob(t, ctx, bucket, thumbKey, true)
|
||||
|
||||
if err := gdb.Transaction(func(tx *gorm.DB) error {
|
||||
return attach.DeleteForOwner(tx, owner, file.AttachmentID, func(keys []string) error {
|
||||
assertBlob(t, ctx, bucket, origKey, true)
|
||||
assertBlob(t, ctx, bucket, thumbKey, true)
|
||||
return fmt.Errorf("rollback after collecting keys")
|
||||
})
|
||||
}); err == nil {
|
||||
t.Fatal("expected rollback")
|
||||
}
|
||||
assertFileRow(t, gdb, file.ID, true)
|
||||
assertBlob(t, ctx, bucket, origKey, true)
|
||||
assertBlob(t, ctx, bucket, thumbKey, true)
|
||||
|
||||
var pending []string
|
||||
if err := gdb.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Unscoped().Delete(&owner).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return attach.DeleteForOwner(tx, owner, file.AttachmentID, func(keys []string) error {
|
||||
pending = append([]string(nil), keys...)
|
||||
var n int64
|
||||
if err := tx.Model(&attach.File{}).Where("id = ?", file.ID).Count(&n).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if n != 0 {
|
||||
return fmt.Errorf("system_files row must be gone inside the force-delete transaction")
|
||||
}
|
||||
exists, err := bucket.Exists(ctx, origKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
return fmt.Errorf("blob must still exist before commit")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertFileRow(t, gdb, file.ID, false)
|
||||
assertBlob(t, ctx, bucket, origKey, true)
|
||||
if err := attach.DeleteKeys(ctx, bucket, pending); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertBlob(t, ctx, bucket, origKey, false)
|
||||
assertBlob(t, ctx, bucket, thumbKey, false)
|
||||
}
|
||||
|
||||
func TestFileCreateDefaultsIsPublic(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("requires testcontainers postgres")
|
||||
}
|
||||
gdb := attachGorm(t)
|
||||
if err := lagoon.Migrate(gdb, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
file := attach.File{
|
||||
DiskName: "abc123xyz.jpg",
|
||||
FileName: "cover.jpg",
|
||||
FileSize: 1,
|
||||
ContentType: "image/jpeg",
|
||||
}
|
||||
if err := gdb.Create(&file).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got attach.File
|
||||
if err := gdb.First(&got, file.ID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !got.Public() {
|
||||
t.Fatalf("Create without IsPublic stored %v, want true", got.IsPublic)
|
||||
}
|
||||
|
||||
priv := false
|
||||
hidden := attach.File{
|
||||
DiskName: "def456uvw.jpg",
|
||||
FileName: "secret.jpg",
|
||||
FileSize: 1,
|
||||
ContentType: "image/jpeg",
|
||||
IsPublic: &priv,
|
||||
}
|
||||
if err := gdb.Create(&hidden).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var gotHidden attach.File
|
||||
if err := gdb.First(&gotHidden, hidden.ID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gotHidden.Public() {
|
||||
t.Fatal("explicit is_public=false must persist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaticHandlerPublicLooksUpIsPublic(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("requires testcontainers postgres")
|
||||
}
|
||||
ctx := t.Context()
|
||||
gdb := attachGorm(t)
|
||||
if err := lagoon.Migrate(gdb, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bucket := memblob.OpenBucket(nil)
|
||||
t.Cleanup(func() { _ = bucket.Close() })
|
||||
|
||||
pub := true
|
||||
priv := false
|
||||
publicFile := attach.File{DiskName: "abc123xyz.jpg", FileName: "cover.jpg", FileSize: 1, ContentType: "image/jpeg", IsPublic: &pub}
|
||||
privateFile := attach.File{DiskName: "def456uvw.jpg", FileName: "secret.jpg", FileSize: 1, ContentType: "image/jpeg", IsPublic: &priv}
|
||||
if err := gdb.Create(&publicFile).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := gdb.Create(&privateFile).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, f := range []attach.File{publicFile, privateFile} {
|
||||
if err := bucket.WriteAll(ctx, attach.BlobKey(f.DiskName), []byte(f.FileName), &blob.WriterOptions{ContentType: "image/jpeg"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
thumbKey := attach.PartitionDirectory(f.DiskName) + attach.ThumbFilename(f.ID, 50, 50, 0, 0, "crop", "jpg")
|
||||
if err := bucket.WriteAll(ctx, thumbKey, []byte("thumb"), &blob.WriterOptions{ContentType: "image/jpeg"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
h := attach.StaticHandlerPublic(bucket, "/storage/uploads", gdb)
|
||||
get := func(path string) int {
|
||||
t.Helper()
|
||||
rr := httptest.NewRecorder()
|
||||
h.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, path, nil))
|
||||
return rr.Code
|
||||
}
|
||||
if code := get("/storage/uploads/abc/123/xyz/abc123xyz.jpg"); code != http.StatusOK {
|
||||
t.Fatalf("public original status = %d, want 200", code)
|
||||
}
|
||||
if code := get("/storage/uploads/abc/123/xyz/" + attach.ThumbFilename(publicFile.ID, 50, 50, 0, 0, "crop", "jpg")); code != http.StatusOK {
|
||||
t.Fatalf("public thumb status = %d, want 200", code)
|
||||
}
|
||||
if code := get("/storage/uploads/def/456/uvw/def456uvw.jpg"); code != http.StatusNotFound {
|
||||
t.Fatalf("private original status = %d, want 404", code)
|
||||
}
|
||||
if code := get("/storage/uploads/def/456/uvw/" + attach.ThumbFilename(privateFile.ID, 50, 50, 0, 0, "crop", "jpg")); code != http.StatusNotFound {
|
||||
t.Fatalf("private thumb status = %d, want 404", code)
|
||||
}
|
||||
if code := get("/storage/uploads/abc/123/xyz/missing.jpg"); code != http.StatusNotFound {
|
||||
t.Fatalf("unknown disk_name status = %d, want 404", code)
|
||||
}
|
||||
}
|
||||
|
||||
func assertFileRow(t *testing.T, gdb *gorm.DB, id uint, want bool) {
|
||||
t.Helper()
|
||||
var n int64
|
||||
if err := gdb.Model(&attach.File{}).Where("id = ?", id).Count(&n).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := n > 0; got != want {
|
||||
t.Fatalf("system_files id %d exists=%v, want %v", id, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func assertBlob(t *testing.T, ctx context.Context, bucket *blob.Bucket, key string, want bool) {
|
||||
t.Helper()
|
||||
got, err := bucket.Exists(ctx, key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("blob %q exists=%v, want %v", key, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func attachGorm(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
ctr, err := postgres.Run(ctx,
|
||||
"postgres:16-alpine",
|
||||
postgres.WithDatabase("attach"),
|
||||
postgres.WithUsername("attach"),
|
||||
postgres.WithPassword("attach"),
|
||||
postgres.BasicWaitStrategies(),
|
||||
testcontainers.WithEnv(map[string]string{
|
||||
"POSTGRES_INITDB_ARGS": "--locale-provider=icu --icu-locale=pl-PL --encoding=UTF8",
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("postgres: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = testcontainers.TerminateContainer(ctr) })
|
||||
dsn, err := ctr.ConnectionString(ctx, "sslmode=disable")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db, err := sql.Open("pgx", dsn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = db.Close() })
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gdb, err := lagoon.Use(ctx, db)
|
||||
if err != nil {
|
||||
t.Fatalf("lagoon.Use: %v", err)
|
||||
}
|
||||
return gdb
|
||||
}
|
||||
Reference in New Issue
Block a user