diff --git a/build.gradle.kts b/build.gradle.kts index ee6c9d454..9834f9336 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -30,6 +30,8 @@ repositories { dependencies { implementation(platform("com.squareup.okhttp3:okhttp-bom:4.11.0")) + implementation("org.eclipse.lsp4j:org.eclipse.lsp4j:0.21.1") + implementation("org.commonmark:commonmark:0.21.0") implementation("com.google.code.gson:gson:2.10.1") implementation("com.segment.analytics.java:analytics:3.4.0") diff --git a/src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt b/src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt index db9098267..7f136add0 100644 --- a/src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt +++ b/src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt @@ -23,7 +23,7 @@ import snyk.iac.IacBulkFileListener import snyk.oss.OssBulkFileListener import java.time.Instant import java.time.temporal.ChronoUnit -import java.util.Date +import java.util.* private val LOG = logger() @@ -66,6 +66,12 @@ class SnykPostStartupActivity : ProjectActivity { if (!ApplicationManager.getApplication().isUnitTestMode) { getSnykTaskQueueService(project)?.downloadLatestRelease() + try { + getSnykTaskQueueService(project)?.initializeLanguageServer() + getAnalyticsScanListener(project)?.initScanListener() + } catch (ignored: Exception) { + // do nothing to not break UX for analytics + } } val feedbackRequestShownMoreThenTwoWeeksAgo = diff --git a/src/main/kotlin/io/snyk/plugin/Utils.kt b/src/main/kotlin/io/snyk/plugin/Utils.kt index 2456a9151..04efde077 100644 --- a/src/main/kotlin/io/snyk/plugin/Utils.kt +++ b/src/main/kotlin/io/snyk/plugin/Utils.kt @@ -14,7 +14,6 @@ import com.intellij.openapi.options.ShowSettingsUtil import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.project.Project import com.intellij.openapi.roots.ProjectRootManager -import com.intellij.openapi.util.Computable import com.intellij.openapi.util.registry.Registry import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.wm.ToolWindow @@ -24,6 +23,7 @@ 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.analytics.AnalyticsScanListener import io.snyk.plugin.net.ClientException import io.snyk.plugin.services.SnykAnalyticsService import io.snyk.plugin.services.SnykApiService @@ -55,7 +55,6 @@ import snyk.oss.OssTextRangeFinder import snyk.whoami.WhoamiService import java.io.File import java.net.URI -import java.net.URL import java.nio.file.Path import java.security.KeyStore import java.util.Objects.nonNull @@ -81,6 +80,7 @@ fun getSnykTaskQueueService(project: Project): SnykTaskQueueService? = project.s fun getSnykToolWindowPanel(project: Project): SnykToolWindowPanel? = project.serviceIfNotDisposed() fun getSnykCachedResults(project: Project): SnykCachedResults? = project.serviceIfNotDisposed() +fun getAnalyticsScanListener(project: Project): AnalyticsScanListener? = project.serviceIfNotDisposed() fun getContainerService(project: Project): ContainerService? = project.serviceIfNotDisposed() diff --git a/src/main/kotlin/io/snyk/plugin/analytics/AnalyticsScanListener.kt b/src/main/kotlin/io/snyk/plugin/analytics/AnalyticsScanListener.kt new file mode 100644 index 000000000..fcc18defd --- /dev/null +++ b/src/main/kotlin/io/snyk/plugin/analytics/AnalyticsScanListener.kt @@ -0,0 +1,117 @@ +package io.snyk.plugin.analytics + +import com.intellij.openapi.components.Service +import com.intellij.openapi.project.Project +import io.snyk.plugin.events.SnykScanListener +import io.snyk.plugin.getSnykTaskQueueService +import io.snyk.plugin.snykcode.SnykCodeResults +import snyk.common.SnykError +import snyk.common.lsp.commands.ScanDoneEvent +import snyk.container.ContainerResult +import snyk.iac.IacResult +import snyk.oss.OssResult + +@Service(Service.Level.PROJECT) +class AnalyticsScanListener(val project: Project) { + fun getScanDoneEvent( + duration: Long, product: String, critical: Int, high: Int, medium: Int, low: Int + ): ScanDoneEvent { + return ScanDoneEvent( + ScanDoneEvent.Data( + attributes = ScanDoneEvent.Attributes( + scanType = product, + uniqueIssueCount = ScanDoneEvent.UniqueIssueCount( + critical = critical, + high = high, + medium = medium, + low = low + ), + durationMs = "$duration", + ) + ) + ) + } + + val snykScanListener = object : SnykScanListener { + var start: Long = 0 + + override fun scanningStarted() { + start = System.currentTimeMillis() + } + + override fun scanningOssFinished(ossResult: OssResult) { + val scanDoneEvent = getScanDoneEvent( + System.currentTimeMillis() - start, + "Snyk Open Source", + ossResult.criticalSeveritiesCount(), + ossResult.highSeveritiesCount(), + ossResult.mediumSeveritiesCount(), + ossResult.lowSeveritiesCount() + ) + getSnykTaskQueueService(project)?.ls?.sendReportAnalyticsCommand(scanDoneEvent) + } + + override fun scanningSnykCodeFinished(snykCodeResults: SnykCodeResults?) { + val duration = System.currentTimeMillis() - start + val product = "Snyk Code" + val scanDoneEvent = if (snykCodeResults != null) { + getScanDoneEvent( + duration, + product, + snykCodeResults.totalCriticalCount, + snykCodeResults.totalErrorsCount, + snykCodeResults.totalWarnsCount, + snykCodeResults.totalInfosCount, + ) + } else { + getScanDoneEvent(duration, product, 0, 0, 0, 0) + } + getSnykTaskQueueService(project)?.ls?.sendReportAnalyticsCommand(scanDoneEvent) + } + + override fun scanningIacFinished(iacResult: IacResult) { + val scanDoneEvent = getScanDoneEvent( + System.currentTimeMillis() - start, + "Snyk IaC", + iacResult.criticalSeveritiesCount(), + iacResult.highSeveritiesCount(), + iacResult.mediumSeveritiesCount(), + iacResult.lowSeveritiesCount() + ) + getSnykTaskQueueService(project)?.ls?.sendReportAnalyticsCommand(scanDoneEvent) + } + + override fun scanningContainerFinished(containerResult: ContainerResult) { + val scanDoneEvent = getScanDoneEvent( + System.currentTimeMillis() - start, + "Snyk Container", + containerResult.criticalSeveritiesCount(), + containerResult.highSeveritiesCount(), + containerResult.mediumSeveritiesCount(), + containerResult.lowSeveritiesCount() + ) + getSnykTaskQueueService(project)?.ls?.sendReportAnalyticsCommand(scanDoneEvent) + } + + override fun scanningOssError(snykError: SnykError) { + // do nothing + } + + override fun scanningIacError(snykError: SnykError) { + // do nothing + } + + override fun scanningSnykCodeError(snykError: SnykError) { + // do nothing + } + + override fun scanningContainerError(snykError: SnykError) { + // do nothing + } + } + + fun initScanListener() = project.messageBus.connect().subscribe( + SnykScanListener.SNYK_SCAN_TOPIC, + snykScanListener, + ) +} diff --git a/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt b/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt index 90cdcb24c..284ac293f 100644 --- a/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt +++ b/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt @@ -6,6 +6,7 @@ import com.intellij.openapi.components.Service import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.progress.BackgroundTaskQueue +import com.intellij.openapi.progress.EmptyProgressIndicator import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.Task import com.intellij.openapi.project.Project @@ -29,8 +30,12 @@ import io.snyk.plugin.net.ClientException import io.snyk.plugin.pluginSettings import io.snyk.plugin.snykcode.core.RunUtils import io.snyk.plugin.ui.SnykBalloonNotifications +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import org.jetbrains.annotations.TestOnly import snyk.common.SnykError +import snyk.common.lsp.LanguageServerWrapper import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded import java.nio.file.Paths @@ -40,6 +45,7 @@ class SnykTaskQueueService(val project: Project) { private val taskQueue = BackgroundTaskQueue(project, "Snyk") private val taskQueueIac = BackgroundTaskQueue(project, "Snyk: Iac") private val taskQueueContainer = BackgroundTaskQueue(project, "Snyk: Container") + val ls = LanguageServerWrapper() private val settings get() = pluginSettings() @@ -73,8 +79,22 @@ class SnykTaskQueueService(val project: Project) { }) } + @OptIn(DelicateCoroutinesApi::class) + fun initializeLanguageServer() { + waitUntilCliDownloadedIfNeeded(EmptyProgressIndicator()) + ls.initialize() + GlobalScope.launch { + ls.process.errorStream.bufferedReader().forEachLine { println(it) } + } + GlobalScope.launch { + ls.startListening() + } + + ls.sendInitializeMessage(project) + } + fun scan() { - taskQueue.run(object : Task.Backgroundable(project, "Snyk wait for changed files to be saved on disk", true) { + taskQueue.run(object : Task.Backgroundable(project, "Snyk: initializing...", true) { override fun run(indicator: ProgressIndicator) { project.basePath?.let { if (!confirmScanningAndSetWorkspaceTrustedStateIfNeeded(project, Paths.get(it))) return @@ -84,14 +104,12 @@ class SnykTaskQueueService(val project: Project) { FileDocumentManager.getInstance().saveAllDocuments() } indicator.checkCanceled() + waitUntilCliDownloadedIfNeeded(indicator) + indicator.checkCanceled() if (settings.snykCodeSecurityIssuesScanEnable || settings.snykCodeQualityIssuesScanEnable) { scheduleSnykCodeScan() } - - waitUntilCliDownloadedIfNeeded(indicator) - indicator.checkCanceled() - if (settings.ossScanEnable) { scheduleOssScan() } @@ -266,10 +284,11 @@ class SnykTaskQueueService(val project: Project) { scanPublisher?.scanningIacFinished(iacResult) } else { val error = iacResult.getFirstError() - if (error == null) + if (error == null) { SnykError("unknown IaC error", project.basePath ?: "") - else + } else { scanPublisher?.scanningIacError(error) + } } } logger.debug("IaC scan completed") diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt new file mode 100644 index 000000000..5e0908ac6 --- /dev/null +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt @@ -0,0 +1,58 @@ +@file:Suppress("unused") + +package snyk.common.lsp + +import com.google.gson.annotations.SerializedName +import io.snyk.plugin.pluginSettings +import org.apache.commons.lang.SystemUtils +import snyk.pluginInfo + +data class LanguageServerSettings( + @SerializedName("activateSnykOpenSource") val activateSnykOpenSource: String? = "false", + @SerializedName("activateSnykCode") val activateSnykCode: String? = "false", + @SerializedName("activateSnykIac") val activateSnykIac: String? = "false", + @SerializedName("insecure") val insecure: String?, + @SerializedName("endpoint") val endpoint: String?, + @SerializedName("additionalParams") val additionalParams: String? = null, + @SerializedName("additionalEnv") val additionalEnv: String? = null, + @SerializedName("path") val path: String? = null, + @SerializedName("sendErrorReports") val sendErrorReports: String? = "false", + @SerializedName("organization") val organization: String? = null, + @SerializedName("enableTelemetry") val enableTelemetry: String? = "false", + @SerializedName("manageBinariesAutomatically") val manageBinariesAutomatically: String? = "false", + @SerializedName("cliPath") val cliPath: String?, + @SerializedName("token") val token: String?, + @SerializedName("integrationName") val integrationName: String? = pluginInfo.integrationName, + @SerializedName("integrationVersion") val integrationVersion: String? = pluginInfo.integrationVersion, + @SerializedName("automaticAuthentication") val automaticAuthentication: String? = "false", + @SerializedName("deviceId") val deviceId: String? = pluginSettings().userAnonymousId, + @SerializedName("filterSeverity") val filterSeverity: SeverityFilter? = null, + @SerializedName("enableTrustedFoldersFeature") val enableTrustedFoldersFeature: String? = "false", + @SerializedName("trustedFolders") val trustedFolders: List? = emptyList(), + @SerializedName("activateSnykCodeSecurity") val activateSnykCodeSecurity: String? = "false", + @SerializedName("activateSnykCodeQuality") val activateSnykCodeQuality: String? = "false", + @SerializedName("osPlatform") val osPlatform: String? = SystemUtils.OS_NAME, + @SerializedName("osArch") val osArch: String? = SystemUtils.OS_ARCH, + @SerializedName("runtimeVersion") val runtimeVersion: String? = SystemUtils.JAVA_VERSION, + @SerializedName("runtimeName") val runtimeName: String? = SystemUtils.JAVA_RUNTIME_NAME, + @SerializedName("scanningMode") val scanningMode: String? = null, + @SerializedName("authenticationMethod") val authenticationMethod: AuthenticationMethod? = null, + @SerializedName("snykCodeApi") val snykCodeApi: String? = null, + @SerializedName("enableSnykLearnCodeActions") val enableSnykLearnCodeActions: String? = null, + @SerializedName("enableAnalytics") val enableAnalytics: Boolean = false // TODO: enable when service ready +) + +data class SeverityFilter( + @SerializedName("critical") val critical: Boolean?, + @SerializedName("high") val high: Boolean?, + @SerializedName("medium") val medium: Boolean?, + @SerializedName("low") val low: Boolean? +) + +enum class AuthenticationMethod { + @SerializedName("token") + TokenAuthentication, + + @SerializedName("oauth") + OAuthAuthentication +} diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt new file mode 100644 index 000000000..90792982c --- /dev/null +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -0,0 +1,104 @@ +package snyk.common.lsp + +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ProjectRootManager +import io.snyk.plugin.getCliFile +import io.snyk.plugin.pluginSettings +import org.eclipse.lsp4j.ClientInfo +import org.eclipse.lsp4j.ExecuteCommandParams +import org.eclipse.lsp4j.InitializeParams +import org.eclipse.lsp4j.WorkspaceFolder +import org.eclipse.lsp4j.jsonrpc.Launcher +import org.eclipse.lsp4j.launch.LSPLauncher +import org.eclipse.lsp4j.services.LanguageClient +import org.eclipse.lsp4j.services.LanguageServer +import snyk.common.getEndpointUrl +import snyk.common.lsp.commands.ScanDoneEvent +import snyk.pluginInfo + +class LanguageServerWrapper(private val lsPath: String = getCliFile().absolutePath) { + private val gson = com.google.gson.Gson() + + /** + * The language client is used to receive messages from LS + */ + lateinit var languageClient: LanguageClient + + /** + * The language server allows access to the actual LS implementation + */ + lateinit var languageServer: LanguageServer + + /** + * The launcher is used to start the language server as a separate process. and provides access to the LS + */ + private lateinit var launcher: Launcher + + /** + * Process is the started IO process + */ + lateinit var process: Process + + fun initialize() { + val snykLanguageClient = SnykLanguageClient() + languageClient = snykLanguageClient + val logLevel = if (snykLanguageClient.logger.isDebugEnabled) "debug" else "info" + val cmd = listOf(lsPath, "language-server", "-l", logLevel) + + val processBuilder = ProcessBuilder(cmd) + process = processBuilder.start() + launcher = LSPLauncher.createClientLauncher(languageClient, process.inputStream, process.outputStream) + languageServer = launcher.remoteProxy + } + + fun startListening() { + // Start the server + launcher.startListening() + } + + fun sendInitializeMessage(project: Project) { + val workspaceFolders = mutableListOf() + ProjectRootManager.getInstance(project) + .contentRoots + .mapNotNull { WorkspaceFolder(it.url, it.name) }.toCollection(workspaceFolders) + + val params = InitializeParams() + params.processId = ProcessHandle.current().pid().toInt() + params.clientInfo = ClientInfo("${pluginInfo.integrationName}/lsp4j") + params.initializationOptions = getInitializationOptions() + params.workspaceFolders = workspaceFolders + + languageServer.initialize(params).get() + } + + fun sendReportAnalyticsCommand(scanDoneEvent: ScanDoneEvent) { + try { + val eventString = gson.toJson(scanDoneEvent) + val param = ExecuteCommandParams() + param.command = "snyk.reportAnalytics" + param.arguments = listOf(eventString) + languageServer.workspaceService.executeCommand(param) + } catch (ignored: Exception) { + // do nothing to not break UX for analytics + } + } + + fun getInitializationOptions(): LanguageServerSettings { + val ps = pluginSettings() + return LanguageServerSettings( + activateSnykOpenSource = "false", + activateSnykCode = "false", + activateSnykIac = "false", + insecure = ps.ignoreUnknownCA.toString(), + endpoint = getEndpointUrl(), + cliPath = getCliFile().absolutePath, + token = ps.token, + filterSeverity = SeverityFilter( + critical = ps.criticalSeverityEnabled, + high = ps.highSeverityEnabled, + medium = ps.mediumSeverityEnabled, + low = ps.lowSeverityEnabled + ), + ) + } +} diff --git a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt new file mode 100644 index 000000000..8c0ac829b --- /dev/null +++ b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt @@ -0,0 +1,42 @@ +package snyk.common.lsp + +import com.intellij.openapi.diagnostic.Logger +import org.eclipse.lsp4j.MessageActionItem +import org.eclipse.lsp4j.MessageParams +import org.eclipse.lsp4j.MessageType +import org.eclipse.lsp4j.PublishDiagnosticsParams +import org.eclipse.lsp4j.ShowMessageRequestParams +import org.eclipse.lsp4j.services.LanguageClient +import java.util.concurrent.CompletableFuture + +class SnykLanguageClient : LanguageClient { + val logger = Logger.getInstance("Snyk Language Server") + override fun telemetryEvent(`object`: Any?) { + // do nothing + } + + override fun publishDiagnostics(diagnostics: PublishDiagnosticsParams?) { + // do nothing + } + + override fun showMessage(messageParams: MessageParams?) { + // do nothing + } + + override fun showMessageRequest(requestParams: ShowMessageRequestParams?): CompletableFuture { + // do nothing + TODO() + } + + override fun logMessage(message: MessageParams?) { + message?.let { + when (it.type) { + MessageType.Error -> logger.error(it.message) + MessageType.Warning -> logger.warn(it.message) + MessageType.Info -> logger.info(it.message) + MessageType.Log -> logger.debug(it.message) + null -> logger.info(it.message) + } + } + } +} diff --git a/src/main/kotlin/snyk/common/lsp/commands/ScanDoneEvent.kt b/src/main/kotlin/snyk/common/lsp/commands/ScanDoneEvent.kt new file mode 100644 index 000000000..822d20971 --- /dev/null +++ b/src/main/kotlin/snyk/common/lsp/commands/ScanDoneEvent.kt @@ -0,0 +1,45 @@ +package snyk.common.lsp.commands + +import com.google.gson.annotations.SerializedName +import io.snyk.plugin.pluginSettings +import org.apache.commons.lang.SystemUtils +import snyk.pluginInfo +import java.time.ZonedDateTime + +data class ScanDoneEvent( + @SerializedName("data") val data: Data +) { + data class Data( + @SerializedName("type") val type: String = "analytics", + @SerializedName("attributes") val attributes: Attributes + ) + + data class Attributes( + @SerializedName("deviceId") val deviceId: String = pluginSettings().userAnonymousId, + @SerializedName("application") val application: String = pluginInfo.integrationEnvironment, + @SerializedName("application_version") + val applicationVersion: String = pluginInfo.integrationEnvironmentVersion, + @SerializedName("os") val os: String = SystemUtils.OS_NAME, + @SerializedName("arch") val arch: String = SystemUtils.OS_ARCH, + @SerializedName("integration_name") val integrationName: String = pluginInfo.integrationName, + @SerializedName("integration_version") val integrationVersion: String = pluginInfo.integrationVersion, + @SerializedName("integration_environment") + val integrationEnvironment: String = pluginInfo.integrationEnvironment, + @SerializedName("integration_environment_version") + val integrationEnvironmentVersion: String = pluginInfo.integrationEnvironmentVersion, + @SerializedName("event_type") val eventType: String = "Scan done", + @SerializedName("status") val status: String = "Succeeded", + @SerializedName("scan_type") val scanType: String, + @SerializedName("unique_issue_count") val uniqueIssueCount: UniqueIssueCount, + @SerializedName("duration_ms") val durationMs: String, + @SerializedName("timestamp_finished") + val timestampFinished: String = ZonedDateTime.now().withZoneSameInstant(java.time.ZoneOffset.UTC).toString() + ) + + data class UniqueIssueCount( + @SerializedName("critical") val critical: Int, + @SerializedName("high") val high: Int, + @SerializedName("medium") val medium: Int, + @SerializedName("low") val low: Int + ) +} diff --git a/src/test/java/snyk/common/lsp/LanguageServerWrapperTest.kt b/src/test/java/snyk/common/lsp/LanguageServerWrapperTest.kt new file mode 100644 index 000000000..e4ac6f32f --- /dev/null +++ b/src/test/java/snyk/common/lsp/LanguageServerWrapperTest.kt @@ -0,0 +1,90 @@ +package snyk.common.lsp + +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ProjectRootManager +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import io.mockk.verify +import io.snyk.plugin.getCliFile +import io.snyk.plugin.pluginSettings +import io.snyk.plugin.services.SnykApplicationSettingsStateService +import junit.framework.TestCase.assertEquals +import org.eclipse.lsp4j.ExecuteCommandParams +import org.eclipse.lsp4j.InitializeParams +import org.eclipse.lsp4j.services.LanguageServer +import org.junit.After +import org.junit.Before +import org.junit.Test +import snyk.pluginInfo +import java.util.concurrent.CompletableFuture + +class LanguageServerWrapperTest { + + private val projectMock: Project = mockk() + private val lsMock: LanguageServer = mockk() + private val settings = SnykApplicationSettingsStateService() + private lateinit var cut: LanguageServerWrapper + + @Before + fun setUp() { + unmockkAll() + mockkStatic("io.snyk.plugin.UtilsKt") + every { pluginSettings() } returns settings + mockkStatic("snyk.PluginInformationKt") + every { pluginInfo } returns mockk(relaxed = true) + every { pluginInfo.integrationName } returns "Snyk Intellij Plugin" + every { pluginInfo.integrationVersion } returns "2.4.61" + every { pluginInfo.integrationEnvironment } returns "IntelliJ IDEA" + every { pluginInfo.integrationEnvironmentVersion } returns "2020.3.2" + + cut = LanguageServerWrapper("dummy") + cut.languageServer = lsMock + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `sendInitializeMessage should send an initialize message to the language server`() { + val rootManagerMock = mockk(relaxed = true) + every { projectMock.getService(ProjectRootManager::class.java) } returns rootManagerMock + every { rootManagerMock.contentRoots } returns emptyArray() + every { lsMock.initialize(any()) } returns CompletableFuture.completedFuture(null) + + cut.sendInitializeMessage(projectMock) + + verify { lsMock.initialize(any()) } + } + + @Test + fun `sendReportAnalyticsCommand should send a reportAnalytics command to the language server`() { + every { + lsMock.workspaceService.executeCommand(any()) + } returns CompletableFuture.completedFuture(null) + + cut.sendReportAnalyticsCommand(mockk(relaxed = true)) + + verify { lsMock.workspaceService.executeCommand(any()) } + } + + @Test + fun getInitializationOptions() { + settings.token = "testToken" + settings.customEndpointUrl = "testEndpoint/" + settings.ignoreUnknownCA = true + settings.cliPath = "testCliPath" + + val actual = cut.getInitializationOptions() + + assertEquals("false", actual.activateSnykCode) + assertEquals("false", actual.activateSnykIac) + assertEquals("false", actual.activateSnykOpenSource) + assertEquals(settings.token, actual.token) + assertEquals("${settings.ignoreUnknownCA}", actual.insecure) + assertEquals(getCliFile().absolutePath, actual.cliPath) + } +} diff --git a/src/test/kotlin/io/snyk/plugin/analytics/AnalyticsScanListenerTest.kt b/src/test/kotlin/io/snyk/plugin/analytics/AnalyticsScanListenerTest.kt new file mode 100644 index 000000000..8cdd6210c --- /dev/null +++ b/src/test/kotlin/io/snyk/plugin/analytics/AnalyticsScanListenerTest.kt @@ -0,0 +1,136 @@ +package io.snyk.plugin.analytics + +import com.intellij.openapi.project.Project +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import io.mockk.verify +import io.snyk.plugin.getSnykTaskQueueService +import io.snyk.plugin.pluginSettings +import io.snyk.plugin.services.SnykApplicationSettingsStateService +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import org.apache.commons.lang.SystemUtils +import org.eclipse.lsp4j.ExecuteCommandParams +import org.eclipse.lsp4j.services.LanguageServer +import org.junit.After +import org.junit.Before +import org.junit.Test +import snyk.common.lsp.LanguageServerWrapper +import snyk.pluginInfo +import java.util.concurrent.CompletableFuture + +class AnalyticsScanListenerTest { + private lateinit var cut: AnalyticsScanListener + private val projectMock: Project = mockk() + private val lsMock: LanguageServer = mockk() + private val settings = SnykApplicationSettingsStateService() + + @Before + fun setUp() { + unmockkAll() + mockkStatic("io.snyk.plugin.UtilsKt") + every { pluginSettings() } returns settings + mockkStatic("snyk.PluginInformationKt") + every { pluginInfo } returns mockk(relaxed = true) + every { pluginInfo.integrationName } returns "Snyk Intellij Plugin" + every { pluginInfo.integrationVersion } returns "2.4.61" + every { pluginInfo.integrationEnvironment } returns "IntelliJ IDEA" + every { pluginInfo.integrationEnvironmentVersion } returns "2020.3.2" + cut = AnalyticsScanListener(projectMock) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun testGetScanDone() { + val scanDoneEvent = cut.getScanDoneEvent( + 1, + "product", + 1, + 1, + 1, + 1 + ) + assertEquals("1", scanDoneEvent.data.attributes.durationMs) + assertEquals("product", scanDoneEvent.data.attributes.scanType) + assertEquals(1, scanDoneEvent.data.attributes.uniqueIssueCount.critical) + assertEquals(1, scanDoneEvent.data.attributes.uniqueIssueCount.high) + assertEquals(1, scanDoneEvent.data.attributes.uniqueIssueCount.medium) + assertEquals(1, scanDoneEvent.data.attributes.uniqueIssueCount.low) + + assertEquals("IntelliJ IDEA", scanDoneEvent.data.attributes.application) + assertEquals("2020.3.2", scanDoneEvent.data.attributes.applicationVersion) + assertEquals("IntelliJ IDEA", scanDoneEvent.data.attributes.integrationEnvironment) + assertEquals("2020.3.2", scanDoneEvent.data.attributes.integrationEnvironmentVersion) + assertEquals(SystemUtils.OS_NAME, scanDoneEvent.data.attributes.os) + assertEquals(SystemUtils.OS_ARCH, scanDoneEvent.data.attributes.arch) + + assertEquals("analytics", scanDoneEvent.data.type) + assertEquals("Scan done", scanDoneEvent.data.attributes.eventType) + assertEquals("Succeeded", scanDoneEvent.data.attributes.status) + assertEquals(settings.userAnonymousId, scanDoneEvent.data.attributes.deviceId) + + assertNotNull(scanDoneEvent.data.attributes.timestampFinished) + } + + @Test + fun `testScanListener scanningIacFinished should call language server to report analytics`() { + val languageServerWrapper = LanguageServerWrapper("dummy") + languageServerWrapper.languageServer = lsMock + every { getSnykTaskQueueService(projectMock)?.ls } returns languageServerWrapper + every { + lsMock.workspaceService.executeCommand(any()) + } returns CompletableFuture.completedFuture(null) + + cut.snykScanListener.scanningIacFinished(mockk(relaxed = true)) + + verify { lsMock.workspaceService.executeCommand(any()) } + } + + @Test + fun `testScanListener scanningOssFinished should call language server to report analytics`() { + val languageServerWrapper = LanguageServerWrapper("dummy") + languageServerWrapper.languageServer = lsMock + every { getSnykTaskQueueService(projectMock)?.ls } returns languageServerWrapper + every { + lsMock.workspaceService.executeCommand(any()) + } returns CompletableFuture.completedFuture(null) + + cut.snykScanListener.scanningOssFinished(mockk(relaxed = true)) + + verify { lsMock.workspaceService.executeCommand(any()) } + } + + @Test + fun `testScanListener scanningCodeFinished should call language server to report analytics`() { + val languageServerWrapper = LanguageServerWrapper("dummy") + languageServerWrapper.languageServer = lsMock + every { getSnykTaskQueueService(projectMock)?.ls } returns languageServerWrapper + every { + lsMock.workspaceService.executeCommand(any()) + } returns CompletableFuture.completedFuture(null) + + cut.snykScanListener.scanningSnykCodeFinished(mockk(relaxed = true)) + + verify { lsMock.workspaceService.executeCommand(any()) } + } + + @Test + fun `testScanListener scanningContainerFinished should call language server to report analytics`() { + val languageServerWrapper = LanguageServerWrapper("dummy") + languageServerWrapper.languageServer = lsMock + every { getSnykTaskQueueService(projectMock)?.ls } returns languageServerWrapper + every { + lsMock.workspaceService.executeCommand(any()) + } returns CompletableFuture.completedFuture(null) + + cut.snykScanListener.scanningContainerFinished(mockk(relaxed = true)) + + verify { lsMock.workspaceService.executeCommand(any()) } + } +}