From d39145a592c1a36f50fe6688becb999aa68d1220 Mon Sep 17 00:00:00 2001 From: Teodora Sandu Date: Tue, 9 Apr 2024 16:55:06 +0100 Subject: [PATCH 1/2] feat: use workspace api --- .circleci/config.yml | 18 +- CONTRIBUTING.md | 4 +- Makefile | 6 +- README.md | 46 ++--- config/config.go | 3 + config/mocks/config.go | 14 ++ http/http.go | 38 ++-- http/http_test.go | 45 ++++- internal/analysis/analysis.go | 124 ++++++++++++- internal/analysis/analysis_test.go | 162 ++++++++++++++++- internal/analysis/mocks/analysis.go | 66 +++++++ internal/bundle/bundle_manager.go | 21 ++- internal/deepcode/client.go | 57 +++--- internal/deepcode/client_pact_test.go | 6 +- internal/deepcode/client_test.go | 121 +++++++++++++ ...i.json => code-client-go-snykcodeapi.json} | 2 +- internal/util/testutil/test_config.go | 44 +++++ .../workspace/2024-03-12/mocks/workspace.go | 78 +++++++++ internal/workspace/2024-03-12/workspace.go | 14 ++ scan.go | 56 ++++-- scan_smoke_test.go | 165 ++++++++++++++++++ scan_test.go | 29 +-- 22 files changed, 1002 insertions(+), 117 deletions(-) create mode 100644 internal/analysis/mocks/analysis.go rename internal/deepcode/pacts/{snykls-snykcodeapi.json => code-client-go-snykcodeapi.json} (97%) create mode 100644 internal/util/testutil/test_config.go create mode 100644 internal/workspace/2024-03-12/mocks/workspace.go create mode 100644 internal/workspace/2024-03-12/workspace.go create mode 100644 scan_smoke_test.go diff --git a/.circleci/config.yml b/.circleci/config.yml index d4cf325e..9ae02c2e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -34,6 +34,16 @@ jobs: - run: name: Run unit tests command: make test + smoke_test: + executor: default + steps: + - checkout + - run: + name: Install tools + command: make tools + - run: + name: Run smoke tests + command: make smoke-test build: executor: default steps: @@ -67,9 +77,15 @@ workflows: name: Unit tests requires: - Lint & Format + - smoke_test: + name: Smoke tests + context: + - code-client-go-smoke-tests-token + requires: + - Unit tests - build: name: Build requires: - - Unit tests + - Smoke tests - Security Scans - Scan repository for secrets diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7270f1dd..e89cffcd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,7 +59,9 @@ make test If writing unit tests, use the mocks generated by [GoMock](https://github.com/golang/mock) by running `make generate`. -If writing `pact` or integration tests, use the test implementations in [./internal/util/testutil](./internal/util/testutil). +If writing `pact`, integration, or smoke tests, use the test implementations in [./internal/util/testutil](./internal/util/testutil). + +The organisation used by the smoke tests is `ide-consistent-ignores-test` in [https://app.dev.snyk.io](https://app.dev.snyk.io) and we are authenticating using a service account api key. If you've changed any of the interfaces you may need to re-run `make generate` to generate the mocks again. diff --git a/Makefile b/Makefile index dac4c18c..c594e6e5 100644 --- a/Makefile +++ b/Makefile @@ -54,7 +54,11 @@ test: .PHONY: testv testv: @echo "Testing verbosely..." - @go test -v ./... + @go test -v + +.PHONY: smoke-test +smoke-test: + @go test -run="Test_SmokeScan" .PHONY: generate generate: $(TOOLS_BIN)/go/mockgen $(TOOLS_BIN)/go/oapi-codegen diff --git a/README.md b/README.md index cc909dd7..13b93ed2 100644 --- a/README.md +++ b/README.md @@ -38,44 +38,30 @@ The HTTP client exposes a `DoCall` function. Implement the `http.Config` interface to configure the Snyk Code API client from applications. -### Snyk Code Client - -Use the Snyk Code Client to make calls to the DeepCode API using the `httpClient` HTTP client created above. - -```go -snykCode := deepcode.NewSnykCodeClient(logger, httpClient, testutil.NewTestInstrumentor()) -``` - -The Snyk Code Client exposes the following functions: -- `GetFilters` -- `CreateBundle` -- `ExtendBundle` - -### Bundle Manager - -Use the Bundle Manager to create bundles using the `snykCode` Snyk Code Client created above and then to extend it by uploading more files to it. - -```go -bundleManager := bundle.NewBundleManager(logger, snykCode, testutil.NewTestInstrumentor(), testutil.NewTestCodeInstrumentor()) -``` - -The Bundle Manager exposes the following functions: -- `Create` -- `Upload` - ### Code Scanner Use the Code Scanner to trigger a scan for a Snyk Code workspace using the Bundle Manager created above. The Code Scanner exposes a `UploadAndAnalyze` function, which can be used like this: ```go -codeScanner := codeclient.NewCodeScanner( - bundleManager, - testutil.NewTestInstrumentor(), - testutil.NewTestErrorReporter(), +import ( + "net/http" + + "github.com/rs/zerolog" + code "github.com/snyk/code-client-go" +) + +logger := zerlog.NewLogger(...) +config := newConfigForMyApp() + +codeScanner := code.NewCodeScanner( + httpClient, + config, + codeInstrumentor, + codeErrorReporter, logger, ) -codeScanner.UploadAndAnalyze(context.Background(), "path/to/workspace", channelForWalkingFiles, changedFiles) +code.UploadAndAnalyze(context.Background(), requestId, "path/to/workspace", channelForWalkingFiles, changedFiles) ``` diff --git a/config/config.go b/config/config.go index ae4bb93e..f670ce7f 100644 --- a/config/config.go +++ b/config/config.go @@ -16,4 +16,7 @@ type Config interface { // SnykCodeApi returns the Snyk Code API URL configured to run against, which could be // the one used by the Local Code Engine. SnykCodeApi() string + + // SnykApi returns the Snyk REST API URL configured to run against, + SnykApi() string } diff --git a/config/mocks/config.go b/config/mocks/config.go index 428d2ca6..c1132b48 100644 --- a/config/mocks/config.go +++ b/config/mocks/config.go @@ -61,6 +61,20 @@ func (mr *MockConfigMockRecorder) Organization() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Organization", reflect.TypeOf((*MockConfig)(nil).Organization)) } +// SnykApi mocks base method. +func (m *MockConfig) SnykApi() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SnykApi") + ret0, _ := ret[0].(string) + return ret0 +} + +// SnykApi indicates an expected call of SnykApi. +func (mr *MockConfigMockRecorder) SnykApi() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SnykApi", reflect.TypeOf((*MockConfig)(nil).SnykApi)) +} + // SnykCodeApi mocks base method. func (m *MockConfig) SnykCodeApi() string { m.ctrl.T.Helper() diff --git a/http/http.go b/http/http.go index 5a2486eb..0c8bcada 100644 --- a/http/http.go +++ b/http/http.go @@ -18,7 +18,8 @@ package http import ( - "errors" + "bytes" + "io" "net/http" "time" @@ -78,19 +79,14 @@ func (s *httpClient) Do(req *http.Request) (response *http.Response, err error) return nil, err // no retries for errors } - err = s.checkResponseCode(response) - if err != nil { - if retryErrorCodes[response.StatusCode] { - s.logger.Debug().Err(err).Str("method", req.Method).Int("attempts done", i+1).Msg("retrying") - if i < retryCount-1 { - time.Sleep(5 * time.Second) - continue - } - // return the error on last try - return nil, err + if retryErrorCodes[response.StatusCode] { + s.logger.Debug().Err(err).Str("method", req.Method).Int("attempts done", i+1).Msg("retrying") + if i < retryCount-1 { + time.Sleep(5 * time.Second) + continue } - return nil, err } + // no error, we can break the retry loop break } @@ -99,7 +95,18 @@ func (s *httpClient) Do(req *http.Request) (response *http.Response, err error) func (s *httpClient) httpCall(req *http.Request) (*http.Response, error) { log := s.logger.With().Str("method", "http.httpCall").Logger() + + // store the request body so that after retrying it can be read again + var copyReqBody io.ReadCloser + if req.Body != nil { + buf, _ := io.ReadAll(req.Body) + reqBody := io.NopCloser(bytes.NewBuffer(buf)) + copyReqBody = io.NopCloser(bytes.NewBuffer(buf)) + req.Body = reqBody + } response, err := s.clientFactory().Do(req) + req.Body = copyReqBody + if err != nil { log.Error().Err(err).Msg("got http error") s.errorReporter.CaptureError(err, observability.ErrorReporterOptions{ErrorDiagnosticPath: req.RequestURI}) @@ -108,10 +115,3 @@ func (s *httpClient) httpCall(req *http.Request) (*http.Response, error) { return response, nil } - -func (s *httpClient) checkResponseCode(r *http.Response) error { - if r.StatusCode >= 200 && r.StatusCode <= 299 { - return nil - } - return errors.New("Unexpected response code: " + r.Status) -} diff --git a/http/http_test.go b/http/http_test.go index d4ef559f..2a8a0c01 100644 --- a/http/http_test.go +++ b/http/http_test.go @@ -16,7 +16,10 @@ package http_test import ( + "fmt" + "io" "net/http" + "strings" "testing" "github.com/golang/mock/gomock" @@ -36,8 +39,17 @@ type dummyTransport struct { calls int } -func (d *dummyTransport) RoundTrip(_ *http.Request) (*http.Response, error) { +func (d *dummyTransport) RoundTrip(req *http.Request) (*http.Response, error) { d.calls++ + if req.Body != nil { + body, err := io.ReadAll(req.Body) + if err != nil { + return nil, err + } + if string(body) == "" { + return nil, fmt.Errorf("body is empty") + } + } return &http.Response{ StatusCode: d.responseCode, Status: d.status, @@ -64,8 +76,35 @@ func TestSnykCodeBackendService_DoCall_shouldRetry(t *testing.T) { require.NoError(t, err) s := codeClientHTTP.NewHTTPClient(newLogger(t), dummyClientFactory, mockInstrumentor, mockErrorReporter) - _, err = s.Do(req) - assert.Error(t, err) + res, err := s.Do(req) + assert.NoError(t, err) + assert.NotNil(t, res) + assert.Equal(t, 3, d.calls) +} + +func TestSnykCodeBackendService_DoCall_shouldRetryWithARequestBody(t *testing.T) { + d := &dummyTransport{responseCode: 502, status: "502 Bad Gateway"} + dummyClientFactory := func() *http.Client { + return &http.Client{ + Transport: d, + } + } + + ctrl := gomock.NewController(t) + mockSpan := mocks.NewMockSpan(ctrl) + mockSpan.EXPECT().GetTraceId().AnyTimes() + mockInstrumentor := mocks.NewMockInstrumentor(ctrl) + mockInstrumentor.EXPECT().StartSpan(gomock.Any(), gomock.Any()).Return(mockSpan).Times(1) + mockInstrumentor.EXPECT().Finish(gomock.Any()).Times(1) + mockErrorReporter := mocks.NewMockErrorReporter(ctrl) + + req, err := http.NewRequest(http.MethodGet, "https://httpstat.us/500", io.NopCloser(strings.NewReader("body"))) + require.NoError(t, err) + + s := codeClientHTTP.NewHTTPClient(newLogger(t), dummyClientFactory, mockInstrumentor, mockErrorReporter) + res, err := s.Do(req) + assert.NoError(t, err) + assert.NotNil(t, res) assert.Equal(t, 3, d.calls) } diff --git a/internal/analysis/analysis.go b/internal/analysis/analysis.go index c47202ee..980dc072 100644 --- a/internal/analysis/analysis.go +++ b/internal/analysis/analysis.go @@ -18,16 +18,138 @@ package analysis import ( + "context" _ "embed" "encoding/json" "fmt" + "github.com/google/uuid" + "github.com/pkg/errors" + "github.com/rs/zerolog" + "github.com/snyk/code-client-go/config" + codeClientHTTP "github.com/snyk/code-client-go/http" + "github.com/snyk/code-client-go/internal/util" + workspaceClient "github.com/snyk/code-client-go/internal/workspace/2024-03-12" + externalRef3 "github.com/snyk/code-client-go/internal/workspace/2024-03-12/workspaces" + "github.com/snyk/code-client-go/observability" "github.com/snyk/code-client-go/sarif" ) //go:embed fake.json var fakeResponse []byte -func RunAnalysis() (*sarif.SarifResponse, error) { +type analysisOrchestrator struct { + httpClient codeClientHTTP.HTTPClient + instrumentor observability.Instrumentor + errorReporter observability.ErrorReporter + logger *zerolog.Logger + config config.Config +} + +//go:generate mockgen -destination=mocks/analysis.go -source=analysis.go -package mocks +type AnalysisOrchestrator interface { + CreateWorkspace(ctx context.Context, orgId string, requestId string, path string, bundleHash string) (string, error) + RunAnalysis() (*sarif.SarifResponse, error) +} + +func NewAnalysisOrchestrator( + logger *zerolog.Logger, + httpClient codeClientHTTP.HTTPClient, + instrumentor observability.Instrumentor, + errorReporter observability.ErrorReporter, + config config.Config, +) *analysisOrchestrator { + return &analysisOrchestrator{ + httpClient, + instrumentor, + errorReporter, + logger, + config, + } +} + +func (a *analysisOrchestrator) CreateWorkspace(ctx context.Context, orgId string, requestId string, path string, bundleHash string) (string, error) { + method := "analysis.CreateWorkspace" + log := a.logger.With().Str("method", method).Logger() + log.Debug().Msg("API: Creating the workspace") + + span := a.instrumentor.StartSpan(ctx, method) + defer a.instrumentor.Finish(span) + + orgUUID := uuid.MustParse(orgId) + + repositoryUri, err := util.GetRepositoryUrl(path) + if err != nil { + a.errorReporter.CaptureError(err, observability.ErrorReporterOptions{ErrorDiagnosticPath: path}) + return "", fmt.Errorf("workspace is not a repository, cannot scan, %w", err) + } + + a.logger.Info().Str("path", path).Str("repositoryUri", repositoryUri).Str("bundleHash", bundleHash).Msg("creating workspace") + + workspace, err := workspaceClient.NewClientWithResponses(fmt.Sprintf("%s/hidden", a.config.SnykApi()), workspaceClient.WithHTTPClient(a.httpClient)) + if err != nil { + a.errorReporter.CaptureError(err, observability.ErrorReporterOptions{ErrorDiagnosticPath: path}) + return "", fmt.Errorf("failed to connect to the workspace API %w", err) + } + + workspaceResponse, err := workspace.CreateWorkspaceWithApplicationVndAPIPlusJSONBodyWithResponse(ctx, orgUUID, &workspaceClient.CreateWorkspaceParams{ + Version: "2024-03-12~experimental", + SnykRequestId: uuid.MustParse(requestId), + ContentType: "application/vnd.api+json", + UserAgent: "cli", + }, workspaceClient.CreateWorkspaceApplicationVndAPIPlusJSONRequestBody{ + Data: struct { + Attributes struct { + BundleId string `json:"bundle_id"` + RepositoryUri string `json:"repository_uri"` + WorkspaceType externalRef3.WorkspacePostRequestDataAttributesWorkspaceType `json:"workspace_type"` + } `json:"attributes"` + Type externalRef3.WorkspacePostRequestDataType `json:"type"` + }(struct { + Attributes struct { + BundleId string `json:"bundle_id"` + RepositoryUri string `json:"repository_uri"` + WorkspaceType externalRef3.WorkspacePostRequestDataAttributesWorkspaceType `json:"workspace_type"` + } + Type externalRef3.WorkspacePostRequestDataType + }{Attributes: struct { + BundleId string `json:"bundle_id"` + RepositoryUri string `json:"repository_uri"` + WorkspaceType externalRef3.WorkspacePostRequestDataAttributesWorkspaceType `json:"workspace_type"` + }(struct { + BundleId string + RepositoryUri string + WorkspaceType externalRef3.WorkspacePostRequestDataAttributesWorkspaceType + }{ + BundleId: bundleHash, + RepositoryUri: repositoryUri, + WorkspaceType: "file_bundle_workspace", + }), + Type: "workspace", + }), + }) + if err != nil { + return "", err + } + + if workspaceResponse.ApplicationvndApiJSON201 == nil { + var msg string + switch workspaceResponse.StatusCode() { + case 400: + msg = workspaceResponse.ApplicationvndApiJSON400.Errors[0].Detail + case 401: + msg = workspaceResponse.ApplicationvndApiJSON401.Errors[0].Detail + case 403: + msg = workspaceResponse.ApplicationvndApiJSON403.Errors[0].Detail + case 500: + msg = workspaceResponse.ApplicationvndApiJSON500.Errors[0].Detail + } + return "", errors.New(msg) + } + + return workspaceResponse.ApplicationvndApiJSON201.Data.Id.String(), nil +} + +func (*analysisOrchestrator) RunAnalysis() (*sarif.SarifResponse, error) { var response sarif.SarifResponse err := json.Unmarshal(fakeResponse, &response) diff --git a/internal/analysis/analysis_test.go b/internal/analysis/analysis_test.go index f67f3464..a0830f8c 100644 --- a/internal/analysis/analysis_test.go +++ b/internal/analysis/analysis_test.go @@ -1,17 +1,177 @@ +/* + * © 2024 Snyk Limited All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package analysis_test import ( + "bytes" + "context" + "fmt" + "github.com/stretchr/testify/mock" + "io" + "net/http" "testing" + "github.com/golang/mock/gomock" + "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + confMocks "github.com/snyk/code-client-go/config/mocks" + httpmocks "github.com/snyk/code-client-go/http/mocks" "github.com/snyk/code-client-go/internal/analysis" + "github.com/snyk/code-client-go/observability/mocks" "github.com/snyk/code-client-go/sarif" ) +func TestAnalysis_CreateWorkspace(t *testing.T) { + ctrl := gomock.NewController(t) + mockSpan := mocks.NewMockSpan(ctrl) + mockSpan.EXPECT().GetTraceId().AnyTimes() + mockSpan.EXPECT().Context().AnyTimes() + mockConfig := confMocks.NewMockConfig(ctrl) + mockConfig.EXPECT().Organization().AnyTimes().Return("") + mockConfig.EXPECT().SnykApi().AnyTimes().Return("http://localhost") + + mockHTTPClient := httpmocks.NewMockHTTPClient(ctrl) + mockHTTPClient.EXPECT().Do( + mock.MatchedBy(func(i interface{}) bool { + req := i.(*http.Request) + return req.URL.String() == "http://localhost/hidden/orgs/4a72d1db-b465-4764-99e1-ecedad03b06a/workspaces?version=2024-03-12~experimental" && + req.Method == "POST" && + req.Header.Get("Content-Type") == "application/vnd.api+json" && + req.Header.Get("Snyk-Request-Id") == "b372d1db-b465-4764-99e1-ecedad03b06a" && + req.Header.Get("User-Agent") == "cli" + }), + ).Return(&http.Response{ + StatusCode: http.StatusCreated, + Header: http.Header{ + "Content-Type": []string{"application/vnd.api+json"}, + }, + Body: io.NopCloser(bytes.NewReader([]byte(`{"data":{"id": "c172d1db-b465-4764-99e1-ecedad03b06a"}}`))), + }, nil).Times(1) + + mockInstrumentor := mocks.NewMockInstrumentor(ctrl) + mockInstrumentor.EXPECT().StartSpan(gomock.Any(), gomock.Any()).Return(mockSpan).AnyTimes() + mockInstrumentor.EXPECT().Finish(gomock.Any()).AnyTimes() + mockErrorReporter := mocks.NewMockErrorReporter(ctrl) + + logger := zerolog.Nop() + + analysisOrchestrator := analysis.NewAnalysisOrchestrator(&logger, mockHTTPClient, mockInstrumentor, mockErrorReporter, mockConfig) + _, err := analysisOrchestrator.CreateWorkspace( + context.Background(), + "4a72d1db-b465-4764-99e1-ecedad03b06a", + "b372d1db-b465-4764-99e1-ecedad03b06a", + "../../", + "testBundleHash") + fmt.Println(err) + assert.NoError(t, err) +} + +func TestAnalysis_CreateWorkspace_NotARepository(t *testing.T) { + ctrl := gomock.NewController(t) + mockSpan := mocks.NewMockSpan(ctrl) + mockSpan.EXPECT().GetTraceId().AnyTimes() + mockSpan.EXPECT().Context().AnyTimes() + mockConfig := confMocks.NewMockConfig(ctrl) + mockConfig.EXPECT().SnykCodeApi().AnyTimes().Return("http://localhost") + + mockHTTPClient := httpmocks.NewMockHTTPClient(ctrl) + + mockInstrumentor := mocks.NewMockInstrumentor(ctrl) + mockInstrumentor.EXPECT().StartSpan(gomock.Any(), gomock.Any()).Return(mockSpan).Times(1) + mockInstrumentor.EXPECT().Finish(gomock.Any()).Times(1) + mockErrorReporter := mocks.NewMockErrorReporter(ctrl) + mockErrorReporter.EXPECT().CaptureError(gomock.Any(), gomock.Any()) + + logger := zerolog.Nop() + + analysisOrchestrator := analysis.NewAnalysisOrchestrator(&logger, mockHTTPClient, mockInstrumentor, mockErrorReporter, mockConfig) + _, err := analysisOrchestrator.CreateWorkspace( + context.Background(), + "4a72d1db-b465-4764-99e1-ecedad03b06a", + "b372d1db-b465-4764-99e1-ecedad03b06a", + "../", + "testBundleHash") + assert.ErrorContains(t, err, "open local repository") +} + +func TestAnalysis_CreateWorkspace_Failure(t *testing.T) { + ctrl := gomock.NewController(t) + mockSpan := mocks.NewMockSpan(ctrl) + mockSpan.EXPECT().GetTraceId().AnyTimes() + mockSpan.EXPECT().Context().AnyTimes() + mockConfig := confMocks.NewMockConfig(ctrl) + mockConfig.EXPECT().SnykApi().AnyTimes().Return("http://localhost") + + mockHTTPClient := httpmocks.NewMockHTTPClient(ctrl) + mockHTTPClient.EXPECT().Do( + mock.MatchedBy(func(i interface{}) bool { + req := i.(*http.Request) + return req.URL.String() == "http://localhost/hidden/orgs/4a72d1db-b465-4764-99e1-ecedad03b06a/workspaces?version=2024-03-12~experimental" && + req.Method == "POST" && + req.Header.Get("Content-Type") == "application/vnd.api+json" && + req.Header.Get("Snyk-Request-Id") == "b372d1db-b465-4764-99e1-ecedad03b06a" && + req.Header.Get("User-Agent") == "cli" + }), + ).Return(&http.Response{ + StatusCode: http.StatusBadRequest, + Header: http.Header{ + "Content-Type": []string{"application/vnd.api+json"}, + }, + Body: io.NopCloser(bytes.NewReader([]byte(`{"errors": [{"detail": "error detail", "status": "400"}], "jsonapi": {"version": "version"}}`))), + }, nil).Times(1) + + mockInstrumentor := mocks.NewMockInstrumentor(ctrl) + mockInstrumentor.EXPECT().StartSpan(gomock.Any(), gomock.Any()).Return(mockSpan).AnyTimes() + mockInstrumentor.EXPECT().Finish(gomock.Any()).AnyTimes() + mockErrorReporter := mocks.NewMockErrorReporter(ctrl) + + logger := zerolog.Nop() + + analysisOrchestrator := analysis.NewAnalysisOrchestrator(&logger, mockHTTPClient, mockInstrumentor, mockErrorReporter, mockConfig) + _, err := analysisOrchestrator.CreateWorkspace( + context.Background(), + "4a72d1db-b465-4764-99e1-ecedad03b06a", + "b372d1db-b465-4764-99e1-ecedad03b06a", + "../../", + "testBundleHash") + assert.ErrorContains(t, err, "error detail") +} + func TestAnalysis_RunAnalysis(t *testing.T) { - actual, err := analysis.RunAnalysis() + ctrl := gomock.NewController(t) + mockSpan := mocks.NewMockSpan(ctrl) + mockSpan.EXPECT().GetTraceId().AnyTimes() + mockSpan.EXPECT().Context().AnyTimes() + mockConfig := confMocks.NewMockConfig(ctrl) + mockConfig.EXPECT().Organization().AnyTimes().Return("") + mockConfig.EXPECT().SnykCodeApi().AnyTimes().Return("http://localhost") + + mockHTTPClient := httpmocks.NewMockHTTPClient(ctrl) + + mockInstrumentor := mocks.NewMockInstrumentor(ctrl) + mockInstrumentor.EXPECT().StartSpan(gomock.Any(), gomock.Any()).Return(mockSpan).AnyTimes() + mockInstrumentor.EXPECT().Finish(gomock.Any()).AnyTimes() + mockErrorReporter := mocks.NewMockErrorReporter(ctrl) + + logger := zerolog.Nop() + + analysisOrchestrator := analysis.NewAnalysisOrchestrator(&logger, mockHTTPClient, mockInstrumentor, mockErrorReporter, mockConfig) + actual, err := analysisOrchestrator.RunAnalysis() require.NoError(t, err) assert.Equal(t, "COMPLETE", actual.Status) assert.Contains(t, actual.Sarif.Runs[0].Results[0].Locations[0].PhysicalLocation.ArtifactLocation.URI, "scripts/db/migrations/20230811153738_add_generated_grouping_columns_to_collections_table.ts") diff --git a/internal/analysis/mocks/analysis.go b/internal/analysis/mocks/analysis.go new file mode 100644 index 00000000..f04680b7 --- /dev/null +++ b/internal/analysis/mocks/analysis.go @@ -0,0 +1,66 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: analysis.go + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + sarif "github.com/snyk/code-client-go/sarif" +) + +// MockAnalysisOrchestrator is a mock of AnalysisOrchestrator interface. +type MockAnalysisOrchestrator struct { + ctrl *gomock.Controller + recorder *MockAnalysisOrchestratorMockRecorder +} + +// MockAnalysisOrchestratorMockRecorder is the mock recorder for MockAnalysisOrchestrator. +type MockAnalysisOrchestratorMockRecorder struct { + mock *MockAnalysisOrchestrator +} + +// NewMockAnalysisOrchestrator creates a new mock instance. +func NewMockAnalysisOrchestrator(ctrl *gomock.Controller) *MockAnalysisOrchestrator { + mock := &MockAnalysisOrchestrator{ctrl: ctrl} + mock.recorder = &MockAnalysisOrchestratorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAnalysisOrchestrator) EXPECT() *MockAnalysisOrchestratorMockRecorder { + return m.recorder +} + +// CreateWorkspace mocks base method. +func (m *MockAnalysisOrchestrator) CreateWorkspace(ctx context.Context, orgId, requestId, path, bundleHash string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateWorkspace", ctx, orgId, requestId, path, bundleHash) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateWorkspace indicates an expected call of CreateWorkspace. +func (mr *MockAnalysisOrchestratorMockRecorder) CreateWorkspace(ctx, orgId, requestId, path, bundleHash interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateWorkspace", reflect.TypeOf((*MockAnalysisOrchestrator)(nil).CreateWorkspace), ctx, orgId, requestId, path, bundleHash) +} + +// RunAnalysis mocks base method. +func (m *MockAnalysisOrchestrator) RunAnalysis() (*sarif.SarifResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RunAnalysis") + ret0, _ := ret[0].(*sarif.SarifResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RunAnalysis indicates an expected call of RunAnalysis. +func (mr *MockAnalysisOrchestratorMockRecorder) RunAnalysis() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunAnalysis", reflect.TypeOf((*MockAnalysisOrchestrator)(nil).RunAnalysis)) +} diff --git a/internal/bundle/bundle_manager.go b/internal/bundle/bundle_manager.go index 5e5618f0..a60d9f34 100644 --- a/internal/bundle/bundle_manager.go +++ b/internal/bundle/bundle_manager.go @@ -21,18 +21,17 @@ import ( "os" "path/filepath" - deepcode2 "github.com/snyk/code-client-go/internal/deepcode" - "github.com/puzpuzpuz/xsync" "github.com/rs/zerolog" + "github.com/snyk/code-client-go/internal/deepcode" "github.com/snyk/code-client-go/internal/util" "github.com/snyk/code-client-go/observability" ) // TODO: add progress tracker for percentage progress type bundleManager struct { - SnykCode deepcode2.SnykCodeClient + SnykCode deepcode.SnykCodeClient instrumentor observability.Instrumentor errorReporter observability.ErrorReporter logger *zerolog.Logger @@ -53,13 +52,13 @@ type BundleManager interface { ctx context.Context, requestId string, originalBundle Bundle, - files map[string]deepcode2.BundleFile, + files map[string]deepcode.BundleFile, ) (Bundle, error) } func NewBundleManager( logger *zerolog.Logger, - SnykCode deepcode2.SnykCodeClient, + SnykCode deepcode.SnykCodeClient, instrumentor observability.Instrumentor, errorReporter observability.ErrorReporter, ) *bundleManager { @@ -84,7 +83,7 @@ func (b *bundleManager) Create(ctx context.Context, var limitToFiles []string fileHashes := make(map[string]string) - bundleFiles := make(map[string]deepcode2.BundleFile) + bundleFiles := make(map[string]deepcode.BundleFile) noFiles := true for absoluteFilePath := range filePaths { noFiles = false @@ -117,7 +116,7 @@ func (b *bundleManager) Create(ctx context.Context, } relativePath = util.EncodePath(relativePath) - bundleFile := deepcode2.BundleFileFrom(fileContent) + bundleFile := deepcode.BundleFileFrom(fileContent) bundleFiles[relativePath] = bundleFile fileHashes[relativePath] = bundleFile.Hash b.logger.Trace().Str("method", "BundleFileFrom").Str("hash", bundleFile.Hash).Str("filePath", absoluteFilePath).Msg("") @@ -153,7 +152,7 @@ func (b *bundleManager) Upload( ctx context.Context, requestId string, bundle Bundle, - files map[string]deepcode2.BundleFile, + files map[string]deepcode.BundleFile, ) (Bundle, error) { method := "code.Batch" s := b.instrumentor.StartSpan(ctx, method) @@ -183,14 +182,14 @@ func (b *bundleManager) Upload( func (b *bundleManager) groupInBatches( ctx context.Context, bundle Bundle, - files map[string]deepcode2.BundleFile, + files map[string]deepcode.BundleFile, ) []*Batch { method := "code.groupInBatches" s := b.instrumentor.StartSpan(ctx, method) defer b.instrumentor.Finish(s) var batches []*Batch - batch := NewBatch(map[string]deepcode2.BundleFile{}) + batch := NewBatch(map[string]deepcode.BundleFile{}) for _, filePath := range bundle.GetMissingFiles() { if len(batches) == 0 { // first batch added after first file found batches = append(batches, batch) @@ -203,7 +202,7 @@ func (b *bundleManager) groupInBatches( batch.documents[filePath] = file } else { b.logger.Trace().Str("path", filePath).Int("size", len(fileContent)).Msgf("created new deepCodeBundle - %v bundles in this upload so far", len(batches)) - newUploadBatch := NewBatch(map[string]deepcode2.BundleFile{}) + newUploadBatch := NewBatch(map[string]deepcode.BundleFile{}) newUploadBatch.documents[filePath] = file batches = append(batches, newUploadBatch) batch = newUploadBatch diff --git a/internal/deepcode/client.go b/internal/deepcode/client.go index 07b9e158..a4d18ea8 100644 --- a/internal/deepcode/client.go +++ b/internal/deepcode/client.go @@ -21,6 +21,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "github.com/snyk/code-client-go/config" "github.com/snyk/code-client-go/internal/util/encoding" "io" @@ -84,26 +85,27 @@ func NewSnykCodeClient( errorReporter observability.ErrorReporter, config config.Config, ) *snykCodeClient { - return &snykCodeClient{httpClient, instrumentor, errorReporter, logger, config} + return &snykCodeClient{ + httpClient, + instrumentor, + errorReporter, + logger, + config, + } } func (s *snykCodeClient) GetFilters(ctx context.Context) ( filters FiltersResponse, err error, ) { - method := "code.GetFilters" + method := "deepcode.GetFilters" log := s.logger.With().Str("method", method).Logger() log.Debug().Msg("API: Getting file extension filters") span := s.instrumentor.StartSpan(ctx, method) defer s.instrumentor.Finish(span) - host, err := s.Host() - if err != nil { - return FiltersResponse{ConfigFiles: nil, Extensions: nil}, err - } - - responseBody, err := s.Request(host, http.MethodGet, "/filters", nil) + responseBody, err := s.Request(http.MethodGet, "/filters", nil) if err != nil { return FiltersResponse{ConfigFiles: nil, Extensions: nil}, err } @@ -120,24 +122,19 @@ func (s *snykCodeClient) CreateBundle( ctx context.Context, filesToFilehashes map[string]string, ) (string, []string, error) { - method := "code.CreateBundle" + method := "deepcode.CreateBundle" log := s.logger.With().Str("method", method).Logger() log.Debug().Msg("API: Creating bundle for " + strconv.Itoa(len(filesToFilehashes)) + " files") span := s.instrumentor.StartSpan(ctx, method) defer s.instrumentor.Finish(span) - host, err := s.Host() - if err != nil { - return "", nil, err - } - requestBody, err := json.Marshal(filesToFilehashes) if err != nil { return "", nil, err } - responseBody, err := s.Request(host, http.MethodPost, "/bundle", requestBody) + responseBody, err := s.Request(http.MethodPost, "/bundle", requestBody) if err != nil { return "", nil, err } @@ -157,7 +154,7 @@ func (s *snykCodeClient) ExtendBundle( files map[string]BundleFile, removedFiles []string, ) (string, []string, error) { - method := "code.ExtendBundle" + method := "deepcode.ExtendBundle" log := s.logger.With().Str("method", method).Logger() log.Debug().Msg("API: Extending bundle for " + strconv.Itoa(len(files)) + " files") defer log.Debug().Str("method", method).Msg("API: Extend done") @@ -165,11 +162,6 @@ func (s *snykCodeClient) ExtendBundle( span := s.instrumentor.StartSpan(ctx, method) defer s.instrumentor.Finish(span) - host, err := s.Host() - if err != nil { - return "", nil, err - } - requestBody, err := json.Marshal(ExtendBundleRequest{ Files: files, RemovedFiles: removedFiles, @@ -178,7 +170,7 @@ func (s *snykCodeClient) ExtendBundle( return "", nil, err } - responseBody, err := s.Request(host, http.MethodPut, "/bundle/"+bundleHash, requestBody) + responseBody, err := s.Request(http.MethodPut, "/bundle/"+bundleHash, requestBody) if err != nil { return "", nil, err } @@ -213,13 +205,19 @@ func (s *snykCodeClient) Host() (string, error) { } func (s *snykCodeClient) Request( - host string, method string, path string, requestBody []byte, ) ([]byte, error) { log := s.logger.With().Str("method", "deepcode.Request").Logger() + host, err := s.Host() + if err != nil { + return nil, err + } + + s.logger.Trace().Str("requestBody", string(requestBody)).Msg("SEND TO REMOTE") + bodyBuffer, err := s.encodeIfNeeded(method, requestBody) if err != nil { return nil, err @@ -236,6 +234,12 @@ func (s *snykCodeClient) Request( if err != nil { return nil, err } + + err = s.checkResponseCode(response) + if err != nil { + return nil, err + } + defer func() { closeErr := response.Body.Close() if closeErr != nil { @@ -286,3 +290,10 @@ func (s *snykCodeClient) encodeIfNeeded(method string, requestBody []byte) (*byt func (s *snykCodeClient) mustBeEncoded(method string) bool { return method == http.MethodPost || method == http.MethodPut } + +func (s *snykCodeClient) checkResponseCode(r *http.Response) error { + if r.StatusCode >= 200 && r.StatusCode <= 299 { + return nil + } + return fmt.Errorf("Unexpected response code: %s", r.Status) +} diff --git a/internal/deepcode/client_pact_test.go b/internal/deepcode/client_pact_test.go index deefc8b6..a743fdba 100644 --- a/internal/deepcode/client_pact_test.go +++ b/internal/deepcode/client_pact_test.go @@ -34,7 +34,7 @@ import ( ) const ( - consumer = "SnykLS" + consumer = "code-client-go" pactDir = "./pacts" pactProvider = "SnykCodeApi" @@ -46,7 +46,7 @@ const ( var pact dsl.Pact var client deepcode.SnykCodeClient -func TestSnykCodeBackendServicePact(t *testing.T) { +func TestSnykCodeClientPact(t *testing.T) { setupPact(t) defer pact.Teardown() @@ -245,7 +245,7 @@ func getSnykRequestIdMatcher() dsl.Matcher { return dsl.Regex("fc763eba-0905-41c5-a27f-3934ab26786c", uuidMatcher) } -func TestSnykCodeBackendServicePact_LocalCodeEngine(t *testing.T) { +func TestSnykCodeClientPact_LocalCodeEngine(t *testing.T) { setupPact(t) defer pact.Teardown() diff --git a/internal/deepcode/client_test.go b/internal/deepcode/client_test.go index b921b338..199e458c 100644 --- a/internal/deepcode/client_test.go +++ b/internal/deepcode/client_test.go @@ -95,6 +95,39 @@ func TestSnykCodeBackendService_GetFilters(t *testing.T) { assert.Equal(t, 1, len(filters.ConfigFiles)) } +func TestSnykCodeBackendService_GetFilters_Failure(t *testing.T) { + ctrl := gomock.NewController(t) + mockSpan := mocks.NewMockSpan(ctrl) + mockSpan.EXPECT().GetTraceId().AnyTimes() + mockSpan.EXPECT().Context().AnyTimes() + mockConfig := confMocks.NewMockConfig(ctrl) + mockConfig.EXPECT().Organization().AnyTimes().Return("") + mockConfig.EXPECT().IsFedramp().AnyTimes().Return(false) + mockConfig.EXPECT().SnykCodeApi().AnyTimes().Return("http://localhost") + + mockHTTPClient := httpmocks.NewMockHTTPClient(ctrl) + mockHTTPClient.EXPECT().Do( + mock.MatchedBy(func(i interface{}) bool { + req := i.(*http.Request) + return req.URL.String() == "http://localhost/filters" && + req.Method == "GET" && + req.Header.Get("Cache-Control") == "private, max-age=0, no-cache" && + req.Header.Get("Content-Type") == "application/json" + }), + ).Return(&http.Response{ + StatusCode: http.StatusBadRequest, + }, nil).Times(1) + + mockInstrumentor := mocks.NewMockInstrumentor(ctrl) + mockInstrumentor.EXPECT().StartSpan(gomock.Any(), gomock.Any()).Return(mockSpan).Times(1) + mockInstrumentor.EXPECT().Finish(gomock.Any()).Times(1) + mockErrorReporter := mocks.NewMockErrorReporter(ctrl) + + s := deepcode.NewSnykCodeClient(newLogger(t), mockHTTPClient, mockInstrumentor, mockErrorReporter, mockConfig) + _, err := s.GetFilters(context.Background()) + assert.Error(t, err) +} + func TestSnykCodeBackendService_CreateBundle(t *testing.T) { ctrl := gomock.NewController(t) mockSpan := mocks.NewMockSpan(ctrl) @@ -136,6 +169,43 @@ func TestSnykCodeBackendService_CreateBundle(t *testing.T) { assert.Equal(t, 1, len(missingFiles)) } +func TestSnykCodeBackendService_CreateBundle_Failure(t *testing.T) { + ctrl := gomock.NewController(t) + mockSpan := mocks.NewMockSpan(ctrl) + mockSpan.EXPECT().GetTraceId().AnyTimes() + mockSpan.EXPECT().Context().AnyTimes() + mockConfig := confMocks.NewMockConfig(ctrl) + mockConfig.EXPECT().Organization().AnyTimes().Return("") + mockConfig.EXPECT().IsFedramp().AnyTimes().Return(false) + mockConfig.EXPECT().SnykCodeApi().AnyTimes().Return("http://localhost") + mockHTTPClient := httpmocks.NewMockHTTPClient(ctrl) + mockHTTPClient.EXPECT().Do( + mock.MatchedBy(func(i interface{}) bool { + req := i.(*http.Request) + return req.URL.String() == "http://localhost/bundle" && + req.Method == "POST" && + req.Header.Get("Cache-Control") == "private, max-age=0, no-cache" && + req.Header.Get("Content-Encoding") == "gzip" && + req.Header.Get("Content-Type") == "application/octet-stream" + }), + ).Return(&http.Response{ + StatusCode: http.StatusBadRequest, + }, nil).Times(1) + + mockInstrumentor := mocks.NewMockInstrumentor(ctrl) + mockInstrumentor.EXPECT().StartSpan(gomock.Any(), gomock.Any()).Return(mockSpan).Times(1) + mockInstrumentor.EXPECT().Finish(gomock.Any()).Times(1) + mockErrorReporter := mocks.NewMockErrorReporter(ctrl) + + s := deepcode.NewSnykCodeClient(newLogger(t), mockHTTPClient, mockInstrumentor, mockErrorReporter, mockConfig) + + files := map[string]string{} + randomAddition := fmt.Sprintf("\n public void random() { System.out.println(\"%d\") }", time.Now().UnixMicro()) + files[path1] = util.Hash([]byte(content + randomAddition)) + _, _, err := s.CreateBundle(context.Background(), files) + assert.Error(t, err) +} + func TestSnykCodeBackendService_ExtendBundle(t *testing.T) { ctrl := gomock.NewController(t) mockSpan := mocks.NewMockSpan(ctrl) @@ -190,6 +260,57 @@ func TestSnykCodeBackendService_ExtendBundle(t *testing.T) { assert.NotEmpty(t, bundleHash) } +func TestSnykCodeBackendService_ExtendBundle_Failure(t *testing.T) { + ctrl := gomock.NewController(t) + mockSpan := mocks.NewMockSpan(ctrl) + mockSpan.EXPECT().GetTraceId().AnyTimes() + mockSpan.EXPECT().Context().AnyTimes() + mockConfig := confMocks.NewMockConfig(ctrl) + mockConfig.EXPECT().Organization().AnyTimes().Return("") + mockConfig.EXPECT().IsFedramp().AnyTimes().Return(false) + mockConfig.EXPECT().SnykCodeApi().AnyTimes().Return("http://localhost") + mockHTTPClient := httpmocks.NewMockHTTPClient(ctrl) + mockHTTPClient.EXPECT().Do( + mock.MatchedBy(func(i interface{}) bool { + req := i.(*http.Request) + return req.URL.String() == "http://localhost/bundle" && + req.Method == "POST" && + req.Header.Get("Cache-Control") == "private, max-age=0, no-cache" && + req.Header.Get("Content-Encoding") == "gzip" && + req.Header.Get("Content-Type") == "application/octet-stream" + }), + ).Return(&http.Response{ + StatusCode: http.StatusCreated, + Body: io.NopCloser(bytes.NewReader([]byte(`{"bundleHash": "bundleHash", "missingFiles": ["test"]}`))), + }, nil).Times(1) + mockHTTPClient.EXPECT().Do( + mock.MatchedBy(func(i interface{}) bool { + req := i.(*http.Request) + return req.URL.String() == "http://localhost/bundle/bundleHash" && + req.Method == "PUT" && + req.Header.Get("Cache-Control") == "private, max-age=0, no-cache" && + req.Header.Get("Content-Encoding") == "gzip" && + req.Header.Get("Content-Type") == "application/octet-stream" + }), + ).Return(&http.Response{ + StatusCode: http.StatusBadRequest, + }, nil).Times(1) + mockInstrumentor := mocks.NewMockInstrumentor(ctrl) + mockInstrumentor.EXPECT().StartSpan(gomock.Any(), gomock.Any()).Return(mockSpan).Times(2) + mockInstrumentor.EXPECT().Finish(gomock.Any()).Times(2) + mockErrorReporter := mocks.NewMockErrorReporter(ctrl) + + s := deepcode.NewSnykCodeClient(newLogger(t), mockHTTPClient, mockInstrumentor, mockErrorReporter, mockConfig) + var removedFiles []string + files := map[string]string{} + files[path1] = util.Hash([]byte(content)) + bundleHash, _, _ := s.CreateBundle(context.Background(), files) + filesExtend := createTestExtendMap() + + _, _, err := s.ExtendBundle(context.Background(), bundleHash, filesExtend, removedFiles) + assert.Error(t, err) +} + func Test_Host(t *testing.T) { ctrl := gomock.NewController(t) mockConfig := confMocks.NewMockConfig(ctrl) diff --git a/internal/deepcode/pacts/snykls-snykcodeapi.json b/internal/deepcode/pacts/code-client-go-snykcodeapi.json similarity index 97% rename from internal/deepcode/pacts/snykls-snykcodeapi.json rename to internal/deepcode/pacts/code-client-go-snykcodeapi.json index a5bfada0..f7d00b06 100644 --- a/internal/deepcode/pacts/snykls-snykcodeapi.json +++ b/internal/deepcode/pacts/code-client-go-snykcodeapi.json @@ -1,6 +1,6 @@ { "consumer": { - "name": "SnykLS" + "name": "code-client-go" }, "provider": { "name": "SnykCodeApi" diff --git a/internal/util/testutil/test_config.go b/internal/util/testutil/test_config.go new file mode 100644 index 00000000..64b57f19 --- /dev/null +++ b/internal/util/testutil/test_config.go @@ -0,0 +1,44 @@ +/* + * © 2024 Snyk Limited All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testutil + +import ( + "github.com/snyk/code-client-go/config" +) + +type localConfig struct { +} + +func (l localConfig) Organization() string { + return "b29cf58e-6684-481d-aca4-b24b58821b85" +} + +func (l localConfig) IsFedramp() bool { + return false +} + +func (l localConfig) SnykCodeApi() string { + return "https://deeproxy.dev.snyk.io" +} + +func (l localConfig) SnykApi() string { + return "https://app.dev.snyk.io/api" +} + +// NewTestConfig is used in pact testing. +func NewTestConfig() config.Config { + return &localConfig{} +} diff --git a/internal/workspace/2024-03-12/mocks/workspace.go b/internal/workspace/2024-03-12/mocks/workspace.go new file mode 100644 index 00000000..23c67bfa --- /dev/null +++ b/internal/workspace/2024-03-12/mocks/workspace.go @@ -0,0 +1,78 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: workspace.go + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + io "io" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + v20240312 "github.com/snyk/code-client-go/internal/workspace/2024-03-12" + v202403120 "github.com/snyk/code-client-go/internal/workspace/2024-03-12/parameters" +) + +// MockWorkspace is a mock of Workspace interface. +type MockWorkspace struct { + ctrl *gomock.Controller + recorder *MockWorkspaceMockRecorder +} + +// MockWorkspaceMockRecorder is the mock recorder for MockWorkspace. +type MockWorkspaceMockRecorder struct { + mock *MockWorkspace +} + +// NewMockWorkspace creates a new mock instance. +func NewMockWorkspace(ctrl *gomock.Controller) *MockWorkspace { + mock := &MockWorkspace{ctrl: ctrl} + mock.recorder = &MockWorkspaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockWorkspace) EXPECT() *MockWorkspaceMockRecorder { + return m.recorder +} + +// CreateWorkspaceWithApplicationVndAPIPlusJSONBodyWithResponse mocks base method. +func (m *MockWorkspace) CreateWorkspaceWithApplicationVndAPIPlusJSONBodyWithResponse(ctx context.Context, orgId v202403120.OrgId, params *v20240312.CreateWorkspaceParams, body v20240312.CreateWorkspaceApplicationVndAPIPlusJSONRequestBody, reqEditors ...v20240312.RequestEditorFn) (*v20240312.CreateWorkspaceResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, orgId, params, body} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CreateWorkspaceWithApplicationVndAPIPlusJSONBodyWithResponse", varargs...) + ret0, _ := ret[0].(*v20240312.CreateWorkspaceResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateWorkspaceWithApplicationVndAPIPlusJSONBodyWithResponse indicates an expected call of CreateWorkspaceWithApplicationVndAPIPlusJSONBodyWithResponse. +func (mr *MockWorkspaceMockRecorder) CreateWorkspaceWithApplicationVndAPIPlusJSONBodyWithResponse(ctx, orgId, params, body interface{}, reqEditors ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, orgId, params, body}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateWorkspaceWithApplicationVndAPIPlusJSONBodyWithResponse", reflect.TypeOf((*MockWorkspace)(nil).CreateWorkspaceWithApplicationVndAPIPlusJSONBodyWithResponse), varargs...) +} + +// CreateWorkspaceWithBodyWithResponse mocks base method. +func (m *MockWorkspace) CreateWorkspaceWithBodyWithResponse(ctx context.Context, orgId v202403120.OrgId, params *v20240312.CreateWorkspaceParams, contentType string, body io.Reader, reqEditors ...v20240312.RequestEditorFn) (*v20240312.CreateWorkspaceResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, orgId, params, contentType, body} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CreateWorkspaceWithBodyWithResponse", varargs...) + ret0, _ := ret[0].(*v20240312.CreateWorkspaceResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateWorkspaceWithBodyWithResponse indicates an expected call of CreateWorkspaceWithBodyWithResponse. +func (mr *MockWorkspaceMockRecorder) CreateWorkspaceWithBodyWithResponse(ctx, orgId, params, contentType, body interface{}, reqEditors ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, orgId, params, contentType, body}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateWorkspaceWithBodyWithResponse", reflect.TypeOf((*MockWorkspace)(nil).CreateWorkspaceWithBodyWithResponse), varargs...) +} diff --git a/internal/workspace/2024-03-12/workspace.go b/internal/workspace/2024-03-12/workspace.go new file mode 100644 index 00000000..d34a476d --- /dev/null +++ b/internal/workspace/2024-03-12/workspace.go @@ -0,0 +1,14 @@ +package v20240312 + +import ( + "context" + "io" + + externalRef2 "github.com/snyk/code-client-go/internal/workspace/2024-03-12/parameters" +) + +//go:generate mockgen -destination=mocks/workspace.go -source=workspace.go -package mocks +type Workspace interface { + CreateWorkspaceWithBodyWithResponse(ctx context.Context, orgId externalRef2.OrgId, params *CreateWorkspaceParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateWorkspaceResponse, error) + CreateWorkspaceWithApplicationVndAPIPlusJSONBodyWithResponse(ctx context.Context, orgId externalRef2.OrgId, params *CreateWorkspaceParams, body CreateWorkspaceApplicationVndAPIPlusJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateWorkspaceResponse, error) +} diff --git a/scan.go b/scan.go index c39e5f2b..8173fcb4 100644 --- a/scan.go +++ b/scan.go @@ -22,7 +22,6 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog" - "github.com/rs/zerolog/log" "github.com/snyk/code-client-go/config" codeClientHTTP "github.com/snyk/code-client-go/http" @@ -34,9 +33,11 @@ import ( ) type codeScanner struct { - bundleManager bundle.BundleManager - errorReporter observability.ErrorReporter - logger *zerolog.Logger + bundleManager bundle.BundleManager + analysisOrchestrator analysis.AnalysisOrchestrator + errorReporter observability.ErrorReporter + logger *zerolog.Logger + config config.Config } type CodeScanner interface { @@ -59,10 +60,13 @@ func NewCodeScanner( ) *codeScanner { snykCode := deepcode.NewSnykCodeClient(logger, httpClient, instrumentor, errorReporter, config) bundleManager := bundle.NewBundleManager(logger, snykCode, instrumentor, errorReporter) + analysisOrchestrator := analysis.NewAnalysisOrchestrator(logger, httpClient, instrumentor, errorReporter, config) return &codeScanner{ - bundleManager: bundleManager, - errorReporter: errorReporter, - logger: logger, + bundleManager: bundleManager, + analysisOrchestrator: analysisOrchestrator, + errorReporter: errorReporter, + logger: logger, + config: config, } } @@ -70,9 +74,23 @@ func NewCodeScanner( // It can be used to replace the bundle manager in tests. func (c *codeScanner) WithBundleManager(bundleManager bundle.BundleManager) *codeScanner { return &codeScanner{ - bundleManager: bundleManager, - errorReporter: c.errorReporter, - logger: c.logger, + bundleManager: bundleManager, + analysisOrchestrator: c.analysisOrchestrator, + errorReporter: c.errorReporter, + logger: c.logger, + config: c.config, + } +} + +// WithAnalysisOrchestrator creates a new Code Scanner from the current one and replaces the analysis orchestrator. +// It can be used to replace the analysis orchestrator in tests. +func (c *codeScanner) WithAnalysisOrchestrator(analysisOrchestrator analysis.AnalysisOrchestrator) *codeScanner { + return &codeScanner{ + bundleManager: c.bundleManager, + analysisOrchestrator: analysisOrchestrator, + errorReporter: c.errorReporter, + logger: c.logger, + config: c.config, } } @@ -118,7 +136,7 @@ func (c *codeScanner) UploadAndAnalyze( c.errorReporter.CaptureError(errors.Wrap(err, msg), observability.ErrorReporterOptions{ErrorDiagnosticPath: path}) return nil, bundleHash, err } else { - log.Info().Msg("Canceling Code scan - Code scanner received cancellation signal") + c.logger.Info().Msg("Canceling Code scan - Code scanner received cancellation signal") return nil, bundleHash, nil } } @@ -128,7 +146,21 @@ func (c *codeScanner) UploadAndAnalyze( return nil, bundleHash, nil } - response, err := analysis.RunAnalysis() + workspaceId, err := c.analysisOrchestrator.CreateWorkspace(ctx, c.config.Organization(), requestId, path, bundleHash) + if err != nil { + if ctx.Err() == nil { // Only handle errors that are not intentional cancellations + msg := "error creating workspace for bundle..." + c.errorReporter.CaptureError(errors.Wrap(err, msg), observability.ErrorReporterOptions{ErrorDiagnosticPath: path}) + return nil, bundleHash, err + } else { + c.logger.Info().Msg("Canceling Code scan - Code scanner received cancellation signal") + return nil, bundleHash, nil + } + } + + c.logger.Info().Str("workspaceId", workspaceId).Msg("finished wrapping the bundle in a workspace") + + response, err := c.analysisOrchestrator.RunAnalysis() if ctx.Err() != nil { c.logger.Info().Msg("Canceling Code scan - Code scanner received cancellation signal") return nil, bundleHash, nil diff --git a/scan_smoke_test.go b/scan_smoke_test.go new file mode 100644 index 00000000..84996a9a --- /dev/null +++ b/scan_smoke_test.go @@ -0,0 +1,165 @@ +/* + * © 2024 Snyk Limited All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package codeclient_test + +import ( + "context" + "fmt" + "github.com/rs/zerolog/log" + "net/http" + "os" + "os/exec" + "path/filepath" + "testing" + "time" + + "github.com/google/uuid" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" + + codeClient "github.com/snyk/code-client-go" + codeClientHTTP "github.com/snyk/code-client-go/http" + "github.com/snyk/code-client-go/internal/util/testutil" +) + +//nolint:dupl // test cases +func Test_SmokeScan_HTTPS(t *testing.T) { + if os.Getenv("SMOKE_TESTS") != "true" { + t.Skip() + } + var cloneTargetDir, err = setupCustomTestRepo(t, "https://github.com/snyk-labs/nodejs-goof", "0336589") + defer func(path string) { _ = os.RemoveAll(path) }(cloneTargetDir) + if err != nil { + t.Fatal(err, "Couldn't setup test repo") + } + files := sliceToChannel([]string{filepath.Join(cloneTargetDir, "app.js"), filepath.Join(cloneTargetDir, "utils.js")}) + + logger := zerolog.New(os.Stdout) + instrumentor := testutil.NewTestInstrumentor() + errorReporter := testutil.NewTestErrorReporter() + config := testutil.NewTestConfig() + httpClient := codeClientHTTP.NewHTTPClient(&logger, func() *http.Client { + client := http.Client{ + Timeout: time.Duration(180) * time.Second, + Transport: TestAuthRoundTripper{http.DefaultTransport}, + } + return &client + }, instrumentor, errorReporter) + + codeScanner := codeClient.NewCodeScanner(httpClient, config, instrumentor, errorReporter, &logger) + response, bundleHash, scanErr := codeScanner.UploadAndAnalyze(context.Background(), uuid.New().String(), cloneTargetDir, files, map[string]bool{}) + require.NoError(t, scanErr) + require.NotEmpty(t, bundleHash) + require.NotNil(t, response) +} + +//nolint:dupl // test cases +func Test_SmokeScan_SSH(t *testing.T) { + if os.Getenv("SMOKE_TESTS") != "true" { + t.Skip() + } + var cloneTargetDir, err = setupCustomTestRepo(t, "git@github.com:snyk-labs/nodejs-goof", "0336589") + defer func(path string) { _ = os.RemoveAll(path) }(cloneTargetDir) + if err != nil { + t.Fatal(err, "Couldn't setup test repo") + } + files := sliceToChannel([]string{filepath.Join(cloneTargetDir, "app.js"), filepath.Join(cloneTargetDir, "utils.js")}) + + logger := zerolog.New(os.Stdout) + instrumentor := testutil.NewTestInstrumentor() + errorReporter := testutil.NewTestErrorReporter() + config := testutil.NewTestConfig() + httpClient := codeClientHTTP.NewHTTPClient(&logger, func() *http.Client { + client := http.Client{ + Timeout: time.Duration(180) * time.Second, + Transport: TestAuthRoundTripper{http.DefaultTransport}, + } + return &client + }, instrumentor, errorReporter) + + codeScanner := codeClient.NewCodeScanner(httpClient, config, instrumentor, errorReporter, &logger) + response, bundleHash, scanErr := codeScanner.UploadAndAnalyze(context.Background(), uuid.New().String(), cloneTargetDir, files, map[string]bool{}) + require.NoError(t, scanErr) + require.NotEmpty(t, bundleHash) + require.NotNil(t, response) +} + +func Test_SmokeScan_Folder(t *testing.T) { + if os.Getenv("SMOKE_TESTS") != "true" { + t.Skip() + } + currDir, err := os.Getwd() + require.NoError(t, err) + cloneTargetDir := filepath.Join(currDir, "internal/util") + files := sliceToChannel([]string{filepath.Join(cloneTargetDir, "hash.go")}) + + logger := zerolog.New(os.Stdout) + instrumentor := testutil.NewTestInstrumentor() + errorReporter := testutil.NewTestErrorReporter() + config := testutil.NewTestConfig() + httpClient := codeClientHTTP.NewHTTPClient(&logger, func() *http.Client { + client := http.Client{ + Timeout: time.Duration(180) * time.Second, + Transport: TestAuthRoundTripper{http.DefaultTransport}, + } + return &client + }, instrumentor, errorReporter) + + codeScanner := codeClient.NewCodeScanner(httpClient, config, instrumentor, errorReporter, &logger) + _, _, scanErr := codeScanner.UploadAndAnalyze(context.Background(), uuid.New().String(), cloneTargetDir, files, map[string]bool{}) + require.ErrorContains(t, scanErr, "workspace is not a repository, cannot scan") +} + +func setupCustomTestRepo(t *testing.T, url string, targetCommit string) (string, error) { + t.Helper() + tempDir := t.TempDir() + repoDir := "1" + absoluteCloneRepoDir := filepath.Join(tempDir, repoDir) + cmd := []string{"clone", url, repoDir} + log.Debug().Interface("cmd", cmd).Msg("clone command") + clone := exec.Command("git", cmd...) + clone.Dir = tempDir + reset := exec.Command("git", "reset", "--hard", targetCommit) + reset.Dir = absoluteCloneRepoDir + + clean := exec.Command("git", "clean", "--force") + clean.Dir = absoluteCloneRepoDir + + output, err := clone.CombinedOutput() + if err != nil { + t.Fatal(err, "clone didn't work") + } + + log.Debug().Msg(string(output)) + output, _ = reset.CombinedOutput() + + log.Debug().Msg(string(output)) + output, err = clean.CombinedOutput() + + log.Debug().Msg(string(output)) + return absoluteCloneRepoDir, err +} + +type TestAuthRoundTripper struct { + http.RoundTripper +} + +func (tart TestAuthRoundTripper) RoundTrip(req *http.Request) (res *http.Response, e error) { + token := os.Getenv("SMOKE_TEST_TOKEN") + req.Header.Set("Authorization", fmt.Sprintf("token %s", token)) + req.Header.Set("session_token", token) + return tart.RoundTripper.RoundTrip(req) +} diff --git a/scan_test.go b/scan_test.go index 8b6e530d..b610d099 100644 --- a/scan_test.go +++ b/scan_test.go @@ -21,11 +21,6 @@ import ( "path/filepath" "testing" - "github.com/snyk/code-client-go/internal/bundle" - bundleMocks "github.com/snyk/code-client-go/internal/bundle/mocks" - "github.com/snyk/code-client-go/internal/deepcode" - deepcodeMocks "github.com/snyk/code-client-go/internal/deepcode/mocks" - "github.com/golang/mock/gomock" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" @@ -34,7 +29,13 @@ import ( codeclient "github.com/snyk/code-client-go" confMocks "github.com/snyk/code-client-go/config/mocks" httpmocks "github.com/snyk/code-client-go/http/mocks" + mockAnalysis "github.com/snyk/code-client-go/internal/analysis/mocks" + "github.com/snyk/code-client-go/internal/bundle" + bundleMocks "github.com/snyk/code-client-go/internal/bundle/mocks" + "github.com/snyk/code-client-go/internal/deepcode" + deepcodeMocks "github.com/snyk/code-client-go/internal/deepcode/mocks" "github.com/snyk/code-client-go/observability/mocks" + "github.com/snyk/code-client-go/sarif" ) func Test_UploadAndAnalyze(t *testing.T) { @@ -52,7 +53,8 @@ func Test_UploadAndAnalyze(t *testing.T) { mockConfig := confMocks.NewMockConfig(ctrl) mockConfig.EXPECT().SnykCodeApi().AnyTimes().Return("") mockConfig.EXPECT().IsFedramp().AnyTimes().Return(false) - mockConfig.EXPECT().Organization().AnyTimes().Return("") + mockConfig.EXPECT().Organization().AnyTimes().Return("4a72d1db-b465-4764-99e1-ecedad03b06a") + mockConfig.EXPECT().SnykApi().AnyTimes().Return("") mockSpan := mocks.NewMockSpan(ctrl) mockSpan.EXPECT().GetTraceId().Return("testRequestId").AnyTimes() mockSpan.EXPECT().Context().Return(context.Background()).AnyTimes() @@ -62,7 +64,7 @@ func Test_UploadAndAnalyze(t *testing.T) { mockErrorReporter := mocks.NewMockErrorReporter(ctrl) t.Run( - "should create bundle when hash empty", func(t *testing.T) { + "should just create bundle when hash empty", func(t *testing.T) { mockBundle := bundle.NewBundle(deepcodeMocks.NewMockSnykCodeClient(ctrl), mockInstrumentor, mockErrorReporter, &logger, "", files, []string{}, []string{}) mockBundleManager := bundleMocks.NewMockBundleManager(ctrl) mockBundleManager.EXPECT().Create(gomock.Any(), "testRequestId", baseDir, gomock.Any(), map[string]bool{}).Return(mockBundle, nil) @@ -81,12 +83,19 @@ func Test_UploadAndAnalyze(t *testing.T) { "should retrieve from backend", func(t *testing.T) { mockBundle := bundle.NewBundle(deepcodeMocks.NewMockSnykCodeClient(ctrl), mockInstrumentor, mockErrorReporter, &logger, "testBundleHash", files, []string{}, []string{}) mockBundleManager := bundleMocks.NewMockBundleManager(ctrl) - mockBundleManager.EXPECT().Create(gomock.Any(), "testRequestId", baseDir, gomock.Any(), map[string]bool{}).Return(mockBundle, nil) - mockBundleManager.EXPECT().Upload(gomock.Any(), "testRequestId", mockBundle, files).Return(mockBundle, nil) + mockBundleManager.EXPECT().Create(gomock.Any(), "b372d1db-b465-4764-99e1-ecedad03b06a", baseDir, gomock.Any(), map[string]bool{}).Return(mockBundle, nil) + mockBundleManager.EXPECT().Upload(gomock.Any(), "b372d1db-b465-4764-99e1-ecedad03b06a", mockBundle, files).Return(mockBundle, nil) + + mockAnalysisOrchestrator := mockAnalysis.NewMockAnalysisOrchestrator(ctrl) + mockAnalysisOrchestrator.EXPECT().CreateWorkspace(gomock.Any(), "4a72d1db-b465-4764-99e1-ecedad03b06a", "b372d1db-b465-4764-99e1-ecedad03b06a", baseDir, "testBundleHash").Return("c172d1db-b465-4764-99e1-ecedad03b06a", nil) + mockAnalysisOrchestrator.EXPECT().RunAnalysis().Return(&sarif.SarifResponse{Status: "COMPLETE"}, nil) codeScanner := codeclient.NewCodeScanner(mockHTTPClient, mockConfig, mockInstrumentor, mockErrorReporter, &logger) - response, bundleHash, err := codeScanner.WithBundleManager(mockBundleManager).UploadAndAnalyze(context.Background(), "testRequestId", baseDir, docs, map[string]bool{}) + response, bundleHash, err := codeScanner. + WithBundleManager(mockBundleManager). + WithAnalysisOrchestrator(mockAnalysisOrchestrator). + UploadAndAnalyze(context.Background(), "b372d1db-b465-4764-99e1-ecedad03b06a", baseDir, docs, map[string]bool{}) require.NoError(t, err) assert.Equal(t, "COMPLETE", response.Status) assert.Equal(t, "testBundleHash", bundleHash) From 4fbe371d18cf02d68e90be45e2801de1735a2355 Mon Sep 17 00:00:00 2001 From: Teodora Sandu Date: Tue, 9 Apr 2024 17:20:42 +0100 Subject: [PATCH 2/2] test: add pact --- internal/analysis/analysis_test.go | 2 - .../workspace/2024-03-12/client_pact_test.go | 158 ++++++++++++++++++ .../pacts/code-client-go-workspaceapi.json | 79 +++++++++ 3 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 internal/workspace/2024-03-12/client_pact_test.go create mode 100644 internal/workspace/2024-03-12/pacts/code-client-go-workspaceapi.json diff --git a/internal/analysis/analysis_test.go b/internal/analysis/analysis_test.go index a0830f8c..2606a154 100644 --- a/internal/analysis/analysis_test.go +++ b/internal/analysis/analysis_test.go @@ -18,7 +18,6 @@ package analysis_test import ( "bytes" "context" - "fmt" "github.com/stretchr/testify/mock" "io" "net/http" @@ -77,7 +76,6 @@ func TestAnalysis_CreateWorkspace(t *testing.T) { "b372d1db-b465-4764-99e1-ecedad03b06a", "../../", "testBundleHash") - fmt.Println(err) assert.NoError(t, err) } diff --git a/internal/workspace/2024-03-12/client_pact_test.go b/internal/workspace/2024-03-12/client_pact_test.go new file mode 100644 index 00000000..0b060131 --- /dev/null +++ b/internal/workspace/2024-03-12/client_pact_test.go @@ -0,0 +1,158 @@ +/* + * © 2022-2024 Snyk Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package v20240312_test + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/google/uuid" + "github.com/pact-foundation/pact-go/dsl" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" + + codeClientHTTP "github.com/snyk/code-client-go/http" + "github.com/snyk/code-client-go/internal/util/testutil" + v20240312 "github.com/snyk/code-client-go/internal/workspace/2024-03-12" + externalRef3 "github.com/snyk/code-client-go/internal/workspace/2024-03-12/workspaces" +) + +const ( + consumer = "code-client-go" + pactDir = "./pacts" + pactProvider = "WorkspaceApi" + + orgUUID = "e7ea34c9-de0f-422c-bf2c-4654c2e2da90" + requestId = "b6ea34c9-de0f-422c-bf2c-4654c2e2da90" + uuidMatcher = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" +) + +// Common test data +var pact dsl.Pact +var client *v20240312.ClientWithResponses + +func TestWorkspaceClientPact(t *testing.T) { + setupPact(t) + defer pact.Teardown() + + defer func() { + if err := pact.WritePact(); err != nil { + t.Fatal(err) + } + }() + + // https://snyk.roadie.so/catalog/default/api/workspace-service_2024-03-12_experimental + t.Run("Create workspace", func(t *testing.T) { + pact.AddInteraction().Given("New workspace").UponReceiving("Create workspace").WithRequest(dsl.Request{ + Method: "POST", + Path: dsl.String(fmt.Sprintf("/orgs/%s/workspaces", orgUUID)), + Query: dsl.MapMatcher{ + "version": dsl.String("2024-03-12~experimental"), + }, + Headers: getHeaderMatcher(), + Body: getBodyMatcher(), + }).WillRespondWith(dsl.Response{ + Status: 200, + Headers: dsl.MapMatcher{ + "Content-Type": dsl.String("application/json"), + }, + Body: dsl.Match(externalRef3.WorkspacePostResponse{}), + }) + + test := func() error { + _, err := client.CreateWorkspaceWithApplicationVndAPIPlusJSONBodyWithResponse(context.Background(), uuid.MustParse(orgUUID), &v20240312.CreateWorkspaceParams{ + Version: "2024-03-12~experimental", + SnykRequestId: uuid.MustParse(requestId), + }, v20240312.CreateWorkspaceApplicationVndAPIPlusJSONRequestBody{ + Data: struct { + Attributes struct { + BundleId string `json:"bundle_id"` + RepositoryUri string `json:"repository_uri"` + WorkspaceType externalRef3.WorkspacePostRequestDataAttributesWorkspaceType `json:"workspace_type"` + } `json:"attributes"` + Type externalRef3.WorkspacePostRequestDataType `json:"type"` + }(struct { + Attributes struct { + BundleId string `json:"bundle_id"` + RepositoryUri string `json:"repository_uri"` + WorkspaceType externalRef3.WorkspacePostRequestDataAttributesWorkspaceType `json:"workspace_type"` + } + Type externalRef3.WorkspacePostRequestDataType + }{Attributes: struct { + BundleId string `json:"bundle_id"` + RepositoryUri string `json:"repository_uri"` + WorkspaceType externalRef3.WorkspacePostRequestDataAttributesWorkspaceType `json:"workspace_type"` + }(struct { + BundleId string + RepositoryUri string + WorkspaceType externalRef3.WorkspacePostRequestDataAttributesWorkspaceType + }{BundleId: "YnVuZGxlSWQK", RepositoryUri: "https://github.com/snyk/code-client-go.git", WorkspaceType: "file_bundle_workspace"}), Type: "workspace"}), + }) + if err != nil { + return err + } + return nil + } + + err := pact.Verify(test) + + if err != nil { + t.Fatalf("Error on verify: %v", err) + } + }) +} + +func setupPact(t *testing.T) { + t.Helper() + + // Proactively start service to get access to the port + pact = dsl.Pact{ + Consumer: consumer, + Provider: pactProvider, + PactDir: pactDir, + } + + pact.Setup(true) + + restApi := fmt.Sprintf("http://localhost:%d", pact.Server.Port) + + logger := zerolog.New(zerolog.NewTestWriter(t)) + instrumentor := testutil.NewTestInstrumentor() + errorReporter := testutil.NewTestErrorReporter() + httpClient := codeClientHTTP.NewHTTPClient(&logger, func() *http.Client { + return http.DefaultClient + }, instrumentor, errorReporter) + var err error + client, err = v20240312.NewClientWithResponses(restApi, v20240312.WithHTTPClient(httpClient)) + require.NoError(t, err) +} + +func getHeaderMatcher() dsl.MapMatcher { + return dsl.MapMatcher{ + "Snyk-Request-Id": getSnykRequestIdMatcher(), + } +} + +func getSnykRequestIdMatcher() dsl.Matcher { + return dsl.Regex("fc763eba-0905-41c5-a27f-3934ab26786c", uuidMatcher) +} + +func getBodyMatcher() dsl.Matcher { + return dsl.Like(make([]byte, 1)) +} diff --git a/internal/workspace/2024-03-12/pacts/code-client-go-workspaceapi.json b/internal/workspace/2024-03-12/pacts/code-client-go-workspaceapi.json new file mode 100644 index 00000000..43335a9f --- /dev/null +++ b/internal/workspace/2024-03-12/pacts/code-client-go-workspaceapi.json @@ -0,0 +1,79 @@ +{ + "consumer": { + "name": "code-client-go" + }, + "provider": { + "name": "WorkspaceApi" + }, + "interactions": [ + { + "description": "Create workspace", + "providerState": "New workspace", + "request": { + "method": "POST", + "path": "/orgs/e7ea34c9-de0f-422c-bf2c-4654c2e2da90/workspaces", + "query": "version=2024-03-12%7Eexperimental", + "headers": { + "Snyk-Request-Id": "fc763eba-0905-41c5-a27f-3934ab26786c" + }, + "body": "AA==", + "matchingRules": { + "$.headers.Snyk-Request-Id": { + "match": "regex", + "regex": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" + }, + "$.body": { + "match": "type" + } + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "data": { + "id": [ + 1 + ], + "type": "string" + }, + "jsonapi": { + "version": "string" + }, + "links": { + "self": { + "href": "string" + } + } + }, + "matchingRules": { + "$.body.data.id": { + "min": 1 + }, + "$.body.data.id[*].*": { + "match": "type" + }, + "$.body.data.id[*]": { + "match": "type" + }, + "$.body.data.type": { + "match": "type" + }, + "$.body.jsonapi.version": { + "match": "type" + }, + "$.body.links.self.href": { + "match": "type" + } + } + } + } + ], + "metadata": { + "pactSpecification": { + "version": "2.0.0" + } + } +} \ No newline at end of file