memos/internal/testutil/minio/server.go

124 lines
3.5 KiB
Go

// Package minio provides a real S3-compatible service backed by a MinIO
// Testcontainer for integration tests.
package minio
import (
"bytes"
"context"
"io"
"os"
"testing"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
tcminio "github.com/testcontainers/testcontainers-go/modules/minio"
storepb "github.com/usememos/memos/proto/gen/store"
)
const image = "minio/minio:RELEASE.2024-01-16T16-07-38Z"
// Server is an S3-compatible MinIO container with an AWS SDK client for
// arranging and inspecting test objects.
type Server struct {
Endpoint string
Region string
Username string
Password string
client *awss3.Client
}
// New starts a MinIO container and creates the requested buckets.
func New(t *testing.T, buckets ...string) *Server {
t.Helper()
if testing.Short() {
t.Skip("skipping MinIO integration test in short mode")
}
if os.Getenv("SKIP_CONTAINER_TESTS") == "1" {
t.Skip("skipping MinIO integration test (SKIP_CONTAINER_TESTS=1)")
}
testcontainers.SkipIfProviderIsNotHealthy(t)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
container, err := tcminio.Run(
ctx,
image,
tcminio.WithUsername("memos-test-access"),
tcminio.WithPassword("memos-test-secret"),
)
require.NoError(t, err)
testcontainers.CleanupContainer(t, container)
connection, err := container.ConnectionString(ctx)
require.NoError(t, err)
server := &Server{
Endpoint: "http://" + connection,
Region: "us-east-1",
Username: container.Username,
Password: container.Password,
}
config, err := awsconfig.LoadDefaultConfig(
ctx,
awsconfig.WithRegion(server.Region),
awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(server.Username, server.Password, "")),
)
require.NoError(t, err)
server.client = awss3.NewFromConfig(config, func(options *awss3.Options) {
options.BaseEndpoint = aws.String(server.Endpoint)
options.UsePathStyle = true
})
for _, bucket := range buckets {
_, err := server.client.CreateBucket(ctx, &awss3.CreateBucketInput{Bucket: aws.String(bucket)})
require.NoError(t, err)
}
return server
}
// Config returns a storage configuration connected to the requested bucket.
func (s *Server) Config(bucket string) *storepb.StorageS3Config {
return &storepb.StorageS3Config{
AccessKeyId: s.Username,
AccessKeySecret: s.Password,
Endpoint: s.Endpoint,
Region: s.Region,
Bucket: bucket,
UsePathStyle: true,
}
}
// PutObject uploads an object directly for arranging a test fixture.
func (s *Server) PutObject(bucket, key, contentType string, content []byte) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
_, err := s.client.PutObject(ctx, &awss3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
ContentType: aws.String(contentType),
Body: bytes.NewReader(content),
})
return err
}
// GetObject reads an object directly for asserting test results.
func (s *Server) GetObject(bucket, key string) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
object, err := s.client.GetObject(ctx, &awss3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
})
if err != nil {
return nil, err
}
defer object.Body.Close()
return io.ReadAll(object.Body)
}