refactor: split simples3.go into logical modules
Split monolithic simples3.go (1064 LOC) into 5 focused modules: - simples3.go (141 LOC) - Core S3 struct and configuration - helpers.go (97 LOC) - Utility functions - iam.go (202 LOC) - IAM credential handling - list.go (260 LOC) - List operations - object.go (409 LOC) - File operations Split simples3_test.go (912 LOC) into 4 test modules: - simples3_test.go (118 LOC) - Shared test helpers - iam_test.go (110 LOC) - IAM tests - list_test.go (321 LOC) - List operation tests - object_test.go (381 LOC) - File operation tests Zero functional changes. All tests pass identically. 72.5% test coverage maintained. "Review v0.10.2 code refactor"
This commit is contained in:
@@ -1,17 +1,8 @@
|
||||
package simples3
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type tConfig struct {
|
||||
@@ -21,613 +12,6 @@ type tConfig struct {
|
||||
Region string
|
||||
}
|
||||
|
||||
func TestS3_FileUploadPostAndPut(t *testing.T) {
|
||||
testTxt, err := os.Open("testdata/test.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open test.txt: %v", err)
|
||||
}
|
||||
defer testTxt.Close()
|
||||
// Note: cannot re-use the same file descriptor due to seeking!
|
||||
testTxtSpecialChars, err := os.Open("testdata/test.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open test.txt for special chars: %v", err)
|
||||
}
|
||||
defer testTxtSpecialChars.Close()
|
||||
testPng, err := os.Open("testdata/avatar.png")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open avatar.png: %v", err)
|
||||
}
|
||||
defer testPng.Close()
|
||||
|
||||
type args struct {
|
||||
u UploadInput
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields tConfig
|
||||
args args
|
||||
testDetails bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Upload test.txt",
|
||||
fields: tConfig{
|
||||
AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"),
|
||||
SecretKey: os.Getenv("AWS_S3_SECRET_KEY"),
|
||||
Endpoint: os.Getenv("AWS_S3_ENDPOINT"),
|
||||
Region: os.Getenv("AWS_S3_REGION"),
|
||||
},
|
||||
args: args{
|
||||
UploadInput{
|
||||
Bucket: os.Getenv("AWS_S3_BUCKET"),
|
||||
ObjectKey: "test.txt",
|
||||
ContentType: "text/plain",
|
||||
FileName: "test.txt",
|
||||
Body: testTxt,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Upload test.txt with custom metadata",
|
||||
fields: tConfig{
|
||||
AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"),
|
||||
SecretKey: os.Getenv("AWS_S3_SECRET_KEY"),
|
||||
Endpoint: os.Getenv("AWS_S3_ENDPOINT"),
|
||||
Region: os.Getenv("AWS_S3_REGION"),
|
||||
},
|
||||
args: args{
|
||||
UploadInput{
|
||||
Bucket: os.Getenv("AWS_S3_BUCKET"),
|
||||
ObjectKey: "test_metadata.txt",
|
||||
ContentType: "text/plain",
|
||||
FileName: "test.txt",
|
||||
Body: testTxt,
|
||||
CustomMetadata: map[string]string{
|
||||
"test-metadata": "foo-bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
testDetails: true,
|
||||
},
|
||||
{
|
||||
name: "Upload avatar.png",
|
||||
fields: tConfig{
|
||||
AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"),
|
||||
SecretKey: os.Getenv("AWS_S3_SECRET_KEY"),
|
||||
Endpoint: os.Getenv("AWS_S3_ENDPOINT"),
|
||||
Region: os.Getenv("AWS_S3_REGION"),
|
||||
},
|
||||
args: args{
|
||||
UploadInput{
|
||||
Bucket: os.Getenv("AWS_S3_BUCKET"),
|
||||
ObjectKey: "xyz/image.png",
|
||||
ContentType: "image/png",
|
||||
FileName: "avatar.png",
|
||||
Body: testPng,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Upload special filename txt",
|
||||
fields: tConfig{
|
||||
AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"),
|
||||
SecretKey: os.Getenv("AWS_S3_SECRET_KEY"),
|
||||
Endpoint: os.Getenv("AWS_S3_ENDPOINT"),
|
||||
Region: os.Getenv("AWS_S3_REGION"),
|
||||
},
|
||||
args: args{
|
||||
UploadInput{
|
||||
Bucket: os.Getenv("AWS_S3_BUCKET"),
|
||||
ObjectKey: "xyz/example file%with$special&chars(1)?.txt",
|
||||
ContentType: "text/plain",
|
||||
FileName: "example file%with$special&chars(1)?.txt",
|
||||
Body: testTxtSpecialChars,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, testcase := range tests {
|
||||
tt := testcase
|
||||
t.Run(tt.name+"_post", func(t *testing.T) {
|
||||
s3 := New(tt.fields.Region, tt.fields.AccessKey, tt.fields.SecretKey)
|
||||
s3.SetEndpoint(tt.fields.Endpoint)
|
||||
|
||||
resp, err := s3.FileUpload(tt.args.u)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("S3.FileUpload() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
// reset file, to reuse in further tests.
|
||||
tt.args.u.Body.Seek(0, 0)
|
||||
|
||||
// check for empty response
|
||||
if (resp == UploadResponse{}) {
|
||||
t.Errorf("S3.FileUpload() returned empty response, %v", resp)
|
||||
}
|
||||
|
||||
if tt.testDetails {
|
||||
dResp, err := s3.FileDetails(DetailsInput{
|
||||
Bucket: tt.args.u.Bucket,
|
||||
ObjectKey: tt.args.u.ObjectKey,
|
||||
})
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("S3.FileDetails() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
if len(dResp.AmzMeta) != len(tt.args.u.CustomMetadata) {
|
||||
t.Errorf("S3.FileDetails() returned incorrect metadata, got: %#v", dResp)
|
||||
}
|
||||
}
|
||||
})
|
||||
t.Run(tt.name+"_put", func(t *testing.T) {
|
||||
s3 := New(tt.fields.Region, tt.fields.AccessKey, tt.fields.SecretKey)
|
||||
s3.SetEndpoint(tt.fields.Endpoint)
|
||||
|
||||
resp, err := s3.FilePut(tt.args.u)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("S3.FileUpload() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
// reset file, to reuse in further tests.
|
||||
tt.args.u.Body.Seek(0, 0)
|
||||
|
||||
// check for empty response
|
||||
if resp.ETag == "" {
|
||||
t.Errorf("S3.FileUpload() returned empty response, %v", resp)
|
||||
}
|
||||
|
||||
if tt.testDetails {
|
||||
dResp, err := s3.FileDetails(DetailsInput{
|
||||
Bucket: tt.args.u.Bucket,
|
||||
ObjectKey: tt.args.u.ObjectKey,
|
||||
})
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("S3.FileUpload() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
if len(dResp.AmzMeta) != len(tt.args.u.CustomMetadata) {
|
||||
t.Errorf("S3.FileDetails() returned incorrect metadata, got: %#v", dResp)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestS3_FileDownload(t *testing.T) {
|
||||
testTxt, err := os.Open("testdata/test.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer testTxt.Close()
|
||||
testTxtData, err := io.ReadAll(testTxt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testPng, err := os.Open("testdata/avatar.png")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer testPng.Close()
|
||||
testPngData, err := io.ReadAll(testPng)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
type args struct {
|
||||
u DownloadInput
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields tConfig
|
||||
args args
|
||||
wantErr bool
|
||||
wantResponse []byte
|
||||
}{
|
||||
{
|
||||
name: "txt",
|
||||
fields: tConfig{
|
||||
AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"),
|
||||
SecretKey: os.Getenv("AWS_S3_SECRET_KEY"),
|
||||
Endpoint: os.Getenv("AWS_S3_ENDPOINT"),
|
||||
Region: os.Getenv("AWS_S3_REGION"),
|
||||
},
|
||||
args: args{
|
||||
u: DownloadInput{
|
||||
Bucket: os.Getenv("AWS_S3_BUCKET"),
|
||||
ObjectKey: "test.txt",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantResponse: testTxtData,
|
||||
},
|
||||
{
|
||||
name: "png",
|
||||
fields: tConfig{
|
||||
AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"),
|
||||
SecretKey: os.Getenv("AWS_S3_SECRET_KEY"),
|
||||
Endpoint: os.Getenv("AWS_S3_ENDPOINT"),
|
||||
Region: os.Getenv("AWS_S3_REGION"),
|
||||
},
|
||||
args: args{
|
||||
u: DownloadInput{
|
||||
Bucket: os.Getenv("AWS_S3_BUCKET"),
|
||||
ObjectKey: "xyz/image.png",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantResponse: testPngData,
|
||||
},
|
||||
{
|
||||
name: "txt-special-filename",
|
||||
fields: tConfig{
|
||||
AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"),
|
||||
SecretKey: os.Getenv("AWS_S3_SECRET_KEY"),
|
||||
Endpoint: os.Getenv("AWS_S3_ENDPOINT"),
|
||||
Region: os.Getenv("AWS_S3_REGION"),
|
||||
},
|
||||
args: args{
|
||||
u: DownloadInput{
|
||||
Bucket: os.Getenv("AWS_S3_BUCKET"),
|
||||
ObjectKey: "xyz/example file%with$special&chars(1)?.txt",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantResponse: testTxtData,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range tests {
|
||||
tt := testcase
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s3 := New(tt.fields.Region, tt.fields.AccessKey, tt.fields.SecretKey)
|
||||
s3.SetEndpoint(tt.fields.Endpoint)
|
||||
resp, err := s3.FileDownload(tt.args.u)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Fatalf("S3.FileDownload() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
got, err := io.ReadAll(resp)
|
||||
if err != nil {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
|
||||
resp.Close()
|
||||
|
||||
if !bytes.Equal(got, tt.wantResponse) {
|
||||
t.Fatalf("S3.FileDownload() = %v, want %v", got, tt.wantResponse)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestS3_FileDelete(t *testing.T) {
|
||||
type args struct {
|
||||
u DeleteInput
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields tConfig
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Delete test.txt",
|
||||
fields: tConfig{
|
||||
AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"),
|
||||
SecretKey: os.Getenv("AWS_S3_SECRET_KEY"),
|
||||
Endpoint: os.Getenv("AWS_S3_ENDPOINT"),
|
||||
Region: os.Getenv("AWS_S3_REGION"),
|
||||
},
|
||||
args: args{
|
||||
DeleteInput{
|
||||
Bucket: os.Getenv("AWS_S3_BUCKET"),
|
||||
ObjectKey: "test.txt",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Delete test_metadata.txt",
|
||||
fields: tConfig{
|
||||
AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"),
|
||||
SecretKey: os.Getenv("AWS_S3_SECRET_KEY"),
|
||||
Endpoint: os.Getenv("AWS_S3_ENDPOINT"),
|
||||
Region: os.Getenv("AWS_S3_REGION"),
|
||||
},
|
||||
args: args{
|
||||
DeleteInput{
|
||||
Bucket: os.Getenv("AWS_S3_BUCKET"),
|
||||
ObjectKey: "test_metadata.txt",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Delete avatar.png",
|
||||
fields: tConfig{
|
||||
AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"),
|
||||
SecretKey: os.Getenv("AWS_S3_SECRET_KEY"),
|
||||
Endpoint: os.Getenv("AWS_S3_ENDPOINT"),
|
||||
Region: os.Getenv("AWS_S3_REGION"),
|
||||
},
|
||||
args: args{
|
||||
DeleteInput{
|
||||
Bucket: os.Getenv("AWS_S3_BUCKET"),
|
||||
ObjectKey: "xyz/image.png",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Delete special filename txt",
|
||||
fields: tConfig{
|
||||
AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"),
|
||||
SecretKey: os.Getenv("AWS_S3_SECRET_KEY"),
|
||||
Endpoint: os.Getenv("AWS_S3_ENDPOINT"),
|
||||
Region: os.Getenv("AWS_S3_REGION"),
|
||||
},
|
||||
args: args{
|
||||
DeleteInput{
|
||||
Bucket: os.Getenv("AWS_S3_BUCKET"),
|
||||
ObjectKey: "xyz/example file%with$special&chars(1)?.txt",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, testcase := range tests {
|
||||
tt := testcase
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s3 := New(tt.fields.Region, tt.fields.AccessKey, tt.fields.SecretKey)
|
||||
s3.SetEndpoint(tt.fields.Endpoint)
|
||||
if err := s3.FileDelete(tt.args.u); (err != nil) != tt.wantErr {
|
||||
t.Errorf("S3.FileDelete() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestS3_NewUsingIAM(t *testing.T) {
|
||||
var (
|
||||
iam = `test-new-s3-using-iam`
|
||||
resp = `{"Code" : "Success","LastUpdated" : "2018-12-24T10:18:01Z",
|
||||
"Type" : "AWS-HMAC","AccessKeyId" : "abc",
|
||||
"SecretAccessKey" : "abc","Token" : "abc",
|
||||
"Expiration" : "2018-12-24T16:24:59Z"}`
|
||||
respIMDSToken = `AQAEAJWopi8yvjKYXyWJbzESE0cms-OoTnptJzS3M9g5iNcl06UEkQ==`
|
||||
)
|
||||
|
||||
tsFail := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(2 * time.Second)
|
||||
}))
|
||||
defer tsFail.Close()
|
||||
|
||||
genServerHandlerFunc := func(failIMDS bool) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
if !failIMDS {
|
||||
// check if token is present
|
||||
if r.Header.Get(imdsTokenHeader) == "" {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
url := securityCredentialsURI
|
||||
if r.URL.EscapedPath() == url {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
io.WriteString(w, iam)
|
||||
}
|
||||
if r.URL.EscapedPath() == url+iam {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
io.WriteString(w, resp)
|
||||
}
|
||||
case "PUT":
|
||||
if failIMDS {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if r.URL.EscapedPath() == imdsTokenURI {
|
||||
if r.Header.Get(imdsTokenTtlHeader) != "60" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
io.WriteString(w, respIMDSToken)
|
||||
}
|
||||
default:
|
||||
t.Errorf("Expected 'GET' or 'PUT' request, got '%s'", r.Method)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(genServerHandlerFunc(false)))
|
||||
defer ts.Close()
|
||||
|
||||
tsFailIMDS := httptest.NewServer(http.HandlerFunc(genServerHandlerFunc(true)))
|
||||
defer tsFailIMDS.Close()
|
||||
|
||||
cl := &http.Client{Timeout: 1 * time.Second}
|
||||
|
||||
// Test for timeout.
|
||||
_, err := newUsingIAM(cl, tsFail.URL, "abc")
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got nil")
|
||||
} else {
|
||||
var timeoutError net.Error
|
||||
|
||||
if errors.As(err, &timeoutError) && !timeoutError.Timeout() {
|
||||
t.Errorf("newUsingIAM() timeout check. got error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test for successful IAM fetch.
|
||||
s3, err := newUsingIAM(cl, ts.URL, "abc")
|
||||
if err != nil {
|
||||
t.Errorf("newUsingIAM() error = %v", err)
|
||||
}
|
||||
|
||||
if s3 == nil {
|
||||
t.Errorf("newUsingIAM() got = %v", s3)
|
||||
}
|
||||
|
||||
if s3.AccessKey != "abc" || s3.SecretKey != "abc" || s3.Region != "abc" {
|
||||
t.Errorf("S3.FileDelete() got = %v", s3)
|
||||
}
|
||||
|
||||
// Test for failed IMDS token fetch.
|
||||
_, err = newUsingIAM(cl, tsFailIMDS.URL, "abc")
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListAll(t *testing.T) {
|
||||
// Setup test environment
|
||||
s3 := New(
|
||||
os.Getenv("AWS_S3_REGION"),
|
||||
os.Getenv("AWS_S3_ACCESS_KEY"),
|
||||
os.Getenv("AWS_S3_SECRET_KEY"),
|
||||
)
|
||||
s3.SetEndpoint(os.Getenv("AWS_S3_ENDPOINT"))
|
||||
|
||||
// Test cases
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func() // Setup function to prepare test data
|
||||
input ListInput
|
||||
validate func([]Object, error)
|
||||
cleanup func() // Cleanup function
|
||||
}{
|
||||
{
|
||||
name: "Empty Bucket Iterator",
|
||||
input: ListInput{
|
||||
Bucket: os.Getenv("AWS_S3_BUCKET"),
|
||||
},
|
||||
validate: func(objects []Object, err error) {
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
if len(objects) != 0 {
|
||||
t.Errorf("Expected empty bucket, got %d objects", len(objects))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Basic Iterator Listing",
|
||||
setup: func() {
|
||||
// Upload test files
|
||||
uploadTestFiles(t, s3, os.Getenv("AWS_S3_BUCKET"), []string{"iter_file1.txt", "iter_file2.txt", "iter_file3.txt"})
|
||||
},
|
||||
input: ListInput{
|
||||
Bucket: os.Getenv("AWS_S3_BUCKET"),
|
||||
},
|
||||
validate: func(objects []Object, err error) {
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
if len(objects) != 3 {
|
||||
t.Errorf("Expected 3 objects, got %d", len(objects))
|
||||
}
|
||||
// Check that all expected files are present
|
||||
expectedKeys := map[string]bool{
|
||||
"iter_file1.txt": false,
|
||||
"iter_file2.txt": false,
|
||||
"iter_file3.txt": false,
|
||||
}
|
||||
for _, obj := range objects {
|
||||
if _, exists := expectedKeys[obj.Key]; exists {
|
||||
expectedKeys[obj.Key] = true
|
||||
}
|
||||
}
|
||||
for key, found := range expectedKeys {
|
||||
if !found {
|
||||
t.Errorf("Expected object %s not found in iterator results", key)
|
||||
}
|
||||
}
|
||||
},
|
||||
cleanup: func() {
|
||||
cleanupTestFiles(t, s3, os.Getenv("AWS_S3_BUCKET"), []string{"iter_file1.txt", "iter_file2.txt", "iter_file3.txt"})
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Iterator with Prefix",
|
||||
setup: func() {
|
||||
uploadTestFiles(t, s3, os.Getenv("AWS_S3_BUCKET"), []string{
|
||||
"iter_docs/file1.txt",
|
||||
"iter_docs/file2.txt",
|
||||
"iter_images/image1.jpg",
|
||||
"iter_images/image2.jpg",
|
||||
})
|
||||
},
|
||||
input: ListInput{
|
||||
Bucket: os.Getenv("AWS_S3_BUCKET"),
|
||||
Prefix: "iter_docs/",
|
||||
},
|
||||
validate: func(objects []Object, err error) {
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
if len(objects) != 2 {
|
||||
t.Errorf("Expected 2 objects with iter_docs/ prefix, got %d", len(objects))
|
||||
}
|
||||
for _, obj := range objects {
|
||||
if !strings.HasPrefix(obj.Key, "iter_docs/") {
|
||||
t.Errorf("Object %s doesn't have expected prefix", obj.Key)
|
||||
}
|
||||
}
|
||||
},
|
||||
cleanup: func() {
|
||||
cleanupTestFiles(t, s3, os.Getenv("AWS_S3_BUCKET"), []string{
|
||||
"iter_docs/file1.txt",
|
||||
"iter_docs/file2.txt",
|
||||
"iter_images/image1.jpg",
|
||||
"iter_images/image2.jpg",
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.setup != nil {
|
||||
tt.setup()
|
||||
}
|
||||
|
||||
// Collect all objects from the iterator and check for errors
|
||||
var objects []Object
|
||||
seq, finish := s3.ListAll(tt.input)
|
||||
for obj := range seq {
|
||||
objects = append(objects, obj)
|
||||
}
|
||||
|
||||
// Check for any iteration errors
|
||||
if err := finish(); err != nil {
|
||||
t.Fatalf("Iterator error: %v", err)
|
||||
}
|
||||
|
||||
if tt.validate != nil {
|
||||
tt.validate(objects, nil) // No error from iterator
|
||||
}
|
||||
|
||||
if tt.cleanup != nil {
|
||||
tt.cleanup()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomEndpoint(t *testing.T) {
|
||||
s3 := New("us-east-1", "AccessKey", "SuperSecretKey")
|
||||
|
||||
@@ -705,184 +89,6 @@ func TestGetURL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
// Setup test environment
|
||||
s3 := New(
|
||||
os.Getenv("AWS_S3_REGION"),
|
||||
os.Getenv("AWS_S3_ACCESS_KEY"),
|
||||
os.Getenv("AWS_S3_SECRET_KEY"),
|
||||
)
|
||||
s3.SetEndpoint(os.Getenv("AWS_S3_ENDPOINT"))
|
||||
|
||||
// Test cases
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func() // Setup function to prepare test data
|
||||
input ListInput
|
||||
validate func(ListResponse, error)
|
||||
cleanup func() // Cleanup function
|
||||
}{
|
||||
{
|
||||
name: "Empty Bucket",
|
||||
input: ListInput{
|
||||
Bucket: os.Getenv("AWS_S3_BUCKET"),
|
||||
},
|
||||
validate: func(result ListResponse, err error) {
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
if len(result.Objects) != 0 {
|
||||
t.Errorf("Expected empty bucket, got %d objects", len(result.Objects))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Basic Listing",
|
||||
setup: func() {
|
||||
// Upload test files
|
||||
uploadTestFiles(t, s3, os.Getenv("AWS_S3_BUCKET"), []string{"file1.txt", "file2.txt", "file3.txt"})
|
||||
},
|
||||
input: ListInput{
|
||||
Bucket: os.Getenv("AWS_S3_BUCKET"),
|
||||
},
|
||||
validate: func(result ListResponse, err error) {
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
if len(result.Objects) != 3 {
|
||||
t.Errorf("Expected 3 objects, got %d", len(result.Objects))
|
||||
}
|
||||
},
|
||||
cleanup: func() {
|
||||
cleanupTestFiles(t, s3, os.Getenv("AWS_S3_BUCKET"), []string{"file1.txt", "file2.txt", "file3.txt"})
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Pagination Test",
|
||||
setup: func() {
|
||||
// Upload many files for pagination testing
|
||||
var filenames []string
|
||||
for i := 1; i <= 25; i++ {
|
||||
filenames = append(filenames, fmt.Sprintf("file%03d.txt", i))
|
||||
}
|
||||
uploadTestFiles(t, s3, os.Getenv("AWS_S3_BUCKET"), filenames)
|
||||
},
|
||||
input: ListInput{
|
||||
Bucket: os.Getenv("AWS_S3_BUCKET"),
|
||||
MaxKeys: 10,
|
||||
},
|
||||
validate: func(result ListResponse, err error) {
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
if len(result.Objects) != 10 {
|
||||
t.Errorf("Expected 10 objects, got %d", len(result.Objects))
|
||||
}
|
||||
if !result.IsTruncated {
|
||||
t.Errorf("Expected IsTruncated=true for pagination")
|
||||
}
|
||||
if result.NextContinuationToken == "" {
|
||||
t.Errorf("Expected NextContinuationToken for pagination: %v", result.NextContinuationToken)
|
||||
}
|
||||
},
|
||||
cleanup: func() {
|
||||
var filenames []string
|
||||
for i := 1; i <= 25; i++ {
|
||||
filenames = append(filenames, fmt.Sprintf("file%03d.txt", i))
|
||||
}
|
||||
cleanupTestFiles(t, s3, os.Getenv("AWS_S3_BUCKET"), filenames)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Prefix Filtering",
|
||||
setup: func() {
|
||||
uploadTestFiles(t, s3, os.Getenv("AWS_S3_BUCKET"), []string{
|
||||
"documents/file1.txt",
|
||||
"documents/file2.txt",
|
||||
"images/image1.jpg",
|
||||
"images/image2.jpg",
|
||||
})
|
||||
},
|
||||
input: ListInput{
|
||||
Bucket: os.Getenv("AWS_S3_BUCKET"),
|
||||
Prefix: "documents/",
|
||||
},
|
||||
validate: func(result ListResponse, err error) {
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
if len(result.Objects) != 2 {
|
||||
t.Errorf("Expected 2 objects with documents/ prefix, got %d", len(result.Objects))
|
||||
}
|
||||
for _, obj := range result.Objects {
|
||||
if !strings.HasPrefix(obj.Key, "documents/") {
|
||||
t.Errorf("Object %s doesn't have expected prefix", obj.Key)
|
||||
}
|
||||
}
|
||||
},
|
||||
cleanup: func() {
|
||||
cleanupTestFiles(t, s3, os.Getenv("AWS_S3_BUCKET"), []string{
|
||||
"documents/file1.txt",
|
||||
"documents/file2.txt",
|
||||
"images/image1.jpg",
|
||||
"images/image2.jpg",
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Delimiter Grouping",
|
||||
setup: func() {
|
||||
uploadTestFiles(t, s3, os.Getenv("AWS_S3_BUCKET"), []string{
|
||||
"documents/important/file1.txt",
|
||||
"documents/backup/file2.txt",
|
||||
"images/landscape/image1.jpg",
|
||||
"images/portrait/image2.jpg",
|
||||
})
|
||||
},
|
||||
input: ListInput{
|
||||
Bucket: os.Getenv("AWS_S3_BUCKET"),
|
||||
Delimiter: "/",
|
||||
Prefix: "documents/",
|
||||
},
|
||||
validate: func(result ListResponse, err error) {
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
// Should return common prefixes instead of individual files
|
||||
if len(result.CommonPrefixes) != 2 {
|
||||
t.Errorf("Expected 2 common prefixes, got %d", len(result.CommonPrefixes))
|
||||
}
|
||||
},
|
||||
cleanup: func() {
|
||||
cleanupTestFiles(t, s3, os.Getenv("AWS_S3_BUCKET"), []string{
|
||||
"documents/important/file1.txt",
|
||||
"documents/backup/file2.txt",
|
||||
"images/landscape/image1.jpg",
|
||||
"images/portrait/image2.jpg",
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.setup != nil {
|
||||
tt.setup()
|
||||
}
|
||||
|
||||
result, err := s3.List(tt.input)
|
||||
|
||||
if tt.validate != nil {
|
||||
tt.validate(result, err)
|
||||
}
|
||||
|
||||
if tt.cleanup != nil {
|
||||
tt.cleanup()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
func uploadTestFiles(t *testing.T, s3 *S3, bucket string, filenames []string) {
|
||||
for _, filename := range filenames {
|
||||
|
||||
Reference in New Issue
Block a user