单元测试 S3 PreSigned API

huangapple go评论83阅读模式
英文:

Unit testing S3 PreSigned API

问题

我正在使用S3的GetObjectRequest API来获取Request,并使用它调用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
}

我想知道如何为这段代码编写单元测试。到目前为止,我有以下代码,但它不起作用。希望能得到一些帮助。

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)
	})
}

出现以下错误:
=== 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]
对于代码中的urlStr, err := req.Presign(30 * 24 * time.Hour)这一行,猜测req返回为nil

英文:

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

答案1

得分: 1

这是我成功模拟GetObjectRequestPresign的方法。

首先,让我们设置我们的类型。

// client.go

type (
	// AWSClient是包装我们的AWS S3客户端的主要客户端
	AWSClient struct {
		s3Client            s3iface.S3API
		objectRequestClient ObjectRequester
	}

	// AWSClientConfig是我们客户端的配置
	AWSClientConfig struct {
		Endpoint string `mapstructure:"endpoint"`
	}

	// ObjectRequester是我们自定义的S3 API实现
	ObjectRequester interface {
		getObjectRequest(bucket, key, filename string) Presignable
	}

	// ObjectRequestClient是上述接口的具体版本,将使用正常的S3 API
	ObjectRequestClient struct {
		s3Client s3iface.S3API
	}

	// Presignable是一个接口,允许我们同时使用request.Request和其他实现Presign的结构体
	Presignable interface {
		Presign(time time.Duration) (string, error)
	}
)

现在让我们创建一个函数来创建我们的自定义S3客户端。

// client.go

// NewS3Client创建一个允许与S3存储桶交互的客户端。
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}}
}

从这一点上你可能可以看出,当我们为测试创建一个AWSClient时,我们将用一个模拟的objectRequestClient替换它,以便它可以按我们的要求执行操作。

现在让我们创建处理创建指向S3资源的预签名URL的函数。

// client.go

// GetPresignedUrl创建一个指向S3对象的预签名URL。
func (s *AWSClient) GetPresignedUrl(bucket, key, filename string) (string, error) {
	r := s.objectRequestClient.getObjectRequest(bucket, key, filename)
	return r.Presign(15 * time.Minute)
}

// getObjectRequest封装了GetObjectRequest,以便可以适应测试。
// 在这里返回一个接口看起来很愚蠢,但它允许我们返回一个不是
// *request.Request的对象,以便我们可以调用something.Presign()
// 而不需要连接到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
}

这里的技巧是,我们将创建我们自己的ObjectRequestsClient,它将以我们想要的方式实现getObjectRequest并返回一个不是request.Request的东西,因为它需要AWS。但是,它将返回在所有重要方面看起来像request.Request的东西,对于这种情况来说,符合Presignable接口。

现在测试应该非常简单。我使用了"github.com/stretchr/testify/mock"来简化模拟我们的AWS交互。我编写了这个非常简单的测试来验证我们可以在没有任何AWS交互的情况下运行该函数。

// 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)

}
英文:

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)

}

huangapple
  • 本文由 发表于 2023年2月27日 04:14:23
  • 转载请务必保留本文链接:https://go.coder-hub.com/75574755.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定