913 lines
22 KiB
Go
913 lines
22 KiB
Go
package simples3
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type tConfig struct {
|
|
AccessKey string
|
|
SecretKey string
|
|
Endpoint string
|
|
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")
|
|
|
|
// no protocol specified, should default to https
|
|
s3.SetEndpoint("example.com")
|
|
if s3.getURL("bucket1") != "https://example.com/bucket1" {
|
|
t.Errorf("S3.SetEndpoint() got = %v", s3.Endpoint)
|
|
}
|
|
|
|
// explicit http protocol
|
|
s3.SetEndpoint("http://localhost:9000")
|
|
if s3.getURL("bucket2") != "http://localhost:9000/bucket2" {
|
|
t.Errorf("S3.SetEndpoint() got = %v", s3.Endpoint)
|
|
}
|
|
|
|
// explicit http protocol
|
|
s3.SetEndpoint("https://example.com")
|
|
if s3.getURL("bucket3") != "https://example.com/bucket3" {
|
|
t.Errorf("S3.SetEndpoint() got = %v", s3.Endpoint)
|
|
}
|
|
|
|
// try with trailing slash
|
|
s3.SetEndpoint("https://example.com/foobar/")
|
|
if s3.getURL("bucket4") != "https://example.com/foobar/bucket4" {
|
|
t.Errorf("S3.SetEndpoint() got = %v", s3.Endpoint)
|
|
}
|
|
}
|
|
|
|
func TestGetURL(t *testing.T) {
|
|
s3 := New("us-east-1", "AccessKey", "SuperSecretKey")
|
|
|
|
type args struct {
|
|
bucket string
|
|
params []string
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want string
|
|
}{
|
|
{
|
|
name: "getURL: basic test",
|
|
args: args{
|
|
bucket: "xyz",
|
|
},
|
|
want: "https://s3.us-east-1.amazonaws.com/xyz",
|
|
},
|
|
{
|
|
name: "getURL: multiple parameters",
|
|
args: args{
|
|
bucket: "xyz",
|
|
params: []string{"hello", "world"},
|
|
},
|
|
want: "https://s3.us-east-1.amazonaws.com/xyz/hello/world",
|
|
},
|
|
{
|
|
name: "getURL: special characters",
|
|
args: args{
|
|
bucket: "xyz",
|
|
params: []string{"hello, world!", "#!@$%^&*(1).txt"},
|
|
},
|
|
want: "https://s3.us-east-1.amazonaws.com/xyz/hello%2C%20world%21/%23%21%40%24%25%5E%26%2A%281%29.txt",
|
|
},
|
|
}
|
|
|
|
for _, testcase := range tests {
|
|
tt := testcase
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
url := s3.getURL(tt.args.bucket, tt.args.params...)
|
|
if url != tt.want {
|
|
t.Errorf("S3.getURL() got = %v, want %v", url, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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 {
|
|
content := strings.NewReader("test content for " + filename)
|
|
_, err := s3.FilePut(UploadInput{
|
|
Bucket: bucket,
|
|
ObjectKey: filename,
|
|
ContentType: "text/plain",
|
|
Body: content,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to upload test file %s: %v", filename, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func cleanupTestFiles(t *testing.T, s3 *S3, bucket string, filenames []string) {
|
|
for _, filename := range filenames {
|
|
err := s3.FileDelete(DeleteInput{
|
|
Bucket: bucket,
|
|
ObjectKey: filename,
|
|
})
|
|
if err != nil {
|
|
t.Logf("Failed to cleanup test file %s: %v", filename, err)
|
|
}
|
|
}
|
|
}
|