From c12e6576f696f99f8e03d54b14e2c700bf978c12 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Sat, 19 Sep 2026 15:34:50 +0200 Subject: [PATCH] fix(05): WR-04 default File.IsPublic to true without trapping false --- lagoon/attach/file.go | 42 ++++++++++++++++++----------- lagoon/attach/file_test.go | 25 +++++++++++++++++ lagoon/attach/lifecycle_test.go | 48 ++++++++++++++++++++++++++++++++- 3 files changed, 99 insertions(+), 16 deletions(-) diff --git a/lagoon/attach/file.go b/lagoon/attach/file.go index 5e6e85a..43dd760 100644 --- a/lagoon/attach/file.go +++ b/lagoon/attach/file.go @@ -22,25 +22,37 @@ type Owner interface { // 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"` + 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"` + // Pointer so GORM can distinguish unset (nil → DEFAULT TRUE) from + // explicit false. A non-pointer bool with default:true cannot persist + // false because false is the zero value GORM replaces with the default. + IsPublic *bool `gorm:"column:is_public;not null;default:true"` + 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" } +// Public reports Winter's is_public flag. A nil pointer is treated as true, +// matching the SQL DEFAULT TRUE and File::create() behaviour. +func (f File) Public() bool { + if f.IsPublic == nil { + return true + } + return *f.IsPublic +} + var all []any // Register appends models so schema tooling can see File without a plugin registry. diff --git a/lagoon/attach/file_test.go b/lagoon/attach/file_test.go index c24cc2b..e3fe502 100644 --- a/lagoon/attach/file_test.go +++ b/lagoon/attach/file_test.go @@ -1,6 +1,8 @@ package attach import ( + "reflect" + "strings" "testing" "gocloud.dev/blob/memblob" @@ -12,6 +14,29 @@ func TestFileTableName(t *testing.T) { } } +func TestFileIsPublicDefaultsTrue(t *testing.T) { + unset := File{} + if !unset.Public() { + t.Fatal("unset IsPublic must be public") + } + f, ok := reflect.TypeOf(File{}).FieldByName("IsPublic") + if !ok { + t.Fatal("missing IsPublic") + } + tag := f.Tag.Get("gorm") + if !strings.Contains(tag, "default:true") { + t.Fatalf("gorm tag = %q, want default:true", tag) + } + priv := false + if (File{IsPublic: &priv}).Public() { + t.Fatal("explicit false must not be public") + } + pub := true + if !(File{IsPublic: &pub}).Public() { + t.Fatal("explicit true must be public") + } +} + func TestFileSelfRegisters(t *testing.T) { for _, m := range All() { switch m.(type) { diff --git a/lagoon/attach/lifecycle_test.go b/lagoon/attach/lifecycle_test.go index 0a16df8..8cbb5d9 100644 --- a/lagoon/attach/lifecycle_test.go +++ b/lagoon/attach/lifecycle_test.go @@ -55,6 +55,7 @@ CREATE TABLE attach_lifecycle_owners ( if err := gdb.Create(&owner).Error; err != nil { t.Fatal(err) } + isPublic := true file := attach.File{ DiskName: "abc123xyz.jpg", FileName: "cover.jpg", @@ -63,7 +64,7 @@ CREATE TABLE attach_lifecycle_owners ( Field: "photos", AttachmentID: strconv.FormatUint(uint64(owner.ID), 10), AttachmentType: owner.MorphName(), - IsPublic: true, + IsPublic: &isPublic, } if err := gdb.Create(&file).Error; err != nil { t.Fatal(err) @@ -139,6 +140,51 @@ CREATE TABLE attach_lifecycle_owners ( 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 assertFileRow(t *testing.T, gdb *gorm.DB, id uint, want bool) { t.Helper() var n int64