//go:build test && dev package s3 import ( "bytes" "context" "fmt" "io" "net" "strings" "sync" "testing" "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/aws/smithy-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestInMemS3_UploadAndDownload(t *testing.T) { t.Parallel() s3 := &inMemS3{objects: make(map[string][]byte)} ctx := context.Background() body := "hello world" err := s3.Upload(ctx, "bucket1", "key1", strings.NewReader(body), "text/plain") require.NoError(t, err) var buf bytes.Buffer err = s3.Download(ctx, "bucket1", "key1", &buf) require.NoError(t, err) assert.Equal(t, body, buf.String()) } func TestInMemS3_UploadOverwrite(t *testing.T) { t.Parallel() s3 := &inMemS3{objects: make(map[string][]byte)} ctx := context.Background() err := s3.Upload(ctx, "bucket", "key", strings.NewReader("first"), "") require.NoError(t, err) err = s3.Upload(ctx, "bucket", "key", strings.NewReader("second"), "") require.NoError(t, err) var buf bytes.Buffer err = s3.Download(ctx, "bucket", "key", &buf) require.NoError(t, err) assert.Equal(t, "second", buf.String()) } func TestInMemS3_Download_NotFound(t *testing.T) { t.Parallel() s3 := &inMemS3{objects: make(map[string][]byte)} ctx := context.Background() var buf bytes.Buffer err := s3.Download(ctx, "bucket", "nonexistent", &buf) assert.Error(t, err) assert.Contains(t, err.Error(), "not found") } func TestInMemS3_Delete(t *testing.T) { t.Parallel() s3 := &inMemS3{objects: make(map[string][]byte)} ctx := context.Background() err := s3.Upload(ctx, "bucket", "key", strings.NewReader("data"), "") require.NoError(t, err) err = s3.Delete(ctx, "bucket", "key") assert.NoError(t, err) var buf bytes.Buffer err = s3.Download(ctx, "bucket", "key", &buf) assert.Error(t, err) assert.Contains(t, err.Error(), "not found") } func TestInMemS3_Delete_NotFound(t *testing.T) { t.Parallel() s3 := &inMemS3{objects: make(map[string][]byte)} ctx := context.Background() err := s3.Delete(ctx, "bucket", "does-not-exist") assert.NoError(t, err) } func TestInMemS3_GetURL(t *testing.T) { t.Parallel() s3 := &inMemS3{objects: make(map[string][]byte)} ctx := context.Background() url, err := s3.GetURL(ctx, "mybucket", "mykey") require.NoError(t, err) assert.Equal(t, "https://cdn.example.com/mybucket/mykey", url) } func TestInMemS3_HealthCheck(t *testing.T) { t.Parallel() s3 := &inMemS3{objects: make(map[string][]byte)} ctx := context.Background() err := s3.HealthCheck(ctx) assert.NoError(t, err) } func TestInMemS3_ConcurrentUpload(t *testing.T) { t.Parallel() s3 := &inMemS3{objects: make(map[string][]byte)} ctx := context.Background() var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func(n int) { defer wg.Done() key := "key-" + string(rune('0'+n)) err := s3.Upload(ctx, "bucket", key, strings.NewReader("data"), "") assert.NoError(t, err) }(i) } wg.Wait() for i := 0; i < 10; i++ { key := "key-" + string(rune('0'+i)) var buf bytes.Buffer err := s3.Download(ctx, "bucket", key, &buf) assert.NoError(t, err) assert.Equal(t, "data", buf.String()) } } func TestInMemS3_DownloadEmptyBody(t *testing.T) { t.Parallel() s3 := &inMemS3{objects: make(map[string][]byte)} ctx := context.Background() err := s3.Upload(ctx, "bucket", "empty", strings.NewReader(""), "") require.NoError(t, err) var buf bytes.Buffer err = s3.Download(ctx, "bucket", "empty", &buf) require.NoError(t, err) assert.Equal(t, "", buf.String()) } func TestConnect_FallbackToInMemory(t *testing.T) { t.Parallel() ctx := context.Background() err := Connect() assert.NoError(t, err) assert.NotNil(t, Client) var buf bytes.Buffer err = Client.Download(ctx, "bucket", "nonexistent", &buf) assert.Error(t, err) assert.Contains(t, err.Error(), "not found") } func TestIsMissingBucket_NoSuchBucketGeneric(t *testing.T) { t.Parallel() err := &smithy.GenericAPIError{Code: "NoSuchBucket", Message: "bucket does not exist"} assert.True(t, isMissingBucket(err)) } func TestIsMissingBucket_NotFoundGeneric(t *testing.T) { t.Parallel() err := &smithy.GenericAPIError{Code: "NotFound", Message: "not found"} assert.True(t, isMissingBucket(err)) } func TestIsMissingBucket_NotFoundTyped(t *testing.T) { t.Parallel() // The concrete error types the vendored SDK surfaces for HeadBucket 404s. assert.True(t, isMissingBucket(&types.NotFound{})) assert.True(t, isMissingBucket(&types.NoSuchBucket{})) } func TestIsMissingBucket_NotFoundViaOperationError(t *testing.T) { t.Parallel() // The SDK wraps API errors in an OperationError; errors.As must still find it. err := &smithy.OperationError{ServiceID: "S3", OperationName: "HeadBucket", Err: &types.NotFound{}} assert.True(t, isMissingBucket(err)) } func TestIsMissingBucket_NotFoundWrapped(t *testing.T) { t.Parallel() err := fmt.Errorf("head bucket: %w", &smithy.GenericAPIError{Code: "NoSuchBucket", Message: "x"}) assert.True(t, isMissingBucket(err)) } func TestIsMissingBucket_ConnectionRefused(t *testing.T) { t.Parallel() err := &net.OpError{Op: "dial", Net: "tcp", Err: fmt.Errorf("connect: connection refused")} assert.False(t, isMissingBucket(err)) } func TestIsMissingBucket_EOF(t *testing.T) { t.Parallel() assert.False(t, isMissingBucket(io.EOF)) } func TestIsMissingBucket_AccessDenied(t *testing.T) { t.Parallel() // Reachable but unauthorized is a connectivity/credentials problem, not a // missing bucket — it must still trigger the fallback. err := &types.AccessDenied{} assert.False(t, isMissingBucket(err)) } func TestIsMissingBucket_Nil(t *testing.T) { t.Parallel() assert.False(t, isMissingBucket(nil)) }