feat(12.2-01): add deferred bindings, guarded upload store and purge
- deferred_bindings migration set under summercms.deferred with backend_user_id - lagoon.DeferredBind/Unbind/Bindings/Forget/Slaves scoped by DeferredKey - lagoon.PurgeDeferred with SKIP LOCKED batches and after-commit blob deletes - attach.Store with the ported image guard, extension and MIME limits - attach.Relation, attach.HasRelations, attach.BlobKeys, File.ThumbKey - lagoon README and attachments docs
This commit is contained in:
116
modules/lagoon/deferred_test.go
Normal file
116
modules/lagoon/deferred_test.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package lagoon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"image"
|
||||
"image/png"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/lagoon/attach"
|
||||
"gocloud.dev/blob/memblob"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// TestDeferredUploadPurgeTracer runs the 12.2 storage path end to end: a
|
||||
// guarded upload held by one admin's deferred binding, then purged with its
|
||||
// row and, after commit, its blob.
|
||||
func TestDeferredUploadPurgeTracer(t *testing.T) {
|
||||
db, _ := dedicatedDB(t, "lagoon_deferred_tracer")
|
||||
gdb, err := Use(t.Context(), db)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := Migrate(gdb, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := t.Context()
|
||||
bucket := memblob.OpenBucket(nil)
|
||||
t.Cleanup(func() { _ = bucket.Close() })
|
||||
|
||||
var img bytes.Buffer
|
||||
if err := png.Encode(&img, image.NewRGBA(image.Rect(0, 0, 2, 2))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, err := attach.Store(ctx, gdb, bucket, attach.Upload{FileName: "cover.png", Body: &img, Public: true}, attach.Limits{Image: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
key := DeferredKey{SessionKey: "tracer-session-key-0123456789abcdef", AdminID: 7, MasterType: "acme_posts"}
|
||||
slaveID := strconv.FormatUint(uint64(f.ID), 10)
|
||||
if err := Transaction(ctx, gdb, func(ctx context.Context, tx *gorm.DB) error {
|
||||
if err := DeferredBind(ctx, tx, key, "cover", DeferredFileType, slaveID, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
// A repeated bind writes nothing.
|
||||
return DeferredBind(ctx, tx, key, "cover", DeferredFileType, slaveID, nil)
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var rows []DeferredBinding
|
||||
if err := gdb.Find(&rows).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(rows) != 1 || !rows[0].IsBind || rows[0].BackendUserID != 7 || rows[0].MasterType != "acme_posts" {
|
||||
t.Fatalf("bindings %+v", rows)
|
||||
}
|
||||
// Another admin's key sees nothing.
|
||||
other := key
|
||||
other.AdminID = 8
|
||||
var seen []DeferredBinding
|
||||
if err := Transaction(ctx, gdb, func(ctx context.Context, tx *gorm.DB) error {
|
||||
var err error
|
||||
seen, err = DeferredBindings(ctx, tx, other, []string{"cover"})
|
||||
return err
|
||||
}); err != nil || len(seen) != 0 {
|
||||
t.Fatalf("foreign admin saw %v (%v)", seen, err)
|
||||
}
|
||||
|
||||
if err := gdb.Exec(`UPDATE deferred_bindings SET created_at = NOW() - INTERVAL '6 days'`).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res, err := PurgeDeferred(ctx, gdb, bucket, PurgeOptions{Before: time.Now().Add(-5 * 24 * time.Hour)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.Bindings != 1 || res.Files != 1 || res.Skipped != 0 {
|
||||
t.Fatalf("purge result %+v", res)
|
||||
}
|
||||
var count int64
|
||||
if err := gdb.Model(&DeferredBinding{}).Count(&count).Error; err != nil || count != 0 {
|
||||
t.Fatalf("bindings left %d (%v)", count, err)
|
||||
}
|
||||
if err := gdb.Model(&attach.File{}).Where("id = ?", f.ID).Count(&count).Error; err != nil || count != 0 {
|
||||
t.Fatalf("file rows left %d (%v)", count, err)
|
||||
}
|
||||
exists, err := bucket.Exists(ctx, attach.BlobKey(f.DiskName))
|
||||
if err != nil || exists {
|
||||
t.Fatalf("blob still exists=%v (%v)", exists, err)
|
||||
}
|
||||
|
||||
// A bind followed by an unbind of the same slave cancels the pair and
|
||||
// hands the cancelled bind back.
|
||||
var cancelled *DeferredBinding
|
||||
if err := Transaction(ctx, gdb, func(ctx context.Context, tx *gorm.DB) error {
|
||||
env := &DeferredEnvelope{Created: true}
|
||||
if err := DeferredBind(ctx, tx, key, "comments", "acme_comments", "41", env); err != nil {
|
||||
return err
|
||||
}
|
||||
var err error
|
||||
cancelled, err = DeferredUnbind(ctx, tx, key, "comments", "acme_comments", "41")
|
||||
return err
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cancelled == nil || !cancelled.IsBind {
|
||||
t.Fatalf("cancelled bind %+v", cancelled)
|
||||
}
|
||||
if env, err := cancelled.Envelope(); err != nil || !env.Created {
|
||||
t.Fatalf("envelope %+v (%v)", env, err)
|
||||
}
|
||||
if err := gdb.Model(&DeferredBinding{}).Count(&count).Error; err != nil || count != 0 {
|
||||
t.Fatalf("bindings after cancel %d (%v)", count, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user