Skip to content

Commit 8893e02

Browse files
authored
feat: update test-service to 2025-04-07 and support new commitId metadata (#124)
1 parent d8e002e commit 8893e02

29 files changed

+4381
-61
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,5 @@ scripts/__pycache__/
5353
internal/workspace/**/**/*.yaml
5454
!internal/workspace/**/**/*.config.yaml
5555
internal/orchestration/**/**/*.yaml
56-
!internal/orchestration/**/**/*.config.yaml
56+
!internal/orchestration/**/**/*.config.yaml
57+
.cursor

internal/analysis/analysis.go

Lines changed: 93 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ import (
3535
"github.com/snyk/code-client-go/bundle"
3636
"github.com/snyk/code-client-go/config"
3737
codeClientHTTP "github.com/snyk/code-client-go/http"
38-
testApi "github.com/snyk/code-client-go/internal/api/test/2024-12-21"
39-
testModels "github.com/snyk/code-client-go/internal/api/test/2024-12-21/models"
38+
testApi "github.com/snyk/code-client-go/internal/api/test/2025-04-07"
39+
testModels "github.com/snyk/code-client-go/internal/api/test/2025-04-07/models"
4040
"github.com/snyk/code-client-go/observability"
4141
"github.com/snyk/code-client-go/sarif"
4242
"github.com/snyk/code-client-go/scan"
@@ -66,7 +66,7 @@ type analysisOrchestrator struct {
6666
logger *zerolog.Logger
6767
trackerFactory scan.TrackerFactory
6868
config config.Config
69-
testType testModels.Scan
69+
testType testModels.ResultType
7070
}
7171

7272
var _ AnalysisOrchestrator = (*analysisOrchestrator)(nil)
@@ -97,7 +97,7 @@ func WithTrackerFactory(factory scan.TrackerFactory) func(*analysisOrchestrator)
9797
}
9898
}
9999

100-
func WithResultType(t testModels.Scan) func(*analysisOrchestrator) {
100+
func WithResultType(t testModels.ResultType) func(*analysisOrchestrator) {
101101
return func(a *analysisOrchestrator) {
102102
a.testType = t
103103
}
@@ -231,14 +231,21 @@ func (a *analysisOrchestrator) createTestAndGetResults(ctx context.Context, orgI
231231
}
232232

233233
func (a *analysisOrchestrator) RunTest(ctx context.Context, orgId string, b bundle.Bundle, target scan.Target, reportingConfig AnalysisConfig) (*sarif.SarifResponse, *scan.ResultMetaData, error) {
234+
var commitId *string = nil
234235
var repoUrl *string = nil
235236
if repoTarget, ok := target.(*scan.RepositoryTarget); ok {
236-
tmp := repoTarget.GetRepositoryUrl()
237-
repoUrl = &tmp
237+
tmpRepoUrl := repoTarget.GetRepositoryUrl()
238+
if len(tmpRepoUrl) > 0 {
239+
repoUrl = &tmpRepoUrl
240+
}
241+
tmpCommitId := repoTarget.GetCommitId()
242+
if len(tmpCommitId) > 0 {
243+
commitId = &tmpCommitId
244+
}
238245
}
239246

240247
body := testApi.NewCreateTestApplicationBody(
241-
testApi.WithInputBundle(b.GetBundleHash(), target.GetPath(), repoUrl, b.GetLimitToFiles()),
248+
testApi.WithInputBundle(b.GetBundleHash(), target.GetPath(), repoUrl, b.GetLimitToFiles(), commitId),
242249
testApi.WithScanType(a.testType),
243250
testApi.WithProjectName(reportingConfig.ProjectName),
244251
testApi.WithTargetName(reportingConfig.TargetName),
@@ -330,38 +337,96 @@ func (a *analysisOrchestrator) retrieveTestURL(ctx context.Context, client *test
330337
}
331338

332339
switch stateDiscriminator {
333-
case string(testModels.TestAcceptedStateStatusAccepted):
340+
case string(testModels.Accepted):
334341
fallthrough
335-
case string(testModels.TestInProgressStateStatusInProgress):
342+
case string(testModels.InProgress):
336343
return nil, false, nil
337-
case string(testModels.TestCompletedStateStatusCompleted):
338-
testCompleted, stateCompleteError := parsedResponse.ApplicationvndApiJSON200.Data.Attributes.AsTestCompletedState()
344+
case string(testModels.Completed):
345+
_, stateCompleteError := parsedResponse.ApplicationvndApiJSON200.Data.Attributes.AsTestCompletedState()
339346
if stateCompleteError != nil {
340347
return nil, false, stateCompleteError
341348
}
342-
343-
findingsUrl := a.host(true) + testCompleted.Documents.EnrichedSarif + "?version=" + testApi.DocumentApiVersion
344-
result := &scan.ResultMetaData{
345-
FindingsUrl: findingsUrl,
346-
}
347-
348-
if testCompleted.Results.Webui != nil {
349-
if testCompleted.Results.Webui.Link != nil {
350-
result.WebUiUrl = *testCompleted.Results.Webui.Link
351-
}
352-
if testCompleted.Results.Webui.ProjectId != nil {
353-
result.ProjectId = testCompleted.Results.Webui.ProjectId.String()
354-
}
355-
if testCompleted.Results.Webui.SnapshotId != nil {
356-
result.SnapshotId = testCompleted.Results.Webui.SnapshotId.String()
357-
}
349+
components, err := a.retrieveTestComponents(ctx, client, org, testId)
350+
if err != nil {
351+
return nil, false, err
358352
}
359353

360-
return result, true, nil
354+
return components, true, nil
361355
default:
362356
return nil, false, fmt.Errorf("unexpected test status \"%s\"", stateDiscriminator)
363357
}
364358
default:
365359
return nil, false, fmt.Errorf("unexpected response status \"%d\"", parsedResponse.StatusCode())
366360
}
367361
}
362+
363+
func (a *analysisOrchestrator) retrieveTestComponents(ctx context.Context, client *testApi.Client, org uuid.UUID, testId openapi_types.UUID) (*scan.ResultMetaData, error) {
364+
method := "analysis.retrieveTestComponents"
365+
logger := a.logger.With().Str("method", method).Logger()
366+
logger.Debug().Msg("retrieving Test Components")
367+
368+
httpResponse, err := client.GetComponents(
369+
ctx,
370+
org,
371+
testId,
372+
&testApi.GetComponentsParams{Version: testApi.ApiVersion},
373+
)
374+
375+
if err != nil {
376+
logger.Err(err).Str("testId", testId.String()).Msg("error requesting the test components")
377+
return nil, err
378+
}
379+
380+
defer func() {
381+
closeErr := httpResponse.Body.Close()
382+
if closeErr != nil {
383+
a.logger.Err(closeErr).Msg("failed to close response body")
384+
}
385+
}()
386+
387+
parsedResponse, err := testApi.ParseGetComponentsResponse(httpResponse)
388+
if err != nil {
389+
return nil, err
390+
}
391+
392+
if parsedResponse.ApplicationvndApiJSON200 == nil {
393+
return nil, fmt.Errorf("%s: unexpected response status \"%d\"", method, parsedResponse.StatusCode())
394+
}
395+
data := parsedResponse.ApplicationvndApiJSON200.Data
396+
var sastComponent *testModels.GetComponentsResponseItem
397+
for _, component := range data {
398+
if component.Attributes.Type == "sast" {
399+
a.logger.Trace().Msgf("inner component: %+v", component)
400+
sastComponent = &component
401+
break
402+
}
403+
}
404+
if sastComponent == nil {
405+
return nil, fmt.Errorf("%s: no sast component found", method)
406+
}
407+
408+
result := &scan.ResultMetaData{}
409+
attributes := sastComponent.Attributes
410+
411+
if !attributes.Success {
412+
return nil, fmt.Errorf("%s: sast scan did not complete successfully", method)
413+
}
414+
415+
if attributes.FindingsDocumentType != nil && *attributes.FindingsDocumentType == testModels.Sarif {
416+
findingsUrl := a.host(true) + *attributes.FindingsDocumentPath + "?version=" + testApi.DocumentApiVersion
417+
result.FindingsUrl = findingsUrl
418+
419+
if attributes.Webui != nil {
420+
if attributes.Webui.Link != nil {
421+
result.WebUiUrl = *attributes.Webui.Link
422+
}
423+
if attributes.Webui.ProjectId != nil {
424+
result.ProjectId = attributes.Webui.ProjectId.String()
425+
}
426+
if attributes.Webui.SnapshotId != nil {
427+
result.SnapshotId = attributes.Webui.SnapshotId.String()
428+
}
429+
}
430+
}
431+
return result, nil
432+
}

internal/analysis/analysis_test.go

Lines changed: 100 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,14 @@ import (
3232
"github.com/stretchr/testify/mock"
3333
"github.com/stretchr/testify/require"
3434

35+
openapi_types "github.com/oapi-codegen/runtime/types"
3536
mocks2 "github.com/snyk/code-client-go/bundle/mocks"
3637
confMocks "github.com/snyk/code-client-go/config/mocks"
3738
httpmocks "github.com/snyk/code-client-go/http/mocks"
3839
"github.com/snyk/code-client-go/internal/analysis"
39-
v20241221 "github.com/snyk/code-client-go/internal/api/test/2024-12-21"
40+
v20250407 "github.com/snyk/code-client-go/internal/api/test/2025-04-07"
41+
externalRef0 "github.com/snyk/code-client-go/internal/api/test/2025-04-07/common"
42+
v20250407Models "github.com/snyk/code-client-go/internal/api/test/2025-04-07/models"
4043
"github.com/snyk/code-client-go/observability/mocks"
4144
"github.com/snyk/code-client-go/sarif"
4245
"github.com/snyk/code-client-go/scan"
@@ -51,11 +54,55 @@ func mockDeriveErrorFromStatusCode(statusCode int) error {
5154
return fmt.Errorf("Statuscode: %d", statusCode)
5255
}
5356

54-
func mockGetDocumentResponse(t *testing.T, sarifResponse sarif.SarifDocument, expectedDocumentPath string, mockHTTPClient *httpmocks.MockHTTPClient, responseCode int) {
57+
func mockTestStatusResponse(t *testing.T, mockHTTPClient *httpmocks.MockHTTPClient, orgId string, testId uuid.UUID, responseCode int) {
58+
t.Helper()
59+
60+
response := v20250407Models.TestResult{
61+
Data: struct {
62+
Attributes v20250407Models.TestState `json:"attributes"`
63+
Id openapi_types.UUID `json:"id"`
64+
Type v20250407Models.TestResultDataType `json:"type"`
65+
}{
66+
Id: testId,
67+
Type: v20250407Models.TestResultDataTypeTest,
68+
},
69+
Jsonapi: externalRef0.JsonApi{Version: "1.0"},
70+
Links: externalRef0.SelfLink{Self: &externalRef0.LinkProperty{}},
71+
}
72+
73+
completedStateJSON := map[string]interface{}{
74+
"created_at": time.Now().Format(time.RFC3339),
75+
"status": "completed",
76+
"result": "passed",
77+
}
78+
79+
stateBytes, err := json.Marshal(completedStateJSON)
80+
assert.NoError(t, err)
81+
response.Data.Attributes = v20250407Models.TestState{}
82+
err = response.Data.Attributes.UnmarshalJSON(stateBytes)
83+
assert.NoError(t, err)
84+
85+
responseBodyBytes, err := json.Marshal(response)
86+
assert.NoError(t, err)
87+
88+
expectedTestStatusUrl := fmt.Sprintf("http://localhost/hidden/orgs/%s/tests/%s?version=%s", orgId, testId, v20250407.ApiVersion)
89+
mockHTTPClient.EXPECT().Do(mock.MatchedBy(func(i interface{}) bool {
90+
req := i.(*http.Request)
91+
return req.URL.String() == expectedTestStatusUrl && req.Method == http.MethodGet
92+
})).Times(1).Return(&http.Response{
93+
StatusCode: responseCode,
94+
Header: http.Header{
95+
"Content-Type": []string{"application/json"},
96+
},
97+
Body: io.NopCloser(bytes.NewReader(responseBodyBytes)),
98+
}, mockDeriveErrorFromStatusCode(responseCode))
99+
}
100+
101+
func mockGetComponentResponse(t *testing.T, sarifResponse sarif.SarifDocument, expectedDocumentPath string, mockHTTPClient *httpmocks.MockHTTPClient, responseCode int) {
55102
t.Helper()
56103
responseBodyBytes, err := json.Marshal(sarifResponse)
57104
assert.NoError(t, err)
58-
expectedDocumentUrl := fmt.Sprintf("http://localhost/hidden%s?version=%s", expectedDocumentPath, v20241221.DocumentApiVersion)
105+
expectedDocumentUrl := fmt.Sprintf("http://localhost/hidden%s?version=%s", expectedDocumentPath, v20250407.DocumentApiVersion)
59106
mockHTTPClient.EXPECT().Do(mock.MatchedBy(func(i interface{}) bool {
60107
req := i.(*http.Request)
61108
return req.URL.String() == expectedDocumentUrl
@@ -70,18 +117,20 @@ func mockGetDocumentResponse(t *testing.T, sarifResponse sarif.SarifDocument, ex
70117

71118
func mockResultCompletedResponse(t *testing.T, mockHTTPClient *httpmocks.MockHTTPClient, expectedWebuilink string, projectId uuid.UUID, snapshotId uuid.UUID, orgId string, testId uuid.UUID, documentPath string, responseCode int) {
72119
t.Helper()
73-
response := v20241221.NewTestResponse()
74-
state := v20241221.NewTestCompleteState()
75-
state.Documents.EnrichedSarif = documentPath
76-
state.Results.Webui.Link = &expectedWebuilink
77-
state.Results.Webui.ProjectId = &projectId
78-
state.Results.Webui.SnapshotId = &snapshotId
79-
stateBytes, err := json.Marshal(state)
80-
assert.NoError(t, err)
81-
response.Data.Attributes.UnmarshalJSON(stateBytes)
82-
responseBodyBytes, err := json.Marshal(response)
120+
state := v20250407.NewGetComponentsState()
121+
state.Data[0].Attributes.Type = "sast"
122+
state.Data[0].Attributes.FindingsDocumentPath = &documentPath
123+
findingsDocumentType := v20250407Models.Sarif
124+
state.Data[0].Attributes.FindingsDocumentType = &findingsDocumentType
125+
state.Data[0].Attributes.Success = true
126+
state.Data[0].Attributes.Webui = &v20250407Models.WebUI{
127+
Link: &expectedWebuilink,
128+
ProjectId: &projectId,
129+
SnapshotId: &snapshotId,
130+
}
131+
responseBodyBytes, err := json.Marshal(state)
83132
assert.NoError(t, err)
84-
expectedRetrieveTestUrl := fmt.Sprintf("http://localhost/hidden/orgs/%s/tests/%s?version=%s", orgId, testId, v20241221.ApiVersion)
133+
expectedRetrieveTestUrl := fmt.Sprintf("http://localhost/hidden/orgs/%s/tests/%s/components?version=%s", orgId, testId, v20250407.ApiVersion)
85134
mockHTTPClient.EXPECT().Do(mock.MatchedBy(func(i interface{}) bool {
86135
req := i.(*http.Request)
87136
return req.URL.String() == expectedRetrieveTestUrl
@@ -96,13 +145,15 @@ func mockResultCompletedResponse(t *testing.T, mockHTTPClient *httpmocks.MockHTT
96145

97146
func mockTestCreatedResponse(t *testing.T, mockHTTPClient *httpmocks.MockHTTPClient, testId uuid.UUID, orgId string, responseCode int) {
98147
t.Helper()
99-
response := v20241221.NewTestResponse()
148+
response := v20250407.NewTestResponse()
100149
response.Data.Id = testId
101150
responseBodyBytes, err := json.Marshal(response)
102151
assert.NoError(t, err)
103-
expectedTestCreatedUrl := fmt.Sprintf("http://localhost/hidden/orgs/%s/tests?version=%s", orgId, v20241221.ApiVersion)
152+
expectedTestCreatedUrl := fmt.Sprintf("http://localhost/hidden/orgs/%s/tests?version=%s", orgId, v20250407.ApiVersion)
104153
mockHTTPClient.EXPECT().Do(mock.MatchedBy(func(i interface{}) bool {
105154
req := i.(*http.Request)
155+
validateTestRequestBody(t, req.Body)
156+
106157
return req.URL.String() == expectedTestCreatedUrl &&
107158
req.Method == http.MethodPost
108159
})).Times(1).Return(&http.Response{
@@ -114,6 +165,23 @@ func mockTestCreatedResponse(t *testing.T, mockHTTPClient *httpmocks.MockHTTPCli
114165
}, mockDeriveErrorFromStatusCode(responseCode))
115166
}
116167

168+
func validateTestRequestBody(t *testing.T, request io.Reader) {
169+
t.Helper()
170+
body, _ := io.ReadAll(request)
171+
var testRequestBody v20250407Models.CreateTestRequestBody
172+
err := json.Unmarshal(body, &testRequestBody)
173+
assert.NoError(t, err)
174+
bundle, err := testRequestBody.Data.Attributes.Input.AsTestInputSourceBundle()
175+
assert.NoError(t, err)
176+
177+
if bundle.Metadata.CommitId != nil {
178+
assert.Regexp(t, "^[0-9a-f]{40}$", *bundle.Metadata.CommitId)
179+
}
180+
if bundle.Metadata.RepoUrl != nil {
181+
assert.Regexp(t, "^[email protected]:[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+.git$", *bundle.Metadata.RepoUrl)
182+
}
183+
}
184+
117185
func setup(t *testing.T, timeout *time.Duration) (*confMocks.MockConfig, *httpmocks.MockHTTPClient, *mocks.MockInstrumentor, *mocks.MockErrorReporter, *trackerMocks.MockTracker, *trackerMocks.MockTrackerFactory, zerolog.Logger) {
118186
t.Helper()
119187
ctrl := gomock.NewController(t)
@@ -164,6 +232,9 @@ func TestAnalysis_RunTest(t *testing.T) {
164232
// Test Created Response
165233
mockTestCreatedResponse(t, mockHTTPClient, testId, orgId, http.StatusCreated)
166234

235+
// Test Status Response
236+
mockTestStatusResponse(t, mockHTTPClient, orgId, testId, http.StatusOK)
237+
167238
// Get Test Result Response
168239
expectedWebuilink := ""
169240
expectedDocumentPath := "/1234"
@@ -173,7 +244,8 @@ func TestAnalysis_RunTest(t *testing.T) {
173244
sarifResponse := sarif.SarifDocument{
174245
Version: "42.0",
175246
}
176-
mockGetDocumentResponse(t, sarifResponse, expectedDocumentPath, mockHTTPClient, http.StatusOK)
247+
248+
mockGetComponentResponse(t, sarifResponse, expectedDocumentPath, mockHTTPClient, http.StatusOK)
177249

178250
analysisOrchestrator := analysis.NewAnalysisOrchestrator(
179251
mockConfig,
@@ -219,6 +291,9 @@ func TestAnalysis_RunTestRemote(t *testing.T) {
219291
// Test Created Response
220292
mockTestCreatedResponse(t, mockHTTPClient, testId, orgId, http.StatusCreated)
221293

294+
// Test Status Response
295+
mockTestStatusResponse(t, mockHTTPClient, orgId, testId, http.StatusOK)
296+
222297
// Get Test Result Response
223298
expectedWebuilink := ""
224299
expectedDocumentPath := "/1234"
@@ -228,7 +303,7 @@ func TestAnalysis_RunTestRemote(t *testing.T) {
228303
sarifResponse := sarif.SarifDocument{
229304
Version: "42.0",
230305
}
231-
mockGetDocumentResponse(t, sarifResponse, expectedDocumentPath, mockHTTPClient, http.StatusOK)
306+
mockGetComponentResponse(t, sarifResponse, expectedDocumentPath, mockHTTPClient, http.StatusOK)
232307

233308
analysisOrchestrator := analysis.NewAnalysisOrchestrator(
234309
mockConfig,
@@ -312,6 +387,9 @@ func TestAnalysis_RunTestRemote_PollingFailed(t *testing.T) {
312387
// Test Created Response
313388
mockTestCreatedResponse(t, mockHTTPClient, testId, orgId, http.StatusCreated)
314389

390+
// Test Status Response
391+
mockTestStatusResponse(t, mockHTTPClient, orgId, testId, http.StatusOK)
392+
315393
// Get Test Result Response
316394
expectedWebuilink := ""
317395
expectedDocumentPath := "/1234"
@@ -356,6 +434,9 @@ func TestAnalysis_RunTestRemote_GetDocumentFailed(t *testing.T) {
356434
// Test Created Response
357435
mockTestCreatedResponse(t, mockHTTPClient, testId, orgId, http.StatusCreated)
358436

437+
// Test Status Response
438+
mockTestStatusResponse(t, mockHTTPClient, orgId, testId, http.StatusOK)
439+
359440
// Get Test Result Response
360441
expectedWebuilink := ""
361442
expectedDocumentPath := "/1234"
@@ -365,7 +446,7 @@ func TestAnalysis_RunTestRemote_GetDocumentFailed(t *testing.T) {
365446
sarifResponse := sarif.SarifDocument{
366447
Version: "42.0",
367448
}
368-
mockGetDocumentResponse(t, sarifResponse, expectedDocumentPath, mockHTTPClient, http.StatusInternalServerError)
449+
mockGetComponentResponse(t, sarifResponse, expectedDocumentPath, mockHTTPClient, http.StatusInternalServerError)
369450

370451
analysisOrchestrator := analysis.NewAnalysisOrchestrator(
371452
mockConfig,

0 commit comments

Comments
 (0)