From 0e7287c6a7652125ce47c17086493753af3e61a6 Mon Sep 17 00:00:00 2001 From: Ismael Ash Date: Sun, 19 Apr 2026 23:33:17 -0300 Subject: [PATCH] feat(minio): ensure bucket exists on storage init Create the configured bucket when missing, matching fix/create-bucket-if-no-exist-minio. Add unit tests (fake client) and optional integration test behind -tags=integration. Made-with: Cursor --- pkg/storage/minio/media_storage.go | 27 +++++++ .../minio/media_storage_integration_test.go | 56 +++++++++++++ pkg/storage/minio/media_storage_test.go | 78 +++++++++++++++++++ 3 files changed, 161 insertions(+) create mode 100644 pkg/storage/minio/media_storage_integration_test.go create mode 100644 pkg/storage/minio/media_storage_test.go diff --git a/pkg/storage/minio/media_storage.go b/pkg/storage/minio/media_storage.go index 042a19c..cdcd5f4 100644 --- a/pkg/storage/minio/media_storage.go +++ b/pkg/storage/minio/media_storage.go @@ -13,6 +13,29 @@ import ( "github.com/minio/minio-go/v7/pkg/credentials" ) +// minioBucketAdmin is satisfied by *minio.Client; extracted for tests. +type minioBucketAdmin interface { + BucketExists(ctx context.Context, bucketName string) (bool, error) + MakeBucket(ctx context.Context, bucketName string, opts minio.MakeBucketOptions) error +} + +func ensureBucketExists(ctx context.Context, client minioBucketAdmin, bucketName, region string) error { + exists, err := client.BucketExists(ctx, bucketName) + if err != nil { + return fmt.Errorf("failed to check if bucket exists: %w", err) + } + + if !exists { + err = client.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{Region: region}) + if err != nil { + return fmt.Errorf("failed to create bucket: %w", err) + } + fmt.Printf("Successfully created bucket %s\n", bucketName) + } + + return nil +} + type MinioMediaStorage struct { client *minio.Client bucketName string @@ -71,6 +94,10 @@ func NewMinioMediaStorage( return nil, fmt.Errorf("failed to create MinIO client: %w", err) } + if err := ensureBucketExists(context.Background(), client, bucketName, region); err != nil { + return nil, err + } + // Try to set bucket policy to allow public access (optional for some providers) err = setBucketPolicy(client, bucketName) if err != nil { diff --git a/pkg/storage/minio/media_storage_integration_test.go b/pkg/storage/minio/media_storage_integration_test.go new file mode 100644 index 0000000..0ae51af --- /dev/null +++ b/pkg/storage/minio/media_storage_integration_test.go @@ -0,0 +1,56 @@ +//go:build integration + +package minio_storage + +import ( + "os" + "strconv" + "testing" +) + +// TestNewMinioMediaStorage_CreatesBucket runs against a real MinIO/S3-compatible endpoint. +// Example: +// +// MINIO_INTEGRATION_TEST=1 \ +// MINIO_ENDPOINT=localhost:9000 \ +// MINIO_ACCESS_KEY=minioadmin \ +// MINIO_SECRET_KEY=minioadmin \ +// MINIO_BUCKET=evolution-test-bucket \ +// MINIO_REGION=us-east-1 \ +// MINIO_USE_SSL=false \ +// go test -tags=integration ./pkg/storage/minio/... +func TestNewMinioMediaStorage_CreatesBucket(t *testing.T) { + if os.Getenv("MINIO_INTEGRATION_TEST") == "" { + t.Skip("set MINIO_INTEGRATION_TEST=1 and MinIO env vars to run") + } + + endpoint := os.Getenv("MINIO_ENDPOINT") + access := os.Getenv("MINIO_ACCESS_KEY") + secret := os.Getenv("MINIO_SECRET_KEY") + bucket := os.Getenv("MINIO_BUCKET") + region := os.Getenv("MINIO_REGION") + if region == "" { + region = "us-east-1" + } + + useSSL := false + if v := os.Getenv("MINIO_USE_SSL"); v != "" { + var err error + useSSL, err = strconv.ParseBool(v) + if err != nil { + t.Fatalf("MINIO_USE_SSL: %v", err) + } + } + + if endpoint == "" || access == "" || secret == "" || bucket == "" { + t.Fatal("MINIO_ENDPOINT, MINIO_ACCESS_KEY, MINIO_SECRET_KEY, MINIO_BUCKET are required") + } + + st, err := NewMinioMediaStorage(endpoint, access, secret, bucket, region, useSSL) + if err != nil { + t.Fatalf("NewMinioMediaStorage: %v", err) + } + if st == nil { + t.Fatal("expected non-nil storage") + } +} diff --git a/pkg/storage/minio/media_storage_test.go b/pkg/storage/minio/media_storage_test.go new file mode 100644 index 0000000..fa92c62 --- /dev/null +++ b/pkg/storage/minio/media_storage_test.go @@ -0,0 +1,78 @@ +package minio_storage + +import ( + "context" + "errors" + "testing" + + "github.com/minio/minio-go/v7" +) + +type fakeMinioBucket struct { + exists bool + existsErr error + makeErr error + makeCalls int + lastBucket string + lastRegion string + existsCalls int +} + +func (f *fakeMinioBucket) BucketExists(ctx context.Context, bucketName string) (bool, error) { + _ = ctx + f.existsCalls++ + f.lastBucket = bucketName + return f.exists, f.existsErr +} + +func (f *fakeMinioBucket) MakeBucket(ctx context.Context, bucketName string, opts minio.MakeBucketOptions) error { + _ = ctx + f.makeCalls++ + f.lastBucket = bucketName + f.lastRegion = opts.Region + return f.makeErr +} + +func TestEnsureBucketExists_AlreadyThere(t *testing.T) { + f := &fakeMinioBucket{exists: true} + err := ensureBucketExists(context.Background(), f, "my-bucket", "us-east-1") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if f.makeCalls != 0 { + t.Fatalf("MakeBucket should not run when bucket exists, calls=%d", f.makeCalls) + } + if f.existsCalls != 1 { + t.Fatalf("expected 1 BucketExists call, got %d", f.existsCalls) + } +} + +func TestEnsureBucketExists_CreatesWhenMissing(t *testing.T) { + f := &fakeMinioBucket{exists: false} + err := ensureBucketExists(context.Background(), f, "new-bucket", "eu-west-1") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if f.makeCalls != 1 { + t.Fatalf("expected 1 MakeBucket call, got %d", f.makeCalls) + } + if f.lastBucket != "new-bucket" || f.lastRegion != "eu-west-1" { + t.Fatalf("unexpected MakeBucket args: bucket=%q region=%q", f.lastBucket, f.lastRegion) + } +} + +func TestEnsureBucketExists_BucketExistsError(t *testing.T) { + f := &fakeMinioBucket{existsErr: errors.New("network down")} + err := ensureBucketExists(context.Background(), f, "b", "r") + if err == nil { + t.Fatal("expected error") + } +} + +func TestEnsureBucketExists_MakeBucketError(t *testing.T) { + f := &fakeMinioBucket{exists: false, makeErr: errors.New("access denied")} + err := ensureBucketExists(context.Background(), f, "b", "r") + if err == nil { + t.Fatal("expected error") + } +}