Skip to content

Commit 45fd713

Browse files
mariiatuzovskaamanjprojuanli16
authored
Run integration test with GCS mock (#55)
Co-authored-by: amanjpro <amanjpro@gmail.com> Co-authored-by: Justin Li <justin.li@optable.co>
1 parent 40e8d24 commit 45fd713

9 files changed

Lines changed: 756 additions & 61 deletions

File tree

.github/workflows/pr.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,25 @@ jobs:
4747
run: go get ./...
4848
- name: Build
4949
run: make build
50+
- name: Start fake-gcs-server
51+
run: |
52+
docker run -d --name fake-gcs-server -p 4443:4443 fsouza/fake-gcs-server \
53+
-scheme http \
54+
-public-host 0.0.0.0:4443
55+
- name: Wait for fake-gcs-server to start
56+
run: |
57+
for i in {1..10}; do
58+
if curl -s http://0.0.0.0:4443/storage/v1/; then
59+
echo "fake-gcs-server is ready"
60+
break
61+
fi
62+
echo "Waiting for fake-gcs-server..."
63+
sleep 2
64+
done
5065
- name: Run go tests
66+
env:
67+
STORAGE_EMULATOR_HOST: http://0.0.0.0:4443
5168
run: go test ./...
69+
- name: Cleanup
70+
if: always()
71+
run: docker stop fake-gcs-server && docker rm fake-gcs-server

.github/workflows/release.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,28 @@ jobs:
5050
run: go get ./...
5151
- name: build
5252
run: make release
53+
- name: Start fake-gcs-server
54+
run: |
55+
docker run -d --name fake-gcs-server -p 4443:4443 fsouza/fake-gcs-server \
56+
-scheme http \
57+
-public-host 0.0.0.0:4443
58+
- name: Wait for fake-gcs-server to start
59+
run: |
60+
for i in {1..10}; do
61+
if curl -s http://0.0.0.0:4443/storage/v1/; then
62+
echo "fake-gcs-server is ready"
63+
break
64+
fi
65+
echo "Waiting for fake-gcs-server..."
66+
sleep 2
67+
done
5368
- name: Run go tests
69+
env:
70+
STORAGE_EMULATOR_HOST: http://0.0.0.0:4443
5471
run: go test ./...
72+
- name: Cleanup
73+
if: always()
74+
run: docker stop fake-gcs-server && docker rm fake-gcs-server
5575
- name: Create release
5676
env:
5777
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Makefile

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ BUILD_VERSION := $(shell git describe --tags --always)
33
GO=$(shell which go)
44
RM=rm -f ./bin/*
55

6+
# fake-gcs-server variables
7+
FAKE_GCS_DOCKER_IMAGE = fsouza/fake-gcs-server
8+
FAKE_GCS_DOCKER_CONTAINER = fake-gcs-server
9+
FAKE_GCS_SCHEME = http
10+
FAKE_GCS_PORT = 4443
11+
FAKE_GCS_HOST = 0.0.0.0
12+
STORAGE_EMULATOR_HOST = $(FAKE_GCS_SCHEME)://$(FAKE_GCS_HOST):$(FAKE_GCS_PORT)
13+
614
# windows specific commands
715
ifeq ($(OS), Windows_NT)
816
MV=move bin\opair bin\opair.exe
@@ -53,7 +61,7 @@ windows:
5361
mkdir -p release && cp bin/opair release/opair-windows-amd64.exe
5462

5563
.PHONY: clean
56-
clean: clean-bin clean-release
64+
clean: clean-bin clean-release clean-fake-gcs-server
5765

5866
.PHONY: clean-bin
5967
clean-bin:
@@ -62,3 +70,32 @@ clean-bin:
6270
.PHONY: clean-release
6371
clean-release:
6472
rm -f release/*
73+
74+
.PHONY: start-fake-gcs-server
75+
start-fake-gcs-server:
76+
@if [ $$(docker ps -q -f name=$(FAKE_GCS_DOCKER_CONTAINER)) ]; then \
77+
echo "Container $(FAKE_GCS_DOCKER_CONTAINER) is already running."; \
78+
elif [ $$(docker ps -aq -f name=$(FAKE_GCS_DOCKER_CONTAINER)) ]; then \
79+
echo "Starting existing container $(FAKE_GCS_DOCKER_CONTAINER)..."; \
80+
docker start $(FAKE_GCS_DOCKER_CONTAINER); \
81+
else \
82+
echo "Creating and starting container $(FAKE_GCS_DOCKER_CONTAINER)..."; \
83+
docker run -d --name $(FAKE_GCS_DOCKER_CONTAINER) -p $(FAKE_GCS_PORT):$(FAKE_GCS_PORT) $(FAKE_GCS_DOCKER_IMAGE) \
84+
-scheme $(FAKE_GCS_SCHEME) \
85+
-public-host $(FAKE_GCS_HOST):$(FAKE_GCS_PORT); \
86+
fi
87+
88+
.PHONY: clean-fake-gcs-server
89+
clean-fake-gcs-server:
90+
@if [ $$(docker ps -q -f name=$(FAKE_GCS_DOCKER_CONTAINER)) ]; then \
91+
echo "Stopping container $(FAKE_GCS_DOCKER_CONTAINER)..."; \
92+
docker stop $(FAKE_GCS_DOCKER_CONTAINER); \
93+
fi
94+
@if [ $$(docker ps -aq -f name=$(FAKE_GCS_DOCKER_CONTAINER)) ]; then \
95+
echo "Removing container $(FAKE_GCS_DOCKER_CONTAINER)..."; \
96+
docker rm $(FAKE_GCS_DOCKER_CONTAINER); \
97+
fi
98+
99+
.PHONY: test
100+
test: start-fake-gcs-server
101+
STORAGE_EMULATOR_HOST=$(STORAGE_EMULATOR_HOST) $(GO) test -v ./...

pkg/bucket/bucket.go

Lines changed: 36 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,19 @@ type (
2525
ReadWriter struct {
2626
client *storage.Client
2727
FileReader io.Reader
28-
srcPrefixedBucket *prefixedBucket
29-
dstPrefixedBucket *prefixedBucket
28+
srcPrefixedBucket *PrefixedBucket
29+
dstPrefixedBucket *PrefixedBucket
3030
ReadWriters []*ReadWriteCloser
3131
}
3232

3333
Completer struct {
3434
client *storage.Client
35-
dstPrefixedBucket *prefixedBucket
35+
dstPrefixedBucket *PrefixedBucket
3636
}
3737

38-
prefixedBucket struct {
39-
bucket string
40-
prefix string
38+
PrefixedBucket struct {
39+
Bucket string
40+
Prefix string
4141
}
4242

4343
// ReadWriteCloser contains the name of the object, its reader and a writer.
@@ -56,6 +56,19 @@ type (
5656
Option func(*bucketOptions)
5757
)
5858

59+
// GCSClientOptions is used to set insucure HTTP client for integration tests.
60+
var GCSClientOptions = []option.ClientOption{}
61+
62+
func gcsClientOptions(token string) []option.ClientOption {
63+
return append(GCSClientOptions, option.WithTokenSource(
64+
oauth2.StaticTokenSource(
65+
&oauth2.Token{
66+
AccessToken: token,
67+
},
68+
),
69+
))
70+
}
71+
5972
// WithReader allows to specify a reader to be used for the bucket.
6073
func WithReader(reader io.Reader) Option {
6174
return func(o *bucketOptions) {
@@ -77,16 +90,7 @@ func NewBucketCompleter(ctx context.Context, downscopedToken string, dstURL stri
7790
return nil, ErrTokenRequired
7891
}
7992

80-
client, err := storage.NewClient(
81-
ctx,
82-
option.WithTokenSource(
83-
oauth2.StaticTokenSource(
84-
&oauth2.Token{
85-
AccessToken: downscopedToken,
86-
},
87-
),
88-
),
89-
)
93+
client, err := storage.NewClient(ctx, gcsClientOptions(downscopedToken)...)
9094
if err != nil {
9195
return nil, fmt.Errorf("failed to create storage client: %w", err)
9296
}
@@ -105,8 +109,8 @@ func NewBucketCompleter(ctx context.Context, downscopedToken string, dstURL stri
105109
// Complete writes a .Completed file to the destination bucket to signal that the transfer is complete.
106110
// It then closes the client.
107111
func (b *Completer) Complete(ctx context.Context) error {
108-
dstBucket := b.client.Bucket(b.dstPrefixedBucket.bucket)
109-
completedWriter := dstBucket.Object(fmt.Sprintf("%s/%s", b.dstPrefixedBucket.prefix, CompletedFile)).NewWriter(ctx)
112+
dstBucket := b.client.Bucket(b.dstPrefixedBucket.Bucket)
113+
completedWriter := dstBucket.Object(fmt.Sprintf("%s/%s", b.dstPrefixedBucket.Prefix, CompletedFile)).NewWriter(ctx)
110114
if _, err := completedWriter.Write([]byte{}); err != nil {
111115
return fmt.Errorf("failed to write completed file: %w", err)
112116
}
@@ -120,8 +124,8 @@ func (b *Completer) Complete(ctx context.Context) error {
120124

121125
// Checks if the .Completed file exists in the destination bucket.
122126
func (b *Completer) HasCompleted(ctx context.Context) (bool, error) {
123-
dstBucket := b.client.Bucket(b.dstPrefixedBucket.bucket)
124-
_, err := dstBucket.Object(fmt.Sprintf("%s/%s", b.dstPrefixedBucket.prefix, CompletedFile)).Attrs(ctx)
127+
dstBucket := b.client.Bucket(b.dstPrefixedBucket.Bucket)
128+
_, err := dstBucket.Object(fmt.Sprintf("%s/%s", b.dstPrefixedBucket.Prefix, CompletedFile)).Attrs(ctx)
125129
if errors.Is(err, storage.ErrObjectNotExist) {
126130
return false, nil
127131
}
@@ -140,16 +144,7 @@ func NewBucketReadWriter(ctx context.Context, downscopedToken string, dstURL str
140144
opt(bucketOption)
141145
}
142146

143-
client, err := storage.NewClient(
144-
ctx,
145-
option.WithTokenSource(
146-
oauth2.StaticTokenSource(
147-
&oauth2.Token{
148-
AccessToken: downscopedToken,
149-
},
150-
),
151-
),
152-
)
147+
client, err := storage.NewClient(ctx, gcsClientOptions(downscopedToken)...)
153148
if err != nil {
154149
return nil, fmt.Errorf("failed to create storage client: %w", err)
155150
}
@@ -193,7 +188,7 @@ func NewBucketReadWriter(ctx context.Context, downscopedToken string, dstURL str
193188
}
194189

195190
// bucketFromObjectURL parses a valid objectURL and returns a Bucket.
196-
func bucketFromObjectURL(objectURL string) (*prefixedBucket, error) {
191+
func bucketFromObjectURL(objectURL string) (*PrefixedBucket, error) {
197192
url, err := url.Parse(objectURL)
198193
if err != nil {
199194
return nil, err
@@ -203,9 +198,9 @@ func bucketFromObjectURL(objectURL string) (*prefixedBucket, error) {
203198
return nil, fmt.Errorf("invalid object URL: %s", objectURL)
204199
}
205200

206-
return &prefixedBucket{
207-
bucket: url.Host,
208-
prefix: strings.TrimLeft(url.Path, "/"),
201+
return &PrefixedBucket{
202+
Bucket: url.Host,
203+
Prefix: strings.TrimLeft(url.Path, "/"),
209204
}, nil
210205
}
211206

@@ -214,10 +209,10 @@ func bucketFromObjectURL(objectURL string) (*prefixedBucket, error) {
214209
// It then opens a writer for each object under the same name specified by the destinationPrefix.
215210
func (b *ReadWriter) newObjectReadWriteCloser(ctx context.Context) error {
216211
logger := zerolog.Ctx(ctx)
217-
query := &storage.Query{Prefix: b.srcPrefixedBucket.prefix + "/"}
212+
query := &storage.Query{Prefix: b.srcPrefixedBucket.Prefix + "/"}
218213

219-
srcBucket := b.client.Bucket(b.srcPrefixedBucket.bucket)
220-
dstBucket := b.client.Bucket(b.dstPrefixedBucket.bucket)
214+
srcBucket := b.client.Bucket(b.srcPrefixedBucket.Bucket)
215+
dstBucket := b.client.Bucket(b.dstPrefixedBucket.Bucket)
221216

222217
it := srcBucket.Objects(ctx, query)
223218
var rwc []*ReadWriteCloser
@@ -227,7 +222,7 @@ func (b *ReadWriter) newObjectReadWriteCloser(ctx context.Context) error {
227222
if errors.Is(err, iterator.Done) {
228223
break
229224
} else if err != nil {
230-
logger.Debug().Err(err).Msgf("failed to list objects from source bucket %s", b.srcPrefixedBucket.prefix)
225+
logger.Debug().Err(err).Msgf("failed to list objects from source bucket %s", b.srcPrefixedBucket.Prefix)
231226
return err
232227
}
233228

@@ -243,7 +238,7 @@ func (b *ReadWriter) newObjectReadWriteCloser(ctx context.Context) error {
243238
rwc = append(rwc, &ReadWriteCloser{
244239
name: blobFromObjectName(obj.Name),
245240
Reader: reader,
246-
Writer: dstBucket.Object(objectPathWithPrefix(obj.Name, b.dstPrefixedBucket.prefix)).NewWriter(ctx),
241+
Writer: dstBucket.Object(objectPathWithPrefix(obj.Name, b.dstPrefixedBucket.Prefix)).NewWriter(ctx),
247242
})
248243
}
249244

@@ -254,8 +249,8 @@ func (b *ReadWriter) newObjectReadWriteCloser(ctx context.Context) error {
254249

255250
// newObjectWriteCloser creates a new writer for the destination bucket.
256251
func (b *ReadWriter) newObjectWriteCloser(ctx context.Context) *ReadWriteCloser {
257-
dstBucket := b.client.Bucket(b.dstPrefixedBucket.bucket)
258-
writer := dstBucket.Object(fmt.Sprintf("%s/data_%s.csv", b.dstPrefixedBucket.prefix, shortHex())).NewWriter(ctx)
252+
dstBucket := b.client.Bucket(b.dstPrefixedBucket.Bucket)
253+
writer := dstBucket.Object(fmt.Sprintf("%s/data_%s.csv", b.dstPrefixedBucket.Prefix, shortHex())).NewWriter(ctx)
259254
return &ReadWriteCloser{
260255
name: CompletedFile,
261256
Writer: writer,

pkg/bucket/readers.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ type (
2626
client *storage.Client
2727
AdvReader []io.ReadCloser
2828
PubReader []io.ReadCloser
29-
AdvPrefixedBucket *prefixedBucket
30-
PubPrefixedBucket *prefixedBucket
29+
AdvPrefixedBucket *PrefixedBucket
30+
PubPrefixedBucket *PrefixedBucket
3131
PubFileReader io.Reader
3232
}
3333
)
@@ -89,7 +89,7 @@ func NewReaders(ctx context.Context, downScopedToken, advURL string, opts ...Opt
8989
// newObjectReaders lists the objects specified by the advPrefixedBucket and pubPrefixedBucket and opens a reader for each object,
9090
// except for the .Completed file.
9191
func (b *Readers) newObjectReaders(ctx context.Context) error {
92-
advReaders, err := readersFromPrefixedBucket(ctx, b.client, b.AdvPrefixedBucket)
92+
advReaders, err := ReadersFromPrefixedBucket(ctx, b.client, b.AdvPrefixedBucket)
9393
if err != nil {
9494
return err
9595
}
@@ -105,7 +105,7 @@ func (b *Readers) newObjectReaders(ctx context.Context) error {
105105
return errors.New("missing publisher bucket URL")
106106
}
107107

108-
pubReaders, err := readersFromPrefixedBucket(ctx, b.client, b.PubPrefixedBucket)
108+
pubReaders, err := ReadersFromPrefixedBucket(ctx, b.client, b.PubPrefixedBucket)
109109
if err != nil {
110110
return err
111111
}
@@ -115,11 +115,11 @@ func (b *Readers) newObjectReaders(ctx context.Context) error {
115115
return nil
116116
}
117117

118-
func readersFromPrefixedBucket(ctx context.Context, client *storage.Client, pBucket *prefixedBucket) ([]io.ReadCloser, error) {
118+
func ReadersFromPrefixedBucket(ctx context.Context, client *storage.Client, pBucket *PrefixedBucket) ([]io.ReadCloser, error) {
119119
logger := zerolog.Ctx(ctx)
120-
query := &storage.Query{Prefix: pBucket.prefix + "/"}
120+
query := &storage.Query{Prefix: pBucket.Prefix + "/"}
121121

122-
bucket := client.Bucket(pBucket.bucket)
122+
bucket := client.Bucket(pBucket.Bucket)
123123

124124
it := bucket.Objects(ctx, query)
125125
var readers []io.ReadCloser
@@ -129,7 +129,7 @@ func readersFromPrefixedBucket(ctx context.Context, client *storage.Client, pBuc
129129
if errors.Is(err, iterator.Done) {
130130
break
131131
} else if err != nil {
132-
logger.Debug().Err(err).Msgf("failed to list objects from source bucket %s", pBucket.prefix)
132+
logger.Debug().Err(err).Msgf("failed to list objects from source bucket %s", pBucket.Prefix)
133133
return nil, err
134134
}
135135

pkg/cmd/cli/get_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"io"
88
"net/http"
99
"net/http/httptest"
10+
"optable-pair-cli/pkg/internal"
1011
"testing"
1112
"time"
1213

@@ -37,19 +38,19 @@ func TestCleanroomRun(t *testing.T) {
3738

3839
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
3940
switch r.URL.Path {
40-
case "/admin/api/external/v1/cleanroom/get":
41+
case internal.AdminCleanroomGetURL:
4142
assertGetRequest(r)
4243

4344
cleanroom := v1.Cleanroom{}
4445
data, err := proto.Marshal(&cleanroom)
4546
if err != nil {
4647
t.Errorf("Failed to marshal response: %v", err)
4748
}
49+
w.WriteHeader(http.StatusOK)
4850
_, err = w.Write(data)
4951
if err != nil {
5052
t.Errorf("Failed to write response body: %v", err)
5153
}
52-
w.WriteHeader(http.StatusOK)
5354

5455
default:
5556
t.Errorf("Unexpected call %s", r.URL.Path)
@@ -84,7 +85,7 @@ func TestCleanroomRun_RequestFail(t *testing.T) {
8485

8586
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
8687
switch r.URL.Path {
87-
case "/admin/api/external/v1/cleanroom/get":
88+
case internal.AdminCleanroomGetURL:
8889
w.WriteHeader(http.StatusNotFound)
8990

9091
default:

0 commit comments

Comments
 (0)