From bddd4ea909ce2e2758587dbceb907ad96ef048af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Scha=CC=88fer?= <101886095+PeterSchafer@users.noreply.github.com> Date: Mon, 3 Mar 2025 22:35:17 +0100 Subject: [PATCH] chore: add basic test for analysis --- internal/analysis/analysis_test.go | 336 +++++++++++++++++++++---- internal/api/test/2024-12-21/helper.go | 35 +++ 2 files changed, 324 insertions(+), 47 deletions(-) diff --git a/internal/analysis/analysis_test.go b/internal/analysis/analysis_test.go index de1ca2b..a3ed25b 100644 --- a/internal/analysis/analysis_test.go +++ b/internal/analysis/analysis_test.go @@ -19,14 +19,15 @@ import ( "bytes" "context" _ "embed" + "encoding/json" + "fmt" "io" "net/http" "testing" "time" - "github.com/google/uuid" - "github.com/golang/mock/gomock" + "github.com/google/uuid" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -35,10 +36,84 @@ import ( 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" + v20241221 "github.com/snyk/code-client-go/internal/api/test/2024-12-21" + mocks2 "github.com/snyk/code-client-go/internal/bundle/mocks" "github.com/snyk/code-client-go/observability/mocks" + "github.com/snyk/code-client-go/sarif" + "github.com/snyk/code-client-go/scan" trackerMocks "github.com/snyk/code-client-go/scan/mocks" ) +func mockDeriveErrorFromStatusCode(statusCode int) error { + if statusCode >= http.StatusOK && statusCode < http.StatusBadRequest { + return nil + } + + return fmt.Errorf("Statuscode: %d", statusCode) +} + +func mockGetDocumentResponse(t *testing.T, sarifResponse sarif.SarifDocument, expectedDocumentPath string, mockHTTPClient *httpmocks.MockHTTPClient, responseCode int) { + t.Helper() + responseBodyBytes, err := json.Marshal(sarifResponse) + assert.NoError(t, err) + expectedDocumentUrl := fmt.Sprintf("http://localhost/hidden%s?version=%s", expectedDocumentPath, v20241221.DocumentApiVersion) + mockHTTPClient.EXPECT().Do(mock.MatchedBy(func(i interface{}) bool { + req := i.(*http.Request) + return req.URL.String() == expectedDocumentUrl + })).Times(1).Return(&http.Response{ + StatusCode: responseCode, + Header: http.Header{ + "Content-Type": []string{"application/json"}, + }, + Body: io.NopCloser(bytes.NewReader(responseBodyBytes)), + }, mockDeriveErrorFromStatusCode(responseCode)) +} + +func mockResultCompletedResponse(t *testing.T, mockHTTPClient *httpmocks.MockHTTPClient, expectedWebuilink string, projectId uuid.UUID, orgId string, testId uuid.UUID, documentPath string, responseCode int) { + t.Helper() + response := v20241221.NewTestResponse() + state := v20241221.NewTestCompleteState() + state.Documents.EnrichedSarif = documentPath + state.Results.Webui.Link = &expectedWebuilink + state.Results.Webui.ProjectId = &projectId + stateBytes, err := json.Marshal(state) + assert.NoError(t, err) + response.Data.Attributes.UnmarshalJSON(stateBytes) + responseBodyBytes, err := json.Marshal(response) + assert.NoError(t, err) + expectedRetrieveTestUrl := fmt.Sprintf("http://localhost/hidden/orgs/%s/tests/%s?version=%s", orgId, testId, v20241221.ApiVersion) + mockHTTPClient.EXPECT().Do(mock.MatchedBy(func(i interface{}) bool { + req := i.(*http.Request) + return req.URL.String() == expectedRetrieveTestUrl + })).Times(1).Return(&http.Response{ + StatusCode: responseCode, + Header: http.Header{ + "Content-Type": []string{"application/json"}, + }, + Body: io.NopCloser(bytes.NewReader(responseBodyBytes)), + }, mockDeriveErrorFromStatusCode(responseCode)) +} + +func mockTestCreatedResponse(t *testing.T, mockHTTPClient *httpmocks.MockHTTPClient, testId uuid.UUID, orgId string, responseCode int) { + t.Helper() + response := v20241221.NewTestResponse() + response.Data.Id = testId + responseBodyBytes, err := json.Marshal(response) + assert.NoError(t, err) + expectedTestCreatedUrl := fmt.Sprintf("http://localhost/hidden/orgs/%s/tests?version=%s", orgId, v20241221.ApiVersion) + mockHTTPClient.EXPECT().Do(mock.MatchedBy(func(i interface{}) bool { + req := i.(*http.Request) + return req.URL.String() == expectedTestCreatedUrl && + req.Method == http.MethodPost + })).Times(1).Return(&http.Response{ + StatusCode: responseCode, + Header: http.Header{ + "Content-Type": []string{"application/json"}, + }, + Body: io.NopCloser(bytes.NewReader(responseBodyBytes)), + }, mockDeriveErrorFromStatusCode(responseCode)) +} + func setup(t *testing.T, timeout *time.Duration) (*confMocks.MockConfig, *httpmocks.MockHTTPClient, *mocks.MockInstrumentor, *mocks.MockErrorReporter, *trackerMocks.MockTracker, *trackerMocks.MockTrackerFactory, zerolog.Logger) { t.Helper() ctrl := gomock.NewController(t) @@ -68,58 +143,88 @@ func setup(t *testing.T, timeout *time.Duration) (*confMocks.MockConfig, *httpmo return mockConfig, mockHTTPClient, mockInstrumentor, mockErrorReporter, mockTracker, mockTrackerFactory, logger } -//go:embed fake.json -var fakeResponse []byte +func TestAnalysis_RunTest(t *testing.T) { + ctrl := gomock.NewController(t) + mockConfig, mockHTTPClient, mockInstrumentor, mockErrorReporter, mockTracker, mockTrackerFactory, logger := setup(t, nil) + mockTracker.EXPECT().Begin(gomock.Eq("Snyk Code analysis for ../mypath/"), gomock.Eq("Retrieving results...")).Return() + mockTracker.EXPECT().End(gomock.Eq("Analysis completed.")).Return() + + orgId := "4a72d1db-b465-4764-99e1-ecedad03b06a" + projectId := uuid.New() + testId := uuid.New() + report := true + inputBundle := mocks2.NewMockBundle(ctrl) + targetId, err := scan.NewRepositoryTarget("../mypath/") + assert.NoError(t, err) + + inputBundle.EXPECT().GetBundleHash().Return("").AnyTimes() + inputBundle.EXPECT().GetLimitToFiles().Return([]string{}).AnyTimes() + + // Test Created Response + mockTestCreatedResponse(t, mockHTTPClient, testId, orgId, http.StatusCreated) + + // Get Test Result Response + expectedWebuilink := "" + expectedDocumentPath := "/1234" + mockResultCompletedResponse(t, mockHTTPClient, expectedWebuilink, projectId, orgId, testId, expectedDocumentPath, http.StatusOK) + + // get document + sarifResponse := sarif.SarifDocument{ + Version: "42.0", + } + mockGetDocumentResponse(t, sarifResponse, expectedDocumentPath, mockHTTPClient, http.StatusOK) + + analysisOrchestrator := analysis.NewAnalysisOrchestrator( + mockConfig, + mockHTTPClient, + analysis.WithLogger(&logger), + analysis.WithInstrumentor(mockInstrumentor), + analysis.WithTrackerFactory(mockTrackerFactory), + analysis.WithErrorReporter(mockErrorReporter), + ) + + // run method under test + result, resultMetadata, err := analysisOrchestrator.RunTest( + context.Background(), + orgId, + inputBundle, + targetId, + analysis.AnalysisConfig{ + Report: report, + }, + ) + + require.NoError(t, err) + assert.NotNil(t, result) + assert.NotNil(t, resultMetadata) + assert.Equal(t, expectedWebuilink, resultMetadata.WebUiUrl) + assert.Equal(t, sarifResponse.Version, result.Sarif.Version) +} func TestAnalysis_RunTestRemote(t *testing.T) { - t.Skip() mockConfig, mockHTTPClient, mockInstrumentor, mockErrorReporter, mockTracker, mockTrackerFactory, logger := setup(t, nil) - mockTracker.EXPECT().Begin(gomock.Eq("Snyk Code analysis for remote project"), gomock.Eq("Retrieving results...")).Return() - mockTracker.EXPECT().End(gomock.Eq("Analysis complete.")).Return() + mockTracker.EXPECT().End(gomock.Eq("Analysis completed.")).Return() + orgId := "4a72d1db-b465-4764-99e1-ecedad03b06a" projectId := uuid.New() + testId := uuid.New() commitId := "abc123" report := true - // Mock the initial test creation request - 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/tests?version=2024-12-21" && - req.Method == http.MethodPost - })).Times(1).Return(&http.Response{ - StatusCode: http.StatusCreated, - Header: http.Header{ - "Content-Type": []string{"application/json"}, - }, - Body: io.NopCloser(bytes.NewReader([]byte(`{ - "data": { - "id": "a6fb2742-b67f-4dc3-bb27-42b67f1dc344", - "type": "test-result", - "attributes": { - "status": "completed", - "documents": { - "enriched_sarif": "/tests/123/sarif" - } - } - }, - "jsonapi": { - "version": "1.0" - }, - "links": { - "self": "http://localhost/hidden/orgs/4a72d1db-b465-4764-99e1-ecedad03b06a/tests/a6fb2742-b67f-4dc3-bb27-42b67f1dc344" - } - }`))), - }, nil) - // Mock the findings retrieval request - mockHTTPClient.EXPECT().Do(mock.MatchedBy(func(i interface{}) bool { - req := i.(*http.Request) - return req.URL.String() == "http://localhost/tests/123/sarif" && - req.Method == http.MethodGet - })).Times(1).Return(&http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewReader(fakeResponse)), - }, nil) + // Test Created Response + mockTestCreatedResponse(t, mockHTTPClient, testId, orgId, http.StatusCreated) + + // Get Test Result Response + expectedWebuilink := "" + expectedDocumentPath := "/1234" + mockResultCompletedResponse(t, mockHTTPClient, expectedWebuilink, projectId, orgId, testId, expectedDocumentPath, http.StatusOK) + + // get document + sarifResponse := sarif.SarifDocument{ + Version: "42.0", + } + mockGetDocumentResponse(t, sarifResponse, expectedDocumentPath, mockHTTPClient, http.StatusOK) analysisOrchestrator := analysis.NewAnalysisOrchestrator( mockConfig, @@ -130,9 +235,10 @@ func TestAnalysis_RunTestRemote(t *testing.T) { analysis.WithErrorReporter(mockErrorReporter), ) - result, _, err := analysisOrchestrator.RunTestRemote( + // run method under test + result, resultMetadata, err := analysisOrchestrator.RunTestRemote( context.Background(), - "4a72d1db-b465-4764-99e1-ecedad03b06a", + orgId, analysis.AnalysisConfig{ ProjectId: &projectId, CommitId: &commitId, @@ -142,6 +248,142 @@ func TestAnalysis_RunTestRemote(t *testing.T) { require.NoError(t, err) assert.NotNil(t, result) + assert.NotNil(t, resultMetadata) + assert.Equal(t, expectedWebuilink, resultMetadata.WebUiUrl) + assert.Equal(t, sarifResponse.Version, result.Sarif.Version) +} + +func TestAnalysis_RunTestRemote_CreateTestFailed(t *testing.T) { + mockConfig, mockHTTPClient, mockInstrumentor, mockErrorReporter, mockTracker, mockTrackerFactory, logger := setup(t, nil) + mockTracker.EXPECT().Begin(gomock.Eq("Snyk Code analysis for remote project"), gomock.Eq("Retrieving results...")).Return() + mockTracker.EXPECT().End(gomock.Eq("Analysis failed.")).Return() + + orgId := "4a72d1db-b465-4764-99e1-ecedad03b06a" + projectId := uuid.New() + testId := uuid.New() + commitId := "abc123" + report := true + + // Test Created Response + mockTestCreatedResponse(t, mockHTTPClient, testId, orgId, http.StatusInternalServerError) + + analysisOrchestrator := analysis.NewAnalysisOrchestrator( + mockConfig, + mockHTTPClient, + analysis.WithLogger(&logger), + analysis.WithInstrumentor(mockInstrumentor), + analysis.WithTrackerFactory(mockTrackerFactory), + analysis.WithErrorReporter(mockErrorReporter), + ) + + // run method under test + result, resultMetadata, err := analysisOrchestrator.RunTestRemote( + context.Background(), + orgId, + analysis.AnalysisConfig{ + ProjectId: &projectId, + CommitId: &commitId, + Report: report, + }, + ) + + require.Error(t, err) + assert.Nil(t, result) + assert.Nil(t, resultMetadata) +} + +func TestAnalysis_RunTestRemote_PollingFailed(t *testing.T) { + mockConfig, mockHTTPClient, mockInstrumentor, mockErrorReporter, mockTracker, mockTrackerFactory, logger := setup(t, nil) + mockTracker.EXPECT().Begin(gomock.Eq("Snyk Code analysis for remote project"), gomock.Eq("Retrieving results...")).Return() + mockTracker.EXPECT().End(gomock.Eq("Analysis failed.")).Return() + + orgId := "4a72d1db-b465-4764-99e1-ecedad03b06a" + projectId := uuid.New() + testId := uuid.New() + commitId := "abc123" + report := true + + // Test Created Response + mockTestCreatedResponse(t, mockHTTPClient, testId, orgId, http.StatusCreated) + + // Get Test Result Response + expectedWebuilink := "" + expectedDocumentPath := "/1234" + mockResultCompletedResponse(t, mockHTTPClient, expectedWebuilink, projectId, orgId, testId, expectedDocumentPath, http.StatusInternalServerError) + + analysisOrchestrator := analysis.NewAnalysisOrchestrator( + mockConfig, + mockHTTPClient, + analysis.WithLogger(&logger), + analysis.WithInstrumentor(mockInstrumentor), + analysis.WithTrackerFactory(mockTrackerFactory), + analysis.WithErrorReporter(mockErrorReporter), + ) + + // run method under test + result, resultMetadata, err := analysisOrchestrator.RunTestRemote( + context.Background(), + orgId, + analysis.AnalysisConfig{ + ProjectId: &projectId, + CommitId: &commitId, + Report: report, + }, + ) + + require.Error(t, err) + assert.Nil(t, result) + assert.Nil(t, resultMetadata) +} + +func TestAnalysis_RunTestRemote_GetDocumentFailed(t *testing.T) { + mockConfig, mockHTTPClient, mockInstrumentor, mockErrorReporter, mockTracker, mockTrackerFactory, logger := setup(t, nil) + mockTracker.EXPECT().Begin(gomock.Eq("Snyk Code analysis for remote project"), gomock.Eq("Retrieving results...")).Return() + mockTracker.EXPECT().End(gomock.Eq("Analysis failed.")).Return() + + orgId := "4a72d1db-b465-4764-99e1-ecedad03b06a" + projectId := uuid.New() + testId := uuid.New() + commitId := "abc123" + report := true + + // Test Created Response + mockTestCreatedResponse(t, mockHTTPClient, testId, orgId, http.StatusCreated) + + // Get Test Result Response + expectedWebuilink := "" + expectedDocumentPath := "/1234" + mockResultCompletedResponse(t, mockHTTPClient, expectedWebuilink, projectId, orgId, testId, expectedDocumentPath, http.StatusOK) + + // get document + sarifResponse := sarif.SarifDocument{ + Version: "42.0", + } + mockGetDocumentResponse(t, sarifResponse, expectedDocumentPath, mockHTTPClient, http.StatusInternalServerError) + + analysisOrchestrator := analysis.NewAnalysisOrchestrator( + mockConfig, + mockHTTPClient, + analysis.WithLogger(&logger), + analysis.WithInstrumentor(mockInstrumentor), + analysis.WithTrackerFactory(mockTrackerFactory), + analysis.WithErrorReporter(mockErrorReporter), + ) + + // run method under test + result, resultMetadata, err := analysisOrchestrator.RunTestRemote( + context.Background(), + orgId, + analysis.AnalysisConfig{ + ProjectId: &projectId, + CommitId: &commitId, + Report: report, + }, + ) + + require.Error(t, err) + assert.Nil(t, result) + assert.Nil(t, resultMetadata) } func TestAnalysis_RunTestRemote_MissingRequiredParams(t *testing.T) { diff --git a/internal/api/test/2024-12-21/helper.go b/internal/api/test/2024-12-21/helper.go index b8a5d7c..69b5b3e 100644 --- a/internal/api/test/2024-12-21/helper.go +++ b/internal/api/test/2024-12-21/helper.go @@ -120,3 +120,38 @@ func NewTestInputLegacyScmProject(projectId openapi_types.UUID, commitId string) Type: v20241221.LegacyScmProject, } } + +func NewTestResponse() *v20241221.TestResult { + return &v20241221.TestResult{ + Data: struct { + Attributes v20241221.TestState `json:"attributes"` + Id openapi_types.UUID `json:"id"` + Type v20241221.TestResultDataType `json:"type"` + }{}, + } +} + +func NewTestCompleteState() *v20241221.TestCompletedState { + return &v20241221.TestCompletedState{ + Status: v20241221.TestCompletedStateStatusCompleted, + Documents: struct { + EnrichedSarif string `json:"enriched_sarif"` + }{}, + Results: struct { + Outcome struct { + Result v20241221.TestCompletedStateResultsOutcomeResult `json:"result"` + } `json:"outcome"` + Webui *struct { + Link *string `json:"link,omitempty"` + ProjectId *openapi_types.UUID `json:"project_id,omitempty"` + SnapshotId *openapi_types.UUID `json:"snapshot_id,omitempty"` + } `json:"webui,omitempty"` + }{ + Webui: &struct { + Link *string `json:"link,omitempty"` + ProjectId *openapi_types.UUID `json:"project_id,omitempty"` + SnapshotId *openapi_types.UUID `json:"snapshot_id,omitempty"` + }{}, + }, + } +}