Skip to content

Commit 930ecd4

Browse files
paulrosca-snykCalamarBicefalo
authored andcommitted
wip: filters client
1 parent 61ac7a4 commit 930ecd4

File tree

12 files changed

+267
-84
lines changed

12 files changed

+267
-84
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
### Local development ###
55
# Local build system configuration
66
/local.mk
7+
.idea
78

89
node_modules

internal/fileupload/client.go

Lines changed: 20 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ package fileupload
22

33
import (
44
"context"
5-
"encoding/json"
65
"fmt"
6+
"github.com/snyk/cli-extension-os-flows/internal/fileupload/filters"
77
"net/http"
88
"os"
99
"path/filepath"
@@ -24,17 +24,16 @@ type Config struct {
2424

2525
// Client provides high-level file upload functionality.
2626
type Client struct {
27-
httpClient *http.Client
28-
lowlevel uploadrevision.SealableClient
29-
cfg Config
30-
filters Filters
27+
uploadRevisionSealableClient uploadrevision.SealableClient
28+
filtersClient filters.Client
29+
cfg Config
30+
filters Filters
3131
}
3232

3333
// NewClient creates a new high-level file upload client.
3434
func NewClient(httpClient *http.Client, cfg Config, opts ...Option) *Client {
3535
client := &Client{
36-
httpClient: httpClient,
37-
cfg: cfg,
36+
cfg: cfg,
3837
filters: Filters{
3938
supportedExtensions: xsync.NewMapOf[bool](),
4039
supportedConfigFiles: xsync.NewMapOf[bool](),
@@ -45,46 +44,24 @@ func NewClient(httpClient *http.Client, cfg Config, opts ...Option) *Client {
4544
opt(client)
4645
}
4746

48-
if client.lowlevel == nil {
49-
client.lowlevel = uploadrevision.NewClient(uploadrevision.Config{
47+
if client.uploadRevisionSealableClient == nil {
48+
client.uploadRevisionSealableClient = uploadrevision.NewClient(uploadrevision.Config{
5049
BaseURL: cfg.BaseURL,
5150
}, uploadrevision.WithHTTPClient(httpClient))
5251
}
5352

54-
return client
55-
}
56-
57-
func (c *Client) getDeeproxyFilters(ctx context.Context) (FiltersResponse, error) {
58-
var filtersResp FiltersResponse
59-
60-
url := fmt.Sprintf("%s/hidden/orgs/%s/code/filters", c.cfg.BaseURL, c.cfg.OrgID)
61-
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
62-
if err != nil {
63-
return filtersResp, fmt.Errorf("failed to create deeproxy filters request: %w", err)
64-
}
65-
66-
req.Header.Set("snyk-org-name", c.cfg.OrgID.String())
67-
68-
resp, err := c.httpClient.Do(req)
69-
if err != nil {
70-
return filtersResp, fmt.Errorf("error making deeproxy filters request: %w", err)
71-
}
72-
defer resp.Body.Close()
73-
74-
if resp.StatusCode < 200 || resp.StatusCode > 299 {
75-
return filtersResp, fmt.Errorf("unexpected response code: %s", resp.Status)
76-
}
77-
78-
if err := json.NewDecoder(resp.Body).Decode(&filtersResp); err != nil {
79-
return filtersResp, fmt.Errorf("failed to decode deeproxy filters response: %w", err)
53+
if client.filtersClient == nil {
54+
client.filtersClient = filters.NewDeeproxyClient(filters.Config{
55+
BaseURL: cfg.BaseURL,
56+
}, filters.WithHTTPClient(httpClient))
8057
}
8158

82-
return filtersResp, nil
59+
return client
8360
}
8461

8562
func (c *Client) loadFilters(ctx context.Context) error {
8663
c.filters.once.Do(func() {
87-
filtersResp, err := c.getDeeproxyFilters(ctx)
64+
filtersResp, err := c.filtersClient.GetFilters(ctx, c.cfg.OrgID)
8865
if err != nil {
8966
c.filters.initErr = err
9067
return
@@ -122,7 +99,7 @@ func (c *Client) createFileFilter(ctx context.Context) (func(string) bool, error
12299
}
123100

124101
func (c *Client) uploadPaths(ctx context.Context, revID RevisionID, rootPath string, paths []string) error {
125-
files := make([]uploadrevision.UploadFile, 0, c.lowlevel.GetLimits().FileCountLimit)
102+
files := make([]uploadrevision.UploadFile, 0, c.uploadRevisionSealableClient.GetLimits().FileCountLimit)
126103
defer func() {
127104
for _, file := range files {
128105
file.File.Close()
@@ -146,7 +123,7 @@ func (c *Client) uploadPaths(ctx context.Context, revID RevisionID, rootPath str
146123
})
147124
}
148125

149-
err := c.lowlevel.UploadFiles(ctx, c.cfg.OrgID, revID, files)
126+
err := c.uploadRevisionSealableClient.UploadFiles(ctx, c.cfg.OrgID, revID, files)
150127
if err != nil {
151128
return fmt.Errorf("failed to upload files: %w", err)
152129
}
@@ -159,13 +136,13 @@ func (c *Client) addPathsToRevision(ctx context.Context, revisionID RevisionID,
159136
var chunks <-chan []string
160137

161138
if opts.SkipFiltering {
162-
chunks = chunkChan(pathsChan, c.lowlevel.GetLimits().FileCountLimit)
139+
chunks = chunkChan(pathsChan, c.uploadRevisionSealableClient.GetLimits().FileCountLimit)
163140
} else {
164141
filter, err := c.createFileFilter(ctx)
165142
if err != nil {
166143
return err
167144
}
168-
chunks = chunkChanFiltered(pathsChan, c.lowlevel.GetLimits().FileCountLimit, filter)
145+
chunks = chunkChanFiltered(pathsChan, c.uploadRevisionSealableClient.GetLimits().FileCountLimit, filter)
169146
}
170147

171148
for chunk := range chunks {
@@ -180,7 +157,7 @@ func (c *Client) addPathsToRevision(ctx context.Context, revisionID RevisionID,
180157

181158
// CreateRevision creates a new revision and returns its ID.
182159
func (c *Client) CreateRevision(ctx context.Context) (RevisionID, error) {
183-
revision, err := c.lowlevel.CreateRevision(ctx, c.cfg.OrgID)
160+
revision, err := c.uploadRevisionSealableClient.CreateRevision(ctx, c.cfg.OrgID)
184161
if err != nil {
185162
return uuid.Nil, fmt.Errorf("failed to create revision: %w", err)
186163
}
@@ -208,7 +185,7 @@ func (c *Client) AddDirToRevision(ctx context.Context, revisionID RevisionID, di
208185

209186
// SealRevision seals a revision, making it immutable.
210187
func (c *Client) SealRevision(ctx context.Context, revisionID RevisionID) error {
211-
_, err := c.lowlevel.SealRevision(ctx, c.cfg.OrgID, revisionID)
188+
_, err := c.uploadRevisionSealableClient.SealRevision(ctx, c.cfg.OrgID, revisionID)
212189
if err != nil {
213190
return fmt.Errorf("failed to seal revision: %w", err)
214191
}

internal/fileupload/client_test.go

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@ package fileupload_test
22

33
import (
44
"context"
5-
"encoding/json"
6-
"fmt"
7-
"net/http"
8-
"net/http/httptest"
5+
"github.com/snyk/cli-extension-os-flows/internal/fileupload/filters"
96
"os"
107
"path/filepath"
118
"slices"
@@ -28,7 +25,7 @@ func Test_GranularAPI(t *testing.T) {
2825
},
2926
}
3027

31-
filters := fileupload.FiltersResponse{
28+
filters := filters.AllowList{
3229
ConfigFiles: []string{"go.mod"},
3330
Extensions: []string{".txt", ".go", ".md"},
3431
}
@@ -133,7 +130,7 @@ func Test_CreateRevisionFromPaths(t *testing.T) {
133130
},
134131
}
135132

136-
filters := fileupload.FiltersResponse{
133+
filters := filters.AllowList{
137134
ConfigFiles: []string{"go.mod"},
138135
Extensions: []string{".txt", ".go", ".md"},
139136
}
@@ -194,7 +191,7 @@ func Test_CreateRevisionFromDir(t *testing.T) {
194191
},
195192
}
196193

197-
filters := fileupload.FiltersResponse{
194+
filters := filters.AllowList{
198195
ConfigFiles: []string{"go.mod"},
199196
Extensions: []string{".txt", ".go", ".md"},
200197
}
@@ -446,40 +443,24 @@ func setupTest(
446443
t *testing.T,
447444
llcfg uploadrevision.FakeClientConfig,
448445
files []uploadrevision.LoadedFile,
449-
filters fileupload.FiltersResponse,
446+
allowList filters.AllowList,
450447
) (context.Context, *uploadrevision.FakeSealableClient, *fileupload.Client, *os.File, func()) {
451448
t.Helper()
452449

453450
ctx := context.Background()
454451
orgID := uuid.New()
455-
filtersCalls := 0
456-
457-
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
458-
filtersCalls++
459-
if filtersCalls > 1 {
460-
t.Fail()
461-
}
462-
expectedURL := fmt.Sprintf("/hidden/orgs/%s/code/filters", orgID)
463-
assert.Equal(t, expectedURL, r.URL.Path)
464-
assert.Equal(t, orgID.String(), r.Header.Get("snyk-org-name"))
465-
466-
w.Header().Set("Content-Type", "application/json")
467-
if err := json.NewEncoder(w).Encode(filters); err != nil {
468-
http.Error(w, "failed to encode response", http.StatusInternalServerError)
469-
return
470-
}
471-
}))
472452

473453
fakeSealeableClient := uploadrevision.NewFakeSealableClient(llcfg)
474-
client := fileupload.NewClient(server.Client(), fileupload.Config{
475-
BaseURL: server.URL,
476-
OrgID: orgID,
477-
}, fileupload.WithLowLevelClient(fakeSealeableClient))
454+
fakeFiltersClient := filters.NewFakeClient(allowList, nil)
455+
client := fileupload.NewClient(nil, fileupload.Config{
456+
OrgID: orgID,
457+
},
458+
fileupload.WithUploadRevisionSealableClient(fakeSealeableClient),
459+
fileupload.WithFiltersClient(fakeFiltersClient))
478460

479461
dir, dirCleanup := createDirWithFiles(t, files)
480462

481463
return ctx, fakeSealeableClient, client, dir, func() {
482-
server.Close()
483464
dirCleanup()
484465
}
485466
}

internal/fileupload/errors.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package fileupload
22

33
import "github.com/snyk/cli-extension-os-flows/internal/fileupload/uploadrevision"
44

5-
// Aliasing lowlevel errors so that they're scoped to the fileupload package as well.
5+
// Aliasing uploadRevisionSealableClient errors so that they're scoped to the fileupload package as well.
66

77
// FileSizeLimitError indicates a file exceeds the maximum allowed size.
88
type FileSizeLimitError = uploadrevision.FileSizeLimitError

internal/fileupload/filters/client.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package filters
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
9+
"github.com/google/uuid"
10+
)
11+
12+
// AllowList represents the response structure from the deeproxy filters API.
13+
type AllowList struct {
14+
ConfigFiles []string `json:"configFiles"`
15+
Extensions []string `json:"extensions"`
16+
}
17+
18+
type Client interface {
19+
GetFilters(ctx context.Context, orgID uuid.UUID) (AllowList, error)
20+
}
21+
22+
type deeproxyClient struct {
23+
httpClient *http.Client
24+
cfg Config
25+
}
26+
27+
type Config struct {
28+
BaseURL string
29+
IsFedRamp bool
30+
}
31+
32+
func NewDeeproxyClient(cfg Config, opts ...Opt) *deeproxyClient {
33+
c := &deeproxyClient{
34+
cfg: cfg,
35+
httpClient: http.DefaultClient,
36+
}
37+
38+
for _, opt := range opts {
39+
opt(c)
40+
}
41+
42+
return c
43+
}
44+
45+
func (c *deeproxyClient) GetFilters(ctx context.Context, orgID uuid.UUID) (AllowList, error) {
46+
var allowList AllowList
47+
48+
url := getFilterUrl(c.cfg.BaseURL, orgID, c.cfg.IsFedRamp)
49+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
50+
if err != nil {
51+
return allowList, fmt.Errorf("failed to create deeproxy filters request: %w", err)
52+
}
53+
54+
req.Header.Set("snyk-org-name", orgID.String())
55+
56+
resp, err := c.httpClient.Do(req)
57+
if err != nil {
58+
return allowList, fmt.Errorf("error making deeproxy filters request: %w", err)
59+
}
60+
defer resp.Body.Close()
61+
62+
if resp.StatusCode < 200 || resp.StatusCode > 299 {
63+
return allowList, fmt.Errorf("unexpected response code: %s", resp.Status)
64+
}
65+
66+
if err := json.NewDecoder(resp.Body).Decode(&allowList); err != nil {
67+
return allowList, fmt.Errorf("failed to decode deeproxy filters response: %w", err)
68+
}
69+
70+
return allowList, nil
71+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package filters
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
9+
"github.com/google/uuid"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestClients(t *testing.T) {
15+
tests := []struct {
16+
getClient func(t *testing.T, orgID uuid.UUID, expectedAllow AllowList) (Client, func())
17+
clientName string
18+
}{
19+
{
20+
clientName: "deeproxyClient",
21+
getClient: func(t *testing.T, orgID uuid.UUID, expectedAllow AllowList) (Client, func()) {
22+
t.Helper()
23+
24+
s := setupServer(t, orgID, expectedAllow)
25+
cleanup := func() {
26+
s.Close()
27+
}
28+
c := NewDeeproxyClient(Config{BaseURL: s.URL, IsFedRamp: true}, WithHTTPClient(s.Client()))
29+
30+
return c, cleanup
31+
},
32+
},
33+
{
34+
clientName: "fakeClient",
35+
getClient: func(t *testing.T, orgID uuid.UUID, expectedAllow AllowList) (Client, func()) {
36+
t.Helper()
37+
38+
c := NewFakeClient(expectedAllow, nil)
39+
40+
return c, func() {}
41+
},
42+
},
43+
}
44+
45+
for _, testData := range tests {
46+
t.Run(testData.clientName+": GetFilters", func(t *testing.T) {
47+
orgID := uuid.New()
48+
expectedAllow := AllowList{
49+
ConfigFiles: []string{"package.json"},
50+
Extensions: []string{".ts", ".js"},
51+
}
52+
client, cleanup := testData.getClient(t, orgID, expectedAllow)
53+
defer cleanup()
54+
55+
allow, err := client.GetFilters(t.Context(), orgID)
56+
require.NoError(t, err)
57+
58+
assert.Equal(t, expectedAllow, allow)
59+
})
60+
}
61+
}
62+
63+
func setupServer(t *testing.T, orgID uuid.UUID, expectedAllow AllowList) *httptest.Server {
64+
t.Helper()
65+
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
66+
expectedURL := getFilterUrl("", orgID, true)
67+
assert.Equal(t, expectedURL, r.URL.Path)
68+
assert.Equal(t, orgID.String(), r.Header.Get("snyk-org-name"))
69+
w.Header().Set("Content-Type", "application/json")
70+
if err := json.NewEncoder(w).Encode(expectedAllow); err != nil {
71+
http.Error(w, "failed to encode response", http.StatusInternalServerError)
72+
return
73+
}
74+
}))
75+
ts.Start()
76+
return ts
77+
}

0 commit comments

Comments
 (0)