1

Im using s3's GetObjectRequest API to get the Request and use it to call Presign API.

type S3Client struct {
    client s3iface.S3API
}

type S3API interface {
    PutObject(input *s3.PutObjectInput) (*s3.PutObjectOutput, error)
    GetObjectRequest(*s3.GetObjectInput) (*request.Request, *s3.GetObjectOutput)
}

type Request interface {
    Presign(expire time.Duration) (string, error)
}

func NewS3Client() S3Client {
    awsConfig := awstools.AWS{Config: &aws.Config{Region: aws.String("us-west-2")}}
    awsSession, _ := awsConfig.Get()
    return S3Client{
        client: s3.New(awsSession),
    }
}

func (s *S3Client) GetPreSignedUrl(bucket string, objectKey string) (string, error) {

    req, _ := s.client.GetObjectRequest(&s3.GetObjectInput{
        Bucket: aws.String(bucket),
        Key:    aws.String(objectKey),
    })

    urlStr, err := req.Presign(30 * 24 * time.Hour)
    if err != nil {
        return "", err
    }

    return urlStr, nil
}

Wondering how I can write unit tests for this snippet. So far I have the following but its not working. Would appreciate some help with this.

type MockRequestImpl struct {
    request.Request
}

func (m *MockRequestImpl) Presign(input time.Duration) (string, error) {
    return preSignFunc(input)
}

type MockS3Client struct {
    s3iface.S3API
}

func init() {
    s = S3Client{
        client: &MockS3Client{},
    }
}

func (m *MockS3Client) GetObjectRequest(input *s3.GetObjectInput) (*request.Request, *s3.GetObjectOutput) {
    return getObjectFunc(input)
}

func TestS3Service_GetPreSignedUrl(t *testing.T) {
    t.Run("should not throw error", func(t *testing.T) {
        getObjectFunc = func(input *s3.GetObjectInput) (*request.Request, *s3.GetObjectOutput) {
            m := MockRequestImpl{}.Request
            return &m, &s3.GetObjectOutput{}
        }
        preSignFunc = func(expire time.Duration) (string, error) {
            return "preSigned", nil
        }

        url, err := s.GetPreSignedUrl("bucket", "objectKey")
        assert.Equal(t, "preSigned", url)
        assert.NoError(t, err)
    })
}

Getting the following error : === RUN TestS3Service_GetPreSignedUrl === RUN TestS3Service_GetPreSignedUrl/should_not_throw_error --- FAIL: TestS3Service_GetPreSignedUrl (0.00s) --- FAIL: TestS3Service_GetPreSignedUrl/should_not_throw_error (0.00s) panic: runtime error: invalid memory address or nil pointer dereference [recovered] panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x2 addr=0x0 pc=0x102ca1eb4] For line urlStr, err := req.Presign(30 * 24 * time.Hour). Guessing req is returned as nil

rsundhar
  • 115
  • 5
  • Try to mode preSignFunc and getObjectFunc into MockS3Client as fields. And don't use init for MockS3Client initialisation, just create mock inside test. – thrownew Feb 27 '23 at 05:03

1 Answers1

1

This is how I was able to successfully mock GetObjectRequest and Presign.

First let's set up our types.

// client.go

type (
    // AWSClient is the main client that wraps our AWS s3 client
    AWSClient struct {
        s3Client            s3iface.S3API
        objectRequestClient ObjectRequester
    }

    // AWSClientConfig is the configuration for our client
    AWSClientConfig struct {
        Endpoint string `mapstructure:"endpoint"`
    }

    // ObjectRequester is our custom implementation of the S3 API
    ObjectRequester interface {
        getObjectRequest(bucket, key, filename string) Presignable
    }

    // ObjectRequestClient is the concrete version of the above, and will use the normal S3 API
    ObjectRequestClient struct {
        s3Client s3iface.S3API
    }

    // Presignable is an interface that will allow us to use both request.Request, as well as whatever other struct
    // we want that implements Presign.
    Presignable interface {
        Presign(time time.Duration) (string, error)
    }
)

Now let's create a function that creates our custom S3 client.

// client.go

// NewS3Client creates a client allowing interaction with an s3 bucket.
func NewS3Client(c AWSClientConfig) *AWSClient {
    cfg := aws.NewConfig().WithRegion("us-east-1")
    if c.Endpoint != "" {
        cfg = &aws.Config{
            Region:           cfg.Region,
            Endpoint:         aws.String(c.Endpoint),
            S3ForcePathStyle: aws.Bool(true),
        }
    }
    s := s3.New(session.Must(session.NewSession(cfg)))
    return &AWSClient{s3Client: s, objectRequestClient: &ObjectRequestClient{s3Client: s}}
}

As you can probably tell at this point, when we make an AWSClient for testing, we're going to replace the objectRequestClient with a mock objectRequestClient that will do what we want.

Now let's create our functions that will handle creating a presigned URL to an s3 resource.

// client.go

// GetPresignedUrl creates a presigned url to an s3 object.
func (s *AWSClient) GetPresignedUrl(bucket, key, filename string) (string, error) {
    r := s.objectRequestClient.getObjectRequest(bucket, key, filename)
    return r.Presign(15 * time.Minute)
}

// getObjectRequest encapsulates GetObjectRequest so that it can be adapted for testing.
// Returning an interface here looks dumb, but it lets us return an object that is not
// *request.Request in the mock of this function so that we can call something.Presign()
// without a connection to AWS.
func (s *ObjectRequestClient) getObjectRequest(bucket, key, filename string) Presignable {
    req, _ := s.s3Client.GetObjectRequest(&s3.GetObjectInput{
        Bucket:                     aws.String(bucket),
        Key:                        aws.String(key),
        ResponseContentDisposition: aws.String(fmt.Sprintf("attachment; filename=\"%s\"", filename)),
    })
    return req
}

The trick here is that we're going to create our own ObjectRequestsClient that will implement getObjectRequest in a way we want it to and return something other than a request.Request because that needs AWS. It will however return something that looks like a request.Request in all the ways that matter which for this case, is conforming to the Presignable interface.

Now testing should be pretty straight forward. I'm using "github.com/stretchr/testify/mock" to make mocking our aws interactions simple. I wrote this very simple test to validate that we can run the function without any AWS interaction.

// client_test.go

type (
    MockS3Client struct {
        s3iface.S3API
        mock.Mock
    }

    MockObjectRequestClient struct {
        mock.Mock
    }

    MockPresignable struct {
        mock.Mock
    }
)

func (m *MockPresignable) Presign(time time.Duration) (string, error) {
    args := m.Called(time)
    return args.String(0), args.Error(1)
}

func (m *MockObjectRequestClient) getObjectRequest(bucket, key, filename string) Presignable {
    args := m.Called(bucket, key, filename)
    return args.Get(0).(Presignable)
}

func TestGetPresignedUrl(t *testing.T) {
    bucket := "bucket"
    key := "key"
    name := "spike"
    url := "https://test.com"
    m := MockS3Client{}
    mp := MockPresignable{}
    mo := MockObjectRequestClient{}

    mo.On("getObjectRequest", bucket, key, name).Return(&mp)
    mp.On("Presign", 15*time.Minute).Return(url, nil)

    client := AWSClient{s3Client: &m, objectRequestClient: &mo}

    returnedUrl, err := client.GetPresignedUrl(bucket, key, name)
    assert.NoError(t, err)
    assert.Equal(t, url, returnedUrl)

}
BubbleNet
  • 13
  • 3