diff --git a/go.mod b/go.mod index 93dc1b9..a1f70bb 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( require ( github.com/alexflint/go-scalar v1.1.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.0 // indirect github.com/google/uuid v1.1.1 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect @@ -24,11 +25,14 @@ require ( github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rs/xid v1.2.1 // indirect github.com/smartystreets/assertions v1.13.0 // indirect + github.com/stretchr/testify v1.7.1 // indirect golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect golang.org/x/text v0.3.3 // indirect gopkg.in/ini.v1 v1.57.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect ) diff --git a/go.sum b/go.sum index a84d438..14343ba 100644 --- a/go.sum +++ b/go.sum @@ -52,6 +52,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= diff --git a/internal/client/client_test.go b/internal/client/client_test.go new file mode 100644 index 0000000..5e00b34 --- /dev/null +++ b/internal/client/client_test.go @@ -0,0 +1,143 @@ +package client_test + +import ( + "context" + "io/ioutil" + "testing" + + "git.kapelle.org/niklas/s3share/internal/client" + "git.kapelle.org/niklas/s3share/internal/db" + "git.kapelle.org/niklas/s3share/internal/s3" + "git.kapelle.org/niklas/s3share/internal/types" + "github.com/stretchr/testify/assert" +) + +func setup(t *testing.T) (*client.Client, context.Context, *assert.Assertions) { + + mockDb := db.NewMock() + mockS3 := s3.NewMockS3() + + service := client.NewClient(mockDb, mockS3) + ctx := context.Background() + assert := assert.New(t) + + return service, ctx, assert +} + +func TestCreateShare(t *testing.T) { + service, ctx, assert := setup(t) + + share, err := service.CreateShare(ctx, "test.txt") + assert.NoError(err) + assert.NotNil(share) + assert.NotEmpty(share.Slug) + assert.Equal("test.txt", share.Key) +} + +func TestCreateShareInvalidKey(t *testing.T) { + service, ctx, assert := setup(t) + + _, err := service.CreateShare(ctx, "not_existing.txt") + assert.Error(err) +} + +func TestGetShare(t *testing.T) { + service, ctx, assert := setup(t) + + createdShare, err := service.CreateShare(ctx, "test.txt") + + share, err := service.GetShare(ctx, createdShare.Slug) + assert.NoError(err) + assert.NotNil(share) + assert.Equal(createdShare.Slug, share.Slug) + assert.Equal("test.txt", share.Key) +} + +func TestGetShareNotFound(t *testing.T) { + service, ctx, assert := setup(t) + + share, err := service.GetShare(ctx, "123456") + assert.NoError(err) + assert.Nil(share) +} + +func TestGetObjFromShare(t *testing.T) { + service, ctx, assert := setup(t) + + createdShare, _ := service.CreateShare(ctx, "test.txt") + + reader, err := service.GetObjectFromShare(ctx, createdShare) + assert.NoError(err) + assert.NotNil(reader) + + content, err := ioutil.ReadAll(reader) + assert.NoError(err) + assert.Equal("test.txt", string(content)) +} + +func TestGetObjFromShareNotFound(t *testing.T) { + service, ctx, assert := setup(t) + + _, err := service.GetObjectFromShare(ctx, &types.Share{Slug: "123456"}) + assert.Error(err) +} + +func TestDeleteShare(t *testing.T) { + service, ctx, assert := setup(t) + + createdShare, _ := service.CreateShare(ctx, "test.txt") + + err := service.DeleteShare(ctx, createdShare.Slug) + assert.NoError(err) + + share, err := service.GetShare(ctx, createdShare.Slug) + assert.NoError(err) + assert.Nil(share) +} + +func TestDeleteShareNotFound(t *testing.T) { + service, ctx, assert := setup(t) + + err := service.DeleteShare(ctx, "123456") + assert.Error(err) +} + +func TestGetMetadata(t *testing.T) { + service, ctx, assert := setup(t) + + metadata, err := service.GetObjectMetadata(ctx, "test.txt") + assert.NoError(err) + assert.NotNil(metadata) + assert.Equal("test.txt", metadata.Filename) + assert.Equal("text/plain", metadata.ContentType) + assert.Equal(int64(8), metadata.Size) +} + +func TestGetMetadataNotFound(t *testing.T) { + service, ctx, assert := setup(t) + + metadata, err := service.GetObjectMetadata(ctx, "not_existing.txt") + assert.NoError(err) + assert.Nil(metadata) +} + +func TestGetAllShares(t *testing.T) { + service, ctx, assert := setup(t) + + share, err := service.CreateShare(ctx, "test.txt") + assert.NoError(err) + + shares, err := service.GetAllShares(ctx) + assert.NoError(err) + assert.Len(shares, 1) + + assert.Equal(share.Slug, shares[0].Slug) + assert.Equal(share.Key, shares[0].Key) + + _, err = service.CreateShare(ctx, "dir/test") + assert.NoError(err) + + shares, err = service.GetAllShares(ctx) + assert.NoError(err) + assert.Len(shares, 2) +} diff --git a/internal/db/mock.go b/internal/db/mock.go new file mode 100644 index 0000000..7a24b20 --- /dev/null +++ b/internal/db/mock.go @@ -0,0 +1,54 @@ +package db + +import ( + "context" + "errors" + + "git.kapelle.org/niklas/s3share/internal/types" +) + +type mockDB struct { + shares map[string]*types.Share +} + +func NewMock() DB { + return &mockDB{ + shares: make(map[string]*types.Share), + } +} + +func (d *mockDB) Close() error { + return nil +} + +func (d *mockDB) CreateShare(ctx context.Context, share *types.Share) error { + if d.shares[share.Slug] != nil { + return errors.New("share already exists") + } + d.shares[share.Slug] = share + return nil +} + +func (d *mockDB) DeleteShare(ctx context.Context, slug string) error { + if d.shares[slug] == nil { + return errors.New("share does not exist") + } + delete(d.shares, slug) + return nil +} + +func (d *mockDB) GetAllShares(ctx context.Context) ([]*types.Share, error) { + // convert map to slice + shares := make([]*types.Share, 0, len(d.shares)) + for _, share := range d.shares { + shares = append(shares, share) + } + return shares, nil +} + +func (d *mockDB) GetShare(ctx context.Context, slug string) (*types.Share, error) { + if d.shares[slug] == nil { + return nil, nil + } + return d.shares[slug], nil +} diff --git a/internal/db/mock_test.go b/internal/db/mock_test.go new file mode 100644 index 0000000..e0e4d67 --- /dev/null +++ b/internal/db/mock_test.go @@ -0,0 +1,111 @@ +package db_test + +import ( + "context" + "testing" + + "git.kapelle.org/niklas/s3share/internal/db" + "git.kapelle.org/niklas/s3share/internal/types" + "github.com/stretchr/testify/assert" +) + +func setup(t *testing.T) (db.DB, context.Context, *assert.Assertions) { + service := db.NewMock() + ctx := context.Background() + assert := assert.New(t) + + return service, ctx, assert +} + +func TestCreateShare(t *testing.T) { + service, ctx, assert := setup(t) + defer service.Close() + + share := &types.Share{ + Slug: "123456", + Key: "test.txt", + } + + err := service.CreateShare(ctx, share) + assert.NoError(err) + assert.NotNil(service.GetShare(ctx, share.Slug)) +} + +func TestCreateShareDup(t *testing.T) { + service, ctx, assert := setup(t) + defer service.Close() + + share := &types.Share{ + Slug: "123456", + Key: "test.txt", + } + + err := service.CreateShare(ctx, share) + assert.NoError(err) + + err = service.CreateShare(ctx, share) + assert.Error(err) +} + +func TestDeleteShare(t *testing.T) { + service, ctx, assert := setup(t) + defer service.Close() + + share := &types.Share{ + Slug: "123456", + Key: "test.txt", + } + + err := service.CreateShare(ctx, share) + assert.NoError(err) + + err = service.DeleteShare(ctx, share.Slug) + assert.NoError(err) + assert.Nil(service.GetShare(ctx, share.Slug)) +} + +func TestDeleteShareNotFound(t *testing.T) { + service, ctx, assert := setup(t) + defer service.Close() + + share := &types.Share{ + Slug: "123456", + Key: "test.txt", + } + + err := service.DeleteShare(ctx, share.Slug) + assert.Error(err) +} + +func TestGetAllShares(t *testing.T) { + service, ctx, assert := setup(t) + defer service.Close() + + share := &types.Share{ + Slug: "123456", + Key: "test.txt", + } + + err := service.CreateShare(ctx, share) + assert.NoError(err) + + shares, err := service.GetAllShares(ctx) + assert.NoError(err) + assert.Len(shares, 1) + + assert.Equal(share.Slug, shares[0].Slug) + assert.Equal(share.Key, shares[0].Key) + + // Create 2nd share + share2 := &types.Share{ + Slug: "abcdef", + Key: "test2", + } + + err = service.CreateShare(ctx, share2) + assert.NoError(err) + + shares, err = service.GetAllShares(ctx) + assert.NoError(err) + assert.Len(shares, 2) +} diff --git a/internal/s3/mock.go b/internal/s3/mock.go new file mode 100644 index 0000000..4d08ce8 --- /dev/null +++ b/internal/s3/mock.go @@ -0,0 +1,80 @@ +package s3 + +import ( + "bytes" + "context" + "crypto/md5" + "fmt" + + "git.kapelle.org/niklas/s3share/internal/types" +) + +type mockS3 struct { + objects map[string]mockObject +} + +type mockObject struct { + content []byte + contentType string +} + +type mockObjectReader struct { + *bytes.Reader +} + +func (r mockObjectReader) Close() error { + // NOOP + return nil +} + +func NewMockS3() S3 { + return &mockS3{ + objects: map[string]mockObject{ + "test.txt": { + content: []byte("test.txt"), + contentType: "text/plain", + }, + "test.png": { + content: []byte("test.png"), + contentType: "image/png", + }, + "dir/test": { + content: []byte("test"), + contentType: "application/octet-stream", + }, + }, + } +} + +func (m *mockS3) GetObject(ctx context.Context, key string) (ObjectReader, error) { + mockObj, exist := m.objects[key] + + if !exist { + return nil, fmt.Errorf("Object not found") + } + + reader := bytes.NewReader(mockObj.content) + + return mockObjectReader{reader}, nil +} + +func (m *mockS3) GetObjectMetadata(ctx context.Context, key string) (*types.Metadata, error) { + mockObj, exist := m.objects[key] + + if !exist { + return nil, nil + } + + return &types.Metadata{ + Size: int64(len(mockObj.content)), + ETag: fmt.Sprintf("%x", md5.Sum(mockObj.content)), + ContentType: mockObj.contentType, + }, nil + +} + +func (m *mockS3) KeyExists(ctx context.Context, key string) (bool, error) { + _, exist := m.objects[key] + + return exist, nil +} diff --git a/internal/s3/mock_test.go b/internal/s3/mock_test.go new file mode 100644 index 0000000..c45e491 --- /dev/null +++ b/internal/s3/mock_test.go @@ -0,0 +1,72 @@ +package s3_test + +import ( + "context" + "io/ioutil" + "testing" + + "git.kapelle.org/niklas/s3share/internal/s3" + "github.com/stretchr/testify/assert" +) + +func setup(t *testing.T) (s3.S3, context.Context, *assert.Assertions) { + service := s3.NewMockS3() + ctx := context.Background() + assert := assert.New(t) + + return service, ctx, assert +} + +func TestGetObject(t *testing.T) { + service, ctx, assert := setup(t) + + reader, err := service.GetObject(ctx, "test.txt") + assert.NoError(err) + assert.NotNil(reader) + + content, err := ioutil.ReadAll(reader) + assert.NoError(err) + assert.Equal("test.txt", string(content)) +} + +func TestGetObjectNotFound(t *testing.T) { + service, ctx, assert := setup(t) + + reader, err := service.GetObject(ctx, "not_existing.txt") + assert.Error(err) + assert.Nil(reader) +} + +func TestGetMetadata(t *testing.T) { + service, ctx, assert := setup(t) + + metadata, err := service.GetObjectMetadata(ctx, "test.txt") + assert.NoError(err) + assert.NotNil(metadata) + assert.Equal("text/plain", metadata.ContentType) + assert.Equal(int64(8), metadata.Size) +} + +func TestGetMetadataNotFound(t *testing.T) { + service, ctx, assert := setup(t) + + metadata, err := service.GetObjectMetadata(ctx, "not_existing.txt") + assert.NoError(err) + assert.Nil(metadata) +} + +func TestKeyExists(t *testing.T) { + service, ctx, assert := setup(t) + + exists, err := service.KeyExists(ctx, "test.txt") + assert.NoError(err) + assert.True(exists) +} + +func TestKeyNotExist(t *testing.T) { + service, ctx, assert := setup(t) + + exists, err := service.KeyExists(ctx, "not_existing.txt") + assert.NoError(err) + assert.False(exists) +}