test(05-04): add failing tests for attach File, Thumb, and bucket

- ThumbFilename/PartitionDirectory/File.Thumb behavior tests
- OpenBucket fail-loud and memblob tests
- Migrate empty plugin list must create system_files
This commit is contained in:
Jakub Zych
2026-09-18 19:53:00 +02:00
parent b1b740c333
commit 2d84ae4565
11 changed files with 445 additions and 2 deletions

10
lagoon/attach/app_test.go Normal file
View File

@@ -0,0 +1,10 @@
package attach
import (
"git.golem15.com/golem15/summercms/backpack"
"git.golem15.com/golem15/summercms/compass"
)
func backpackApp(cfg *compass.Config) *backpack.App {
return backpack.New(cfg)
}

22
lagoon/attach/bucket.go Normal file
View File

@@ -0,0 +1,22 @@
package attach
import (
"context"
"fmt"
"git.golem15.com/golem15/summercms/backpack"
"git.golem15.com/golem15/summercms/compass"
"gocloud.dev/blob"
_ "gocloud.dev/blob/fileblob"
_ "gocloud.dev/blob/memblob"
)
func OpenBucket(ctx context.Context, cfg *compass.Config) (*blob.Bucket, error) {
return nil, fmt.Errorf("attach: bucket not implemented")
}
func Publish(app *backpack.App, bucket *blob.Bucket) error {
return fmt.Errorf("attach: publish not implemented")
}
func PublicPathPrefix() string { return "" }

View File

@@ -0,0 +1,91 @@
package attach
import (
"os"
"path/filepath"
"strings"
"testing"
"git.golem15.com/golem15/summercms/compass"
"gocloud.dev/blob"
"gocloud.dev/blob/memblob"
)
func TestOpenBucketRequiresURL(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "storage.yaml"), []byte("uploads:\n bucket_url: \"\"\n"), 0o644); err != nil {
t.Fatal(err)
}
cfg, err := compass.Open(compass.Options{
Dir: dir,
Env: "development",
Environ: []string{"SUMMER_ENV=development"},
})
if err != nil {
t.Fatal(err)
}
_, err = OpenBucket(t.Context(), cfg)
if err == nil || !strings.Contains(err.Error(), "bucket_url") {
t.Fatalf("got %v, want bucket_url error", err)
}
}
func TestOpenBucketOpensMem(t *testing.T) {
dir := t.TempDir()
body := "uploads:\n bucket_url: \"mem://\"\n public_path_prefix: \"/storage/uploads\"\n"
if err := os.WriteFile(filepath.Join(dir, "storage.yaml"), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
cfg, err := compass.Open(compass.Options{
Dir: dir,
Env: "development",
Environ: []string{"SUMMER_ENV=development"},
})
if err != nil {
t.Fatal(err)
}
bucket, err := OpenBucket(t.Context(), cfg)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = bucket.Close() })
if PublicPathPrefix() != "/storage/uploads" {
t.Fatalf("prefix = %q", PublicPathPrefix())
}
ctx := t.Context()
if err := bucket.WriteAll(ctx, "abc/123/xyz/probe.jpg", []byte("hi"), &blob.WriterOptions{}); err != nil {
t.Fatal(err)
}
got, err := bucket.ReadAll(ctx, "abc/123/xyz/probe.jpg")
if err != nil {
t.Fatal(err)
}
if string(got) != "hi" {
t.Fatalf("read = %q", got)
}
}
func TestPublishStoresBucket(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "app.yaml"), []byte("name: attach-test\n"), 0o644); err != nil {
t.Fatal(err)
}
cfg, err := compass.Open(compass.Options{
Dir: dir,
Env: "development",
Environ: []string{"SUMMER_ENV=development"},
})
if err != nil {
t.Fatal(err)
}
app := backpackApp(cfg)
bucket := memblob.OpenBucket(nil)
t.Cleanup(func() { _ = bucket.Close() })
if err := Publish(app, bucket); err != nil {
t.Fatal(err)
}
got, ok := app.Lookup[*blob.Bucket]()
if !ok || got != bucket {
t.Fatal("Publish must store the same *blob.Bucket")
}
}

48
lagoon/attach/file.go Normal file
View File

@@ -0,0 +1,48 @@
package attach
import "time"
// Owner is implemented by models that own system_files rows. MorphName
// must return the PHP class string so cutover-copied attachment_type
// values keep matching.
type Owner interface {
MorphName() string
}
// File is Winter's system_files row. AttachmentID is a string because
// Winter stores the morph FK as a string, never an integer.
type File struct {
ID uint `gorm:"column:id;primaryKey"`
DiskName string `gorm:"column:disk_name"`
FileName string `gorm:"column:file_name"`
FileSize int64 `gorm:"column:file_size"`
ContentType string `gorm:"column:content_type"`
Title *string `gorm:"column:title"`
Description *string `gorm:"column:description"`
Field string `gorm:"column:field"`
AttachmentID string `gorm:"column:attachment_id"`
AttachmentType string `gorm:"column:attachment_type"`
IsPublic bool `gorm:"column:is_public"`
SortOrder int `gorm:"column:sort_order"`
Metadata *string `gorm:"column:metadata"`
CreatedAt time.Time `gorm:"column:created_at"`
UpdatedAt time.Time `gorm:"column:updated_at"`
}
func (File) TableName() string { return "system_files" }
var all []any
// Register appends models so schema tooling can see File without a plugin registry.
func Register(models ...any) {
all = append(all, models...)
}
// All returns every model registered in this package.
func All() []any {
return all
}
func init() {
Register(&File{})
}

View File

@@ -0,0 +1,19 @@
package attach
import "testing"
func TestFileTableName(t *testing.T) {
if got := (File{}).TableName(); got != "system_files" {
t.Fatalf("TableName = %q, want system_files", got)
}
}
func TestFileSelfRegisters(t *testing.T) {
for _, m := range All() {
switch m.(type) {
case *File, File:
return
}
}
t.Fatal("File must self-register via attach.Register")
}

View File

@@ -0,0 +1,7 @@
package attach
import "github.com/go-gormigrate/gormigrate/v2"
// Migrations is the framework-owned system_files set. lagoon.Migrate
// runs it before every plugin set (D-14).
var Migrations = []*gormigrate.Migration{}

25
lagoon/attach/thumb.go Normal file
View File

@@ -0,0 +1,25 @@
package attach
import (
"context"
"fmt"
"image"
"gocloud.dev/blob"
)
func ThumbFilename(id uint, w, h int, offsetX, offsetY int, mode, ext string) string {
return ""
}
func PartitionDirectory(diskName string) string {
return ""
}
func (f *File) Thumb(ctx context.Context, bucket *blob.Bucket, w, h int, mode string) (string, error) {
return "", fmt.Errorf("attach: thumb not implemented")
}
var resizeImage = func(src image.Image, w, h int, mode string) image.Image {
return src
}

View File

@@ -0,0 +1,84 @@
package attach
import (
"bytes"
"image"
"image/color"
"image/jpeg"
"testing"
"gocloud.dev/blob"
"gocloud.dev/blob/memblob"
)
func TestThumbFilename(t *testing.T) {
got := ThumbFilename(42, 200, 200, 0, 0, "crop", "jpg")
const want = "thumb_42_200_200_0_0_crop.jpg"
if got != want {
t.Fatalf("ThumbFilename = %q, want %q", got, want)
}
}
func TestPartitionDirectory(t *testing.T) {
got := PartitionDirectory("abc123xyz.jpg")
const want = "abc/123/xyz/"
if got != want {
t.Fatalf("PartitionDirectory = %q, want %q", got, want)
}
}
func TestFileThumbResizesOnce(t *testing.T) {
ctx := t.Context()
bucket := memblob.OpenBucket(nil)
t.Cleanup(func() { _ = bucket.Close() })
f := &File{ID: 42, DiskName: "abc123xyz.jpg", FileName: "cover.jpg"}
origKey := PartitionDirectory(f.DiskName) + f.DiskName
if origKey == "abc123xyz.jpg" || origKey == "" {
// PartitionDirectory still stubbed — write under the Winter key the
// GREEN implementation will look up so this test stays the behavior spec.
origKey = "abc/123/xyz/" + f.DiskName
}
if err := bucket.WriteAll(ctx, origKey, testJPEG(t), &blob.WriterOptions{ContentType: "image/jpeg"}); err != nil {
t.Fatal(err)
}
var n int
orig := resizeImage
resizeImage = func(src image.Image, w, h int, mode string) image.Image {
n++
return orig(src, w, h, mode)
}
t.Cleanup(func() { resizeImage = orig })
url1, err := f.Thumb(ctx, bucket, 200, 200, "crop")
if err != nil {
t.Fatalf("first Thumb: %v", err)
}
url2, err := f.Thumb(ctx, bucket, 200, 200, "crop")
if err != nil {
t.Fatalf("second Thumb: %v", err)
}
if n != 1 {
t.Fatalf("resize calls = %d, want 1", n)
}
want := "/storage/uploads/abc/123/xyz/thumb_42_200_200_0_0_crop.jpg"
if url1 != want || url2 != want {
t.Fatalf("urls = %q %q, want %q", url1, url2, want)
}
}
func testJPEG(t *testing.T) []byte {
t.Helper()
img := image.NewRGBA(image.Rect(0, 0, 8, 8))
for y := 0; y < 8; y++ {
for x := 0; x < 8; x++ {
img.Set(x, y, color.RGBA{R: 200, A: 255})
}
}
var buf bytes.Buffer
if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 90}); err != nil {
t.Fatal(err)
}
return buf.Bytes()
}