Skip to content

Commit

Permalink
refactor with bucket processor and add unit tests
Browse files Browse the repository at this point in the history
refactor

refactor

refactor

process

refactor

unit tests

tests

io mock

refactor

app test

fix

add test

add test

comment

tests
  • Loading branch information
go-to-k committed Feb 2, 2025
1 parent a5dbaf6 commit 535c14f
Show file tree
Hide file tree
Showing 22 changed files with 1,814 additions and 222 deletions.
124 changes: 50 additions & 74 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"
"os"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/go-to-k/cls3/internal/io"
"github.com/go-to-k/cls3/internal/wrapper"
"github.com/go-to-k/cls3/pkg/client"
Expand All @@ -30,6 +29,9 @@ type App struct {
DirectoryBucketsMode bool
TableBucketsMode bool
targetBuckets []string // bucket names for S3, bucket arns for S3Tables
bucketSelector IBucketSelector
bucketProcessor IBucketProcessor
s3Wrapper wrapper.IWrapper
}

func NewApp(version string) *App {
Expand Down Expand Up @@ -138,21 +140,64 @@ func (a *App) getAction() func(c *cli.Context) error {
return err
}

s3Wrapper, err := a.createS3Wrapper(c.Context)
if err != nil {
if err := a.initS3Wrapper(c.Context); err != nil {
return err
}
if err := a.initBucketSelector(); err != nil {
return err
}

continuation, err := a.setTargetBuckets(c.Context, s3Wrapper)
selectedBuckets, continuation, err := a.bucketSelector.SelectBuckets(c.Context)
if err != nil {
return err
}
if !continuation {
return nil
}
a.targetBuckets = append(a.targetBuckets, selectedBuckets...)

if err := a.initBucketProcessor(); err != nil {
return err
}
return a.bucketProcessor.Process(c.Context)
}
}

func (a *App) initS3Wrapper(ctx context.Context) error {
if a.s3Wrapper == nil {
awsConfig, err := client.LoadAWSConfig(ctx, a.Region, a.Profile)
if err != nil {
return err
}
a.s3Wrapper = wrapper.CreateS3Wrapper(awsConfig, a.TableBucketsMode, a.DirectoryBucketsMode)
}
return nil
}

func (a *App) initBucketSelector() error {
if a.bucketSelector == nil {
a.bucketSelector = NewBucketSelector(a.InteractiveMode, a.BucketNames, a.s3Wrapper)
}
return nil
}

return a.processBuckets(c.Context, s3Wrapper)
func (a *App) initBucketProcessor() error {
if a.bucketProcessor == nil {
processorConfig := BucketProcessorConfig{
TargetBuckets: a.targetBuckets,
QuietMode: a.QuietMode,
ConcurrentMode: a.ConcurrentMode,
ConcurrencyNumber: a.ConcurrencyNumber,
ForceMode: a.ForceMode,
OldVersionsOnly: a.OldVersionsOnly,
}
processor, err := NewBucketProcessor(processorConfig, a.s3Wrapper)
if err != nil {
return err
}
a.bucketProcessor = processor
}
return nil
}

func (a *App) validateOptions() error {
Expand Down Expand Up @@ -200,72 +245,3 @@ func (a *App) validateOptions() error {
}
return nil
}

func (a *App) createS3Wrapper(ctx context.Context) (wrapper.IWrapper, error) {
config, err := client.LoadAWSConfig(ctx, a.Region, a.Profile)
if err != nil {
return nil, err
}

return wrapper.CreateS3Wrapper(config, a.TableBucketsMode, a.DirectoryBucketsMode), nil
}

func (a *App) setTargetBuckets(ctx context.Context, s3Wrapper wrapper.IWrapper) (bool, error) {
if a.InteractiveMode {
continuation, err := a.doInteractiveMode(ctx, s3Wrapper)
if err != nil {
return false, err
}
return continuation, nil
}

outputBuckets, err := s3Wrapper.CheckAllBucketsExist(ctx, a.BucketNames.Value())
if err != nil {
return false, err
}
a.targetBuckets = append(a.targetBuckets, outputBuckets...)
return true, nil
}

func (a *App) doInteractiveMode(ctx context.Context, s3Wrapper wrapper.IWrapper) (bool, error) {
keyword := io.InputKeywordForFilter("Filter a keyword of bucket names: ")
outputs, err := s3Wrapper.ListBucketNamesFilteredByKeyword(ctx, aws.String(keyword))
if err != nil {
return false, err
}

bucketNames := []string{}
for _, output := range outputs {
bucketNames = append(bucketNames, output.BucketName)
}

label := []string{"Select buckets."}
checkboxes, continuation, err := io.GetCheckboxes(label, bucketNames)
if err != nil {
return false, err
}
if !continuation {
return false, nil
}

for _, bucket := range checkboxes {
for _, output := range outputs {
if output.BucketName == bucket {
a.targetBuckets = append(a.targetBuckets, output.TargetBucket)
}
}
}
return true, nil
}

func (a *App) processBuckets(ctx context.Context, s3Wrapper wrapper.IWrapper) error {
processor := newBucketProcessor(
a.targetBuckets,
a.QuietMode,
a.ConcurrentMode,
a.ForceMode,
a.OldVersionsOnly,
a.ConcurrencyNumber,
)
return processor.Process(ctx, s3Wrapper)
}
103 changes: 103 additions & 0 deletions internal/app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ package app

import (
"bytes"
"flag"
"fmt"
"testing"

"github.com/go-to-k/cls3/internal/io"
"github.com/go-to-k/cls3/internal/wrapper"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2"
"go.uber.org/mock/gomock"
)

func Test_validateOptions(t *testing.T) {
Expand Down Expand Up @@ -377,3 +381,102 @@ func Test_validateOptions(t *testing.T) {
})
}
}

func TestApp_getAction(t *testing.T) {
tests := []struct {
name string
prepareMockFn func(m *wrapper.MockIWrapper, ms *MockIBucketSelector, mp *MockIBucketProcessor)
app *App
wantErr bool
expectedErr string
expectedTargetBuckets []string
}{
{
name: "successfully process buckets",
prepareMockFn: func(m *wrapper.MockIWrapper, ms *MockIBucketSelector, mp *MockIBucketProcessor) {
ms.EXPECT().SelectBuckets(gomock.Any()).Return([]string{"bucket1", "bucket2"}, true, nil)
mp.EXPECT().Process(gomock.Any()).Return(nil)
},
app: &App{
BucketNames: cli.NewStringSlice("bucket1", "bucket2"),
targetBuckets: []string{},
ConcurrencyNumber: UnspecifiedConcurrencyNumber,
},
wantErr: false,
expectedTargetBuckets: []string{"bucket1", "bucket2"},
},
{
name: "error when select buckets fails",
prepareMockFn: func(m *wrapper.MockIWrapper, ms *MockIBucketSelector, mp *MockIBucketProcessor) {
ms.EXPECT().SelectBuckets(gomock.Any()).Return(nil, false, fmt.Errorf("SelectBucketsError"))
},
app: &App{
BucketNames: cli.NewStringSlice("bucket1"),
targetBuckets: []string{},
ConcurrencyNumber: UnspecifiedConcurrencyNumber,
},
wantErr: true,
expectedErr: "SelectBucketsError",
expectedTargetBuckets: []string{},
},
{
name: "no error when select buckets returns no continuation",
prepareMockFn: func(m *wrapper.MockIWrapper, ms *MockIBucketSelector, mp *MockIBucketProcessor) {
ms.EXPECT().SelectBuckets(gomock.Any()).Return(nil, false, nil)
},
app: &App{
BucketNames: cli.NewStringSlice("bucket1"),
targetBuckets: []string{},
ConcurrencyNumber: UnspecifiedConcurrencyNumber,
},
wantErr: false,
expectedTargetBuckets: []string{},
},
{
name: "error when process buckets fails",
prepareMockFn: func(m *wrapper.MockIWrapper, ms *MockIBucketSelector, mp *MockIBucketProcessor) {
ms.EXPECT().SelectBuckets(gomock.Any()).Return([]string{"bucket1"}, true, nil)
mp.EXPECT().Process(gomock.Any()).Return(fmt.Errorf("ProcessError"))
},
app: &App{
BucketNames: cli.NewStringSlice("bucket1"),
targetBuckets: []string{},
ConcurrencyNumber: UnspecifiedConcurrencyNumber,
},
wantErr: true,
expectedErr: "ProcessError",
expectedTargetBuckets: []string{"bucket1"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
mockWrapper := wrapper.NewMockIWrapper(ctrl)
mockSelector := NewMockIBucketSelector(ctrl)
mockProcessor := NewMockIBucketProcessor(ctrl)

// Set up the mocks before calling prepareMockFn
tt.app.s3Wrapper = mockWrapper
tt.app.bucketSelector = mockSelector
tt.app.bucketProcessor = mockProcessor

// Set up the mock expectations
tt.prepareMockFn(mockWrapper, mockSelector, mockProcessor)

action := tt.app.getAction()
err := action(cli.NewContext(tt.app.Cli, &flag.FlagSet{}, nil))

if (err != nil) != tt.wantErr {
t.Errorf("error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr {
assert.EqualError(t, err, tt.expectedErr)
}

// Verify targetBuckets
assert.Equal(t, tt.expectedTargetBuckets, tt.app.targetBuckets, "targetBuckets mismatch")
})
}
}
Loading

0 comments on commit 535c14f

Please sign in to comment.