Skip to content

Commit 71eef51

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/IDE-1314_prepending-paths
2 parents 97dd637 + c3de6bd commit 71eef51

File tree

13 files changed

+378
-22
lines changed

13 files changed

+378
-22
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ require (
3131
github.com/oapi-codegen/oapi-codegen/v2 v2.4.1
3232
github.com/oapi-codegen/runtime v1.1.1
3333
github.com/patrickmn/go-cache v2.1.0+incompatible
34-
github.com/snyk/error-catalog-golang-public v0.0.0-20250625135845-2d6f9a31f318
34+
github.com/snyk/error-catalog-golang-public v0.0.0-20250812140843-a01d75260003
3535
github.com/subosito/gotenv v1.6.0
3636
golang.org/x/net v0.38.0
3737
golang.org/x/sync v0.13.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,8 +230,8 @@ github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnB
230230
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
231231
github.com/snyk/code-client-go v1.21.3 h1:2+HPXCA9FGn3gaI1Jw1C4Ifn/NRAbSnmohFUvz4GC4I=
232232
github.com/snyk/code-client-go v1.21.3/go.mod h1:WH6lNkJc785hfXmwhixxWHix3O6z+1zwz40oK8vl/zg=
233-
github.com/snyk/error-catalog-golang-public v0.0.0-20250625135845-2d6f9a31f318 h1:2bNOlUstBBWHa3doBvdOBlMSu8AC01IHyNexT9MoKiM=
234-
github.com/snyk/error-catalog-golang-public v0.0.0-20250625135845-2d6f9a31f318/go.mod h1:Ytttq7Pw4vOCu9NtRQaOeDU2dhBYUyNBe6kX4+nIIQ4=
233+
github.com/snyk/error-catalog-golang-public v0.0.0-20250812140843-a01d75260003 h1:qeXih9sVe/WvhccE3MfEgglnSVKN1xTQBcsA/N96Kzo=
234+
github.com/snyk/error-catalog-golang-public v0.0.0-20250812140843-a01d75260003/go.mod h1:Ytttq7Pw4vOCu9NtRQaOeDU2dhBYUyNBe6kX4+nIIQ4=
235235
github.com/snyk/go-httpauth v0.0.0-20231117135515-eb445fea7530 h1:s9PHNkL6ueYRiAKNfd8OVxlUOqU3qY0VDbgCD1f6WQY=
236236
github.com/snyk/go-httpauth v0.0.0-20231117135515-eb445fea7530/go.mod h1:88KbbvGYlmLgee4OcQ19yr0bNpXpOr2kciOthaSzCAg=
237237
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=

internal/api/api.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type ApiClient interface {
2525
GetUserMe() (string, error)
2626
GetSelf() (contract.SelfResponse, error)
2727
GetSastSettings(orgId string) (*sast_contract.SastResponse, error)
28+
GetOrgSettings(orgId string) (*contract.OrgSettingsResponse, error)
2829
}
2930

3031
var _ ApiClient = (*snykApiClient)(nil)
@@ -247,6 +248,29 @@ func (a *snykApiClient) GetSastSettings(orgId string) (*sast_contract.SastRespon
247248
return &response, err
248249
}
249250

251+
func (a *snykApiClient) GetOrgSettings(orgId string) (*contract.OrgSettingsResponse, error) {
252+
endpoint := fmt.Sprintf("%s/v1/org/%s/settings", a.url, url.QueryEscape(orgId))
253+
254+
res, err := a.client.Get(endpoint)
255+
if err != nil {
256+
return nil, fmt.Errorf("unable to retrieve org settings: %w", err)
257+
}
258+
//goland:noinspection GoUnhandledErrorResult
259+
defer res.Body.Close()
260+
261+
body, err := io.ReadAll(res.Body)
262+
if err != nil {
263+
return nil, fmt.Errorf("unable to retrieve org settings: %w", err)
264+
}
265+
266+
var response contract.OrgSettingsResponse
267+
if err = json.Unmarshal(body, &response); err != nil {
268+
return nil, fmt.Errorf("unable to retrieve org settings (status: %d): %w", res.StatusCode, err)
269+
}
270+
271+
return &response, err
272+
}
273+
250274
// clientGet performs an HTTP GET request to the Snyk API, handling query parameters,
251275
// API versioning, and basic error checking.
252276
//
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package contract
2+
3+
// API reference: https://docs.snyk.io/snyk-api/reference/organizations-v1#get-org-orgid-settings
4+
5+
type OrgIgnoreSettings struct {
6+
ReasonRequired bool `json:"reasonRequired,omitempty"`
7+
AutoApproveIgnores bool `json:"autoApproveIgnores,omitempty"`
8+
ApprovalWorkflowEnabled bool `json:"approvalWorkflowEnabled,omitempty"`
9+
}
10+
11+
type OrgRequestAccessSettings struct {
12+
Enabled bool `json:"enabled,omitempty"`
13+
}
14+
15+
type OrgSettingsResponse struct {
16+
Ignores *OrgIgnoreSettings `json:"ignores"`
17+
RequestAccess *OrgRequestAccessSettings `json:"requestAccess"`
18+
}

internal/mocks/api.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/apiclients/testapi/testapi.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"context"
77
"errors"
88
"fmt"
9+
"math/rand"
910
"net/http"
1011
"net/url"
1112
"sync"
@@ -24,6 +25,7 @@ type config struct {
2425
APIVersion string
2526
Logger *zerolog.Logger
2627
lowLevelClientOptions []ClientOption // Options for the oapi-codegen client
28+
jitterFunc func(time.Duration) time.Duration
2729
}
2830

2931
// ConfigOption allows setting custom parameters during construction
@@ -85,6 +87,13 @@ func WithCustomRequestEditorFn(fn RequestEditorFn) ConfigOption {
8587
}
8688
}
8789

90+
// WithJitterFunc allows setting a custom jitter function for polling.
91+
func WithJitterFunc(fn func(time.Duration) time.Duration) ConfigOption {
92+
return func(c *config) {
93+
c.jitterFunc = fn
94+
}
95+
}
96+
8897
type client struct {
8998
lowLevelClient ClientWithResponsesInterface
9099
config config
@@ -235,6 +244,7 @@ func NewTestClient(serverBaseUrl string, options ...ConfigOption) (TestClient, e
235244
cfg := config{
236245
PollInterval: DefaultPollInterval,
237246
APIVersion: DefaultAPIVersion,
247+
jitterFunc: Jitter,
238248
}
239249

240250
for _, opt := range options {
@@ -441,7 +451,9 @@ func contextCanceledError(operationDescription string, contextError error) error
441451

442452
// Query the job endpoint until we're redirected to its 'related' link containing results
443453
func (h *testHandle) pollJobToCompletion(ctx context.Context) (*uuid.UUID, error) {
444-
ticker := time.NewTicker(h.client.config.PollInterval)
454+
cfg := h.client.config
455+
456+
ticker := time.NewTicker(cfg.PollInterval)
445457
defer ticker.Stop()
446458

447459
getJobParams := &GetJobParams{Version: h.client.config.APIVersion}
@@ -463,6 +475,7 @@ func (h *testHandle) pollJobToCompletion(ctx context.Context) (*uuid.UUID, error
463475
if stopPolling {
464476
return nil, jobErr
465477
}
478+
ticker.Reset(cfg.jitterFunc(cfg.PollInterval))
466479
continue
467480

468481
case http.StatusSeeOther:
@@ -478,6 +491,7 @@ func (h *testHandle) pollJobToCompletion(ctx context.Context) (*uuid.UUID, error
478491
Str("orgID", h.orgID.String()).
479492
Str("jobID", h.jobID.String()).
480493
Msg("Job polling returned 404 Not Found, continuing polling in case of delayed job creation")
494+
ticker.Reset(cfg.jitterFunc(cfg.PollInterval))
481495
continue
482496

483497
default:
@@ -691,3 +705,13 @@ func (r *testResult) handleFindingsError(err error, partialFindings []FindingDat
691705
func ptr[T any](v T) *T {
692706
return &v
693707
}
708+
709+
// Jitter returns a random duration between 0.5 and 1.5 of the given duration.
710+
func Jitter(d time.Duration) time.Duration {
711+
if d <= 0 {
712+
return d
713+
}
714+
minDur := int64(float64(d) * 0.5)
715+
maxDur := int64(float64(d) * 1.5)
716+
return time.Duration(rand.Int63n(maxDur-minDur) + minDur)
717+
}

pkg/apiclients/testapi/testapi_test.go

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ func Test_StartTest_Success(t *testing.T) {
180180
// Create our test client
181181
testHTTPClient := newTestHTTPClient(t, server)
182182
testClient, err := testapi.NewTestClient(server.URL,
183-
testapi.WithPollInterval(1*time.Second), // short poll interval for faster tests
183+
testapi.WithPollInterval(1*time.Second),
184184
testapi.WithCustomHTTPClient(testHTTPClient),
185185
)
186186
require.NoError(t, err)
@@ -1047,3 +1047,85 @@ func assertTestFinishedWithOutcomeErrorsAndWarnings(t *testing.T, result testapi
10471047
assert.Nil(t, result.GetWarnings())
10481048
}
10491049
}
1050+
1051+
// TestJitter verifies times returned are 0.5-1.5 times the original value,
1052+
// and invalid times are return unmodified.
1053+
func TestJitter(t *testing.T) {
1054+
t.Parallel()
1055+
t.Run("returns original duration for zero or negative input", func(t *testing.T) {
1056+
t.Parallel()
1057+
assert.Equal(t, time.Duration(0), testapi.Jitter(0))
1058+
assert.Equal(t, time.Duration(-1), testapi.Jitter(-1))
1059+
})
1060+
1061+
t.Run("returns duration within 0.5x to 1.5x of input", func(t *testing.T) {
1062+
t.Parallel()
1063+
duration := 100 * time.Millisecond
1064+
minDur := time.Duration(float64(duration) * 0.5)
1065+
maxDur := time.Duration(float64(duration) * 1.5)
1066+
1067+
for range 100 {
1068+
jittered := testapi.Jitter(duration)
1069+
assert.GreaterOrEqual(t, jittered, minDur)
1070+
assert.LessOrEqual(t, jittered, maxDur)
1071+
}
1072+
})
1073+
}
1074+
1075+
// Test_Wait_CallsJitter ensures Jitter is called while polling for job completion.
1076+
func Test_Wait_CallsJitter(t *testing.T) {
1077+
t.Parallel()
1078+
ctx := context.Background()
1079+
1080+
testData := setupTestScenario(t)
1081+
1082+
params := testapi.StartTestParams{
1083+
OrgID: testData.OrgID.String(),
1084+
Subject: testData.TestSubjectCreate,
1085+
}
1086+
1087+
// Mock Jitter
1088+
var jitterCalled bool
1089+
jitterFunc := func(d time.Duration) time.Duration {
1090+
jitterCalled = true
1091+
return d
1092+
}
1093+
1094+
// Mock server handler
1095+
handlerConfig := TestAPIHandlerConfig{
1096+
OrgID: testData.OrgID,
1097+
JobID: testData.JobID,
1098+
TestID: testData.TestID,
1099+
APIVersion: testapi.DefaultAPIVersion,
1100+
PollCounter: testData.PollCounter,
1101+
JobPollResponses: []JobPollResponseConfig{
1102+
{Status: testapi.Pending}, // First poll
1103+
{ShouldRedirect: true}, // Second poll, redirects
1104+
},
1105+
FinalTestResult: FinalTestResultConfig{
1106+
Outcome: testapi.Pass,
1107+
},
1108+
}
1109+
handler := newTestAPIMockHandler(t, handlerConfig)
1110+
server, cleanup := startMockServer(t, handler)
1111+
defer cleanup()
1112+
1113+
// Act
1114+
testHTTPClient := newTestHTTPClient(t, server)
1115+
testClient, err := testapi.NewTestClient(server.URL,
1116+
testapi.WithPollInterval(1*time.Second),
1117+
testapi.WithCustomHTTPClient(testHTTPClient),
1118+
testapi.WithJitterFunc(jitterFunc),
1119+
)
1120+
require.NoError(t, err)
1121+
1122+
handle, err := testClient.StartTest(ctx, params)
1123+
require.NoError(t, err)
1124+
require.NotNil(t, handle)
1125+
1126+
err = handle.Wait(ctx)
1127+
require.NoError(t, err)
1128+
1129+
// Assert
1130+
assert.True(t, jitterCalled)
1131+
}

pkg/app/app.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func defaultFuncOrganization(engine workflow.Engine, config configuration.Config
7373
logger.Print("Failed to determine default value for \"ORGANIZATION\":", err)
7474
}
7575

76-
return orgId, nil
76+
return orgId, err
7777
}
7878
return callback
7979
}

pkg/app/app_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"path/filepath"
1111
"runtime"
1212
"testing"
13+
"time"
1314

1415
"github.com/golang/mock/gomock"
1516
zlog "github.com/rs/zerolog/log"
@@ -211,6 +212,38 @@ func Test_initConfiguration_useDefaultOrg(t *testing.T) {
211212
assert.Equal(t, defaultOrgSlug, actualOrgSlug)
212213
}
213214

215+
func Test_initConfiguration_failDefaultOrgLookup(t *testing.T) {
216+
orgId := "someOrgId"
217+
// setup mock
218+
ctrl := gomock.NewController(t)
219+
mockApiClient := mocks.NewMockApiClient(ctrl)
220+
221+
// mock assertion
222+
mockApiClient.EXPECT().Init(gomock.Any(), gomock.Any()).AnyTimes()
223+
mockApiClient.EXPECT().GetDefaultOrgId().Return("", errors.New("error")).Times(2)
224+
mockApiClient.EXPECT().GetDefaultOrgId().Return(orgId, nil).Times(1)
225+
226+
config := configuration.NewWithOpts(configuration.WithCachingEnabled(10 * time.Second))
227+
engine := workflow.NewWorkFlowEngine(config)
228+
apiClientFactory := func(url string, client *http.Client) api.ApiClient {
229+
return mockApiClient
230+
}
231+
initConfiguration(engine, config, &zlog.Logger, apiClientFactory)
232+
233+
actualOrgId, orgIdError := config.GetStringWithError(configuration.ORGANIZATION)
234+
assert.Error(t, orgIdError)
235+
assert.Empty(t, actualOrgId)
236+
237+
actualOrgSlug, slugError := config.GetStringWithError(configuration.ORGANIZATION_SLUG)
238+
assert.Error(t, slugError)
239+
assert.Empty(t, actualOrgSlug)
240+
241+
// ensure that if the error resolves, a valid value is returned
242+
actualOrgId, orgIdError = config.GetStringWithError(configuration.ORGANIZATION)
243+
assert.NoError(t, orgIdError)
244+
assert.Equal(t, orgId, actualOrgId)
245+
}
246+
214247
func Test_initConfiguration_useDefaultOrgAsFallback(t *testing.T) {
215248
orgName := "someOrgName"
216249
defaultOrgId := "someDefaultOrgId"

0 commit comments

Comments
 (0)