diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 5eff19681..c57917423 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -18,8 +18,28 @@ on: pull_request: jobs: + lint: + name: lint + runs-on: ubuntu-latest + steps: + - name: Prepare git + run: git config --global core.autocrlf false + + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: "./go.mod" + cache: "true" + + - name: Lint source code + run: | + make tools lint + unit-tests: name: unit tests + needs: [lint] runs-on: ubuntu-latest steps: - name: Prepare git @@ -49,10 +69,6 @@ jobs: run: | make tools - - name: Lint source code - run: | - make tools lint - - name: Run tests env: DEEPROXY_API_URL: ${{secrets.DEEPROXY_API_URL}} @@ -70,6 +86,7 @@ jobs: integration-tests: name: integration-tests + needs: [lint] runs-on: ${{ matrix.os }} strategy: matrix: @@ -99,12 +116,13 @@ jobs: run: | make tools - - name: Run integration tests with Pact + - name: Run integration & smoke tests with Pact if: matrix.os == 'ubuntu-latest' env: DEEPROXY_API_URL: ${{secrets.DEEPROXY_API_URL}} SNYK_TOKEN: ${{secrets.SNYK_TOKEN }} INTEG_TESTS: "true" + SMOKE_TESTS: "true" run: | export PATH=$PATH:~/pact/bin @@ -121,6 +139,7 @@ jobs: DEEPROXY_API_URL: ${{secrets.DEEPROXY_API_URL}} SNYK_TOKEN: ${{secrets.SNYK_TOKEN }} INTEG_TESTS: "true" + SMOKE_TESTS: "true" run: | export PATH=$PATH:~/pact/bin @@ -136,12 +155,14 @@ jobs: DEEPROXY_API_URL: ${{secrets.DEEPROXY_API_URL}} SNYK_TOKEN: ${{secrets.SNYK_TOKEN }} INTEG_TESTS: "true" + SMOKE_TESTS: "true" run: | make clean test proxy-test: name: proxy-test + needs: [lint] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -154,6 +175,7 @@ jobs: race-tests: name: race-test + needs: [lint] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -216,6 +238,7 @@ jobs: push: true test-release: name: test-release + needs: [lint, unit-tests] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9723a7d1e..a28483a09 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -133,24 +133,30 @@ jobs: chmod +x build/snyk-ls_linux_amd64_v1/snyk-ls build/snyk-ls_linux_amd64_v1/snyk-ls -licenses + # we only want to upload when we consciously release, not on merge - name: Login to AWS + if: {{ github.event_name == 'workflow_dispatch }} run: | .github/setup_aws_credentials.py \ --role-arn "arn:aws:iam::198361731867:role/Snyk-Assets-WriteOnly" \ --region "${{ secrets.AWS_REGION }}" - name: Upload binaries to static.snyk.io + if: {{ github.event_name == 'workflow_dispatch }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} AWS_S3_BUCKET_NAME: ${{ secrets.AWS_S3_BUCKET_NAME }} run: | .github/upload-to-s3.sh + # creating PR in cli repository needs ssh agent - uses: webfactory/ssh-agent@v0.9.0 + if: {{ github.event_name != 'workflow_dispatch }} with: ssh-private-key: ${{ secrets.TEAM_IDE_USER_SSH }} - name: Create PR in CLI to integrate LS + if: {{ github.event_name != 'workflow_dispatch }} env: GH_TOKEN: ${{ secrets.HAMMERHEAD_GITHUB_PAT_SNYKLS }} GITHUB_TOKEN: ${{ secrets.HAMMERHEAD_GITHUB_PAT_SNYKLS }} diff --git a/application/config/config.go b/application/config/config.go index 058589a78..b66cdce60 100644 --- a/application/config/config.go +++ b/application/config/config.go @@ -358,9 +358,21 @@ func (c *Config) Format() string { defer c.m.Unlock() return c.format } -func (c *Config) CLIDownloadLockFileName() string { - return filepath.Join(c.cliSettings.DefaultBinaryInstallPath(), "snyk-cli-download.lock") +func (c *Config) CLIDownloadLockFileName() (string, error) { + c.cliSettings.cliPathAccessMutex.Lock() + defer c.cliSettings.cliPathAccessMutex.Unlock() + var path string + if c.cliSettings.cliPath == "" { + c.cliSettings.cliPath = c.cliSettings.DefaultBinaryInstallPath() + } + path = filepath.Dir(c.cliSettings.cliPath) + err := os.MkdirAll(path, 0755) + if err != nil { + return "", err + } + return filepath.Join(path, "snyk-cli-download.lock"), nil } + func (c *Config) IsErrorReportingEnabled() bool { return c.isErrorReportingEnabled.Get() } func (c *Config) IsSnykOssEnabled() bool { return c.isSnykOssEnabled.Get() } func (c *Config) IsSnykCodeEnabled() bool { return c.isSnykCodeEnabled.Get() } diff --git a/application/server/notification.go b/application/server/notification.go index 734bbce7f..eff30dbcc 100644 --- a/application/server/notification.go +++ b/application/server/notification.go @@ -18,7 +18,6 @@ package server import ( "context" - "reflect" "time" "github.com/rs/zerolog" @@ -32,7 +31,6 @@ import ( ) func notifier(c *config.Config, srv types.Server, method string, params any) { - c.Logger().Debug().Str("method", "notifier").Str("type", reflect.TypeOf(params).String()).Msgf("Notifying") err := srv.Notify(context.Background(), method, params) logError(c.Logger(), err, "notifier") } @@ -99,7 +97,7 @@ func registerNotifier(c *config.Config, srv types.Server) { logger.Debug().Msg("sending cli path") case sglsp.ShowMessageParams: notifier(c, srv, "window/showMessage", params) - logger.Debug().Interface("message", params).Msg("showing message") + logger.Debug().Interface("message", params.Message).Msg("showing message") case types.SnykTrustedFoldersParams: notifier(c, srv, "$/snyk.addTrustedFolders", params) logger.Info(). diff --git a/application/server/parallelization_test.go b/application/server/parallelization_test.go index ca40ef9eb..c37323f74 100644 --- a/application/server/parallelization_test.go +++ b/application/server/parallelization_test.go @@ -70,6 +70,8 @@ func Test_Concurrent_CLI_Runs(t *testing.T) { } wg.Wait() + setUniqueCliPath(t, c) + clientParams := types.InitializeParams{ WorkspaceFolders: workspaceFolders, InitializationOptions: types.Settings{ @@ -79,6 +81,8 @@ func Test_Concurrent_CLI_Runs(t *testing.T) { FilterSeverity: types.DefaultSeverityFilter(), AuthenticationMethod: types.TokenAuthentication, AutomaticAuthentication: "false", + ManageBinariesAutomatically: "true", + CliPath: c.CliSettings().Path(), }, } diff --git a/application/server/server_smoke_test.go b/application/server/server_smoke_test.go index bd82626a9..3a56e47a1 100644 --- a/application/server/server_smoke_test.go +++ b/application/server/server_smoke_test.go @@ -22,6 +22,7 @@ import ( "os" "path" "path/filepath" + "runtime" "strconv" "strings" "testing" @@ -29,6 +30,8 @@ import ( "github.com/creachadair/jrpc2" "github.com/creachadair/jrpc2/server" + "github.com/rs/zerolog" + "github.com/snyk/go-application-framework/pkg/configuration" sglsp "github.com/sourcegraph/go-lsp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -37,6 +40,8 @@ import ( "github.com/snyk/snyk-ls/application/di" "github.com/snyk/snyk-ls/domain/ide/hover" "github.com/snyk/snyk-ls/domain/ide/workspace" + "github.com/snyk/snyk-ls/domain/snyk" + "github.com/snyk/snyk-ls/infrastructure/cli/install" "github.com/snyk/snyk-ls/infrastructure/code" "github.com/snyk/snyk-ls/internal/product" "github.com/snyk/snyk-ls/internal/testutil" @@ -141,6 +146,7 @@ func Test_SmokeWorkspaceScan(t *testing.T) { } func Test_SmokeIssueCaching(t *testing.T) { + testutil.NotOnWindows(t, "git clone fails on juiceshop ") // TODO remove & fix t.Run("adds issues to cache correctly", func(t *testing.T) { loc, jsonRPCRecorder := setupServer(t) c := testutil.SmokeTest(t, false) @@ -161,8 +167,12 @@ func Test_SmokeIssueCaching(t *testing.T) { ossIssuesForFile := folderGoof.IssuesForFile(filepath.Join(cloneTargetDirGoof, "package.json")) require.Greater(t, len(ossIssuesForFile), 1) // 108 is the number of issues in the package.json file as of now - codeIssuesForFile := folderGoof.IssuesForFile(filepath.Join(cloneTargetDirGoof, "app.js")) - require.Greater(t, len(codeIssuesForFile), 1) // 5 is the number of issues in the app.js file as of now + var codeIssuesForFile []snyk.Issue + + require.Eventually(t, func() bool { + codeIssuesForFile = folderGoof.IssuesForFile(filepath.Join(cloneTargetDirGoof, "app.js")) + return len(codeIssuesForFile) > 1 + }, time.Second*5, time.Second) checkDiagnosticPublishingForCachingSmokeTest(t, jsonRPCRecorder, 1, 1, c) @@ -170,6 +180,12 @@ func Test_SmokeIssueCaching(t *testing.T) { jsonRPCRecorder.ClearCallbacks() // now we add juice shop as second folder/repo + if runtime.GOOS == "windows" { + t.Setenv("SNYK_LOG_LEVEL", "trace") + c.ConfigureLogging(nil) + c.SetLogLevel(zerolog.TraceLevel.String()) + } + folderJuice := addJuiceShopAsWorkspaceFolder(t, loc, c) // scan both created folders @@ -434,7 +450,7 @@ func checkDiagnosticPublishingForCachingSmokeTest( packageJsonCount == expectedOSS return result - }, time.Second*5, time.Second) + }, time.Second*600, time.Second) } func runSmokeTest(t *testing.T, repo string, commit string, file1 string, file2 string, useConsistentIgnores bool, @@ -657,7 +673,6 @@ func getIssueListFromPublishDiagnosticsNotification(t *testing.T, jsonRPCRecorde issueList = append(issueList, diagnostic.Data) } } - return issueList } @@ -712,6 +727,8 @@ func prepareInitParams(t *testing.T, cloneTargetDir string, c *config.Config) ty Uri: uri.PathToUri(cloneTargetDir), } + setUniqueCliPath(t, c) + clientParams := types.InitializeParams{ WorkspaceFolders: []types.WorkspaceFolder{folder}, InitializationOptions: types.Settings{ @@ -724,11 +741,20 @@ func prepareInitParams(t *testing.T, cloneTargetDir string, c *config.Config) ty ActivateSnykCode: strconv.FormatBool(c.IsSnykCodeEnabled()), ActivateSnykIac: strconv.FormatBool(c.IsSnykIacEnabled()), ActivateSnykOpenSource: strconv.FormatBool(c.IsSnykOssEnabled()), + ActivateSnykCodeQuality: strconv.FormatBool(c.IsSnykCodeQualityEnabled()), + ActivateSnykCodeSecurity: strconv.FormatBool(c.IsSnykCodeSecurityEnabled()), + CliPath: c.CliSettings().Path(), }, } return clientParams } +func setUniqueCliPath(t *testing.T, c *config.Config) { + t.Helper() + discovery := install.Discovery{} + c.CliSettings().SetPath(filepath.Join(t.TempDir(), discovery.ExecutableName(false))) +} + func checkFeatureFlagStatus(t *testing.T, c *config.Config, loc *server.Local) { t.Helper() // only check on mt-us @@ -795,7 +821,7 @@ func Test_SmokeSnykCodeFileScan(t *testing.T) { _ = textDocumentDidSave(t, &loc, testPath) - assert.Eventually(t, checkForPublishedDiagnostics(t, testPath, 6, jsonRPCRecorder), maxIntegTestDuration, 10*time.Millisecond) + assert.Eventually(t, checkForPublishedDiagnostics(t, testPath, -1, jsonRPCRecorder), 2*time.Minute, 10*time.Millisecond) } func Test_SmokeUncFilePath(t *testing.T) { @@ -825,7 +851,7 @@ func Test_SmokeUncFilePath(t *testing.T) { assert.Eventually(t, checkForPublishedDiagnostics(t, testPath, -1, jsonRPCRecorder), maxIntegTestDuration, 10*time.Millisecond) } -func Test_SmokeSnykCodeDelta_OneNewVuln(t *testing.T) { +func Test_SmokeSnykCodeDelta_NewVulns(t *testing.T) { loc, jsonRPCRecorder := setupServer(t) c := testutil.SmokeTest(t, false) c.SetSnykCodeEnabled(true) @@ -834,11 +860,18 @@ func Test_SmokeSnykCodeDelta_OneNewVuln(t *testing.T) { di.Init() fileWithNewVulns := "vulns.js" - var cloneTargetDir, err = testutil.SetupCustomTestRepo(t, t.TempDir(), "https://github.com/snyk-labs/nodejs-goof", "0336589", c.Logger()) + var cloneTargetDir, err = testutil.SetupCustomTestRepo(t, t.TempDir(), nodejsGoof, "0336589", c.Logger()) assert.NoError(t, err) - newFileInCurrentDir(t, cloneTargetDir, fileWithNewVulns, "var token = 'SECRET_TOKEN_f8ed84e8f41e4146403dd4a6bbcea5e418d23a9';") + sourceContent, err := os.ReadFile(filepath.Join(cloneTargetDir, "app.js")) + require.NoError(t, err) + + newFileInCurrentDir(t, cloneTargetDir, fileWithNewVulns, string(sourceContent)) + c.SetSnykOssEnabled(false) + c.SetSnykIacEnabled(false) + c.EnableSnykCodeQuality(false) + c.SetManageBinariesAutomatically(false) initParams := prepareInitParams(t, cloneTargetDir, c) ensureInitialized(t, c, loc, initParams) @@ -848,7 +881,7 @@ func Test_SmokeSnykCodeDelta_OneNewVuln(t *testing.T) { checkForScanParams(t, jsonRPCRecorder, cloneTargetDir, product.ProductCode) issueList := getIssueListFromPublishDiagnosticsNotification(t, jsonRPCRecorder, product.ProductCode, cloneTargetDir) - assert.Equal(t, len(issueList), 1) + assert.Greater(t, len(issueList), 0) assert.Contains(t, issueList[0].FilePath, fileWithNewVulns) } @@ -903,6 +936,10 @@ func Test_SmokeSnykCodeDelta_NoNewIssuesFound(t *testing.T) { func ensureInitialized(t *testing.T, c *config.Config, loc server.Local, initParams types.InitializeParams) { t.Helper() + // temporary until policy engine doesn't output to stdout anymore + t.Setenv("SNYK_LOG_LEVEL", "info") + c.ConfigureLogging(nil) + c.Engine().GetConfiguration().Set(configuration.DEBUG, false) _, err := loc.Client.Call(ctx, "initialize", initParams) assert.NoError(t, err) diff --git a/domain/ide/command/get_active_user_test.go b/domain/ide/command/get_active_user_test.go index 6b904553f..5922f5402 100644 --- a/domain/ide/command/get_active_user_test.go +++ b/domain/ide/command/get_active_user_test.go @@ -43,7 +43,7 @@ func Test_getActiveUser_Execute_User_found(t *testing.T) { expectedUser, expectedUserData := whoamiWorkflowResponse(t) - mockEngine, engineConfig := setUpEngineMock(t, c) + mockEngine, engineConfig := testutil.SetUpEngineMock(t, c) mockEngine.EXPECT().GetConfiguration().Return(engineConfig).AnyTimes() mockEngine.EXPECT().InvokeWithConfig(localworkflows.WORKFLOWID_WHOAMI, gomock.Any()).Return(expectedUserData, nil) @@ -76,7 +76,7 @@ func Test_getActiveUser_Execute_Result_Empty(t *testing.T) { c := testutil.UnitTest(t) cmd := setupCommandWithAuthService(t, c) - mockEngine, engineConfig := setUpEngineMock(t, c) + mockEngine, engineConfig := testutil.SetUpEngineMock(t, c) mockEngine.EXPECT().GetConfiguration().Return(engineConfig).AnyTimes() mockEngine.EXPECT().InvokeWithConfig(localworkflows.WORKFLOWID_WHOAMI, gomock.Any()).Return([]workflow.Data{}, nil) @@ -90,7 +90,7 @@ func Test_getActiveUser_Execute_Error_Result(t *testing.T) { c := testutil.UnitTest(t) cmd := setupCommandWithAuthService(t, c) - mockEngine, engineConfig := setUpEngineMock(t, c) + mockEngine, engineConfig := testutil.SetUpEngineMock(t, c) mockEngine.EXPECT().GetConfiguration().Return(engineConfig).AnyTimes() testError := errors.New("test error") mockEngine.EXPECT().InvokeWithConfig(localworkflows.WORKFLOWID_WHOAMI, gomock.Any()).Return([]workflow.Data{}, testError) diff --git a/domain/ide/command/report_analytics_test.go b/domain/ide/command/report_analytics_test.go index 4356ce810..17bf7c366 100644 --- a/domain/ide/command/report_analytics_test.go +++ b/domain/ide/command/report_analytics_test.go @@ -18,11 +18,13 @@ package command import ( "context" + "encoding/json" "testing" "github.com/golang/mock/gomock" - localworkflows "github.com/snyk/go-application-framework/pkg/local_workflows" + "github.com/snyk/go-application-framework/pkg/workflow" + "github.com/stretchr/testify/mock" "github.com/snyk/snyk-ls/application/config" "github.com/snyk/snyk-ls/infrastructure/authentication" @@ -41,7 +43,7 @@ func Test_ReportAnalyticsCommand_IsCallingExtension(t *testing.T) { testInput := "some data" cmd := setupReportAnalyticsCommand(t, c, testInput) - mockEngine, engineConfig := setUpEngineMock(t, c) + mockEngine, engineConfig := testutil.SetUpEngineMock(t, c) mockEngine.EXPECT().GetConfiguration().Return(engineConfig).AnyTimes() mockEngine.EXPECT().InvokeWithInputAndConfig(localworkflows.WORKFLOWID_REPORT_ANALYTICS, gomock.Any(), gomock.Any()).Return(nil, nil) @@ -51,6 +53,52 @@ func Test_ReportAnalyticsCommand_IsCallingExtension(t *testing.T) { require.Emptyf(t, output, "output should be empty") } +func Test_ReportAnalyticsCommand_PlugInstalledEvent(t *testing.T) { + c := testutil.UnitTest(t) + + testInput := types.AnalyticsEventParam{ + InteractionType: "plugin installed", + Category: []string{"install"}, + Status: "success", + TargetId: "pkg:file/none", + TimestampMs: 123, + Extension: map[string]any{"device_id": c.DeviceID()}, + } + + marshal, err := json.Marshal(testInput) + if err != nil { + return + } + + cmd := setupReportAnalyticsCommand(t, c, string(marshal)) + + mockEngine, engineConfig := testutil.SetUpEngineMock(t, c) + mockEngine.EXPECT().GetConfiguration().Return(engineConfig).AnyTimes() + + mockEngine.EXPECT().InvokeWithInputAndConfig( + localworkflows.WORKFLOWID_REPORT_ANALYTICS, + mock.MatchedBy(func(i interface{}) bool { + inputData, ok := i.([]workflow.Data) + require.Truef(t, ok, "input should be workflow data") + require.Lenf(t, inputData, 1, "should only have one input") + + payload := string(inputData[0].GetPayload().([]byte)) + + require.Contains(t, payload, "plugin installed") + require.Contains(t, payload, "install") + require.Contains(t, payload, "device_id") + require.Contains(t, payload, "123") + + return true + }), + gomock.Any(), + ).Return(nil, nil) + + output, err := cmd.Execute(context.Background()) + require.NoError(t, err) + require.Emptyf(t, output, "output should be empty") +} + func setupReportAnalyticsCommand(t *testing.T, c *config.Config, testInput string) *reportAnalyticsCommand { t.Helper() provider := authentication.NewFakeCliAuthenticationProvider(c) diff --git a/domain/ide/command/test_helpers.go b/domain/ide/command/test_helpers.go deleted file mode 100644 index 404820109..000000000 --- a/domain/ide/command/test_helpers.go +++ /dev/null @@ -1,37 +0,0 @@ -/* - * © 2023 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 command - -import ( - "testing" - - "github.com/golang/mock/gomock" - - "github.com/snyk/go-application-framework/pkg/configuration" - "github.com/snyk/go-application-framework/pkg/mocks" - - "github.com/snyk/snyk-ls/application/config" -) - -func setUpEngineMock(t *testing.T, c *config.Config) (*mocks.MockEngine, configuration.Configuration) { - t.Helper() - ctrl := gomock.NewController(t) - mockEngine := mocks.NewMockEngine(ctrl) - engineConfig := c.Engine().GetConfiguration() - c.SetEngine(mockEngine) - return mockEngine, engineConfig -} diff --git a/domain/ide/workspace/folder.go b/domain/ide/workspace/folder.go index a9c6cdfb6..a81bda240 100644 --- a/domain/ide/workspace/folder.go +++ b/domain/ide/workspace/folder.go @@ -32,12 +32,10 @@ import ( "github.com/snyk/snyk-ls/internal/delta" - "github.com/snyk/snyk-ls/internal/types" - "github.com/snyk/snyk-ls/internal/util" - - "github.com/google/uuid" "github.com/puzpuzpuz/xsync/v3" + "github.com/snyk/snyk-ls/internal/types" + gafanalytics "github.com/snyk/go-application-framework/pkg/analytics" "github.com/snyk/go-application-framework/pkg/instrumentation" "github.com/snyk/go-application-framework/pkg/local_workflows/json_schemas" @@ -375,7 +373,6 @@ func (f *Folder) updateGlobalCacheAndSeverityCounts(scanData *snyk.ScanData) { func sendAnalytics(data *snyk.ScanData) { c := config.CurrentConfig() - gafConfig := c.Engine().GetConfiguration() logger := c.Logger().With().Str("method", "folder.sendAnalytics").Logger() if data.Product == "" { @@ -388,38 +385,29 @@ func sendAnalytics(data *snyk.ScanData) { return } - ic := gafanalytics.NewInstrumentationCollector() - - //Add to the interaction attribute in the analytics event - iid := instrumentation.AssembleUrnFromUUID(uuid.NewString()) - ic.SetInteractionId(iid) - ic.SetTimestamp(data.TimestampFinished) - ic.SetStage("dev") - ic.SetInteractionType("Scan done") - + // this information is not filled automatically, so we need to collect it categories := setupCategories(data, c) - ic.SetCategory(categories) - ic.SetStatus(gafanalytics.Success) - - summary := createTestSummary(data, c) - ic.SetTestSummary(summary) - targetId, err := instrumentation.GetTargetId(data.Path, instrumentation.AutoDetectedTargetId) if err != nil { logger.Err(err).Msg("Error creating the Target Id") } - ic.SetTargetId(targetId) + summary := createTestSummary(data, c) - ic.AddExtension("device_id", c.DeviceID()) - ic.AddExtension("is_delta_scan", data.IsDeltaScan) + param := types.AnalyticsEventParam{ + InteractionType: "Scan done", + Category: categories, + Status: string(gafanalytics.Success), + TargetId: targetId, + TimestampMs: data.TimestampFinished.UnixMilli(), + DurationMs: int64(data.DurationMs), + Extension: map[string]any{"is_delta_scan": data.IsDeltaScan}, + } - //Populate the runtime attribute of the analytics event - ua := util.GetUserAgent(gafConfig, config.Version) - ic.SetUserAgent(ua) - ic.SetDuration(data.DurationMs) + ic := analytics.PayloadForAnalyticsEventParam(c, param) - //Set the final type attribute of the analytics event - ic.SetType("analytics") + // test specific data is not handled in the PayloadForAnalytics helper + // and must be added explicitly + ic.SetTestSummary(summary) analyticsData, err := gafanalytics.GetV2InstrumentationObject(ic) if err != nil { diff --git a/infrastructure/analytics/analytics.go b/infrastructure/analytics/analytics.go index 97cc77b0b..1f8b7b25c 100644 --- a/infrastructure/analytics/analytics.go +++ b/infrastructure/analytics/analytics.go @@ -17,32 +17,62 @@ package analytics import ( + "encoding/json" "sync" + "time" + "github.com/google/uuid" + "github.com/snyk/go-application-framework/pkg/analytics" configuration2 "github.com/snyk/go-application-framework/pkg/configuration" + "github.com/snyk/go-application-framework/pkg/instrumentation" localworkflows "github.com/snyk/go-application-framework/pkg/local_workflows" "github.com/snyk/go-application-framework/pkg/workflow" "github.com/snyk/snyk-ls/application/config" + "github.com/snyk/snyk-ls/internal/types" + "github.com/snyk/snyk-ls/internal/util" ) var analyticsMu = sync.RWMutex{} func SendAnalyticsToAPI(c *config.Config, payload []byte) error { logger := c.Logger().With().Str("method", "analytics.sendAnalyticsToAPI").Logger() - logger.Debug().Str("payload", string(payload)).Msg("Analytics Payload") - inputData := workflow.NewData( - workflow.NewTypeIdentifier(localworkflows.WORKFLOWID_REPORT_ANALYTICS, "reportAnalytics"), - "application/json", - payload, - ) + var eventsParam types.AnalyticsEventParam + err := json.Unmarshal(payload, &eventsParam) + var inputData workflow.Data + if err == nil && eventsParam.TimestampMs > 0 { + ic := PayloadForAnalyticsEventParam(c, eventsParam) + instrumentationObject, icErr := analytics.GetV2InstrumentationObject(ic) + if icErr != nil { + return err + } + + bytes, marshalErr := json.Marshal(instrumentationObject) + if marshalErr != nil { + return marshalErr + } + + logger.Debug().Str("payload", string(bytes)).Msg("Analytics Payload") + inputData = workflow.NewData( + workflow.NewTypeIdentifier(localworkflows.WORKFLOWID_REPORT_ANALYTICS, "reportAnalytics"), + "application/json", + bytes, + ) + } else { + logger.Debug().Str("payload", string(payload)).Msg("Analytics Payload") + inputData = workflow.NewData( + workflow.NewTypeIdentifier(localworkflows.WORKFLOWID_REPORT_ANALYTICS, "reportAnalytics"), + "application/json", + payload, + ) + } engine := c.Engine() configuration := engine.GetConfiguration().Clone() configuration.Set(configuration2.FLAG_EXPERIMENTAL, true) analyticsMu.Lock() - _, err := engine.InvokeWithInputAndConfig( + _, err = engine.InvokeWithInputAndConfig( localworkflows.WORKFLOWID_REPORT_ANALYTICS, []workflow.Data{inputData}, configuration, @@ -55,3 +85,31 @@ func SendAnalyticsToAPI(c *config.Config, payload []byte) error { } return nil } + +func PayloadForAnalyticsEventParam(c *config.Config, param types.AnalyticsEventParam) analytics.InstrumentationCollector { + ic := analytics.NewInstrumentationCollector() + // Add to the interaction attribute in the analytics event + if param.InteractionUUID == "" { + param.InteractionUUID = uuid.New().String() + } + + iid := instrumentation.AssembleUrnFromUUID(param.InteractionUUID) + + //Set the final type attribute of the analytics event + ic.SetType("analytics") + ic.SetInteractionId(iid) + ic.SetStage("dev") + ic.AddExtension("device_id", c.DeviceID()) + for s, a := range param.Extension { + ic.AddExtension(s, a) + } + ua := util.GetUserAgent(c.Engine().GetConfiguration(), config.Version) + ic.SetUserAgent(ua) + + ic.SetTimestamp(time.UnixMilli(param.TimestampMs)) + ic.SetInteractionType(param.InteractionType) + ic.SetStatus(analytics.Status(param.Status)) + ic.SetCategory(param.Category) + ic.SetTargetId(param.TargetId) + return ic +} diff --git a/infrastructure/analytics/pact_test.go b/infrastructure/analytics/pact_test.go index 1a672f43f..685c06656 100644 --- a/infrastructure/analytics/pact_test.go +++ b/infrastructure/analytics/pact_test.go @@ -19,6 +19,7 @@ import ( "github.com/snyk/snyk-ls/application/config" "github.com/snyk/snyk-ls/internal/testutil" + "github.com/snyk/snyk-ls/internal/types" ) func TestAnalyticsProviderPactV2(t *testing.T) { @@ -38,7 +39,7 @@ func TestAnalyticsProviderPactV2(t *testing.T) { pact.Setup(true) base := fmt.Sprintf("http://localhost:%d", pact.Server.Port) orgUUID := "54125374-3f93-402e-b693-e0724794d71f" - expectedBody := testGetAnalyticsV2Payload() + expectedBody := testGetAnalyticsV2Payload(t) v2InstrumentationData := utils.ValueOf(json.Marshal(expectedBody)) var test = func() (err error) { @@ -75,7 +76,100 @@ func TestAnalyticsProviderPactV2(t *testing.T) { } } -func testGetAnalyticsV2Payload() interface{} { +func TestAnalyticsPluginInstalled(t *testing.T) { + testutil.NotOnWindows(t, "we don't have a pact cli") + c := testutil.UnitTest(t) + + pact := &dsl.Pact{ + Consumer: "snyk-ls", + Provider: "AnalyticsProvider", + LogDir: "logs", + PactDir: "./pacts", + LogLevel: "DEBUG", + } + + defer pact.Teardown() + + pact.Setup(true) + base := fmt.Sprintf("http://localhost:%d", pact.Server.Port) + orgUUID := "54125374-3f93-402e-b693-e0724794d71f" + v2Object, expectedOutputData := testGetAnalyticsEventParam(t) + inputData := utils.ValueOf(json.Marshal(v2Object)) + + var test = func() (err error) { + //prepare + c.SetToken("token") + c.SetOrganization(orgUUID) + c.UpdateApiEndpoints(base) + + // invoke function under test + err = SendAnalyticsToAPI(c, inputData) + assert.NoError(t, err) + + return nil + } + + pact. + AddInteraction(). + Given("Analytics data is ready"). + UponReceiving("An AnalyticsEventParam json payload"). + WithRequest(dsl.Request{ + Method: "POST", + Path: dsl.String("/hidden/orgs/" + orgUUID + "/analytics"), + Body: expectedOutputData, + }). + WillRespondWith(dsl.Response{ + Status: 201, + Headers: dsl.MapMatcher{"Content-Type": dsl.Term("application/json", `^application\/json$`)}, + Body: map[string]interface{}{}, + }) + + // Verify runs the current test case against a Mock Service. + if err := pact.Verify(test); err != nil { + t.Fatalf("Error on Verify: %v", err) + } +} + +func testGetAnalyticsEventParam(t *testing.T) (types.AnalyticsEventParam, any) { + t.Helper() + + now := time.Now() + event := types.AnalyticsEventParam{ + InteractionType: "plugin installed", + Category: []string{"install"}, + Status: string(analytics.Success), + TimestampMs: now.UnixMilli(), + TargetId: "pkg:github/package-url/purl-spec@244fd47e07d1004f0aed9c", + InteractionUUID: uuid.NewString(), + } + + ic := testPopulateICWithStdValues(t, event.InteractionUUID) + ic.SetInteractionType(event.InteractionType) + ic.SetCategory(event.Category) + ic.SetTimestamp(now) + + v2InstrumentationObject, _ := analytics.GetV2InstrumentationObject(ic) + + return event, v2InstrumentationObject +} + +func testGetAnalyticsV2Payload(t *testing.T) any { + t.Helper() + ic := testPopulateICWithStdValues(t, "00000000-0000-0000-0000-000000000000") + + summary := createTestSummary() + ic.SetTestSummary(summary) + ic.SetInteractionType("Scan done") + ic.SetCategory([]string{"oss", "test"}) + ic.SetDuration(10 * time.Millisecond) + + actualV2InstrumentationObject, _ := analytics.GetV2InstrumentationObject(ic) + + return actualV2InstrumentationObject +} + +func testPopulateICWithStdValues(t *testing.T, interactionUUID string) analytics.InstrumentationCollector { + t.Helper() gafConfig := configuration.NewInMemory() conf := config.CurrentConfig() ic := analytics.NewInstrumentationCollector() @@ -83,24 +177,16 @@ func testGetAnalyticsV2Payload() interface{} { ua := networking.UserAgent(networking.UaWithConfig(gafConfig), networking.UaWithApplication("snyk-ls", config.Version)) ic.SetUserAgent(ua) - iid := instrumentation.AssembleUrnFromUUID(uuid.NewString()) + iid := instrumentation.AssembleUrnFromUUID(interactionUUID) ic.SetInteractionId(iid) ic.SetTimestamp(time.Now()) - ic.SetDuration(10 * time.Millisecond) - ic.SetCategory([]string{"oss", "test"}) ic.SetStage("dev") ic.SetStatus("success") //or get result status from scan - ic.SetInteractionType("Scan done") - ic.SetInteractionId("urn:snyk:interaction:00000000-0000-0000-0000-000000000000") + ic.SetInteractionId(iid) ic.SetTargetId("pkg:github/package-url/purl-spec@244fd47e07d1004f0aed9c") - summary := createTestSummary() - ic.SetTestSummary(summary) - ic.AddExtension("deviceid", conf.DeviceID()) + ic.AddExtension("device_id", conf.DeviceID()) ic.SetType("analytics") - - actualV2InstrumentationObject, _ := analytics.GetV2InstrumentationObject(ic) - - return actualV2InstrumentationObject + return ic } func createTestSummary() json_schemas.TestSummary { diff --git a/infrastructure/authentication/auth_service_impl.go b/infrastructure/authentication/auth_service_impl.go index d70077277..2719c9b5d 100644 --- a/infrastructure/authentication/auth_service_impl.go +++ b/infrastructure/authentication/auth_service_impl.go @@ -30,10 +30,12 @@ import ( "github.com/erni27/imcache" "github.com/rs/zerolog" + "github.com/snyk/go-application-framework/pkg/analytics" sglsp "github.com/sourcegraph/go-lsp" "golang.org/x/oauth2" "github.com/snyk/snyk-ls/application/config" + analytics2 "github.com/snyk/snyk-ls/infrastructure/analytics" "github.com/snyk/snyk-ls/internal/data_structure" noti "github.com/snyk/snyk-ls/internal/notification" "github.com/snyk/snyk-ls/internal/observability/error_reporting" @@ -73,6 +75,7 @@ func (a *AuthenticationServiceImpl) Authenticate(ctx context.Context) (token str if token == "" || err != nil { a.c.Logger().Warn().Err(err).Msgf("Failed to authenticate using auth provider %v", reflect.TypeOf(a.provider)) + a.sendAuthenticationAnalytics(analytics.Failure, err) return token, err } @@ -87,10 +90,42 @@ func (a *AuthenticationServiceImpl) Authenticate(ctx context.Context) (token str a.c.UpdateApiEndpoints(prioritizedUrl) a.UpdateCredentials(token, true) a.ConfigureProviders(a.c) - + a.sendAuthenticationAnalytics(analytics.Success, nil) return token, err } +func (a *AuthenticationServiceImpl) sendAuthenticationAnalytics(status analytics.Status, err error) { + logger := a.c.Logger().With().Str("method", "sendAuthenticationAnalytics").Logger() + event := types.AnalyticsEventParam{ + InteractionType: "authenticated", + Category: []string{"auth", string(a.c.AuthenticationMethod())}, + Status: string(status), + } + + ic := analytics2.PayloadForAnalyticsEventParam(a.c, event) + + if err != nil { + ic.AddError(err) + } + + analyticsRequestBody, err := analytics.GetV2InstrumentationObject(ic) + if err != nil { + logger.Err(err).Msg("Failed to get analytics request body") + return + } + + bytes, err := json.Marshal(analyticsRequestBody) + if err != nil { + logger.Err(err).Msg("Failed to marshal analytics request body") + return + } + + err = analytics2.SendAnalyticsToAPI(a.c, bytes) + if err != nil { + logger.Err(err).Msg("Failed to send analytics") + } +} + func getPrioritizedApiUrl(customUrl string, engineUrl string) string { defaultUrl := config.DefaultSnykApiUrl customUrl = strings.TrimRight(customUrl, "/ ") diff --git a/infrastructure/authentication/auth_service_impl_test.go b/infrastructure/authentication/auth_service_impl_test.go index c31621fc9..1a7db21c5 100644 --- a/infrastructure/authentication/auth_service_impl_test.go +++ b/infrastructure/authentication/auth_service_impl_test.go @@ -23,9 +23,13 @@ import ( "testing" "time" + "github.com/golang/mock/gomock" + "github.com/snyk/go-application-framework/pkg/analytics" "github.com/snyk/go-application-framework/pkg/configuration" - + localworkflows "github.com/snyk/go-application-framework/pkg/local_workflows" + "github.com/snyk/go-application-framework/pkg/workflow" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "golang.org/x/oauth2" @@ -36,6 +40,53 @@ import ( "github.com/snyk/snyk-ls/internal/types" ) +func TestAuthenticateSendsAuthenticationEventOnSuccess(t *testing.T) { + c := testutil.UnitTest(t) + + err := runAuthEventTest(t, c, analytics.Success) + + assert.NoError(t, err) +} + +func TestAuthenticateSendsAuthenticationEventOnFailure(t *testing.T) { + c := testutil.UnitTest(t) + + err := runAuthEventTest(t, c, analytics.Failure) + + assert.Error(t, err) +} + +func runAuthEventTest(t *testing.T, c *config.Config, status analytics.Status) error { + t.Helper() + gafConfig := configuration.New() + authenticator := NewFakeOauthAuthenticator(defaultExpiry, true, gafConfig, status == analytics.Success).(*fakeOauthAuthenticator) + mockEngine, engineConfig := testutil.SetUpEngineMock(t, c) + mockEngine.EXPECT().GetConfiguration().Return(engineConfig).AnyTimes() + + mockEngine.EXPECT().InvokeWithInputAndConfig( + localworkflows.WORKFLOWID_REPORT_ANALYTICS, + mock.MatchedBy(func(i interface{}) bool { + inputData, ok := i.([]workflow.Data) + require.Truef(t, ok, "input should be workflow data") + require.Lenf(t, inputData, 1, "should only have one input") + + payload := string(inputData[0].GetPayload().([]byte)) + + require.Contains(t, payload, "authenticated") + require.Contains(t, payload, "auth") + require.Contains(t, payload, status) + return true + }), + gomock.Any(), + ).Return(nil, nil) + + provider := newOAuthProvider(gafConfig, authenticator, c.Logger()) + service := NewAuthenticationService(c, provider, error_reporting.NewTestErrorReporter(), notification.NewMockNotifier()) + + _, err := service.Authenticate(context.Background()) + return err +} + func Test_UpdateCredentials(t *testing.T) { t.Run("CLI Authentication", func(t *testing.T) { c := testutil.UnitTest(t) diff --git a/infrastructure/authentication/oauth_provider_test.go b/infrastructure/authentication/oauth_provider_test.go index b7506812b..72d4ec67d 100644 --- a/infrastructure/authentication/oauth_provider_test.go +++ b/infrastructure/authentication/oauth_provider_test.go @@ -19,6 +19,7 @@ package authentication import ( "context" "encoding/json" + "errors" "net/http" url2 "net/url" "sync" @@ -42,13 +43,15 @@ type fakeOauthAuthenticator struct { expiry time.Time isSupported bool config configuration.Configuration + success bool } -func NewFakeOauthAuthenticator(tokenExpiry time.Time, isSupported bool, config configuration.Configuration) auth.Authenticator { +func NewFakeOauthAuthenticator(tokenExpiry time.Time, isSupported bool, config configuration.Configuration, success bool) auth.Authenticator { return &fakeOauthAuthenticator{ isSupported: isSupported, config: config, expiry: tokenExpiry, + success: success, } } @@ -88,6 +91,10 @@ func (f *fakeOauthAuthenticator) GetAllCalls(op string) [][]any { func (f *fakeOauthAuthenticator) Authenticate() error { f.addCall(nil, "Authenticate") + if !f.success { + return errors.New("fake auth error") + } + token := &oauth2.Token{AccessToken: "a", TokenType: "b", RefreshToken: "c", Expiry: f.expiry} tokenString, err := json.Marshal(token) @@ -98,7 +105,7 @@ func (f *fakeOauthAuthenticator) Authenticate() error { return nil } -func (f *fakeOauthAuthenticator) AddAuthenticationHeader(request *http.Request) error { +func (f *fakeOauthAuthenticator) AddAuthenticationHeader(_ *http.Request) error { f.addCall(nil, "AddAuthenticationHeader") return nil } @@ -109,7 +116,7 @@ func (f *fakeOauthAuthenticator) IsSupported() bool { func TestAuthenticateUsesAuthenticator(t *testing.T) { config := configuration.New() - authenticator := NewFakeOauthAuthenticator(defaultExpiry, true, config).(*fakeOauthAuthenticator) + authenticator := NewFakeOauthAuthenticator(defaultExpiry, true, config, true).(*fakeOauthAuthenticator) provider := newOAuthProvider(config, authenticator, config2.CurrentConfig().Logger()) @@ -122,7 +129,7 @@ func TestAuthenticateUsesAuthenticator(t *testing.T) { func TestAuthURL_ShouldReturnURL(t *testing.T) { config := configuration.New() - authenticator := NewFakeOauthAuthenticator(time.Now().Add(10*time.Second), true, config).(*fakeOauthAuthenticator) + authenticator := NewFakeOauthAuthenticator(time.Now().Add(10*time.Second), true, config, true).(*fakeOauthAuthenticator) provider := newOAuthProvider(config, authenticator, config2.CurrentConfig().Logger()) provider.SetAuthURL("https://auth.fake.snyk.io") url := provider.AuthURL(context.Background()) diff --git a/infrastructure/cli/initializer_test.go b/infrastructure/cli/initializer_test.go index 4d74a7cbc..57e76758d 100644 --- a/infrastructure/cli/initializer_test.go +++ b/infrastructure/cli/initializer_test.go @@ -24,6 +24,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/snyk/snyk-ls/application/config" "github.com/snyk/snyk-ls/infrastructure/cli/filename" @@ -47,38 +48,38 @@ func SetupInitializerWithInstaller(t *testing.T, installer install.Installer) *I } func Test_EnsureCliShouldFindOrDownloadCliAndAddPathToEnv(t *testing.T) { - testutil.IntegTest(t) + c := testutil.IntegTest(t) initializer := SetupInitializer(t) testutil.CreateDummyProgressListener(t) - config.CurrentConfig().CliSettings().SetPath("") - if !config.CurrentConfig().NonEmptyToken() { - config.CurrentConfig().SetToken("dummy") // we don't want to authenticate + c.CliSettings().SetPath("") + if !c.NonEmptyToken() { + c.SetToken("dummy") // we don't want to authenticate } _ = initializer.Init() - assert.NotEmpty(t, config.CurrentConfig().CliSettings().Path()) + assert.NotEmpty(t, c.CliSettings().Path()) } func Test_EnsureCLIShouldRespectCliPathInEnv(t *testing.T) { - testutil.UnitTest(t) + c := testutil.UnitTest(t) initializer := SetupInitializer(t) tempDir := t.TempDir() tempFile := testutil.CreateTempFile(t, tempDir) - config.CurrentConfig().CliSettings().SetPath(tempFile.Name()) + c.CliSettings().SetPath(tempFile.Name()) _ = initializer.Init() - assert.Equal(t, tempFile.Name(), config.CurrentConfig().CliSettings().Path()) + assert.Equal(t, tempFile.Name(), c.CliSettings().Path()) } func TestInitializer_whenNoCli_Installs(t *testing.T) { c := testutil.UnitTest(t) - config.CurrentConfig().SetManageBinariesAutomatically(true) + c.SetManageBinariesAutomatically(true) settings := &config.CliSettings{C: c} testCliPath := filepath.Join(t.TempDir(), "dummy.cli") settings.SetPath(testCliPath) - config.CurrentConfig().SetCliSettings(settings) + c.SetCliSettings(settings) installer := install.NewFakeInstaller() initializer := SetupInitializerWithInstaller(t, installer) @@ -91,10 +92,10 @@ func TestInitializer_whenNoCli_Installs(t *testing.T) { } func TestInitializer_whenNoCli_InstallsToDefaultCliPath(t *testing.T) { - testutil.SmokeTest(t, false) + c := testutil.SmokeTest(t, false) // arrange - config.CurrentConfig().SetManageBinariesAutomatically(true) + c.SetManageBinariesAutomatically(true) clientFunc := func() *http.Client { return http.DefaultClient } installer := install.NewInstaller(error_reporting.NewTestErrorReporter(), clientFunc) @@ -111,8 +112,9 @@ func TestInitializer_whenNoCli_InstallsToDefaultCliPath(t *testing.T) { go func() { _ = initializer.Init() }() // assert - lockFileName := config.CurrentConfig().CLIDownloadLockFileName() - expectedCliPath := filepath.Join(config.CurrentConfig().CliSettings().DefaultBinaryInstallPath(), + lockFileName, err := c.CLIDownloadLockFileName() + require.NoError(t, err) + expectedCliPath := filepath.Join(c.CliSettings().DefaultBinaryInstallPath(), filename.ExecutableName) defer func() { // defer clean up @@ -131,19 +133,19 @@ func TestInitializer_whenNoCli_InstallsToDefaultCliPath(t *testing.T) { return err != nil }, time.Second*10, time.Millisecond) - config.CurrentConfig().CliSettings().SetPath("") // reset CLI path during download for foolproofing + c.CliSettings().SetPath("") // reset CLI path during download for foolproofing assert.Eventually(t, func() bool { _, err := installer.Find() return err == nil }, time.Minute*10, time.Second) - assert.Equal(t, expectedCliPath, config.CurrentConfig().CliSettings().Path()) + assert.Equal(t, expectedCliPath, c.CliSettings().Path()) } func TestInitializer_whenBinaryUpdatesNotAllowed_DoesNotInstall(t *testing.T) { - testutil.UnitTest(t) - config.CurrentConfig().SetManageBinariesAutomatically(false) + c := testutil.UnitTest(t) + c.SetManageBinariesAutomatically(false) installer := install.NewFakeInstaller() initializer := SetupInitializerWithInstaller(t, installer) @@ -157,9 +159,9 @@ func TestInitializer_whenBinaryUpdatesNotAllowed_DoesNotInstall(t *testing.T) { } func TestInitializer_whenOutdated_Updates(t *testing.T) { - testutil.UnitTest(t) - config.CurrentConfig().SetManageBinariesAutomatically(true) - createDummyCliBinaryWithCreatedDate(t, fiveDaysAgo) + c := testutil.UnitTest(t) + c.SetManageBinariesAutomatically(true) + createDummyCliBinaryWithCreatedDate(t, c, fiveDaysAgo) installer := install.NewFakeInstaller() initializer := SetupInitializerWithInstaller(t, installer) @@ -168,14 +170,14 @@ func TestInitializer_whenOutdated_Updates(t *testing.T) { assert.Eventually(t, func() bool { return installer.Updates() == 1 && installer.Installs() == 0 - }, time.Second, time.Millisecond) + }, time.Minute, time.Millisecond) } func TestInitializer_whenUpToDate_DoesNotUpdates(t *testing.T) { - testutil.UnitTest(t) - config.CurrentConfig().SetManageBinariesAutomatically(true) + c := testutil.UnitTest(t) + c.SetManageBinariesAutomatically(true) threeDaysAgo := time.Now().Add(time.Hour * 24 * 3) // exactly 4 days is considered as not outdated. - createDummyCliBinaryWithCreatedDate(t, threeDaysAgo) + createDummyCliBinaryWithCreatedDate(t, c, threeDaysAgo) installer := install.NewFakeInstaller() initializer := SetupInitializerWithInstaller(t, installer) @@ -188,9 +190,9 @@ func TestInitializer_whenUpToDate_DoesNotUpdates(t *testing.T) { } func TestInitializer_whenBinaryUpdatesNotAllowed_PreventsUpdate(t *testing.T) { - testutil.UnitTest(t) - config.CurrentConfig().SetManageBinariesAutomatically(false) - createDummyCliBinaryWithCreatedDate(t, fiveDaysAgo) + c := testutil.UnitTest(t) + c.SetManageBinariesAutomatically(false) + createDummyCliBinaryWithCreatedDate(t, c, fiveDaysAgo) installer := install.NewFakeInstaller() initializer := SetupInitializerWithInstaller(t, installer) @@ -199,12 +201,12 @@ func TestInitializer_whenBinaryUpdatesNotAllowed_PreventsUpdate(t *testing.T) { assert.Eventually(t, func() bool { return installer.Updates() == 0 - }, time.Second, time.Millisecond) + }, time.Second*60, time.Millisecond) } func TestInitializer_whenBinaryUpdatesNotAllowed_PreventsInstall(t *testing.T) { - testutil.UnitTest(t) - config.CurrentConfig().SetManageBinariesAutomatically(false) + c := testutil.UnitTest(t) + c.SetManageBinariesAutomatically(false) installer := install.NewFakeInstaller() initializer := SetupInitializerWithInstaller(t, installer) @@ -217,9 +219,9 @@ func TestInitializer_whenBinaryUpdatesNotAllowed_PreventsInstall(t *testing.T) { } func TestInitializer_whenBinaryUpdatesAllowed_Updates(t *testing.T) { - testutil.UnitTest(t) - config.CurrentConfig().SetManageBinariesAutomatically(true) - createDummyCliBinaryWithCreatedDate(t, fiveDaysAgo) + c := testutil.UnitTest(t) + c.SetManageBinariesAutomatically(true) + createDummyCliBinaryWithCreatedDate(t, c, fiveDaysAgo) installer := install.NewFakeInstaller() initializer := SetupInitializerWithInstaller(t, installer) @@ -231,13 +233,13 @@ func TestInitializer_whenBinaryUpdatesAllowed_Updates(t *testing.T) { }, time.Second, time.Millisecond) } -func createDummyCliBinaryWithCreatedDate(t *testing.T, binaryCreationDate time.Time) { +func createDummyCliBinaryWithCreatedDate(t *testing.T, c *config.Config, binaryCreationDate time.Time) { t.Helper() // prepare user directory with OS specific dummy CLI binary temp := t.TempDir() file := testutil.CreateTempFile(t, temp) - config.CurrentConfig().CliSettings().SetPath(file.Name()) + c.CliSettings().SetPath(file.Name()) err := os.Chtimes(file.Name(), binaryCreationDate, binaryCreationDate) if err != nil { diff --git a/infrastructure/cli/install/downloader.go b/infrastructure/cli/install/downloader.go index bb680e5cc..d75b37446 100644 --- a/infrastructure/cli/install/downloader.go +++ b/infrastructure/cli/install/downloader.go @@ -71,7 +71,7 @@ func onProgress(downloaded, total int64, progressTracker *progress.Tracker) { progressTracker.Report(int(percentage)) } -func (d *Downloader) lockFileName() string { +func (d *Downloader) lockFileName() (string, error) { return config.CurrentConfig().CLIDownloadLockFileName() } @@ -189,7 +189,10 @@ func (d *Downloader) Download(r *Release, isUpdate bool) error { } func (d *Downloader) createLockFile() error { - lockFile := d.lockFileName() + lockFile, err := d.lockFileName() + if err != nil { + return err + } file, err := os.Create(lockFile) if err != nil { diff --git a/infrastructure/cli/install/downloader_test.go b/infrastructure/cli/install/downloader_test.go index 5ab197fe4..66afa4154 100644 --- a/infrastructure/cli/install/downloader_test.go +++ b/infrastructure/cli/install/downloader_test.go @@ -23,6 +23,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/snyk/snyk-ls/application/config" "github.com/snyk/snyk-ls/internal/progress" @@ -42,11 +43,12 @@ func TestDownloader_Download(t *testing.T) { exec := (&Discovery{}).ExecutableName(false) destination := filepath.Join(t.TempDir(), exec) config.CurrentConfig().CliSettings().SetPath(destination) - lockFileName := d.lockFileName() + lockFileName, err := d.lockFileName() + require.NoError(t, err) // remove any existing lockfile _ = os.RemoveAll(lockFileName) - err := d.Download(r, false) + err = d.Download(r, false) assert.NoError(t, err) assert.NotEmpty(t, progressCh) @@ -80,7 +82,9 @@ func Test_DoNotDownloadIfCancelled(t *testing.T) { assert.Error(t, err) // make sure cancellation cleanup works - _, err = os.Stat(config.CurrentConfig().CLIDownloadLockFileName()) + lockFileName, err := config.CurrentConfig().CLIDownloadLockFileName() + require.NoError(t, err) + _, err = os.Stat(lockFileName) assert.Error(t, err) } diff --git a/infrastructure/cli/install/installer.go b/infrastructure/cli/install/installer.go index e43df9870..ea772ba89 100644 --- a/infrastructure/cli/install/installer.go +++ b/infrastructure/cli/install/installer.go @@ -204,7 +204,12 @@ func expectedChecksum(r *Release, cliDiscovery *Discovery) (HashSum, error) { func createLockFile(d *Downloader) (lockfileName string, err error) { logger := config.CurrentConfig().Logger() - lockFileName := config.CurrentConfig().CLIDownloadLockFileName() + lockFileName, err := config.CurrentConfig().CLIDownloadLockFileName() + if err != nil { + msg := "installer lockfile directory could not be created " + logger.Error().Str("method", "Download").Str("lockfile", lockFileName).Msg(msg) + return "", errors.New(msg) + } fileInfo, err := os.Stat(lockFileName) if err == nil && (time.Since(fileInfo.ModTime()) < 10*time.Minute) { msg := fmt.Sprintf("installer lockfile from %v found", fileInfo.ModTime()) diff --git a/infrastructure/cli/install/installer_test.go b/infrastructure/cli/install/installer_test.go index b68080ddf..e37e58310 100644 --- a/infrastructure/cli/install/installer_test.go +++ b/infrastructure/cli/install/installer_test.go @@ -26,6 +26,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/snyk/snyk-ls/application/config" "github.com/snyk/snyk-ls/internal/observability/error_reporting" @@ -88,7 +89,8 @@ func Test_Find_CliPathInSettings_CliPathFound(t *testing.T) { func TestInstaller_Install_DoNotDownloadIfLockfileFound(t *testing.T) { r := getTestAsset() - lockFileName := config.CurrentConfig().CLIDownloadLockFileName() + lockFileName, err := config.CurrentConfig().CLIDownloadLockFileName() + require.NoError(t, err) file, err := os.Create(lockFileName) if err != nil { t.Fatal("couldn't create lockfile") diff --git a/infrastructure/oss/oss_integration_test.go b/infrastructure/oss/oss_integration_test.go index 4128fe8f7..5ec703a90 100644 --- a/infrastructure/oss/oss_integration_test.go +++ b/infrastructure/oss/oss_integration_test.go @@ -23,6 +23,7 @@ import ( "strings" "testing" + "github.com/snyk/go-application-framework/pkg/configuration" "github.com/stretchr/testify/assert" "github.com/snyk/snyk-ls/application/config" @@ -69,6 +70,11 @@ func Test_Scan(t *testing.T) { workingDir, _ := os.Getwd() path, _ := filepath.Abs(filepath.Join(workingDir, "testdata", "package.json")) + // temporary until policy engine doesn't output to stdout anymore + t.Setenv("SNYK_LOG_LEVEL", "info") + c.ConfigureLogging(nil) + c.Engine().GetConfiguration().Set(configuration.DEBUG, false) + issues, _ := scanner.Scan(ctx, path, workingDir) assert.NotEqual(t, 0, len(issues)) diff --git a/internal/progress/progress.go b/internal/progress/progress.go index ba988545f..f0d04a17b 100644 --- a/internal/progress/progress.go +++ b/internal/progress/progress.go @@ -40,6 +40,7 @@ type Tracker struct { lastReportPercentage int finished bool lastMessage string + m sync.Mutex } func NewTestTracker(channel chan types.ProgressParams, cancelChannel chan bool) *Tracker { @@ -101,6 +102,8 @@ func (t *Tracker) BeginWithMessage(title, message string) { } func (t *Tracker) ReportWithMessage(percentage int, message string) { + t.m.Lock() + defer t.m.Unlock() logger := config.CurrentConfig().Logger().With().Str("token", string(t.token)).Str("method", "progress.ReportWithMessage").Logger() if time.Now().Before(t.lastReport.Add(200 * time.Millisecond)) { return diff --git a/internal/testutil/test_setup.go b/internal/testutil/test_setup.go index 18b253502..ca6f8d20a 100644 --- a/internal/testutil/test_setup.go +++ b/internal/testutil/test_setup.go @@ -23,7 +23,10 @@ import ( "runtime" "testing" + "github.com/golang/mock/gomock" "github.com/rs/zerolog" + "github.com/snyk/go-application-framework/pkg/configuration" + "github.com/snyk/go-application-framework/pkg/mocks" "github.com/stretchr/testify/assert" "github.com/snyk/snyk-ls/application/config" @@ -81,7 +84,7 @@ func cleanupFakeCliFile(c *config.Config) { func CLIDownloadLockFileCleanUp(t *testing.T) { t.Helper() // remove lock file before test and after test - lockFileName := config.CurrentConfig().CLIDownloadLockFileName() + lockFileName, _ := config.CurrentConfig().CLIDownloadLockFileName() file, _ := os.Open(lockFileName) _ = file.Close() _ = os.Remove(lockFileName) @@ -158,7 +161,7 @@ func SetupCustomTestRepo(t *testing.T, rootDir string, url string, targetCommit assert.NoError(t, os.MkdirAll(tempDir, 0755)) repoDir := "1" absoluteCloneRepoDir := filepath.Join(tempDir, repoDir) - cmd := []string{"clone", url, repoDir} + cmd := []string{"clone", "-v", url, repoDir} logger.Debug().Interface("cmd", cmd).Msg("clone command") clone := exec.Command("git", cmd...) clone.Dir = tempDir @@ -182,3 +185,12 @@ func SetupCustomTestRepo(t *testing.T, rootDir string, url string, targetCommit logger.Debug().Msg(string(output)) return absoluteCloneRepoDir, err } + +func SetUpEngineMock(t *testing.T, c *config.Config) (*mocks.MockEngine, configuration.Configuration) { + t.Helper() + ctrl := gomock.NewController(t) + mockEngine := mocks.NewMockEngine(ctrl) + engineConfig := c.Engine().GetConfiguration() + c.SetEngine(mockEngine) + return mockEngine, engineConfig +} diff --git a/internal/types/command.go b/internal/types/command.go index c8e3b62a8..76faf91c0 100644 --- a/internal/types/command.go +++ b/internal/types/command.go @@ -119,3 +119,16 @@ func (service *CommandServiceMock) ExecutedCommands() []CommandData { service.m.Unlock() return cmds } + +type AnalyticsEventParam struct { + InteractionType string `json:"interactionType"` + Category []string `json:"category"` + Status string `json:"status"` + TargetId string `json:"targetId"` + TimestampMs int64 `json:"timestampMs"` + DurationMs int64 `json:"durationMs"` + Results map[string]any `json:"results"` + Errors []any `json:"errors"` + Extension map[string]any `json:"extension"` + InteractionUUID string `json:"interactionId"` +}