diff --git a/CHANGELOG.md b/CHANGELOG.md index 217c051a7..9741fdcc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Snyk Changelog +## [2.4.48] +### Added + +- Project trust feature. + ## [2.4.47] ### Fixed diff --git a/build.gradle.kts b/build.gradle.kts index 855df8261..be37e0342 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { exclude(group = "org.slf4j") } implementation("ly.iterative.itly:sdk-jvm:1.2.11") + testImplementation("com.google.jimfs:jimfs:1.2") testImplementation("com.squareup.okhttp3:mockwebserver:4.10.0") testImplementation("junit:junit:4.13.2") { exclude(group = "org.hamcrest") diff --git a/src/integTest/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt b/src/integTest/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt index 13c9b7e2b..ea28b0938 100644 --- a/src/integTest/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt +++ b/src/integTest/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt @@ -36,6 +36,8 @@ import org.junit.Test import snyk.container.ContainerResult import snyk.iac.IacResult import snyk.oss.OssResult +import snyk.trust.WorkspaceTrustService +import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded import java.util.concurrent.TimeUnit @Suppress("FunctionName") @@ -51,6 +53,7 @@ class SnykTaskQueueServiceTest : LightPlatformTestCase() { mockSnykApiServiceSastEnabled() replaceSnykApiServiceMockInContainer() mockkStatic("io.snyk.plugin.UtilsKt") + mockkStatic("snyk.trust.TrustedProjectsKt") downloaderServiceMock = spyk(SnykCliDownloaderService()) every { downloaderServiceMock.requestLatestReleasesInformation() } returns LatestReleaseInfo( "http://testUrl", @@ -58,6 +61,7 @@ class SnykTaskQueueServiceTest : LightPlatformTestCase() { "testTag" ) every { getSnykCliDownloaderService() } returns downloaderServiceMock + every { confirmScanningAndSetWorkspaceTrustedStateIfNeeded(any()) } returns true } private fun mockSnykApiServiceSastEnabled() { diff --git a/src/integTest/kotlin/io/snyk/plugin/ui/toolwindow/SnykAuthPanelIntegTest.kt b/src/integTest/kotlin/io/snyk/plugin/ui/toolwindow/SnykAuthPanelIntegTest.kt index 8c130b336..4a5cf6195 100644 --- a/src/integTest/kotlin/io/snyk/plugin/ui/toolwindow/SnykAuthPanelIntegTest.kt +++ b/src/integTest/kotlin/io/snyk/plugin/ui/toolwindow/SnykAuthPanelIntegTest.kt @@ -6,12 +6,15 @@ import com.intellij.testFramework.LightPlatform4TestCase import com.intellij.testFramework.replaceService import io.mockk.every import io.mockk.mockk +import io.mockk.mockkStatic import io.mockk.unmockkAll import io.mockk.verify import io.snyk.plugin.services.SnykAnalyticsService import io.snyk.plugin.services.SnykCliAuthenticationService import io.snyk.plugin.ui.toolwindow.panels.SnykAuthPanel import org.junit.Test +import snyk.trust.WorkspaceTrustService +import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded import javax.swing.JButton import javax.swing.JLabel @@ -19,12 +22,16 @@ class SnykAuthPanelIntegTest : LightPlatform4TestCase() { private val analyticsService: SnykAnalyticsService = mockk(relaxed = true) private val cliAuthenticationService = mockk(relaxed = true) + private val workspaceTrustServiceMock = mockk(relaxed = true) override fun setUp() { super.setUp() unmockkAll() + mockkStatic("snyk.trust.TrustedProjectsKt") + every { confirmScanningAndSetWorkspaceTrustedStateIfNeeded(any()) } returns true val application = ApplicationManager.getApplication() application.replaceService(SnykAnalyticsService::class.java, analyticsService, application) + application.replaceService(WorkspaceTrustService::class.java, workspaceTrustServiceMock, application) project.replaceService(SnykCliAuthenticationService::class.java, cliAuthenticationService, project) } @@ -32,6 +39,7 @@ class SnykAuthPanelIntegTest : LightPlatform4TestCase() { unmockkAll() val application = ApplicationManager.getApplication() application.replaceService(SnykAnalyticsService::class.java, SnykAnalyticsService(), application) + application.replaceService(WorkspaceTrustService::class.java, workspaceTrustServiceMock, application) project.replaceService(SnykCliAuthenticationService::class.java, SnykCliAuthenticationService(project), project) super.tearDown() } @@ -40,20 +48,25 @@ class SnykAuthPanelIntegTest : LightPlatform4TestCase() { fun `should display right authenticate button text`() { val cut = SnykAuthPanel(project) val authenticateButton = UIComponentFinder.getComponentByCondition(cut, JButton::class) { - it.text == SnykAuthPanel.AUTHENTICATE_BUTTON_TEXT + it.text == SnykAuthPanel.TRUST_AND_SCAN_BUTTON_TEXT } assertNotNull(authenticateButton) - assertEquals("Test code now", authenticateButton!!.text) + assertEquals("Trust project and scan", authenticateButton!!.text) } @Test fun `should display right description label`() { val expectedText = """ - |
    + | + |
      |
    1. Authenticate to Snyk.io
    2. |
    3. Analyze code for issues and vulnerabilities
    4. |
    5. Improve your code and upgrade dependencies
    6. |
    + |
    + |When scanning project files, Snyk may automatically execute code
    such as invoking the package manager to get dependency information.
    You should only scan projects you trust. More info + |
    + |
    | """.trimMargin() @@ -72,7 +85,7 @@ class SnykAuthPanelIntegTest : LightPlatform4TestCase() { val cut = SnykAuthPanel(project) val authenticateButton = UIComponentFinder.getComponentByCondition(cut, JButton::class) { - it.text == SnykAuthPanel.AUTHENTICATE_BUTTON_TEXT + it.text == SnykAuthPanel.TRUST_AND_SCAN_BUTTON_TEXT } assertNotNull(authenticateButton) diff --git a/src/integTest/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelIntegTest.kt b/src/integTest/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelIntegTest.kt index 1fbd6e585..b22c33f8f 100644 --- a/src/integTest/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelIntegTest.kt +++ b/src/integTest/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelIntegTest.kt @@ -66,6 +66,7 @@ import snyk.iac.IgnoreButtonActionListener import snyk.iac.ui.toolwindow.IacFileTreeNode import snyk.iac.ui.toolwindow.IacIssueTreeNode import snyk.oss.Vulnerability +import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded import javax.swing.JButton import javax.swing.JEditorPane import javax.swing.JLabel @@ -94,12 +95,14 @@ class SnykToolWindowPanelIntegTest : HeavyPlatformTestCase() { super.setUp() unmockkAll() resetSettings(project) + mockkStatic("snyk.trust.TrustedProjectsKt") pluginSettings().token = fakeApiToken // needed to avoid forced Auth panel showing pluginSettings().pluginFirstRun = false // ToolWindow need to be reinitialised for every test as Project is recreated for Heavy tests // also we MUST do it *before* any actual test code due to initialisation of SnykScanListener in init{} toolWindowPanel = project.service() setupDummyCliFile() + every { confirmScanningAndSetWorkspaceTrustedStateIfNeeded(any()) } returns true } override fun tearDown() { diff --git a/src/integTest/kotlin/snyk/trust/WorkspaceTrustServiceTest.kt b/src/integTest/kotlin/snyk/trust/WorkspaceTrustServiceTest.kt new file mode 100644 index 000000000..f7e033817 --- /dev/null +++ b/src/integTest/kotlin/snyk/trust/WorkspaceTrustServiceTest.kt @@ -0,0 +1,59 @@ +@file:Suppress("FunctionName") + +package snyk.trust + +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationManager +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.testFramework.replaceService +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import org.hamcrest.core.IsEqual.equalTo +import org.junit.Assert.assertThat +import org.junit.Before +import org.junit.Test +import java.nio.file.Paths + +class WorkspaceTrustServiceTest : BasePlatformTestCase() { + + private val workspaceTrustSettingsMock = mockk() + private lateinit var cut: WorkspaceTrustService + + private class IntegTestDisposable : Disposable { + override fun dispose() {} + } + + @Before + public override fun setUp() { + super.setUp() + unmockkAll() + + val application = ApplicationManager.getApplication() + application.replaceService( + WorkspaceTrustSettings::class.java, + workspaceTrustSettingsMock, + IntegTestDisposable() + ) + + cut = WorkspaceTrustService() + } + + @Test + fun `test isPathTrusted should return false if no trusted path in settings available`() { + every { workspaceTrustSettingsMock.getTrustedPaths() } returns listOf() + + val path = Paths.get("/project") + + assertThat(cut.isPathTrusted(path), equalTo(false)) + } + + @Test + fun `test isPathTrusted should return true if trusted path in settings available`() { + every { workspaceTrustSettingsMock.getTrustedPaths() } returns listOf("/project") + + val path = Paths.get("/project") + + assertThat(cut.isPathTrusted(path), equalTo(true)) + } +} diff --git a/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt b/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt index 8785e3b73..6fc5ae2c3 100644 --- a/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt +++ b/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt @@ -30,6 +30,8 @@ import io.snyk.plugin.snykcode.core.RunUtils import io.snyk.plugin.ui.SnykBalloonNotifications import org.jetbrains.annotations.TestOnly import snyk.common.SnykError +import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded +import java.nio.file.Paths @Service class SnykTaskQueueService(val project: Project) { @@ -73,6 +75,10 @@ class SnykTaskQueueService(val project: Project) { fun scan() { taskQueue.run(object : Task.Backgroundable(project, "Snyk wait for changed files to be saved on disk", true) { override fun run(indicator: ProgressIndicator) { + project.basePath?.let { + if (!confirmScanningAndSetWorkspaceTrustedStateIfNeeded(Paths.get(it))) return + } + ApplicationManager.getApplication().invokeAndWait { FileDocumentManager.getInstance().saveAllDocuments() } diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/SnykAuthPanel.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/SnykAuthPanel.kt index 6e1b0a1c7..47314cba7 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/SnykAuthPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/SnykAuthPanel.kt @@ -2,6 +2,7 @@ package io.snyk.plugin.ui.toolwindow.panels import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import com.intellij.uiDesigner.core.GridConstraints.ANCHOR_EAST import com.intellij.uiDesigner.core.GridConstraints.ANCHOR_NORTHWEST @@ -24,12 +25,15 @@ import io.snyk.plugin.ui.baseGridConstraints import io.snyk.plugin.ui.boldLabel import io.snyk.plugin.ui.getReadOnlyClickableHtmlJEditorPaneFixedSize import io.snyk.plugin.ui.getStandardLayout +import snyk.SnykBundle import snyk.amplitude.api.ExperimentUser import snyk.analytics.AuthenticateButtonIsClicked import snyk.analytics.AuthenticateButtonIsClicked.EventSource import snyk.analytics.AuthenticateButtonIsClicked.Ide import snyk.analytics.AuthenticateButtonIsClicked.builder +import snyk.trust.WorkspaceTrustService import java.awt.event.ActionEvent +import java.nio.file.Paths import javax.swing.AbstractAction import javax.swing.JButton import javax.swing.JLabel @@ -39,7 +43,7 @@ class SnykAuthPanel(val project: Project) : JPanel(), Disposable { init { name = "authPanel" - val authButton = JButton(object : AbstractAction(AUTHENTICATE_BUTTON_TEXT) { + val authButton = JButton(object : AbstractAction(TRUST_AND_SCAN_BUTTON_TEXT) { override fun actionPerformed(e: ActionEvent?) { val analytics = getSnykAnalyticsService() analytics.logAuthenticateButtonIsClicked(authenticateEvent()) @@ -49,6 +53,12 @@ class SnykAuthPanel(val project: Project) : JPanel(), Disposable { pluginSettings().token = token SnykCodeParams.instance.sessionToken = token + // explicitly add the project to workspace trusted paths, because + // scan can be auto-triggered depending on "settings.pluginFirstRun" value + project.basePath?.let { + service().addTrustedPath(Paths.get(it)) + } + val userId = analytics.obtainUserId(token) if (userId.isNotBlank()) { analytics.setUserId(userId) @@ -102,12 +112,18 @@ class SnykAuthPanel(val project: Project) : JPanel(), Disposable { } private fun descriptionLabelText(): String { + val trustWarningDescription = SnykBundle.message("snyk.panel.auth.trust.warning.text") return """ - |
      + | + |
        |
      1. Authenticate to Snyk.io
      2. |
      3. Analyze code for issues and vulnerabilities
      4. |
      5. Improve your code and upgrade dependencies
      6. |
      + |
      + |$trustWarningDescription + |
      + |
      | """.trimMargin() } @@ -115,7 +131,7 @@ class SnykAuthPanel(val project: Project) : JPanel(), Disposable { override fun dispose() {} companion object { - const val AUTHENTICATE_BUTTON_TEXT = "Test code now" + const val TRUST_AND_SCAN_BUTTON_TEXT = "Trust project and scan" val messagePolicyAndTermsHtml = """
      diff --git a/src/main/kotlin/snyk/trust/TrustedProjects.kt b/src/main/kotlin/snyk/trust/TrustedProjects.kt new file mode 100644 index 000000000..2c548c61f --- /dev/null +++ b/src/main/kotlin/snyk/trust/TrustedProjects.kt @@ -0,0 +1,73 @@ +@file:JvmName("TrustedProjectsKt") +package snyk.trust + +import com.intellij.openapi.application.invokeAndWaitIfNeeded +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.ui.MessageDialogBuilder +import com.intellij.openapi.ui.Messages +import snyk.SnykBundle +import java.nio.file.Files +import java.nio.file.Path + +private val LOG = Logger.getInstance("snyk.trust.TrustedProjects") + +/** + * Shows the "Trust and Scan Project?" dialog, if the user wasn't asked yet if they trust this project, + * and sets the project trusted state according to the user choice. + * + * @return `false` if the user chose not to scan the project at all; `true` otherwise + */ +fun confirmScanningAndSetWorkspaceTrustedStateIfNeeded(projectFileOrDir: Path): Boolean { + val projectDir = if (Files.isDirectory(projectFileOrDir)) projectFileOrDir else projectFileOrDir.parent + + val trustService = service() + val trustedState = trustService.isPathTrusted(projectDir) + if (!trustedState) { + LOG.info("Asking user to trust the project ${projectDir.fileName}") + return when (confirmScanningUntrustedProject(projectDir)) { + ScanUntrustedProjectChoice.TRUST_AND_SCAN -> { + trustService.addTrustedPath(projectDir) + true + } + + ScanUntrustedProjectChoice.CANCEL -> false + } + } + + return true +} + +private fun confirmScanningUntrustedProject(projectDir: Path): ScanUntrustedProjectChoice { + val fileName = projectDir.fileName ?: projectDir.toString() + val title = SnykBundle.message("snyk.trust.dialog.warning.title", fileName) + val message = SnykBundle.message("snyk.trust.dialog.warning.text") + val trustButton = SnykBundle.message("snyk.trust.dialog.warning.button.trust") + val distrustButton = SnykBundle.message("snyk.trust.dialog.warning.button.distrust") + + var choice = ScanUntrustedProjectChoice.CANCEL + + invokeAndWaitIfNeeded { + val result = MessageDialogBuilder + .yesNo(title, message) + .icon(Messages.getWarningIcon()) + .yesText(trustButton) + .noText(distrustButton) + .show() + + choice = if (result == Messages.YES) { + LOG.info("User trusts the project $fileName for scans") + ScanUntrustedProjectChoice.TRUST_AND_SCAN + } else { + LOG.info("User doesn't trust the project $fileName for scans") + ScanUntrustedProjectChoice.CANCEL + } + } + + return choice +} + +enum class ScanUntrustedProjectChoice { + TRUST_AND_SCAN, + CANCEL; +} diff --git a/src/main/kotlin/snyk/trust/WorkspaceTrustService.kt b/src/main/kotlin/snyk/trust/WorkspaceTrustService.kt new file mode 100644 index 000000000..bff86267e --- /dev/null +++ b/src/main/kotlin/snyk/trust/WorkspaceTrustService.kt @@ -0,0 +1,38 @@ +package snyk.trust + +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.logger +import java.nio.file.Path +import java.nio.file.Paths + +private val LOG = logger() + +@Service +class WorkspaceTrustService { + + val settings + get() = service() + + fun addTrustedPath(path: Path) { + LOG.debug("Adding trusted path: $path") + settings.addTrustedPath(path.toString()) + } + + fun isPathTrusted(path: Path): Boolean { + LOG.debug("Verifying if path is trusted: $path") + return settings.getTrustedPaths().asSequence().mapNotNull { + try { + Paths.get(it) + } catch (e: Exception) { + LOG.warn(e) + null + } + }.any { + LOG.debug("Checking if the $it is an ancestor $path") + it.isAncestor(path) + } + } +} + +internal fun Path.isAncestor(child: Path): Boolean = child.startsWith(this) diff --git a/src/main/kotlin/snyk/trust/WorkspaceTrustSettings.kt b/src/main/kotlin/snyk/trust/WorkspaceTrustSettings.kt new file mode 100644 index 000000000..147fc5bf5 --- /dev/null +++ b/src/main/kotlin/snyk/trust/WorkspaceTrustSettings.kt @@ -0,0 +1,29 @@ +package snyk.trust + +import com.intellij.openapi.components.BaseState +import com.intellij.openapi.components.RoamingType +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.SimplePersistentStateComponent +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.util.xmlb.annotations.OptionTag +import java.util.Collections + +@Service +@State( + name = "Workspace.Trust.Settings", + storages = [Storage("snyk.settings.xml", roamingType = RoamingType.DISABLED)] +) +class WorkspaceTrustSettings : SimplePersistentStateComponent(State()) { + class State : BaseState() { + @get:OptionTag("TRUSTED_PATHS") + var trustedPaths by list() + } + + fun addTrustedPath(path: String) { + state.trustedPaths.add(path) + } + + fun getTrustedPaths(): List = Collections.unmodifiableList(state.trustedPaths) +} + diff --git a/src/main/resources/SnykBundle.properties b/src/main/resources/SnykBundle.properties index 60d254675..f3c817d22 100644 --- a/src/main/resources/SnykBundle.properties +++ b/src/main/resources/SnykBundle.properties @@ -1,3 +1,10 @@ +snyk.panel.auth.trust.warning.text=When scanning project files, Snyk may automatically execute code
      such as invoking the package manager to get dependency information.
      You should only scan projects you trust. More info + snyk.settings.organization.tooltip.description=

      Specify an organization slug name to run tests for that organization.

      It must match the URL slug as displayed in the URL of your org in the Snyk UI: https://app.snyk.io/org/[orgslugname]

      snyk.settings.organization.tooltip.linkText=Learn more about organization snyk.settings.organization.tooltip.link=https://docs.snyk.io/integrations/ide-tools/jetbrains-plugins#organization-setting + +snyk.trust.dialog.warning.button.distrust=Don't scan +snyk.trust.dialog.warning.button.trust=Trust project and continue +snyk.trust.dialog.warning.text=When scanning project files for vulnerabilities, Snyk may automatically execute code such as invoking the package manager to get dependency information.

      You should only scan projects you trust.

      More info +snyk.trust.dialog.warning.title=Trust and Scan Project ''{0}''? diff --git a/src/test/kotlin/snyk/InMemoryFsRule.kt b/src/test/kotlin/snyk/InMemoryFsRule.kt new file mode 100644 index 000000000..520373416 --- /dev/null +++ b/src/test/kotlin/snyk/InMemoryFsRule.kt @@ -0,0 +1,33 @@ +package snyk + +import com.google.common.jimfs.Configuration +import com.google.common.jimfs.Jimfs +import org.junit.rules.ExternalResource +import org.junit.runner.Description +import org.junit.runners.model.Statement +import java.net.URLEncoder +import java.nio.file.FileSystem +import kotlin.properties.Delegates + +class InMemoryFsRule : ExternalResource() { + private var _fs: FileSystem? = null + private var sanitizedName: String by Delegates.notNull() + + override fun apply(base: Statement, description: Description): Statement { + sanitizedName = URLEncoder.encode(description.methodName, Charsets.UTF_8.name()) + return super.apply(base, description) + } + + val fs: FileSystem + get() { + if (_fs == null) { + _fs = Jimfs.newFileSystem(Configuration.unix()) + } + return _fs!! + } + + override fun after() { + _fs?.close() + _fs = null + } +} diff --git a/src/test/kotlin/snyk/trust/WorkspaceTrustServiceTest.kt b/src/test/kotlin/snyk/trust/WorkspaceTrustServiceTest.kt new file mode 100644 index 000000000..574c64d06 --- /dev/null +++ b/src/test/kotlin/snyk/trust/WorkspaceTrustServiceTest.kt @@ -0,0 +1,69 @@ +package snyk.trust + +import org.hamcrest.core.IsEqual.equalTo +import org.junit.Assert.assertThat +import org.junit.Rule +import org.junit.Test +import snyk.InMemoryFsRule + +class WorkspaceTrustServiceTest { + + @JvmField + @Rule + val memoryFs = InMemoryFsRule() + + @Test + fun `isAncestor should return true for itself`() { + val absoluteSimpleDir = memoryFs.fs.getPath("/opt/projects/simple") + val relativeSimpleDir = memoryFs.fs.getPath("projects/simple") + + assertThat(absoluteSimpleDir.isAncestor(absoluteSimpleDir), equalTo(true)) + assertThat(relativeSimpleDir.isAncestor(relativeSimpleDir), equalTo(true)) + } + + @Test + fun `isAncestor should return true for inner folder inside of outer`() { + val absoluteOuterDir = memoryFs.fs.getPath("/opt/projects/outer") + val absoluteInnerDir = memoryFs.fs.getPath("/opt/projects/outer/inner") + val relativeOuterDir = memoryFs.fs.getPath("projects/outer") + val relativeInnerDir = memoryFs.fs.getPath("projects/outer/inner") + + assertThat(absoluteOuterDir.isAncestor(absoluteInnerDir), equalTo(true)) + assertThat(relativeOuterDir.isAncestor(relativeInnerDir), equalTo(true)) + } + + @Test + fun `isAncestor should return true for inner folder with more than one level inside of outer`() { + val absoluteOuterDir = memoryFs.fs.getPath("/opt/projects/outer") + val absoluteInnerDir = memoryFs.fs.getPath("/opt/projects/outer/level1/level2/level3/inner") + val relativeOuterDir = memoryFs.fs.getPath("projects/outer") + val relativeInnerDir = memoryFs.fs.getPath("projects/outer/level1/level2/level3/inner") + + assertThat(absoluteOuterDir.isAncestor(absoluteInnerDir), equalTo(true)) + assertThat(relativeOuterDir.isAncestor(relativeInnerDir), equalTo(true)) + } + + @Test + fun `isAncestor should return false for outer folder`() { + val absoluteOuterDir = memoryFs.fs.getPath("/opt/projects/outer") + val absoluteInnerDir = memoryFs.fs.getPath("/opt/projects/outer/inner") + val relativeOuterDir = memoryFs.fs.getPath("projects/outer") + val relativeInnerDir = memoryFs.fs.getPath("projects/outer/inner") + + assertThat(absoluteInnerDir.isAncestor(absoluteOuterDir), equalTo(false)) + assertThat(relativeInnerDir.isAncestor(relativeOuterDir), equalTo(false)) + } + + @Test + fun `isAncestor should return false for folders on different levels`() { + val absoluteFirstDir = memoryFs.fs.getPath("/opt/projects/first") + val absoluteSecondDir = memoryFs.fs.getPath("/opt/projects/second") + val relativeFirstDir = memoryFs.fs.getPath("projects/first") + val relativeSecondDir = memoryFs.fs.getPath("projects/second") + + assertThat(absoluteFirstDir.isAncestor(absoluteSecondDir), equalTo(false)) + assertThat(absoluteSecondDir.isAncestor(absoluteFirstDir), equalTo(false)) + assertThat(relativeFirstDir.isAncestor(relativeSecondDir), equalTo(false)) + assertThat(relativeSecondDir.isAncestor(relativeFirstDir), equalTo(false)) + } +}