Skip to content

Commit 955e5d1

Browse files
committed
test: add tests for s3 fetcher, workerpool and populator
Signed-off-by: Adam Shannag <shannagadam11@gmail.com>
1 parent 53b3a97 commit 955e5d1

File tree

5 files changed

+507
-2
lines changed

5 files changed

+507
-2
lines changed
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package populator_test
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"github.com/AdamShannag/volare/internal/populator"
8+
"github.com/AdamShannag/volare/pkg/fetcher"
9+
"github.com/AdamShannag/volare/pkg/types"
10+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
12+
"strings"
13+
"sync"
14+
"testing"
15+
)
16+
17+
type mockFetcher struct {
18+
mu sync.Mutex
19+
Called []types.Source
20+
Fail bool
21+
FetchErr error
22+
}
23+
24+
func (m *mockFetcher) Fetch(_ context.Context, _ string, src types.Source) error {
25+
m.mu.Lock()
26+
m.Called = append(m.Called, src)
27+
m.mu.Unlock()
28+
29+
if m.Fail {
30+
return m.FetchErr
31+
}
32+
return nil
33+
}
34+
35+
func TestPopulate_Success(t *testing.T) {
36+
t.Parallel()
37+
38+
reg := fetcher.NewRegistry()
39+
mock := &mockFetcher{}
40+
_ = reg.Register("s3", mock)
41+
42+
spec := newValidSpec(t)
43+
err := populator.Populate(context.Background(), spec, "/tmp/populate", reg)
44+
if err != nil {
45+
t.Fatalf("expected success, got: %v", err)
46+
}
47+
if len(mock.Called) != 2 {
48+
t.Errorf("expected 2 fetch calls, got: %d", len(mock.Called))
49+
}
50+
}
51+
52+
func TestPopulate_FetcherGetFails(t *testing.T) {
53+
t.Parallel()
54+
55+
reg := fetcher.NewRegistry()
56+
57+
spec := newValidSpec(t)
58+
err := populator.Populate(context.Background(), spec, "/tmp", reg)
59+
if err == nil {
60+
t.Fatal("expected fetcher not found error, got nil")
61+
}
62+
}
63+
64+
func TestPopulate_FetchFails(t *testing.T) {
65+
t.Parallel()
66+
67+
reg := fetcher.NewRegistry()
68+
mock := &mockFetcher{Fail: true, FetchErr: errors.New("boom")}
69+
_ = reg.Register("s3", mock)
70+
71+
spec := newValidSpec(t)
72+
err := populator.Populate(context.Background(), spec, "/tmp", reg)
73+
if err == nil {
74+
t.Fatal("expected fetch error, got nil")
75+
}
76+
}
77+
78+
func TestPopulate_InvalidSpecJSON(t *testing.T) {
79+
t.Parallel()
80+
81+
err := populator.Populate(context.Background(), `{"invalid":`, "/tmp", nil)
82+
if err == nil {
83+
t.Fatal("expected JSON error, got nil")
84+
}
85+
}
86+
87+
func TestPopulate_EmptySpec(t *testing.T) {
88+
t.Parallel()
89+
90+
err := populator.Populate(context.Background(), "", "/tmp", nil)
91+
if err == nil {
92+
t.Fatal("expected error for empty spec, got nil")
93+
}
94+
}
95+
96+
func TestArgsFactory_Success(t *testing.T) {
97+
t.Setenv("FOO", "bar")
98+
99+
vp := types.VolarePopulator{
100+
TypeMeta: metav1.TypeMeta{Kind: "VolarePopulator", APIVersion: "volare/v1"},
101+
ObjectMeta: metav1.ObjectMeta{Name: "test-populator"},
102+
Spec: types.VolarePopulatorSpec{
103+
Sources: []types.Source{
104+
{Type: "http", TargetPath: "path/to/target"},
105+
},
106+
Workers: nil,
107+
},
108+
}
109+
110+
unstructuredMap, err := toUnstructured(vp)
111+
if err != nil {
112+
t.Fatalf("failed to convert to unstructured: %v", err)
113+
}
114+
u := &unstructured.Unstructured{Object: unstructuredMap}
115+
116+
mountPath := "/mnt/test"
117+
argsFunc := populator.ArgsFactory(mountPath)
118+
119+
args, err := argsFunc(false, u)
120+
if err != nil {
121+
t.Fatalf("ArgsFactory returned error: %v", err)
122+
}
123+
124+
if len(args) != 4 {
125+
t.Fatalf("expected 4 args, got %d: %v", len(args), args)
126+
}
127+
128+
if !strings.HasPrefix(args[0], "--mode=populator") {
129+
t.Errorf("expected args[0] to start with --mode=populator, got %s", args[0])
130+
}
131+
132+
if !strings.HasPrefix(args[1], "--spec=") {
133+
t.Errorf("expected args[1] to start with --spec=, got %s", args[1])
134+
}
135+
136+
if !strings.HasPrefix(args[2], "--envs=") {
137+
t.Errorf("expected args[2] to start with --envs=, got %s", args[2])
138+
}
139+
140+
if !strings.HasPrefix(args[3], "--mountpath=") {
141+
t.Errorf("expected args[3] to start with --mountpath=, got %s", args[3])
142+
}
143+
144+
if args[3] != "--mountpath="+mountPath {
145+
t.Errorf("expected mountpath arg %q, got %q", "--mountpath="+mountPath, args[3])
146+
}
147+
}
148+
149+
func TestArgsFactory_InvalidUnstructured(t *testing.T) {
150+
t.Parallel()
151+
152+
u := &unstructured.Unstructured{
153+
Object: map[string]interface{}{
154+
"spec": func() {},
155+
},
156+
}
157+
158+
argsFunc := populator.ArgsFactory("/mnt/test")
159+
160+
args, err := argsFunc(false, u)
161+
if err == nil {
162+
t.Fatal("expected error converting invalid unstructured, got nil")
163+
}
164+
if len(args) != 0 {
165+
t.Errorf("expected empty args slice on error, got %v", args)
166+
}
167+
}
168+
169+
func toUnstructured(obj interface{}) (map[string]interface{}, error) {
170+
b, err := json.Marshal(obj)
171+
if err != nil {
172+
return nil, err
173+
}
174+
var m map[string]interface{}
175+
err = json.Unmarshal(b, &m)
176+
return m, err
177+
}
178+
179+
func newValidSpec(t *testing.T) string {
180+
spec := types.VolarePopulatorSpec{
181+
Sources: []types.Source{
182+
{Type: "s3", TargetPath: "file1.txt"},
183+
{Type: "s3", TargetPath: "file2.txt"},
184+
},
185+
}
186+
specBytes, err := json.Marshal(spec)
187+
if err != nil {
188+
t.Fatal(err)
189+
}
190+
return string(specBytes)
191+
}

pkg/fetcher/s3/adapter.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package s3
2+
3+
import (
4+
"context"
5+
"github.com/minio/minio-go/v7"
6+
"io"
7+
)
8+
9+
type minioAdapter struct {
10+
client *minio.Client
11+
}
12+
13+
func (m *minioAdapter) ListObjects(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo {
14+
return m.client.ListObjects(ctx, bucket, opts)
15+
}
16+
17+
func (m *minioAdapter) GetObject(ctx context.Context, bucket, object string, opts minio.GetObjectOptions) (io.ReadCloser, error) {
18+
obj, err := m.client.GetObject(ctx, bucket, object, opts)
19+
if err != nil {
20+
return nil, err
21+
}
22+
return obj, nil
23+
}

pkg/fetcher/s3/fetcher.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import (
1919

2020
type Client interface {
2121
ListObjects(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo
22-
GetObject(ctx context.Context, bucket, object string, opts minio.GetObjectOptions) (*minio.Object, error)
22+
GetObject(ctx context.Context, bucket, object string, opts minio.GetObjectOptions) (io.ReadCloser, error)
2323
}
2424

2525
type ClientFactory func(opts types.S3Options) (Client, error)
@@ -137,9 +137,13 @@ func downloadObject(ctx context.Context, client Client, mountPath, bucket, key s
137137
}
138138

139139
func MinioClientFactory(opts types.S3Options) (Client, error) {
140-
return minio.New(opts.Endpoint, &minio.Options{
140+
c, err := minio.New(opts.Endpoint, &minio.Options{
141141
Creds: credentials.NewStaticV4(utils.FromEnv(opts.AccessKeyID), utils.FromEnv(opts.SecretAccessKey), utils.FromEnv(opts.SessionToken)),
142142
Secure: opts.Secure,
143143
Region: opts.Region,
144144
})
145+
if err != nil {
146+
return nil, err
147+
}
148+
return &minioAdapter{client: c}, nil
145149
}

pkg/fetcher/s3/fetcher_test.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package s3_test
2+
3+
import (
4+
"context"
5+
"errors"
6+
"io"
7+
"strings"
8+
"sync/atomic"
9+
"testing"
10+
11+
"github.com/AdamShannag/volare/pkg/fetcher/s3"
12+
"github.com/AdamShannag/volare/pkg/types"
13+
"github.com/minio/minio-go/v7"
14+
)
15+
16+
type mockClient struct {
17+
listObjectsFunc func(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo
18+
getObjectFunc func(ctx context.Context, bucket, object string, opts minio.GetObjectOptions) (io.ReadCloser, error)
19+
}
20+
21+
func (m *mockClient) ListObjects(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo {
22+
return m.listObjectsFunc(ctx, bucket, opts)
23+
}
24+
25+
func (m *mockClient) GetObject(ctx context.Context, bucket, object string, opts minio.GetObjectOptions) (io.ReadCloser, error) {
26+
return m.getObjectFunc(ctx, bucket, object, opts)
27+
}
28+
29+
func TestFetcher_Fetch_Success(t *testing.T) {
30+
t.Parallel()
31+
32+
objects := []minio.ObjectInfo{
33+
{Key: "file1.txt"},
34+
{Key: "file2.txt"},
35+
{Key: "dir/"},
36+
}
37+
38+
var calls int32
39+
mock := &mockClient{
40+
listObjectsFunc: func(_ context.Context, _ string, _ minio.ListObjectsOptions) <-chan minio.ObjectInfo {
41+
ch := make(chan minio.ObjectInfo, len(objects))
42+
for _, o := range objects {
43+
ch <- o
44+
}
45+
close(ch)
46+
return ch
47+
},
48+
getObjectFunc: func(_ context.Context, _, _ string, _ minio.GetObjectOptions) (io.ReadCloser, error) {
49+
atomic.AddInt32(&calls, 1)
50+
return io.NopCloser(strings.NewReader("data")), nil
51+
},
52+
}
53+
54+
fetcher := s3.NewFetcher(func(opts types.S3Options) (s3.Client, error) {
55+
return mock, nil
56+
})
57+
58+
src := types.Source{
59+
S3: &types.S3Options{
60+
Bucket: "bucket",
61+
Paths: []string{""},
62+
},
63+
}
64+
65+
tmpDir := t.TempDir()
66+
err := fetcher.Fetch(context.Background(), tmpDir, src)
67+
if err != nil {
68+
t.Fatalf("unexpected error: %v", err)
69+
}
70+
71+
if got := atomic.LoadInt32(&calls); got != 2 {
72+
t.Errorf("expected 2 downloads, got %d", got)
73+
}
74+
}
75+
76+
func TestFetcher_Fetch_ListObjectsError(t *testing.T) {
77+
t.Parallel()
78+
79+
mock := &mockClient{
80+
listObjectsFunc: func(_ context.Context, _ string, _ minio.ListObjectsOptions) <-chan minio.ObjectInfo {
81+
ch := make(chan minio.ObjectInfo, 1)
82+
ch <- minio.ObjectInfo{Err: errors.New("list error")}
83+
close(ch)
84+
return ch
85+
},
86+
getObjectFunc: func(_ context.Context, _, _ string, _ minio.GetObjectOptions) (io.ReadCloser, error) {
87+
t.Fatal("GetObject should not be called")
88+
return nil, nil
89+
},
90+
}
91+
92+
fetcher := s3.NewFetcher(func(opts types.S3Options) (s3.Client, error) {
93+
return mock, nil
94+
})
95+
96+
err := fetcher.Fetch(context.Background(), t.TempDir(), types.Source{
97+
S3: &types.S3Options{
98+
Bucket: "bucket",
99+
Paths: []string{"/bad"},
100+
},
101+
})
102+
if err == nil || !strings.Contains(err.Error(), "failed to list objects") {
103+
t.Fatalf("expected list error, got %v", err)
104+
}
105+
}
106+
107+
func TestFetcher_Fetch_DownloadError(t *testing.T) {
108+
t.Parallel()
109+
110+
mock := &mockClient{
111+
listObjectsFunc: func(_ context.Context, _ string, _ minio.ListObjectsOptions) <-chan minio.ObjectInfo {
112+
ch := make(chan minio.ObjectInfo, 1)
113+
ch <- minio.ObjectInfo{Key: "file.txt"}
114+
close(ch)
115+
return ch
116+
},
117+
getObjectFunc: func(_ context.Context, _, _ string, _ minio.GetObjectOptions) (io.ReadCloser, error) {
118+
return nil, errors.New("download error")
119+
},
120+
}
121+
122+
fetcher := s3.NewFetcher(func(opts types.S3Options) (s3.Client, error) {
123+
return mock, nil
124+
})
125+
126+
err := fetcher.Fetch(context.Background(), t.TempDir(), types.Source{
127+
S3: &types.S3Options{
128+
Bucket: "bucket",
129+
Paths: []string{"a"},
130+
},
131+
})
132+
if err == nil || !strings.Contains(err.Error(), "download error") {
133+
t.Fatalf("expected download error, got %v", err)
134+
}
135+
}
136+
137+
func TestFetcher_Fetch_InvalidConfig(t *testing.T) {
138+
t.Parallel()
139+
140+
fetcher := s3.NewFetcher(nil)
141+
142+
err := fetcher.Fetch(context.Background(), t.TempDir(), types.Source{})
143+
if err == nil || !strings.Contains(err.Error(), "invalid source configuration") {
144+
t.Fatalf("expected invalid source config error, got %v", err)
145+
}
146+
}

0 commit comments

Comments
 (0)