diff --git a/.snyk b/.snyk index 565dd37a0..d7c33d884 100644 --- a/.snyk +++ b/.snyk @@ -4,7 +4,15 @@ version: v1.25.0 ignore: '*': - build/*: - reason: None Given + reason: Build data expires: 2032-05-20T07:12:38.106Z created: 2022-04-20T07:12:38.108Z + - src/integTest/*: + reason: Test data + expires: 2032-07-14T12:33:33.331Z + created: 2022-06-14T12:33:33.333Z + - src/test/*: + reason: Test data + expires: 2032-07-14T12:33:33.331Z + created: 2022-06-14T12:33:33.333Z patch: {} diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ccbda9eb..0a3fb947a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,22 @@ # Snyk Changelog +## [2.4.38] + +### Added + +- option to disable automatic CLI downloads +- option to specify the file path of the Snyk CLI executable + ## [2.4.37] ### Fixed + - Found Container vulnerabilities now grouped by ID (similar to OSS results); - For Container images with no remediation/fix available issues count(grouped by severity) now is shown. - Container multi-images (OSS multi-build-managers) scan with no auth now redirect to auth panel. ### Added + - In the result's tree, second level nodes(file/image) now have number of vulnerabilities/issues found in it. ## [2.4.36] diff --git a/build.gradle.kts b/build.gradle.kts index d70c2fc0a..8cc0013d9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,26 +25,26 @@ repositories { } dependencies { - implementation("org.commonmark:commonmark:0.18.2") + implementation("org.commonmark:commonmark:0.19.0") implementation("com.google.code.gson:gson:2.9.0") implementation("com.segment.analytics.java:analytics:3.2.0") - implementation("io.sentry:sentry:5.7.2") + implementation("io.sentry:sentry:6.0.0") implementation("io.snyk.code.sdk:snyk-code-client:2.3.4") implementation("ly.iterative.itly:plugin-iteratively:1.2.11") implementation("ly.iterative.itly:plugin-schema-validator:1.2.11") { exclude(group = "org.slf4j") } implementation("ly.iterative.itly:sdk-jvm:1.2.11") - testImplementation("com.squareup.okhttp3:mockwebserver:4.9.3") + testImplementation("com.squareup.okhttp3:mockwebserver:4.10.0") testImplementation("junit:junit:4.13.2") { exclude(group = "org.hamcrest") } testImplementation("org.hamcrest:hamcrest:2.2") - testImplementation("io.mockk:mockk:1.12.2") + testImplementation("io.mockk:mockk:1.12.2") // updating this breaks tests testImplementation("org.awaitility:awaitility:4.2.0") runtimeOnly("org.jetbrains.kotlin:kotlin-reflect:1.4.32") - detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.19.0") + detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.20.0") } // configuration for gradle-intellij-plugin plugin. diff --git a/src/integTest/kotlin/io/snyk/plugin/TestUtils.kt b/src/integTest/kotlin/io/snyk/plugin/TestUtils.kt index c700ce83f..89be37cc2 100644 --- a/src/integTest/kotlin/io/snyk/plugin/TestUtils.kt +++ b/src/integTest/kotlin/io/snyk/plugin/TestUtils.kt @@ -44,9 +44,11 @@ fun resetSettings(project: Project?) { } /** low level avoiding download the CLI file */ -fun mockCliDownload() { +fun mockCliDownload(): RequestBuilder { val requestBuilderMockk = mockk(relaxed = true) justRun { requestBuilderMockk.saveToFile(any(), any()) } mockkObject(HttpRequestHelper) every { HttpRequestHelper.createRequest(CliDownloader.LATEST_RELEASE_DOWNLOAD_URL) } returns requestBuilderMockk + every { HttpRequestHelper.createRequest(CliDownloader.LATEST_RELEASES_URL) } returns requestBuilderMockk + return requestBuilderMockk } diff --git a/src/integTest/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt b/src/integTest/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt index 03abedfd8..b3ec0866a 100644 --- a/src/integTest/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt +++ b/src/integTest/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt @@ -7,6 +7,7 @@ import com.intellij.testFramework.PlatformTestUtil import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic +import io.mockk.spyk import io.mockk.unmockkAll import io.mockk.verify import io.snyk.plugin.getCliFile @@ -14,6 +15,7 @@ import io.snyk.plugin.getContainerService import io.snyk.plugin.getIacService import io.snyk.plugin.getOssService import io.snyk.plugin.getSnykCachedResults +import io.snyk.plugin.getSnykCliDownloaderService import io.snyk.plugin.isCliInstalled import io.snyk.plugin.isContainerEnabled import io.snyk.plugin.isIacEnabled @@ -21,6 +23,7 @@ import io.snyk.plugin.pluginSettings import io.snyk.plugin.removeDummyCliFile import io.snyk.plugin.resetSettings import io.snyk.plugin.services.download.CliDownloader +import io.snyk.plugin.services.download.LatestReleaseInfo import io.snyk.plugin.services.download.SnykCliDownloaderService import io.snyk.plugin.setupDummyCliFile import org.awaitility.Awaitility.await @@ -35,10 +38,20 @@ import java.util.concurrent.TimeUnit @Suppress("FunctionName") class SnykTaskQueueServiceTest : LightPlatformTestCase() { + private lateinit var downloaderServiceMock: SnykCliDownloaderService + override fun setUp() { super.setUp() unmockkAll() resetSettings(project) + mockkStatic("io.snyk.plugin.UtilsKt") + downloaderServiceMock = spyk(SnykCliDownloaderService()) + every { downloaderServiceMock.requestLatestReleasesInformation() } returns LatestReleaseInfo( + "http://testUrl", + "testReleaseInfo", + "testTag" + ) + every { getSnykCliDownloaderService() } returns downloaderServiceMock } override fun tearDown() { @@ -70,15 +83,38 @@ class SnykTaskQueueServiceTest : LightPlatformTestCase() { @Test fun testCliDownloadBeforeScanIfNeeded() { val cliFile = getCliFile() - - mockkStatic("io.snyk.plugin.UtilsKt") - every { getCliFile().exists() } returns false - every { isCliInstalled() } returns false - - val downloaderMock = mockk() - service().downloader = downloaderMock + val downloaderMock = setupMockForDownloadTest() every { downloaderMock.expectedSha() } returns "test" every { downloaderMock.downloadFile(any(), any(), any()) } returns cliFile + setupAppSettingsForDownloadTests() + + val snykTaskQueueService = project.service() + snykTaskQueueService.scan() + // needed due to luck of disposing services by Idea test framework (bug?) + Disposer.dispose(service()) + + assertTrue(snykTaskQueueService.getTaskQueue().isEmpty) + + verify { downloaderMock.downloadFile(any(), any(), any()) } + } + + @Test + fun testDontDownloadCLIIfUpdatesDisabled() { + val downloaderMock = setupMockForDownloadTest() + val settings = setupAppSettingsForDownloadTests() + settings.manageBinariesAutomatically = false + + val snykTaskQueueService = project.service() + snykTaskQueueService.scan() + // needed due to luck of disposing services by Idea test framework (bug?) + Disposer.dispose(service()) + + assertTrue(snykTaskQueueService.getTaskQueue().isEmpty) + + verify(exactly = 0) { downloaderMock.downloadFile(any(), any(), any()) } + } + + private fun setupAppSettingsForDownloadTests(): SnykApplicationSettingsStateService { every { getOssService(project)?.scan() } returns OssResult(null) val settings = pluginSettings() @@ -87,15 +123,16 @@ class SnykTaskQueueServiceTest : LightPlatformTestCase() { settings.snykCodeQualityIssuesScanEnable = false settings.iacScanEnabled = false settings.containerScanEnabled = false + return settings + } - val snykTaskQueueService = project.service() - snykTaskQueueService.scan() - // needed due to luck of disposing services by Idea test framework (bug?) - Disposer.dispose(service()) - - assertTrue(snykTaskQueueService.getTaskQueue().isEmpty) + private fun setupMockForDownloadTest(): CliDownloader { + every { getCliFile().exists() } returns false + every { isCliInstalled() } returns false - verify { downloaderMock.downloadFile(any(), any(), any()) } + val downloaderMock = mockk() + getSnykCliDownloaderService().downloader = downloaderMock + return downloaderMock } @Test diff --git a/src/integTest/kotlin/io/snyk/plugin/services/download/CliDownloaderServiceIntegTest.kt b/src/integTest/kotlin/io/snyk/plugin/services/download/CliDownloaderServiceIntegTest.kt index 7ee52cf2a..558cb5f98 100644 --- a/src/integTest/kotlin/io/snyk/plugin/services/download/CliDownloaderServiceIntegTest.kt +++ b/src/integTest/kotlin/io/snyk/plugin/services/download/CliDownloaderServiceIntegTest.kt @@ -7,6 +7,7 @@ import com.intellij.util.io.HttpRequests import io.mockk.every import io.mockk.justRun import io.mockk.mockk +import io.mockk.mockkStatic import io.mockk.spyk import io.mockk.unmockkAll import io.mockk.verify @@ -15,8 +16,10 @@ import io.snyk.plugin.mockCliDownload import io.snyk.plugin.pluginSettings import io.snyk.plugin.removeDummyCliFile import io.snyk.plugin.resetSettings +import io.snyk.plugin.services.SnykApplicationSettingsStateService import org.apache.http.HttpStatus import org.junit.Test +import java.io.File import java.net.SocketTimeoutException import java.time.LocalDate import java.time.LocalDateTime @@ -28,12 +31,15 @@ class CliDownloaderServiceIntegTest : LightPlatformTestCase() { private lateinit var downloader: CliDownloader private lateinit var cut: SnykCliDownloaderService private lateinit var cutSpy: SnykCliDownloaderService - private val cliFile = getCliFile() + private lateinit var cliFile: File override fun setUp() { super.setUp() unmockkAll() resetSettings(project) + mockkStatic("io.snyk.plugin.UtilsKt") + every { pluginSettings() } returns SnykApplicationSettingsStateService() + cliFile = getCliFile() cut = project.service() cutSpy = spyk(cut) errorHandler = mockk() @@ -51,6 +57,9 @@ class CliDownloaderServiceIntegTest : LightPlatformTestCase() { super.tearDown() } + /** + * Needs an internet connection - real test if release info can be downloaded + */ @Test fun testGetLatestReleasesInformation() { val latestReleaseInfo = project.service().requestLatestReleasesInformation() @@ -170,25 +179,18 @@ class CliDownloaderServiceIntegTest : LightPlatformTestCase() { @Test fun testCliSilentAutoUpdateWhenPreviousUpdateInfoIsNull() { val currentDate = LocalDate.now() - val settings = pluginSettings() - - settings.cliVersion = "" settings.lastCheckDate = null - ensureCliFileExistent() - - every { downloader.downloadFile(any(), any(), any()) } returns cliFile + every { cutSpy.requestLatestReleasesInformation() } returns LatestReleaseInfo( + "http://testUrl", "testReleaseInfo", "testTag" + ) + justRun { cutSpy.downloadLatestRelease(any(), any()) } cutSpy.cliSilentAutoUpdate(EmptyProgressIndicator(), project) - assertTrue(getCliFile().exists()) - assertEquals(currentDate, settings.getLastCheckDate()) - assertEquals( - cutSpy.getLatestReleaseInfo()!!.tagName, - "v" + settings.cliVersion - ) + verify { cutSpy.downloadLatestRelease(any(), any()) } } @Test diff --git a/src/main/kotlin/io/snyk/plugin/Utils.kt b/src/main/kotlin/io/snyk/plugin/Utils.kt index f85304ffd..d05717e6d 100644 --- a/src/main/kotlin/io/snyk/plugin/Utils.kt +++ b/src/main/kotlin/io/snyk/plugin/Utils.kt @@ -22,7 +22,6 @@ import com.intellij.psi.PsiManager import com.intellij.util.Alarm import com.intellij.util.FileContentUtil import com.intellij.util.messages.Topic -import io.snyk.plugin.cli.Platform import io.snyk.plugin.services.SnykAnalyticsService import io.snyk.plugin.services.SnykApiService import io.snyk.plugin.services.SnykApplicationSettingsStateService @@ -85,7 +84,7 @@ fun getSnykCliDownloaderService(): SnykCliDownloaderService = getApplicationServ fun getSnykProjectSettingsService(project: Project): SnykProjectSettingsStateService? = project.serviceIfNotDisposed() -fun getCliFile() = File(getPluginPath(), Platform.current().snykWrapperFileName) +fun getCliFile() = File(pluginSettings().cliPath) fun isCliInstalled(): Boolean = ApplicationManager.getApplication().isUnitTestMode || getCliFile().exists() diff --git a/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt b/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt index bbf8679ac..674e2e63d 100644 --- a/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt +++ b/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt @@ -8,8 +8,11 @@ import com.intellij.openapi.components.Storage import com.intellij.openapi.project.Project import com.intellij.util.xmlb.XmlSerializerUtil import io.snyk.plugin.Severity +import io.snyk.plugin.cli.Platform +import io.snyk.plugin.getPluginPath import io.snyk.plugin.getSnykProjectSettingsService import io.snyk.plugin.isProjectSettingsAvailable +import org.jetbrains.kotlin.konan.file.File import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime @@ -24,6 +27,8 @@ import java.util.UUID ) class SnykApplicationSettingsStateService : PersistentStateComponent { + var cliPath: String = getPluginPath() + File.separator + Platform.current().snykWrapperFileName + var manageBinariesAutomatically: Boolean = true var fileListenerEnabled: Boolean = true var token: String? = null var customEndpointUrl: String? = null diff --git a/src/main/kotlin/io/snyk/plugin/services/download/CliDownloaderService.kt b/src/main/kotlin/io/snyk/plugin/services/download/CliDownloaderService.kt index 49ba88b47..9335c5889 100644 --- a/src/main/kotlin/io/snyk/plugin/services/download/CliDownloaderService.kt +++ b/src/main/kotlin/io/snyk/plugin/services/download/CliDownloaderService.kt @@ -9,9 +9,11 @@ import com.intellij.util.io.HttpRequests import io.snyk.plugin.cli.Platform import io.snyk.plugin.events.SnykCliDownloadListener import io.snyk.plugin.getCliFile +import io.snyk.plugin.isCliInstalled import io.snyk.plugin.pluginSettings import io.snyk.plugin.services.download.HttpRequestHelper.createRequest import io.snyk.plugin.tail +import io.snyk.plugin.ui.SnykBalloonNotificationHelper import java.io.IOException import java.time.LocalDate import java.time.temporal.ChronoUnit @@ -42,6 +44,7 @@ class SnykCliDownloaderService { } fun requestLatestReleasesInformation(): LatestReleaseInfo? { + if (!pluginSettings().manageBinariesAutomatically) return null try { val result = createRequest(CliDownloader.LATEST_RELEASES_URL).readString() val response = "v" + result.removeSuffix("\n") @@ -57,6 +60,15 @@ class SnykCliDownloaderService { } fun downloadLatestRelease(indicator: ProgressIndicator, project: Project) { + if (!pluginSettings().manageBinariesAutomatically) { + if (!isCliInstalled()) { + val msg = + "The plugin cannot scan without Snyk CLI, but automatic download is disabled. " + + "Please put a Snyk CLI executable in ${pluginSettings().cliPath} and retry." + SnykBalloonNotificationHelper.showError(msg, project) + } + return + } cliDownloadPublisher.cliDownloadStarted() indicator.isIndeterminate = true currentProgressIndicator = indicator diff --git a/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt b/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt index 0d406828c..3af6f9316 100644 --- a/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt +++ b/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt @@ -18,11 +18,11 @@ import javax.swing.JComponent class SnykProjectSettingsConfigurable(val project: Project) : SearchableConfigurable { - private val applicationSettingsStateService + private val settingsStateService get() = pluginSettings() - val snykSettingsDialog: SnykSettingsDialog = - SnykSettingsDialog(project, applicationSettingsStateService, this) + var snykSettingsDialog: SnykSettingsDialog = + SnykSettingsDialog(project, settingsStateService, this) override fun getId(): String = "io.snyk.plugin.settings.SnykProjectSettingsConfigurable" @@ -35,7 +35,9 @@ class SnykProjectSettingsConfigurable(val project: Project) : SearchableConfigur isSendUsageAnalyticsModified() || isCrashReportingModified() || snykSettingsDialog.isScanTypeChanged() || - snykSettingsDialog.isSeverityEnablementChanged() + snykSettingsDialog.isSeverityEnablementChanged() || + snykSettingsDialog.manageBinariesAutomatically() != settingsStateService.manageBinariesAutomatically || + snykSettingsDialog.getCliPath() != settingsStateService.cliPath private fun isCoreParamsModified() = isTokenModified() || isCustomEndpointModified() || @@ -54,17 +56,22 @@ class SnykProjectSettingsConfigurable(val project: Project) : SearchableConfigur val productSelectionChanged = snykSettingsDialog.isScanTypeChanged() val severitySelectionChanged = snykSettingsDialog.isSeverityEnablementChanged() - applicationSettingsStateService.customEndpointUrl = customEndpoint + settingsStateService.customEndpointUrl = customEndpoint SnykCodeParams.instance.apiUrl = customEndpoint SnykCodeParams.instance.isDisableSslVerification = snykSettingsDialog.isIgnoreUnknownCA() - applicationSettingsStateService.token = snykSettingsDialog.getToken() + settingsStateService.token = snykSettingsDialog.getToken() SnykCodeParams.instance.sessionToken = snykSettingsDialog.getToken() - applicationSettingsStateService.organization = snykSettingsDialog.getOrganization() - applicationSettingsStateService.ignoreUnknownCA = snykSettingsDialog.isIgnoreUnknownCA() - applicationSettingsStateService.usageAnalyticsEnabled = snykSettingsDialog.isUsageAnalyticsEnabled() - applicationSettingsStateService.crashReportingEnabled = snykSettingsDialog.isCrashReportingEnabled() + settingsStateService.organization = snykSettingsDialog.getOrganization() + settingsStateService.ignoreUnknownCA = snykSettingsDialog.isIgnoreUnknownCA() + + settingsStateService.usageAnalyticsEnabled = snykSettingsDialog.isUsageAnalyticsEnabled() + settingsStateService.crashReportingEnabled = snykSettingsDialog.isCrashReportingEnabled() + + settingsStateService.manageBinariesAutomatically = snykSettingsDialog.manageBinariesAutomatically() + settingsStateService.cliPath = snykSettingsDialog.getCliPath().trim() + snykSettingsDialog.saveScanTypeChanges() snykSettingsDialog.saveSeveritiesEnablementChanges() @@ -77,29 +84,29 @@ class SnykProjectSettingsConfigurable(val project: Project) : SearchableConfigur getSyncPublisher(project, SnykSettingsListener.SNYK_SETTINGS_TOPIC)?.settingsChanged() } if (productSelectionChanged || severitySelectionChanged) { - applicationSettingsStateService.matchFilteringWithEnablement() + settingsStateService.matchFilteringWithEnablement() getSyncPublisher(project, SnykResultsFilteringListener.SNYK_FILTERING_TOPIC)?.filtersChanged() getSyncPublisher(project, SnykProductsOrSeverityListener.SNYK_ENABLEMENT_TOPIC)?.enablementChanged() } } private fun isTokenModified(): Boolean = - snykSettingsDialog.getToken() != applicationSettingsStateService.token + snykSettingsDialog.getToken() != settingsStateService.token private fun isCustomEndpointModified(): Boolean = - snykSettingsDialog.getCustomEndpoint() != applicationSettingsStateService.customEndpointUrl + snykSettingsDialog.getCustomEndpoint() != settingsStateService.customEndpointUrl private fun isOrganizationModified(): Boolean = - snykSettingsDialog.getOrganization() != applicationSettingsStateService.organization + snykSettingsDialog.getOrganization() != settingsStateService.organization private fun isIgnoreUnknownCAModified(): Boolean = - snykSettingsDialog.isIgnoreUnknownCA() != applicationSettingsStateService.ignoreUnknownCA + snykSettingsDialog.isIgnoreUnknownCA() != settingsStateService.ignoreUnknownCA private fun isSendUsageAnalyticsModified(): Boolean = - snykSettingsDialog.isUsageAnalyticsEnabled() != applicationSettingsStateService.usageAnalyticsEnabled + snykSettingsDialog.isUsageAnalyticsEnabled() != settingsStateService.usageAnalyticsEnabled private fun isCrashReportingModified(): Boolean = - snykSettingsDialog.isCrashReportingEnabled() != applicationSettingsStateService.crashReportingEnabled + snykSettingsDialog.isCrashReportingEnabled() != settingsStateService.crashReportingEnabled private fun isAdditionalParametersModified(): Boolean = isProjectSettingsAvailable(project) && snykSettingsDialog.getAdditionalParameters() != getSnykProjectSettingsService(project)?.additionalParameters diff --git a/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt b/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt index 1bfe2b426..3baeec740 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt @@ -3,8 +3,11 @@ package io.snyk.plugin.ui import com.intellij.ide.BrowserUtil import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.fileChooser.FileChooserDescriptor import com.intellij.openapi.project.Project import com.intellij.openapi.ui.ComponentValidator +import com.intellij.openapi.ui.TextComponentAccessor +import com.intellij.openapi.ui.TextFieldWithBrowseButton import com.intellij.openapi.ui.ValidationInfo import com.intellij.ui.ContextHelpLabel import com.intellij.ui.DocumentAdapter @@ -13,8 +16,12 @@ import com.intellij.ui.components.JBPasswordField import com.intellij.ui.components.fields.ExpandableTextField import com.intellij.uiDesigner.core.Spacer import com.intellij.util.Alarm +import com.intellij.util.FontUtil +import com.intellij.util.ui.GridBag +import com.intellij.util.ui.UI import io.snyk.plugin.events.SnykCliDownloadListener import io.snyk.plugin.getAmplitudeExperimentService +import io.snyk.plugin.getCliFile import io.snyk.plugin.getSnykAnalyticsService import io.snyk.plugin.getSnykCliAuthenticationService import io.snyk.plugin.getSnykCliDownloaderService @@ -26,6 +33,8 @@ import io.snyk.plugin.ui.settings.ScanTypesPanel import io.snyk.plugin.ui.settings.SeveritiesEnablementPanel import snyk.SnykBundle import snyk.amplitude.api.ExperimentUser +import java.awt.GridBagConstraints +import java.awt.GridBagLayout import java.awt.Insets import java.util.Objects.nonNull import java.util.UUID @@ -68,6 +77,9 @@ class SnykSettingsDialog( private val severityEnablementPanel = SeveritiesEnablementPanel().panel + private val manageBinariesAutomatically: JCheckBox = JCheckBox() + private val cliPathTextBoxWithFileBrowser = TextFieldWithBrowseButton() + init { initializeUiComponents() initializeValidation() @@ -75,15 +87,18 @@ class SnykSettingsDialog( receiveTokenButton.isEnabled = !getSnykCliDownloaderService().isCliDownloading() ApplicationManager.getApplication().messageBus.connect(rootPanel) - .subscribe(SnykCliDownloadListener.CLI_DOWNLOAD_TOPIC, object : SnykCliDownloadListener { - override fun cliDownloadStarted() { - receiveTokenButton.isEnabled = false - } - - override fun cliDownloadFinished(succeed: Boolean) { - receiveTokenButton.isEnabled = true + .subscribe( + SnykCliDownloadListener.CLI_DOWNLOAD_TOPIC, + object : SnykCliDownloadListener { + override fun cliDownloadStarted() { + receiveTokenButton.isEnabled = false + } + + override fun cliDownloadFinished(succeed: Boolean) { + receiveTokenButton.isEnabled = true + } } - }) + ) receiveTokenButton.addActionListener { ApplicationManager.getApplication().invokeLater { @@ -106,7 +121,9 @@ class SnykSettingsDialog( ignoreUnknownCACheckBox.isSelected = applicationSettings.ignoreUnknownCA usageAnalyticsCheckBox.isSelected = applicationSettings.usageAnalyticsEnabled crashReportingCheckBox.isSelected = applicationSettings.crashReportingEnabled + manageBinariesAutomatically.isSelected = applicationSettings.manageBinariesAutomatically + cliPathTextBoxWithFileBrowser.text = applicationSettings.cliPath additionalParametersTextField.text = applicationSettings.getAdditionalParameters(project) } } @@ -377,6 +394,8 @@ class SnykSettingsDialog( ) } + createExecutableSettingsPanel(4) + /** User experience ------------------ */ val userExperiencePanel = JPanel(UIGridLayoutManager(5, 4, Insets(0, 0, 0, 0), -1, -1)) @@ -385,7 +404,7 @@ class SnykSettingsDialog( rootPanel.add( userExperiencePanel, baseGridConstraints( - row = 4, + row = 5, anchor = UIGridConstraints.ANCHOR_NORTHWEST, fill = UIGridConstraints.FILL_HORIZONTAL, hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK or UIGridConstraints.SIZEPOLICY_CAN_GROW, @@ -424,6 +443,59 @@ class SnykSettingsDialog( ) } + private fun createExecutableSettingsPanel(row: Int) { + val executableSettingsPanel = JPanel(GridBagLayout()) + executableSettingsPanel.border = IdeBorderFactory.createTitledBorder("Executable settings") + val gb = GridBag().setDefaultWeightX(1.0) + .setDefaultAnchor(GridBagConstraints.LINE_START) + .setDefaultFill(GridBagConstraints.HORIZONTAL) + + rootPanel.add( + executableSettingsPanel, + baseGridConstraints( + row = row, + anchor = UIGridConstraints.ANCHOR_NORTHWEST, + fill = UIGridConstraints.FILL_HORIZONTAL, + hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK or UIGridConstraints.SIZEPOLICY_CAN_GROW, + indent = 0 + ) + ) + + cliPathTextBoxWithFileBrowser.toolTipText = "The default path is ${getCliFile().canonicalPath}." + val descriptor = FileChooserDescriptor(true, false, false, false, false, false) + cliPathTextBoxWithFileBrowser.addBrowseFolderListener( + "", "Please choose the Snyk CLI you want to use:", null, + descriptor, + TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT + ) + + executableSettingsPanel.add( + UI.PanelFactory + .panel(cliPathTextBoxWithFileBrowser) + .withLabel("Path to Snyk CLI: ").createPanel(), + gb.nextLine() + ) + + manageBinariesAutomatically.text = "Automatically manage needed binaries" + executableSettingsPanel.add( + manageBinariesAutomatically, + gb.nextLine() + ) + val descriptionLabelManageBinaries = + JLabel( + "These options allow you to customize the handling, where and how plugin " + + "dependencies are downloaded.
" + + "If Automatically manage needed binaries is unchecked, " + + "please make sure to select a valid path to an
" + + "existing Snyk CLI." + ) + descriptionLabelManageBinaries.font = FontUtil.minusOne(descriptionLabelManageBinaries.font) + executableSettingsPanel.add( + descriptionLabelManageBinaries, + gb.nextLine() + ) + } + fun getToken(): String = try { tokenTextField.document.getText(0, tokenTextField.document.length) } catch (exception: BadLocationException) { @@ -465,7 +537,6 @@ class SnykSettingsDialog( validationInfo }).installOn(textField) - textField.document.addDocumentListener(object : DocumentAdapter() { override fun textChanged(event: DocumentEvent) { ComponentValidator.getInstance(textField).ifPresent { @@ -488,4 +559,7 @@ class SnykSettingsDialog( false } } + + fun getCliPath(): String = cliPathTextBoxWithFileBrowser.text + fun manageBinariesAutomatically() = manageBinariesAutomatically.isSelected } diff --git a/src/test/kotlin/io/snyk/plugin/services/download/SnykCliDownloaderServiceTest.kt b/src/test/kotlin/io/snyk/plugin/services/download/SnykCliDownloaderServiceTest.kt new file mode 100644 index 000000000..8e6d26d6a --- /dev/null +++ b/src/test/kotlin/io/snyk/plugin/services/download/SnykCliDownloaderServiceTest.kt @@ -0,0 +1,69 @@ +package io.snyk.plugin.services.download + +import io.mockk.confirmVerified +import io.mockk.every +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import io.mockk.unmockkObject +import io.mockk.verify +import io.snyk.plugin.isCliInstalled +import io.snyk.plugin.pluginSettings +import io.snyk.plugin.services.SnykApplicationSettingsStateService +import io.snyk.plugin.ui.SnykBalloonNotificationHelper +import org.junit.After +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test + +class SnykCliDownloaderServiceTest { + private val settingsStateService: SnykApplicationSettingsStateService = mockk() + + @Before + fun setUp() { + unmockkAll() + mockkStatic("io.snyk.plugin.UtilsKt") + every { pluginSettings() } returns settingsStateService + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `requestLatestReleasesInformation should returns null if updates disabled`() { + val cut = SnykCliDownloaderService() + every { settingsStateService.manageBinariesAutomatically } returns false + + assertNull(cut.requestLatestReleasesInformation()) + + verify { settingsStateService.manageBinariesAutomatically } + confirmVerified(settingsStateService) + } + + @Test + fun `downloadLatestRelease should return without doing anything if updates disabled`() { + val cut = SnykCliDownloaderService() + every { settingsStateService.manageBinariesAutomatically } returns false + every { settingsStateService.cliPath } returns "dummyPath" + every { isCliInstalled() } returns false + mockkObject(SnykBalloonNotificationHelper) + justRun { SnykBalloonNotificationHelper.showError(any(), any()) } + + cut.downloadLatestRelease(mockk(), mockk()) + + verify { settingsStateService.manageBinariesAutomatically } + verify { settingsStateService.cliPath } + verify(exactly = 1) { + SnykBalloonNotificationHelper.showError( + any(), any() + ) + } + confirmVerified(settingsStateService) // this makes sure, no publisher, no cli path, nothing is used + confirmVerified(SnykBalloonNotificationHelper) + unmockkObject(SnykBalloonNotificationHelper) + } +}