Skip to content

Commit f61428a

Browse files
authored
feat: add initial fileupload api client (#460)
1 parent 8c9e565 commit f61428a

31 files changed

+3161
-0
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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+
// Client defines the interface for the filters client.
19+
type Client interface {
20+
GetFilters(ctx context.Context, orgID uuid.UUID) (AllowList, error)
21+
}
22+
23+
// DeeproxyClient is the deeproxy implementation of the Client interface.
24+
type DeeproxyClient struct {
25+
httpClient *http.Client
26+
cfg Config
27+
}
28+
29+
// Config contains the configuration for the filters client.
30+
type Config struct {
31+
BaseURL string
32+
IsFedRamp bool
33+
}
34+
35+
var _ Client = (*DeeproxyClient)(nil)
36+
37+
// NewDeeproxyClient creates a new DeeproxyClient with the given configuration and options.
38+
func NewDeeproxyClient(cfg Config, opts ...Opt) *DeeproxyClient {
39+
c := &DeeproxyClient{
40+
cfg: cfg,
41+
httpClient: http.DefaultClient,
42+
}
43+
44+
for _, opt := range opts {
45+
opt(c)
46+
}
47+
48+
return c
49+
}
50+
51+
// GetFilters returns the deeproxy filters in the form of an AllowList.
52+
func (c *DeeproxyClient) GetFilters(ctx context.Context, orgID uuid.UUID) (AllowList, error) {
53+
var allowList AllowList
54+
55+
url := getFilterURL(c.cfg.BaseURL, orgID, c.cfg.IsFedRamp)
56+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
57+
if err != nil {
58+
return allowList, fmt.Errorf("failed to create deeproxy filters request: %w", err)
59+
}
60+
61+
req.Header.Set("snyk-org-name", orgID.String())
62+
63+
resp, err := c.httpClient.Do(req)
64+
if err != nil {
65+
return allowList, fmt.Errorf("error making deeproxy filters request: %w", err)
66+
}
67+
defer resp.Body.Close()
68+
69+
if resp.StatusCode < 200 || resp.StatusCode > 299 {
70+
return allowList, fmt.Errorf("unexpected response code: %s", resp.Status)
71+
}
72+
73+
if err := json.NewDecoder(resp.Body).Decode(&allowList); err != nil {
74+
return allowList, fmt.Errorf("failed to decode deeproxy filters response: %w", err)
75+
}
76+
77+
return allowList, nil
78+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package filters //nolint:testpackage // Testing private utility functions.
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, _ uuid.UUID, expectedAllow AllowList) (Client, func()) {
36+
t.Helper()
37+
38+
c := NewFakeClient(expectedAllow)
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+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package filters
2+
3+
import (
4+
"context"
5+
6+
"github.com/google/uuid"
7+
)
8+
9+
type FakeClient struct {
10+
getFilters func(ctx context.Context, orgID uuid.UUID) (AllowList, error)
11+
}
12+
13+
var _ Client = (*FakeClient)(nil)
14+
15+
func NewFakeClient(allowList AllowList) *FakeClient {
16+
return &FakeClient{
17+
getFilters: func(ctx context.Context, orgID uuid.UUID) (AllowList, error) {
18+
return allowList, nil
19+
},
20+
}
21+
}
22+
23+
func (f *FakeClient) GetFilters(ctx context.Context, orgID uuid.UUID) (AllowList, error) {
24+
return f.getFilters(ctx, orgID)
25+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package filters
2+
3+
import "net/http"
4+
5+
// Opt is a function that configures an deeproxyClient instance.
6+
type Opt func(*DeeproxyClient)
7+
8+
// WithHTTPClient sets a custom HTTP client for the filters client.
9+
func WithHTTPClient(httpClient *http.Client) Opt {
10+
return func(c *DeeproxyClient) {
11+
c.httpClient = httpClient
12+
}
13+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package filters
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/google/uuid"
8+
)
9+
10+
func getFilterURL(baseURL string, orgID uuid.UUID, isFedRamp bool) string {
11+
if isFedRamp {
12+
return fmt.Sprintf("%s/hidden/orgs/%s/code/filters", baseURL, orgID)
13+
}
14+
15+
deeproxyURL := strings.ReplaceAll(baseURL, "api", "deeproxy")
16+
return fmt.Sprintf("%s/filters", deeproxyURL)
17+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package filters //nolint:testpackage // Testing private utility functions.
2+
3+
import (
4+
"testing"
5+
6+
"github.com/google/uuid"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
var orgID = uuid.MustParse("738ef92e-21cc-4a11-8c13-388d89272f4b")
11+
12+
func Test_getBaseUrl_notFedramp(t *testing.T) {
13+
actualURL := getFilterURL("https://api.snyk.io", orgID, false)
14+
assert.Equal(t, "https://deeproxy.snyk.io/filters", actualURL)
15+
}
16+
17+
func Test_getBaseUrl_fedramp(t *testing.T) {
18+
actualURL := getFilterURL("https://api.snyk.io", orgID, true)
19+
assert.Equal(t, "https://api.snyk.io/hidden/orgs/738ef92e-21cc-4a11-8c13-388d89272f4b/code/filters", actualURL)
20+
}

internal/api/fileupload/types.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package fileupload
2+
3+
import (
4+
"sync"
5+
6+
"github.com/puzpuzpuz/xsync"
7+
)
8+
9+
// Filters holds the filtering configuration for file uploads with thread-safe maps.
10+
type Filters struct {
11+
SupportedExtensions *xsync.MapOf[string, bool]
12+
SupportedConfigFiles *xsync.MapOf[string, bool]
13+
Once sync.Once
14+
InitErr error
15+
}

0 commit comments

Comments
 (0)