From 3f83eebb992e562b9e6eaa9e2217d53e842f76e2 Mon Sep 17 00:00:00 2001 From: Aniket Shinde Date: Thu, 3 Sep 2026 15:39:49 +0530 Subject: [PATCH 1/4] Unit test cases --- .../META-INF/MANIFEST.MF | 4 + checkmarx-ast-eclipse-plugin-tests/pom.xml | 4 +- .../backend/GlobalScannerControllerTest.java | 154 ++++++ .../backend/ScannerRegistryTest.java | 111 +++++ .../CheckmarxDocumentListenerTest.java | 127 +++++ .../basescanner/BaseScannerCommandTest.java | 141 ++++++ .../basescanner/BaseScannerServiceTest.java | 161 +++++++ .../devassist/common/ScannerFactoryTest.java | 182 +++++++ .../configuration/McpInstallServiceTest.java | 106 ++++ .../McpSettingsInjectorTest.java | 171 +++++++ .../ignore/IgnoreFileManagerTest.java | 244 ++++++++++ .../devassist/ignore/IgnoreManagerTest.java | 259 ++++++++++ .../inspection/DevAssistInspectionTest.java | 50 ++ .../DevAssistScanSchedulerTest.java | 192 ++++++++ .../problems/ProblemBuilderTest.java | 105 ++++ .../problems/ProblemDecoratorTest.java | 268 +++++++++++ .../problems/ProblemHolderServiceTest.java | 124 +++++ .../problems/ScanIssueProcessorTest.java | 146 ++++++ .../remediation/DevAssistFixPromptsTest.java | 97 ++++ .../RemediationLinkHandlerTest.java | 114 +++++ .../remediation/RemediationManagerTest.java | 228 +++++++++ .../remediation/ViewDetailsPromptsTest.java | 114 +++++ .../asca/AscaScanResultAdaptorTest.java | 227 +++++++++ .../scanners/asca/AscaScannerCommandTest.java | 70 +++ .../scanners/asca/AscaScannerServiceTest.java | 119 +++++ .../ContainerScanResultAdaptorTest.java | 152 ++++++ .../ContainerScannerCommandTest.java | 93 ++++ .../ContainerScannerServiceTest.java | 116 +++++ .../iac/IacScanResultAdaptorTest.java | 180 +++++++ .../scanners/iac/IacScannerCommandTest.java | 77 +++ .../scanners/iac/IacScannerServiceTest.java | 95 ++++ .../oss/OssScanResultAdaptorTest.java | 168 +++++++ .../scanners/oss/OssScannerCommandTest.java | 78 +++ .../scanners/oss/OssScannerServiceTest.java | 78 +++ .../secrets/SecretsScanResultAdaptorTest.java | 125 +++++ .../secrets/SecretsScannerCommandTest.java | 69 +++ .../secrets/SecretsScannerServiceTest.java | 89 ++++ .../devassist/utils/DevAssistUtilsTest.java | 243 ++++++++++ .../ActionFilterStatePreferenceTest.java | 241 ---------- devassist-lib/META-INF/MANIFEST.MF | 1 + .../unit/views/DataProviderExtendedTest.java | 426 ---------------- .../views/UISynchronizeImplExtendedTest.java | 197 -------- .../actions/ActionStartScanExtendedTest.java | 455 ------------------ 43 files changed, 5081 insertions(+), 1320 deletions(-) create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/backend/GlobalScannerControllerTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/backend/ScannerRegistryTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/backend/listener/CheckmarxDocumentListenerTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/basescanner/BaseScannerCommandTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/basescanner/BaseScannerServiceTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/common/ScannerFactoryTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/configuration/McpInstallServiceTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/configuration/McpSettingsInjectorTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/ignore/IgnoreFileManagerTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/ignore/IgnoreManagerTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/inspection/DevAssistInspectionTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/inspection/DevAssistScanSchedulerTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/problems/ProblemBuilderTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/problems/ProblemDecoratorTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/problems/ProblemHolderServiceTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/problems/ScanIssueProcessorTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/remediation/DevAssistFixPromptsTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/remediation/RemediationLinkHandlerTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/remediation/RemediationManagerTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/remediation/ViewDetailsPromptsTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/scanners/asca/AscaScanResultAdaptorTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/scanners/asca/AscaScannerCommandTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/scanners/asca/AscaScannerServiceTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/scanners/containers/ContainerScanResultAdaptorTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/scanners/containers/ContainerScannerCommandTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/scanners/containers/ContainerScannerServiceTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/scanners/iac/IacScanResultAdaptorTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/scanners/iac/IacScannerCommandTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/scanners/iac/IacScannerServiceTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/scanners/oss/OssScanResultAdaptorTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/scanners/oss/OssScannerCommandTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/scanners/oss/OssScannerServiceTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/scanners/secrets/SecretsScanResultAdaptorTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/scanners/secrets/SecretsScannerCommandTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/scanners/secrets/SecretsScannerServiceTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/utils/DevAssistUtilsTest.java delete mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/com/checkmarx/eclipse/views/actions/ActionFilterStatePreferenceTest.java delete mode 100644 src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/views/DataProviderExtendedTest.java delete mode 100644 src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/views/UISynchronizeImplExtendedTest.java delete mode 100644 src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/views/actions/ActionStartScanExtendedTest.java diff --git a/checkmarx-ast-eclipse-plugin-tests/META-INF/MANIFEST.MF b/checkmarx-ast-eclipse-plugin-tests/META-INF/MANIFEST.MF index 0f4cbb8c..808b61e3 100644 --- a/checkmarx-ast-eclipse-plugin-tests/META-INF/MANIFEST.MF +++ b/checkmarx-ast-eclipse-plugin-tests/META-INF/MANIFEST.MF @@ -5,6 +5,7 @@ Bundle-SymbolicName: com.checkmarx.ast.eclipse.tests Bundle-Version: 1.0.0.qualifier Fragment-Host: com.checkmarx.eclipse.plugin;bundle-version="1.0.0" Require-Bundle: + com.checkmarx.eclipse.devassist, org.eclipse.swtbot.swt.finder, org.eclipse.swtbot.eclipse.finder, org.eclipse.swtbot.junit5_x, @@ -15,4 +16,7 @@ Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ClassPath: .,lib/mockito-core-5.14.2.jar,lib/powermock-core-*.jar, lib/byte-buddy-1.17.8.jar, lib/byte-buddy-agent-1.17.8.jar Automatic-Module-Name: com.checkmarx.ast.eclipse.tests Import-Package: com.checkmarx.eclipse.common.runner, + com.fasterxml.jackson.annotation, + com.fasterxml.jackson.core, + com.fasterxml.jackson.databind, org.slf4j;version="[2.0.0,3.0.0)" diff --git a/checkmarx-ast-eclipse-plugin-tests/pom.xml b/checkmarx-ast-eclipse-plugin-tests/pom.xml index 70e9bd00..01f03251 100644 --- a/checkmarx-ast-eclipse-plugin-tests/pom.xml +++ b/checkmarx-ast-eclipse-plugin-tests/pom.xml @@ -5,7 +5,8 @@ 4.0.0 - **/Test*.java,**/*Test.java,**/*Tests.java,**/*TestCase.java + **/unit/**/Test*.java,**/unit/**/*Test.java,**/unit/**/*Tests.java,**/unit/**/*TestCase.java + com.checkmarx.ast.eclipse.tests com.checkmarx.ast.eclipse.tests @@ -43,6 +44,7 @@ XML CSV + HTML diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/backend/GlobalScannerControllerTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/backend/GlobalScannerControllerTest.java new file mode 100644 index 00000000..5fb5acdb --- /dev/null +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/backend/GlobalScannerControllerTest.java @@ -0,0 +1,154 @@ +package checkmarx.ast.eclipse.plugin.tests.unit.devassist.backend; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.checkmarx.eclipse.devassist.backend.GlobalScannerController; +import com.checkmarx.eclipse.devassist.backend.GlobalScannerController.ScannerStateListener; +import com.checkmarx.eclipse.devassist.backend.ScannerRegistry; +import com.checkmarx.eclipse.devassist.backend.ScannerRegistry.ScannerType; + +/** + * Unit tests for {@link GlobalScannerController}. It is a JVM-wide singleton, + * so every test resets its internal state map/listener list via reflection + * in {@code @BeforeEach} to avoid bleeding state across tests (and across + * other test classes in this module, e.g. {@code ScannerFactoryTest}, that + * also go through {@code getInstance()}). + */ +class GlobalScannerControllerTest { + + private GlobalScannerController controller; + + @BeforeEach + @SuppressWarnings("unchecked") + void resetSingletonState() throws Exception { + controller = GlobalScannerController.getInstance(); + + Field stateField = GlobalScannerController.class.getDeclaredField("scannerState"); + stateField.setAccessible(true); + ((Map) stateField.get(controller)).clear(); + + Field listenersField = GlobalScannerController.class.getDeclaredField("stateListeners"); + listenersField.setAccessible(true); + ((List) listenersField.get(controller)).clear(); + } + + @Test + @DisplayName("isScannerEnabled defaults to true for a type that was never explicitly set") + void isScannerEnabledDefaultsToTrue() { + assertTrue(controller.isScannerEnabled(ScannerType.OSS)); + } + + @Test + @DisplayName("isScannerEnabled returns false for a null type") + void isScannerEnabledHandlesNullType() { + assertFalse(controller.isScannerEnabled(null)); + } + + @Test + @DisplayName("disableScanner then isScannerEnabled reflects the disabled state") + void disableScannerThenIsScannerEnabled() { + controller.disableScanner(ScannerType.SECRETS); + assertFalse(controller.isScannerEnabled(ScannerType.SECRETS)); + + controller.enableScanner(ScannerType.SECRETS); + assertTrue(controller.isScannerEnabled(ScannerType.SECRETS)); + } + + @Test + @DisplayName("enableScanner/disableScanner with a null type is a safe no-op") + void enableDisableHandleNullType() { + controller.enableScanner(null); + controller.disableScanner(null); + // No exception, and no scanner type is affected. + assertEquals(ScannerType.values().length, controller.getEnabledScannerCount()); + } + + @Test + @DisplayName("disableAllScanners then enableAllScanners toggles every scanner type") + void disableThenEnableAllScanners() { + controller.disableAllScanners(); + assertEquals(0, controller.getEnabledScannerCount()); + for (ScannerType type : ScannerType.values()) { + assertFalse(controller.isScannerEnabled(type)); + } + + controller.enableAllScanners(); + assertEquals(ScannerType.values().length, controller.getEnabledScannerCount()); + } + + @Test + @DisplayName("Listener is notified only on an actual state transition, not on a redundant call") + void listenerNotifiedOnlyOnRealTransition() { + // Note: wasEnabled/wasDisabled are computed from the map's PREVIOUS explicit + // value, not from isScannerEnabled()'s default-true fallback - so after the + // @BeforeEach map .clear(), the type has no explicit entry yet. Prime one + // with an explicit enableScanner() call (itself not guaranteed to notify) + // before attaching the listener, so the subsequent disable really is a + // transition from a known "true" state. + controller.enableScanner(ScannerType.IAC); + List notifications = new ArrayList<>(); + ScannerStateListener listener = (type, enabled) -> notifications.add(enabled); + controller.addScannerStateListener(listener); + + controller.disableScanner(ScannerType.IAC); + controller.disableScanner(ScannerType.IAC); + controller.enableScanner(ScannerType.IAC); + controller.enableScanner(ScannerType.IAC); + + assertEquals(List.of(false, true), notifications); + } + + @Test + @DisplayName("removeScannerStateListener stops further notifications") + void removeScannerStateListenerStopsNotifications() { + controller.enableScanner(ScannerType.ASCA); + List notifications = new ArrayList<>(); + ScannerStateListener listener = (type, enabled) -> notifications.add(enabled); + controller.addScannerStateListener(listener); + controller.removeScannerStateListener(listener); + + controller.disableScanner(ScannerType.ASCA); + + assertTrue(notifications.isEmpty()); + } + + @Test + @DisplayName("A listener that throws does not prevent other listeners from being notified") + void listenerExceptionDoesNotBlockOtherListeners() { + controller.enableScanner(ScannerType.CONTAINERS); + List notifications = new ArrayList<>(); + controller.addScannerStateListener((type, enabled) -> { + throw new RuntimeException("boom"); + }); + controller.addScannerStateListener((type, enabled) -> notifications.add(enabled)); + + controller.disableScanner(ScannerType.CONTAINERS); + + assertEquals(List.of(false), notifications); + } + + @Test + @DisplayName("getStateReport lists every scanner type with its enabled/disabled state") + void getStateReportListsAllTypes() { + controller.disableScanner(ScannerType.OSS); + + String report = controller.getStateReport(); + + assertTrue(report.contains("DISABLED")); + assertTrue(report.contains("ENABLED")); + for (ScannerType type : ScannerType.values()) { + assertTrue(report.contains(type.getDisplayName())); + } + } +} diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/backend/ScannerRegistryTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/backend/ScannerRegistryTest.java new file mode 100644 index 00000000..e0b90376 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/backend/ScannerRegistryTest.java @@ -0,0 +1,111 @@ +package checkmarx.ast.eclipse.plugin.tests.unit.devassist.backend; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.eclipse.core.resources.IProject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.checkmarx.eclipse.devassist.backend.ScannerRegistry; +import com.checkmarx.eclipse.devassist.backend.ScannerRegistry.ScannerType; +import com.checkmarx.eclipse.devassist.basescanner.ScannerService; + +/** + * Unit tests for {@link ScannerRegistry}'s lazy-creation/caching/disposal + * lifecycle. Each {@code ScannerType} maps to a real (but network-free at + * construction time) scanner command wrapper - constructing one only stores + * references and logs, matching the pattern already validated for each + * scanner's own Command test in the scanners batch. + */ +class ScannerRegistryTest { + + private IProject project; + private ScannerRegistry registry; + + @BeforeEach + void setUp() { + project = mock(IProject.class); + when(project.getName()).thenReturn("TestProject"); + registry = new ScannerRegistry(project); + } + + @Test + @DisplayName("getProject returns the project the registry was created for") + void getProjectReturnsProject() { + assertSame(project, registry.getProject()); + } + + @Test + @DisplayName("A freshly created registry is not disposed and has no registered scanners") + void freshRegistryIsNotDisposed() { + assertFalse(registry.isDisposed()); + for (ScannerType type : ScannerType.values()) { + assertFalse(registry.hasScannerService(type)); + } + } + + @Test + @DisplayName("getScannerService lazily creates a scanner for every supported type") + void getScannerServiceCreatesEveryType() { + for (ScannerType type : ScannerType.values()) { + Object scanner = registry.getScannerService(type); + assertNotNull(scanner, "Expected a scanner instance for type: " + type); + assertTrue(scanner instanceof ScannerService, "Scanner should implement ScannerService for: " + type); + assertTrue(registry.hasScannerService(type)); + } + } + + @Test + @DisplayName("getScannerService returns the same cached instance on repeated calls") + void getScannerServiceCachesInstance() { + Object first = registry.getScannerService(ScannerType.OSS); + Object second = registry.getScannerService(ScannerType.OSS); + + assertSame(first, second); + } + + @Test + @DisplayName("deregisterAllScanners clears all registered scanners and marks the registry disposed") + void deregisterAllScannersClearsAndDisposes() { + registry.getScannerService(ScannerType.OSS); + registry.getScannerService(ScannerType.SECRETS); + assertTrue(registry.hasScannerService(ScannerType.OSS)); + + assertDoesNotThrow(registry::deregisterAllScanners); + + assertTrue(registry.isDisposed()); + assertFalse(registry.hasScannerService(ScannerType.OSS)); + assertFalse(registry.hasScannerService(ScannerType.SECRETS)); + } + + @Test + @DisplayName("getScannerService returns null once the registry has been disposed") + void getScannerServiceReturnsNullAfterDispose() { + registry.deregisterAllScanners(); + + Object scanner = registry.getScannerService(ScannerType.ASCA); + + assertNotNull(registry); // sanity: registry object itself still usable + assertFalse(registry.hasScannerService(ScannerType.ASCA)); + org.junit.jupiter.api.Assertions.assertNull(scanner); + } + + @Test + @DisplayName("getStatistics reports the project name, scanner count and disposed flag") + void getStatisticsReportsSummary() { + registry.getScannerService(ScannerType.CONTAINERS); + + String stats = registry.getStatistics(); + + assertTrue(stats.contains("TestProject")); + assertTrue(stats.contains("Scanners: 1")); + assertTrue(stats.contains("Disposed: false")); + } +} diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/backend/listener/CheckmarxDocumentListenerTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/backend/listener/CheckmarxDocumentListenerTest.java new file mode 100644 index 00000000..565026c5 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/backend/listener/CheckmarxDocumentListenerTest.java @@ -0,0 +1,127 @@ +package checkmarx.ast.eclipse.plugin.tests.unit.devassist.backend.listener; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.eclipse.core.resources.IFile; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.checkmarx.eclipse.devassist.backend.listener.CheckmarxDocumentListener; +import com.checkmarx.eclipse.devassist.backend.listener.RealTimeScanJob; +import com.checkmarx.eclipse.devassist.inspection.DevAssistScanScheduler; + +/** + * Unit tests for {@link CheckmarxDocumentListener}, the real-time-scan + * debounce trigger fired on every document edit. {@code documentChanged} + * never reads its {@code DocumentEvent} argument, so {@code null} is passed + * for it throughout - matching the actual (unused-parameter) implementation. + */ +class CheckmarxDocumentListenerTest { + + private IFile file() { + IFile file = mock(IFile.class); + when(file.getName()).thenReturn("Main.java"); + return file; + } + + @Test + @DisplayName("documentChanged reschedules the debounced scan via the scheduler when one is available") + void documentChangedReschedulesViaScheduler() { + DevAssistScanScheduler scheduler = mock(DevAssistScanScheduler.class); + IFile file = file(); + CheckmarxDocumentListener listener = new CheckmarxDocumentListener("Main.java", null, file, scheduler); + + listener.documentChanged(null); + + verify(scheduler).rescheduleInspection(file, 1000); + } + + @Test + @DisplayName("Two rapid edits within the throttle window only reschedule once") + void rapidEditsAreThrottled() { + DevAssistScanScheduler scheduler = mock(DevAssistScanScheduler.class); + CheckmarxDocumentListener listener = new CheckmarxDocumentListener("Main.java", null, file(), scheduler); + + listener.documentChanged(null); + listener.documentChanged(null); // fires within the same test method, well under the 100ms throttle window + + verify(scheduler, times(1)).rescheduleInspection(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.eq(1000L)); + } + + @Test + @DisplayName("setSkipNextChange(true) suppresses exactly the next reschedule, then resets") + void skipNextChangeSuppressesOneReschedule() { + DevAssistScanScheduler scheduler = mock(DevAssistScanScheduler.class); + IFile file = file(); + CheckmarxDocumentListener listener = new CheckmarxDocumentListener("Main.java", null, file, scheduler); + + listener.setSkipNextChange(true); + listener.documentChanged(null); + + verify(scheduler, never()).rescheduleInspection(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.anyLong()); + } + + @Test + @DisplayName("Falls back to the scanJob's own reschedule when no scheduler is provided") + void fallsBackToScanJobWhenSchedulerNull() { + RealTimeScanJob scanJob = mock(RealTimeScanJob.class); + CheckmarxDocumentListener listener = new CheckmarxDocumentListener("Main.java", scanJob, null, null); + + listener.documentChanged(null); + + verify(scanJob).reschedule(1000); + } + + @Test + @DisplayName("An exception from the scheduler is caught and does not propagate") + void schedulerExceptionIsCaughtSafely() { + DevAssistScanScheduler scheduler = mock(DevAssistScanScheduler.class); + IFile file = file(); + doThrow(new RuntimeException("boom")).when(scheduler).rescheduleInspection(file, 1000); + CheckmarxDocumentListener listener = new CheckmarxDocumentListener("Main.java", null, file, scheduler); + + assertDoesNotThrow(() -> listener.documentChanged(null)); + } + + @Test + @DisplayName("documentAboutToBeChanged is a safe no-op") + void documentAboutToBeChangedIsNoOp() { + CheckmarxDocumentListener listener = new CheckmarxDocumentListener("Main.java", null, null, null); + + assertDoesNotThrow(() -> listener.documentAboutToBeChanged(null)); + } + + @Test + @DisplayName("dispose cancels the underlying scan job when one is present") + void disposeCancelsScanJob() { + RealTimeScanJob scanJob = mock(RealTimeScanJob.class); + CheckmarxDocumentListener listener = new CheckmarxDocumentListener("Main.java", scanJob, null, null); + + listener.dispose(); + + verify(scanJob).cancel(); + } + + @Test + @DisplayName("dispose is a safe no-op when there is no scan job") + void disposeWithoutScanJobIsNoOp() { + CheckmarxDocumentListener listener = new CheckmarxDocumentListener("Main.java", null, null, null); + + assertDoesNotThrow(listener::dispose); + } + + @Test + @DisplayName("getFileName returns the file name passed to the constructor") + void getFileNameReturnsConstructorValue() { + CheckmarxDocumentListener listener = new CheckmarxDocumentListener("Main.java", null, null, null); + + assertEquals("Main.java", listener.getFileName()); + } +} diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/basescanner/BaseScannerCommandTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/basescanner/BaseScannerCommandTest.java new file mode 100644 index 00000000..dbd5e0db --- /dev/null +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/basescanner/BaseScannerCommandTest.java @@ -0,0 +1,141 @@ +package checkmarx.ast.eclipse.plugin.tests.unit.devassist.basescanner; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.eclipse.core.resources.IProject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.checkmarx.eclipse.devassist.basescanner.BaseScannerCommand; +import com.checkmarx.eclipse.devassist.common.ScannerConfig; +import com.checkmarx.eclipse.devassist.model.ScanEngine; + +/** + * Unit tests for {@link BaseScannerCommand} using a minimal concrete + * subclass, exercising the register/deregister lifecycle directly rather + * than only indirectly through each scanner's own Command subclass. + */ +class BaseScannerCommandTest { + + private static class TestScannerCommand extends BaseScannerCommand { + int initializeCount = 0; + + TestScannerCommand(IProject project, ScannerConfig config) { + super(project, config); + } + + @Override + public void initializeScanner() { + initializeCount++; + } + + ScanEngine callGetScannerType() { + return getScannerType(); + } + } + + private IProject project; + + @BeforeEach + void setUp() { + project = mock(IProject.class); + when(project.getName()).thenReturn("TestProject"); + } + + private ScannerConfig configFor(String engineName) { + return ScannerConfig.builder().engineName(engineName).enabledMessage("started") + .disabledMessage("disabled").build(); + } + + @Test + @DisplayName("register() initializes the scanner when the config has a valid engine name") + void registerInitializesWhenConfigValid() { + TestScannerCommand command = new TestScannerCommand(project, configFor("OSS")); + + command.register(project); + + assertEquals(1, command.initializeCount); + } + + @Test + @DisplayName("register() does nothing when the config is null (scanner considered inactive)") + void registerDoesNothingWhenConfigNull() { + TestScannerCommand command = new TestScannerCommand(project, null); + + command.register(project); + + assertEquals(0, command.initializeCount); + } + + @Test + @DisplayName("register() does nothing when the config's engine name is null") + void registerDoesNothingWhenEngineNameNull() { + TestScannerCommand command = new TestScannerCommand(project, ScannerConfig.builder().build()); + + command.register(project); + + assertEquals(0, command.initializeCount); + } + + @Test + @DisplayName("Calling register() again while already registered does not re-initialize") + void registerIsIdempotentWhileAlreadyRegistered() { + TestScannerCommand command = new TestScannerCommand(project, configFor("OSS")); + + command.register(project); + command.register(project); + + assertEquals(1, command.initializeCount); + } + + @Test + @DisplayName("deregister() then register() again re-initializes the scanner") + void deregisterThenRegisterReinitializes() { + TestScannerCommand command = new TestScannerCommand(project, configFor("OSS")); + + command.register(project); + command.deregister(project); + command.register(project); + + assertEquals(2, command.initializeCount); + } + + @Test + @DisplayName("deregister() on a never-registered command is a safe no-op") + void deregisterWithoutRegisterIsNoOp() { + TestScannerCommand command = new TestScannerCommand(project, configFor("OSS")); + + assertDoesNotThrow(() -> command.deregister(project)); + assertEquals(0, command.initializeCount); + } + + @Test + @DisplayName("getScannerType() resolves the ScanEngine matching the config's engine name") + void getScannerTypeResolvesEngine() { + TestScannerCommand command = new TestScannerCommand(project, configFor("asca")); + + assertEquals(ScanEngine.ASCA, command.callGetScannerType()); + } + + @Test + @DisplayName("getConfig() returns the same config instance passed to the constructor") + void getConfigReturnsSameInstance() { + ScannerConfig config = configFor("OSS"); + TestScannerCommand command = new TestScannerCommand(project, config); + + assertSame(config, command.getConfig()); + } + + @Test + @DisplayName("dispose() (base implementation) completes without throwing") + void disposeDoesNotThrow() { + TestScannerCommand command = new TestScannerCommand(project, configFor("OSS")); + + assertDoesNotThrow(command::dispose); + } +} diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/basescanner/BaseScannerServiceTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/basescanner/BaseScannerServiceTest.java new file mode 100644 index 00000000..3e191396 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/basescanner/BaseScannerServiceTest.java @@ -0,0 +1,161 @@ +package checkmarx.ast.eclipse.plugin.tests.unit.devassist.basescanner; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.stream.Stream; + +import org.eclipse.core.resources.IProject; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.checkmarx.eclipse.devassist.basescanner.BaseScannerService; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.common.ScannerConfig; + +/** + * Unit tests for {@link BaseScannerService} using a minimal concrete + * subclass, exercising {@code shouldScanFile}'s node_modules exclusion and + * the temp-folder helpers directly. + */ +class BaseScannerServiceTest { + + private static class TestScannerService extends BaseScannerService { + boolean supported; + + TestScannerService(IProject project, ScannerConfig config) { + super(project, config); + } + + @Override + protected boolean isFileTypeSupported(String filePath) { + return supported; + } + + @Override + public ScanResult scan(String filePath) { + return null; + } + + String callGetTempSubFolderPath(String baseDir) { + return getTempSubFolderPath(baseDir); + } + + void callCreateTempFolder(Path path) { + createTempFolder(path); + } + + void callDeleteTempFolder(Path path) { + deleteTempFolder(path); + } + } + + private IProject project; + private TestScannerService service; + private Path tempDir; + + @BeforeEach + void setUp() { + project = mock(IProject.class); + when(project.getName()).thenReturn("TestProject"); + service = new TestScannerService(project, ScannerConfig.builder().engineName("TEST").build()); + } + + @AfterEach + void cleanUp() throws IOException { + if (tempDir == null || !Files.exists(tempDir)) { + return; + } + try (Stream walk = Files.walk(tempDir)) { + walk.sorted(Comparator.reverseOrder()).forEach(p -> { + try { + Files.deleteIfExists(p); + } catch (IOException ignored) { + // best-effort cleanup + } + }); + } + } + + @Test + @DisplayName("shouldScanFile rejects null or empty path without consulting isFileTypeSupported") + void shouldScanFileRejectsNullOrEmpty() { + service.supported = true; + assertFalse(service.shouldScanFile(null)); + assertFalse(service.shouldScanFile("")); + } + + @Test + @DisplayName("shouldScanFile rejects any path under a node_modules directory, forward or back slash") + void shouldScanFileRejectsNodeModules() { + service.supported = true; + assertFalse(service.shouldScanFile("/repo/node_modules/lodash/index.js")); + assertFalse(service.shouldScanFile("C:\\repo\\node_modules\\lodash\\index.js")); + } + + @Test + @DisplayName("shouldScanFile delegates to isFileTypeSupported for non-excluded paths") + void shouldScanFileDelegatesToSubclass() { + service.supported = false; + assertFalse(service.shouldScanFile("/repo/Main.java")); + + service.supported = true; + assertTrue(service.shouldScanFile("/repo/Main.java")); + } + + @Test + @DisplayName("getConfig returns the same config instance passed to the constructor") + void getConfigReturnsSameInstance() { + ScannerConfig config = ScannerConfig.builder().engineName("TEST").build(); + TestScannerService withConfig = new TestScannerService(project, config); + + assertSame(config, withConfig.getConfig()); + } + + @Test + @DisplayName("getTempSubFolderPath builds a path under the system temp directory") + void getTempSubFolderPathBuildsUnderSystemTemp() { + String path = service.callGetTempSubFolderPath("CxTestScanner"); + + assertTrue(path.endsWith("CxTestScanner")); + assertTrue(path.startsWith(System.getProperty("java.io.tmpdir"))); + } + + @Test + @DisplayName("createTempFolder creates a missing directory, deleteTempFolder removes it") + void createAndDeleteTempFolderRoundTrip() { + tempDir = Path.of(System.getProperty("java.io.tmpdir"), "CxBaseScannerServiceTest-" + System.nanoTime()); + assertFalse(Files.exists(tempDir)); + + service.callCreateTempFolder(tempDir); + assertTrue(Files.exists(tempDir)); + assertTrue(Files.isDirectory(tempDir)); + + service.callDeleteTempFolder(tempDir); + assertFalse(Files.exists(tempDir)); + } + + @Test + @DisplayName("deleteTempFolder on a path that doesn't exist is a safe no-op") + void deleteTempFolderOnMissingPathIsNoOp() { + Path missing = Path.of(System.getProperty("java.io.tmpdir"), "CxDoesNotExist-" + System.nanoTime()); + + assertDoesNotThrow(() -> service.callDeleteTempFolder(missing)); + } + + @Test + @DisplayName("close() (base implementation) completes without throwing") + void closeDoesNotThrow() { + assertDoesNotThrow(service::close); + } +} diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/common/ScannerFactoryTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/common/ScannerFactoryTest.java new file mode 100644 index 00000000..a5ce97d9 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/common/ScannerFactoryTest.java @@ -0,0 +1,182 @@ +package checkmarx.ast.eclipse.plugin.tests.unit.devassist.common; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.checkmarx.eclipse.devassist.backend.GlobalScannerController; +import com.checkmarx.eclipse.devassist.backend.ScannerRegistry; +import com.checkmarx.eclipse.devassist.backend.ScannerRegistry.ScannerType; +import com.checkmarx.eclipse.devassist.basescanner.ScannerService; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.common.ScannerConfig; +import com.checkmarx.eclipse.devassist.common.ScannerFactory; + +/** + * Unit tests for {@link ScannerFactory}. {@link ScannerRegistry} is mocked + * (its own lifecycle/lazy-creation behavior is covered by + * {@code ScannerRegistryTest}) so this focuses purely on the + * global-enabled + file-type-support filtering logic. Resets the + * {@link GlobalScannerController} JVM-wide singleton before each test - see + * {@code GlobalScannerControllerTest} for why. + */ +class ScannerFactoryTest { + + private ScannerRegistry registry; + private ScannerFactory factory; + + private static class FakeScannerService implements ScannerService { + private final boolean shouldScan; + + FakeScannerService(boolean shouldScan) { + this.shouldScan = shouldScan; + } + + @Override + public boolean shouldScanFile(String filePath) { + return shouldScan; + } + + @Override + public ScanResult scan(String filePath) { + return null; + } + + @Override + public ScannerConfig getConfig() { + return null; + } + + @Override + public void close() throws Exception { + // no-op + } + } + + @BeforeEach + @SuppressWarnings("unchecked") + void setUp() throws Exception { + GlobalScannerController controller = GlobalScannerController.getInstance(); + Field stateField = GlobalScannerController.class.getDeclaredField("scannerState"); + stateField.setAccessible(true); + ((Map) stateField.get(controller)).clear(); + + registry = mock(ScannerRegistry.class); + factory = new ScannerFactory(registry); + } + + @Test + @DisplayName("getAllSupportedScanners returns only scanners that are enabled and support the file") + void getAllSupportedScannersFiltersByEnabledAndSupport() { + FakeScannerService ossScanner = new FakeScannerService(true); + when(registry.getScannerService(ScannerType.OSS)).thenReturn(ossScanner); + // Every other type: registry has nothing registered (returns null) + + List> supported = factory.getAllSupportedScanners("/repo/pom.xml"); + + assertEquals(1, supported.size()); + assertSame(ossScanner, supported.get(0)); + } + + @Test + @DisplayName("getAllSupportedScanners excludes a scanner disabled globally, even if it supports the file") + void getAllSupportedScannersExcludesGloballyDisabled() { + FakeScannerService ossScanner = new FakeScannerService(true); + when(registry.getScannerService(ScannerType.OSS)).thenReturn(ossScanner); + GlobalScannerController.getInstance().disableScanner(ScannerType.OSS); + + List> supported = factory.getAllSupportedScanners("/repo/pom.xml"); + + assertTrue(supported.isEmpty()); + } + + @Test + @DisplayName("getAllSupportedScanners excludes a scanner that does not support the file type") + void getAllSupportedScannersExcludesUnsupportedFileType() { + FakeScannerService ossScanner = new FakeScannerService(false); + when(registry.getScannerService(ScannerType.OSS)).thenReturn(ossScanner); + + List> supported = factory.getAllSupportedScanners("/repo/Main.java"); + + assertTrue(supported.isEmpty()); + } + + @Test + @DisplayName("getAllSupportedScanners skips a registry entry that is not a ScannerService instance") + void getAllSupportedScannersSkipsNonScannerServiceObjects() { + when(registry.getScannerService(ScannerType.OSS)).thenReturn(new Object()); + + List> supported = factory.getAllSupportedScanners("/repo/pom.xml"); + + assertTrue(supported.isEmpty()); + } + + @Test + @DisplayName("getAllSupportedScanners tolerates the registry throwing for a given type") + void getAllSupportedScannersTolerantOfRegistryException() { + when(registry.getScannerService(ScannerType.OSS)).thenThrow(new RuntimeException("boom")); + FakeScannerService secretsScanner = new FakeScannerService(true); + when(registry.getScannerService(ScannerType.SECRETS)).thenReturn(secretsScanner); + + List> supported = factory.getAllSupportedScanners("/repo/config.properties"); + + assertEquals(1, supported.size()); + assertSame(secretsScanner, supported.get(0)); + } + + @Test + @DisplayName("getScannerForFile returns null for a null file path or scanner type") + void getScannerForFileHandlesNullInputs() { + assertNull(factory.getScannerForFile(null, ScannerType.OSS)); + assertNull(factory.getScannerForFile("/repo/pom.xml", null)); + } + + @Test + @DisplayName("getScannerForFile returns null when the scanner type is disabled globally") + void getScannerForFileReturnsNullWhenDisabled() { + GlobalScannerController.getInstance().disableScanner(ScannerType.SECRETS); + when(registry.getScannerService(ScannerType.SECRETS)).thenReturn(new FakeScannerService(true)); + + assertNull(factory.getScannerForFile("/repo/config.properties", ScannerType.SECRETS)); + } + + @Test + @DisplayName("getScannerForFile returns null when the scanner does not support the file") + void getScannerForFileReturnsNullWhenUnsupported() { + when(registry.getScannerService(ScannerType.SECRETS)).thenReturn(new FakeScannerService(false)); + + assertNull(factory.getScannerForFile("/repo/config.properties", ScannerType.SECRETS)); + } + + @Test + @DisplayName("getScannerForFile returns the scanner when enabled, registered and supporting the file") + void getScannerForFileReturnsScannerWhenEligible() { + FakeScannerService secretsScanner = new FakeScannerService(true); + when(registry.getScannerService(ScannerType.SECRETS)).thenReturn(secretsScanner); + + assertSame(secretsScanner, factory.getScannerForFile("/repo/config.properties", ScannerType.SECRETS)); + } + + @Test + @DisplayName("getStatistics reports the enabled scanner count out of the total scanner type count") + void getStatisticsReportsEnabledCount() { + GlobalScannerController.getInstance().disableScanner(ScannerType.IAC); + + String stats = factory.getStatistics(); + + assertNotNull(stats); + assertTrue(stats.contains((ScannerType.values().length - 1) + "/" + ScannerType.values().length)); + } +} diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/configuration/McpInstallServiceTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/configuration/McpInstallServiceTest.java new file mode 100644 index 00000000..6cb93c43 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/configuration/McpInstallServiceTest.java @@ -0,0 +1,106 @@ +package checkmarx.ast.eclipse.plugin.tests.unit.devassist.configuration; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import java.util.concurrent.CompletableFuture; + +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.eclipse.core.runtime.preferences.InstanceScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.checkmarx.eclipse.common.listener.IMcpInstallCallback; +import com.checkmarx.eclipse.common.preferences.Preferences; +import com.checkmarx.eclipse.common.utils.PluginConstants; +import com.checkmarx.eclipse.devassist.configuration.McpInstallService; + +/** + * Unit tests for {@link McpInstallService}. Scoped to the paths that are safe + * to exercise without mocking - the guard clauses that return before ever + * reaching {@code TenantSettingsProvider} (real network I/O), plus the + * "not authenticated" callback path, which is naturally true in this test + * environment since {@code Preferences.STORE}'s {@code credentialsValidated} + * flag defaults to false and nothing in this module sets it. + */ +class McpInstallServiceTest { + + private static final String COPILOT_UI_BUNDLE_ID = "com.microsoft.copilot.eclipse.ui"; + private static final String MCP_PREFERENCE_KEY = "mcp"; + + @BeforeEach + @AfterEach + void clearMcpPreference() throws Exception { + IEclipsePreferences node = InstanceScope.INSTANCE.getNode(COPILOT_UI_BUNDLE_ID); + node.remove(MCP_PREFERENCE_KEY); + node.flush(); + } + + @Test + @DisplayName("This test environment is unauthenticated by default (no test sets credentialsValidated)") + void environmentIsUnauthenticatedByDefault() { + assertFalse(Preferences.isAuthenticated()); + } + + @Test + @DisplayName("attemptAutoInstall() is a safe no-op when the user is not authenticated") + void attemptAutoInstallNoOpWhenNotAuthenticated() { + assertDoesNotThrow(() -> McpInstallService.attemptAutoInstall()); + } + + @Test + @DisplayName("attemptAutoInstall(apiKey, params) is a safe no-op for a null or blank apiKey") + void attemptAutoInstallTwoArgNoOpForBlankApiKey() { + assertDoesNotThrow(() -> McpInstallService.attemptAutoInstall(null, null)); + assertDoesNotThrow(() -> McpInstallService.attemptAutoInstall(" ", "params")); + } + + @Test + @DisplayName("installSilentlyAsync completes immediately with false for a null or blank credential") + void installSilentlyAsyncCompletesFalseForBlankCredential() { + CompletableFuture nullResult = McpInstallService.installSilentlyAsync(null); + CompletableFuture blankResult = McpInstallService.installSilentlyAsync(" "); + + assertFalse(nullResult.join()); + assertFalse(blankResult.join()); + } + + @Test + @DisplayName("installFromUi reports onFailure with the not-authenticated message when unauthenticated") + void installFromUiReportsNotAuthenticated() { + IMcpInstallCallback callback = mock(IMcpInstallCallback.class); + + McpInstallService.installFromUi(callback); + + verify(callback).onFailure(PluginConstants.MCP_NOT_AUTHENTICATED_MESSAGE); + verify(callback, never()).onSuccess(); + verify(callback, never()).onAlreadyUpToDate(); + } + + @Test + @DisplayName("uninstall() returns false when there is no MCP entry to remove") + void uninstallReturnsFalseWhenNothingToRemove() { + assertFalse(McpInstallService.uninstall()); + } + + @Test + @DisplayName("uninstallSilentlyAsync completes with false when there is no MCP entry to remove") + void uninstallSilentlyAsyncCompletesFalseWhenNothingToRemove() { + assertFalse(McpInstallService.uninstallSilentlyAsync().join()); + } + + @Test + @DisplayName("installSilentlyAsync with a real (non-network) credential actually installs the MCP entry") + void installSilentlyAsyncInstallsWithRealCredential() { + Boolean changed = McpInstallService.installSilentlyAsync("some-token").join(); + + assertTrue(changed); + assertTrue(McpInstallService.uninstall(), "The entry installed above should now be removable"); + } +} diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/configuration/McpSettingsInjectorTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/configuration/McpSettingsInjectorTest.java new file mode 100644 index 00000000..c6cc1266 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/configuration/McpSettingsInjectorTest.java @@ -0,0 +1,171 @@ +package checkmarx.ast.eclipse.plugin.tests.unit.devassist.configuration; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.eclipse.core.runtime.preferences.InstanceScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.checkmarx.eclipse.devassist.configuration.McpSettingsInjector; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Unit tests for {@link McpSettingsInjector}. This writes to a real + * {@code IEclipsePreferences} node (Copilot for Eclipse's UI bundle + * preference scope) - there's no network I/O involved, just local preference + * store read/write, so this is safe to exercise directly. The "mcp" key on + * that node is cleared before and after every test for isolation. + */ +class McpSettingsInjectorTest { + + private static final String COPILOT_UI_BUNDLE_ID = "com.microsoft.copilot.eclipse.ui"; + private static final String MCP_PREFERENCE_KEY = "mcp"; + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private IEclipsePreferences node; + + @BeforeEach + void clearPreferenceBefore() throws Exception { + node = InstanceScope.INSTANCE.getNode(COPILOT_UI_BUNDLE_ID); + node.remove(MCP_PREFERENCE_KEY); + node.flush(); + } + + @AfterEach + void clearPreferenceAfter() throws Exception { + node.remove(MCP_PREFERENCE_KEY); + node.flush(); + } + + private String jwtWithIssuer(String issuer) { + String payloadJson = "{\"iss\":\"" + issuer + "\"}"; + String payload = Base64.getUrlEncoder().withoutPadding() + .encodeToString(payloadJson.getBytes(StandardCharsets.UTF_8)); + return "header." + payload + ".signature"; + } + + @SuppressWarnings("unchecked") + private Map readRawServers() throws Exception { + String raw = node.get(MCP_PREFERENCE_KEY, ""); + if (raw.isBlank()) { + return new LinkedHashMap<>(); + } + Map parsed = MAPPER.readValue(raw, Map.class); + Object servers = parsed.get("servers"); + return servers instanceof Map ? (Map) servers : parsed; + } + + @Test + @DisplayName("installForCopilot returns false and writes nothing for a null or blank token") + void installForCopilotRejectsBlankToken() throws Exception { + assertFalse(McpSettingsInjector.installForCopilot(null)); + assertFalse(McpSettingsInjector.installForCopilot(" ")); + assertTrue(readRawServers().isEmpty()); + } + + @Test + @DisplayName("installForCopilot derives the base URL from an iam.checkmarx.* JWT issuer") + void installForCopilotDerivesBaseUrlFromIssuer() throws Exception { + String token = jwtWithIssuer("https://iam.checkmarx.com"); + + boolean changed = McpSettingsInjector.installForCopilot(token); + + assertTrue(changed); + Map servers = readRawServers(); + assertTrue(servers.containsKey("checkmarx")); + @SuppressWarnings("unchecked") + Map entry = (Map) servers.get("checkmarx"); + assertEquals("https://ast.checkmarx.com" + McpSettingsInjector.MCP_ENDPOINT, entry.get("url")); + } + + @Test + @DisplayName("installForCopilot falls back to the default base URL for a non-JWT token") + void installForCopilotFallsBackForNonJwtToken() throws Exception { + boolean changed = McpSettingsInjector.installForCopilot("not-a-jwt-token"); + + assertTrue(changed); + Map servers = readRawServers(); + @SuppressWarnings("unchecked") + Map entry = (Map) servers.get("checkmarx"); + assertTrue(((String) entry.get("url")).contains(McpSettingsInjector.MCP_ENDPOINT)); + } + + @Test + @DisplayName("Installing the exact same token twice reports no change the second time") + void installForCopilotIsIdempotentForSameToken() throws Exception { + String token = jwtWithIssuer("https://iam.checkmarx.com"); + + assertTrue(McpSettingsInjector.installForCopilot(token)); + assertFalse(McpSettingsInjector.installForCopilot(token), "Second install with the identical token should be a no-op"); + } + + @Test + @DisplayName("Installing a different token after an existing install reports a change") + void installForCopilotDetectsTokenChange() throws Exception { + McpSettingsInjector.installForCopilot(jwtWithIssuer("https://iam.checkmarx.com")); + + boolean changed = McpSettingsInjector.installForCopilot(jwtWithIssuer("https://iam.checkmarx.com") + "-different"); + + assertTrue(changed); + } + + @Test + @DisplayName("installForCopilot preserves other server entries already present in the preference") + void installForCopilotPreservesOtherServers() throws Exception { + Map existing = new LinkedHashMap<>(); + existing.put("other-server", Map.of("type", "http", "url", "https://example.com/mcp")); + Map root = new LinkedHashMap<>(); + root.put("servers", existing); + node.put(MCP_PREFERENCE_KEY, MAPPER.writeValueAsString(root)); + node.flush(); + + McpSettingsInjector.installForCopilot(jwtWithIssuer("https://iam.checkmarx.com")); + + Map servers = readRawServers(); + assertTrue(servers.containsKey("other-server"), "Pre-existing unrelated server entry should be preserved"); + assertTrue(servers.containsKey("checkmarx")); + } + + @Test + @DisplayName("uninstallFromCopilot returns false when no Checkmarx entry exists") + void uninstallFromCopilotReturnsFalseWhenNotInstalled() throws Exception { + assertFalse(McpSettingsInjector.uninstallFromCopilot()); + } + + @Test + @DisplayName("uninstallFromCopilot removes an existing Checkmarx entry and returns true") + void uninstallFromCopilotRemovesExistingEntry() throws Exception { + McpSettingsInjector.installForCopilot(jwtWithIssuer("https://iam.checkmarx.com")); + + assertTrue(McpSettingsInjector.uninstallFromCopilot()); + assertFalse(readRawServers().containsKey("checkmarx")); + } + + @Test + @DisplayName("uninstallFromCopilot preserves other server entries while removing only Checkmarx's") + void uninstallFromCopilotPreservesOtherServers() throws Exception { + McpSettingsInjector.installForCopilot(jwtWithIssuer("https://iam.checkmarx.com")); + Map servers = readRawServers(); + servers.put("other-server", Map.of("type", "http", "url", "https://example.com/mcp")); + Map root = new LinkedHashMap<>(); + root.put("servers", servers); + node.put(MCP_PREFERENCE_KEY, MAPPER.writeValueAsString(root)); + node.flush(); + + McpSettingsInjector.uninstallFromCopilot(); + + Map remaining = readRawServers(); + assertFalse(remaining.containsKey("checkmarx")); + assertTrue(remaining.containsKey("other-server")); + } +} diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/ignore/IgnoreFileManagerTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/ignore/IgnoreFileManagerTest.java new file mode 100644 index 00000000..e298a106 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/ignore/IgnoreFileManagerTest.java @@ -0,0 +1,244 @@ +package checkmarx.ast.eclipse.plugin.tests.unit.devassist.ignore; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.stream.Stream; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.IPath; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.checkmarx.eclipse.devassist.ignore.IgnoreEntry; +import com.checkmarx.eclipse.devassist.ignore.IgnoreFileManager; +import com.checkmarx.eclipse.devassist.utils.ScanEngine; + +/** + * Unit tests for {@link IgnoreFileManager}. Each test uses a fresh mocked + * {@link IProject} backed by a manually managed temp directory as its + * location, so file I/O (ensureIgnoreFileExists/save/load) exercises the + * real disk path without touching the actual workspace. The temp directory + * is created in {@code @BeforeEach} and deleted in {@code @AfterEach} + * (JUnit5's built-in {@code @TempDir} parameter resolver is not available in + * this Eclipse-bundled JUnit5 runtime). + */ +class IgnoreFileManagerTest { + + private Path tempDir; + + @BeforeEach + void createTempDir() throws IOException { + tempDir = Files.createTempDirectory("ignore-file-manager-test"); + } + + @AfterEach + void deleteTempDir() throws IOException { + if (tempDir == null || !Files.exists(tempDir)) { + return; + } + try (Stream walk = Files.walk(tempDir)) { + walk.sorted(Comparator.reverseOrder()).forEach(path -> { + try { + Files.deleteIfExists(path); + } catch (IOException ignored) { + // best-effort cleanup + } + }); + } + } + + private IProject projectAt(Path root) { + IProject project = mock(IProject.class); + IPath ipath = org.eclipse.core.runtime.Path.fromOSString(root.toAbsolutePath().toString()); + when(project.getLocation()).thenReturn(ipath); + return project; + } + + private IgnoreEntry.FileReference fileRef(String path, boolean active, int line) { + return new IgnoreEntry.FileReference(path, active, line, ""); + } + + @Test + @DisplayName("Constructing a manager creates the .checkmarx/.checkmarxIgnored file with empty content") + void constructorCreatesIgnoreFile() { + IProject project = projectAt(tempDir); + IgnoreFileManager manager = new IgnoreFileManager(project); + + Path ignoreFile = tempDir.resolve(".checkmarx").resolve(".checkmarxIgnored"); + assertTrue(Files.exists(ignoreFile)); + assertTrue(manager.getIgnoreData().isEmpty()); + } + + @Test + @DisplayName("updateIgnoreData stores the entry in memory and getAllIgnoreEntries reflects it") + void updateIgnoreDataStoresEntry() { + IgnoreFileManager manager = new IgnoreFileManager(projectAt(tempDir)); + IgnoreEntry entry = new IgnoreEntry(); + entry.type = ScanEngine.OSS; + entry.packageName = "lodash"; + + manager.updateIgnoreData("OSS:lodash:1.0.0:npm", entry); + + assertEquals(1, manager.getAllIgnoreEntries().size()); + assertTrue(manager.getIgnoreData().containsKey("OSS:lodash:1.0.0:npm")); + } + + @Test + @DisplayName("saveIgnoreDataToDisk persists data that a fresh manager instance reloads for the same project") + void savedDataIsReloadedByANewInstance() { + IProject project = projectAt(tempDir); + IgnoreFileManager writer = new IgnoreFileManager(project); + IgnoreEntry entry = new IgnoreEntry(); + entry.type = ScanEngine.SECRETS; + entry.packageName = "AWS Key"; + writer.updateIgnoreData("SECRETS:AWS Key:secretvalue:path", entry); + + // Bypass the static getInstance() cache to verify the on-disk file itself, + // not just the in-memory singleton. + IgnoreFileManager reader = new IgnoreFileManager(project); + assertTrue(reader.getIgnoreData().containsKey("SECRETS:AWS Key:secretvalue:path")); + } + + @Test + @DisplayName("normalizePath relativizes a path under the project root to a forward-slash relative path") + void normalizePathRelativizesUnderProjectRoot() { + IgnoreFileManager manager = new IgnoreFileManager(projectAt(tempDir)); + Path file = tempDir.resolve("src").resolve("Main.java"); + + String normalized = manager.normalizePath(file.toString()); + + assertEquals("src/Main.java", normalized); + } + + @Test + @DisplayName("normalizePath falls back to a slash-converted raw path when relativizing fails") + void normalizePathFallsBackOnMismatchedRoot() { + IgnoreFileManager manager = new IgnoreFileManager(projectAt(tempDir)); + + String normalized = manager.normalizePath("Z:\\unrelated\\Main.java"); + + assertEquals("Z:/unrelated/Main.java", normalized); + } + + @Test + @DisplayName("normalizePath returns empty string for null or empty input") + void normalizePathHandlesNullOrEmpty() { + IgnoreFileManager manager = new IgnoreFileManager(projectAt(tempDir)); + assertEquals("", manager.normalizePath(null)); + assertEquals("", manager.normalizePath("")); + } + + @Test + @DisplayName("isIgnored(similarityId) reflects presence of the key in the ignore data map") + void isIgnoredChecksSimilarityIdKey() { + IgnoreFileManager manager = new IgnoreFileManager(projectAt(tempDir)); + assertFalse(manager.isIgnored("some-key")); + + manager.updateIgnoreData("some-key", new IgnoreEntry()); + assertTrue(manager.isIgnored("some-key")); + assertFalse(manager.isIgnored(null)); + assertFalse(manager.isIgnored("")); + } + + @Test + @DisplayName("matchesEntry compares OSS entries by package name, version and manager") + void matchesEntryComparesOssFields() { + IgnoreFileManager manager = new IgnoreFileManager(projectAt(tempDir)); + + IgnoreEntry a = new IgnoreEntry(); + a.type = ScanEngine.OSS; + a.packageName = "lodash"; + a.packageVersion = "1.0.0"; + a.packageManager = "npm"; + + IgnoreEntry sameIdentity = new IgnoreEntry(); + sameIdentity.type = ScanEngine.OSS; + sameIdentity.packageName = "lodash"; + sameIdentity.packageVersion = "1.0.0"; + sameIdentity.packageManager = "npm"; + + IgnoreEntry differentVersion = new IgnoreEntry(); + differentVersion.type = ScanEngine.OSS; + differentVersion.packageName = "lodash"; + differentVersion.packageVersion = "2.0.0"; + differentVersion.packageManager = "npm"; + + assertTrue(manager.matchesEntry(a, sameIdentity)); + assertFalse(manager.matchesEntry(a, differentVersion)); + } + + @Test + @DisplayName("matchesEntry returns false when entry types differ or type is unhandled") + void matchesEntryRejectsDifferentOrUnhandledTypes() { + IgnoreFileManager manager = new IgnoreFileManager(projectAt(tempDir)); + + IgnoreEntry oss = new IgnoreEntry(); + oss.type = ScanEngine.OSS; + IgnoreEntry secrets = new IgnoreEntry(); + secrets.type = ScanEngine.SECRETS; + assertFalse(manager.matchesEntry(oss, secrets)); + + IgnoreEntry allA = new IgnoreEntry(); + allA.type = ScanEngine.ALL; + IgnoreEntry allB = new IgnoreEntry(); + allB.type = ScanEngine.ALL; + assertFalse(manager.matchesEntry(allA, allB), "ALL is not handled by any case branch, so it falls to default false"); + } + + @Test + @DisplayName("reviveEntry deactivates all file references for a matching entry and persists the change") + void reviveEntryDeactivatesMatchingEntry() { + IgnoreFileManager manager = new IgnoreFileManager(projectAt(tempDir)); + IgnoreEntry entry = new IgnoreEntry(); + entry.type = ScanEngine.CONTAINERS; + entry.imageName = "nginx"; + entry.imageTag = "latest"; + entry.files.add(fileRef("Dockerfile", true, 1)); + manager.updateIgnoreData("CONTAINERS:nginx:latest", entry); + + IgnoreEntry toRevive = new IgnoreEntry(); + toRevive.type = ScanEngine.CONTAINERS; + toRevive.imageName = "nginx"; + toRevive.imageTag = "latest"; + + assertTrue(manager.reviveEntry(toRevive)); + assertFalse(manager.getIgnoreData().get("CONTAINERS:nginx:latest").getFiles().get(0).isActive()); + } + + @Test + @DisplayName("reviveEntry returns false when no matching entry exists") + void reviveEntryReturnsFalseWhenNotFound() { + IgnoreFileManager manager = new IgnoreFileManager(projectAt(tempDir)); + IgnoreEntry toRevive = new IgnoreEntry(); + toRevive.type = ScanEngine.CONTAINERS; + toRevive.imageName = "does-not-exist"; + toRevive.imageTag = "latest"; + + assertFalse(manager.reviveEntry(toRevive)); + } + + @Test + @DisplayName("deleteIgnoreFiles clears in-memory data and removes the ignore file from disk") + void deleteIgnoreFilesClearsStateAndFiles() { + IgnoreFileManager manager = new IgnoreFileManager(projectAt(tempDir)); + IgnoreEntry entry = new IgnoreEntry(); + entry.type = ScanEngine.OSS; + manager.updateIgnoreData("OSS:pkg:1.0:npm", entry); + assertTrue(Files.exists(manager.getIgnoreFilePath())); + + manager.deleteIgnoreFiles(); + + assertTrue(manager.getIgnoreData().isEmpty()); + assertFalse(Files.exists(manager.getIgnoreFilePath())); + } +} diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/ignore/IgnoreManagerTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/ignore/IgnoreManagerTest.java new file mode 100644 index 00000000..62a7a75e --- /dev/null +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/ignore/IgnoreManagerTest.java @@ -0,0 +1,259 @@ +package checkmarx.ast.eclipse.plugin.tests.unit.devassist.ignore; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.IPath; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.checkmarx.eclipse.devassist.ignore.IgnoreEntry; +import com.checkmarx.eclipse.devassist.ignore.IgnoreFileManager; +import com.checkmarx.eclipse.devassist.ignore.IgnoreManager; +import com.checkmarx.eclipse.devassist.model.Location; +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.model.Vulnerability; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; +import com.checkmarx.eclipse.devassist.utils.ScanEngine; + +/** + * Unit tests for {@link IgnoreManager}. Uses a mocked {@link IProject} backed + * by a manually managed temp directory (see {@link IgnoreFileManagerTest} for + * why {@code @TempDir} isn't used) so the wrapped {@link IgnoreFileManager}'s + * real (but isolated) file I/O runs without touching the actual workspace. + *

+ * Operations that would trigger a rescan ({@code addIgnoredEntry}, + * {@code addAllIgnoredEntry}) resolve the target file via + * {@code ResourcesPlugin.getWorkspace()...getFileForLocation(...)}, which + * returns null for these synthetic test paths (they were never added to the + * real Eclipse workspace) - so the rescan branch safely no-ops instead of + * scheduling a real {@code RealTimeScanJob}. + */ +class IgnoreManagerTest { + + private Path tempDir; + private IProject project; + + @BeforeEach + void setUp() throws IOException { + tempDir = Files.createTempDirectory("ignore-manager-test"); + project = mock(IProject.class); + IPath ipath = org.eclipse.core.runtime.Path.fromOSString(tempDir.toAbsolutePath().toString()); + when(project.getLocation()).thenReturn(ipath); + } + + @AfterEach + void tearDown() throws IOException { + IgnoreManager.dispose(project); + IgnoreFileManager.dispose(project); + if (tempDir == null || !Files.exists(tempDir)) { + return; + } + try (Stream walk = Files.walk(tempDir)) { + walk.sorted(Comparator.reverseOrder()).forEach(path -> { + try { + Files.deleteIfExists(path); + } catch (IOException ignored) { + // best-effort cleanup + } + }); + } + } + + private ScanIssue ossIssue(String path) { + ScanIssue issue = new ScanIssue(); + issue.setScanEngine(com.checkmarx.eclipse.devassist.model.ScanEngine.OSS); + issue.setScanIssueId("issue-oss-1"); + issue.setTitle("lodash"); + issue.setPackageManager("npm"); + issue.setPackageVersion("1.0.0"); + issue.setFilePath(path); + Location location = new Location(); + location.setLine(3); + issue.getLocations().add(location); + return issue; + } + + private ScanIssue ascaIssueWithVulnerability(String path, int line, String ruleName, String problematicLine) { + ScanIssue issue = new ScanIssue(); + issue.setScanEngine(com.checkmarx.eclipse.devassist.model.ScanEngine.ASCA); + issue.setScanIssueId("issue-asca-1"); + issue.setTitle(ruleName); + issue.setFilePath(path); + Location location = new Location(); + location.setLine(line); + issue.getLocations().add(location); + Vulnerability vulnerability = new Vulnerability(); + vulnerability.setVulnerabilityId("issue-asca-1"); + vulnerability.setTitle(ruleName); + vulnerability.setRuleId(1); + vulnerability.setProblematicLine(problematicLine); + issue.getVulnerabilities().add(vulnerability); + return issue; + } + + @Test + @DisplayName("createJsonKeyForIgnoreEntry builds the OSS composite key from manager/title/version") + void createJsonKeyForOss() { + IgnoreManager manager = IgnoreManager.getInstance(project); + String filePath = tempDir.resolve("package.json").toString(); + String key = manager.createJsonKeyForIgnoreEntry(ossIssue(filePath), ""); + assertEquals("OSS:npm:lodash:1.0.0", key); + } + + @Test + @DisplayName("createJsonKeyForIgnoreEntry returns empty string for a null issue or missing scan engine") + void createJsonKeyHandlesNullInputs() { + IgnoreManager manager = IgnoreManager.getInstance(project); + assertEquals("", manager.createJsonKeyForIgnoreEntry(null, "")); + assertEquals("", manager.createJsonKeyForIgnoreEntry(new ScanIssue(), "")); + } + + @Test + @DisplayName("createJsonKeyForIgnoreEntry resolves the ASCA key via the matching vulnerability's rule id") + void createJsonKeyForAsca() { + IgnoreManager manager = IgnoreManager.getInstance(project); + String filePath = tempDir.resolve("Main.java").toString(); + ScanIssue issue = ascaIssueWithVulnerability(filePath, 10, "SQLInjection", "eval(x)"); + + String key = manager.createJsonKeyForIgnoreEntry(issue, DevAssistConstants.QUICK_FIX); + + assertEquals("ASCA:SQLInjection:1:Main.java", key); + } + + @Test + @DisplayName("hasIgnoredEntries reflects whether any ignore entry exists for the given engine") + void hasIgnoredEntriesReflectsEngineType() { + IgnoreManager manager = IgnoreManager.getInstance(project); + assertFalse(manager.hasIgnoredEntries(ScanEngine.OSS)); + + String filePath = tempDir.resolve("package.json").toString(); + manager.addIgnoredEntry(ossIssue(filePath), DevAssistConstants.QUICK_FIX); + + assertTrue(manager.hasIgnoredEntries(ScanEngine.OSS)); + assertFalse(manager.hasIgnoredEntries(ScanEngine.SECRETS)); + } + + @Test + @DisplayName("addIgnoredEntry followed by isIgnored reports the issue as ignored for its file") + void addIgnoredEntryThenIsIgnored() { + IgnoreManager manager = IgnoreManager.getInstance(project); + String filePath = tempDir.resolve("package.json").toString(); + ScanIssue issue = ossIssue(filePath); + + manager.addIgnoredEntry(issue, DevAssistConstants.QUICK_FIX); + + assertTrue(manager.isIgnored(issue)); + } + + @Test + @DisplayName("isIgnored returns false for an issue that was never ignored") + void isIgnoredReturnsFalseForUnknownIssue() { + IgnoreManager manager = IgnoreManager.getInstance(project); + String filePath = tempDir.resolve("package.json").toString(); + assertFalse(manager.isIgnored(ossIssue(filePath))); + } + + @Test + @DisplayName("isIgnored always returns false for ASCA issues (filtering happens upstream in the adaptor)") + void isIgnoredAlwaysFalseForAsca() { + IgnoreManager manager = IgnoreManager.getInstance(project); + String filePath = tempDir.resolve("Main.java").toString(); + ScanIssue issue = ascaIssueWithVulnerability(filePath, 10, "SQLInjection", "eval(x)"); + + manager.addIgnoredEntry(issue, DevAssistConstants.QUICK_FIX); + + assertFalse(manager.isIgnored(issue)); + } + + @Test + @DisplayName("isIgnored returns false when null is passed") + void isIgnoredHandlesNull() { + IgnoreManager manager = IgnoreManager.getInstance(project); + assertFalse(manager.isIgnored(null)); + } + + @Test + @DisplayName("addAllIgnoredEntry covers the clicked occurrence even when the problem holder has no matches") + void addAllIgnoredEntryCoversClickedOccurrenceWhenHolderEmpty() { + IgnoreManager manager = IgnoreManager.getInstance(project); + String filePath = tempDir.resolve("package.json").toString(); + ScanIssue issue = ossIssue(filePath); + + manager.addAllIgnoredEntry(issue, DevAssistConstants.QUICK_FIX); + + assertTrue(manager.isIgnored(issue)); + } + + @Test + @DisplayName("isAscaVulnerabilityIgnored matches by rule name, file path and problematic line") + void isAscaVulnerabilityIgnoredMatchesByRuleNameAndProblematicLine() { + IgnoreManager manager = IgnoreManager.getInstance(project); + String filePath = tempDir.resolve("Main.java").toString(); + + IgnoreEntry entry = new IgnoreEntry(); + entry.type = ScanEngine.ASCA; + entry.packageName = "SQLInjection"; + IgnoreEntry.FileReference ref = new IgnoreEntry.FileReference("Main.java", true, 10, "eval(x)"); + entry.files.add(ref); + + Vulnerability matching = new Vulnerability(); + matching.setTitle("SQLInjection"); + matching.setProblematicLine("eval(x)"); + + Vulnerability differentLine = new Vulnerability(); + differentLine.setTitle("SQLInjection"); + differentLine.setProblematicLine("execute(y)"); + + Vulnerability differentRule = new Vulnerability(); + differentRule.setTitle("XSS"); + differentRule.setProblematicLine("eval(x)"); + + assertTrue(manager.isAscaVulnerabilityIgnored(matching, List.of(entry), filePath)); + assertFalse(manager.isAscaVulnerabilityIgnored(differentLine, List.of(entry), filePath)); + assertFalse(manager.isAscaVulnerabilityIgnored(differentRule, List.of(entry), filePath)); + } + + @Test + @DisplayName("isAscaVulnerabilityIgnored returns false for a null vulnerability or null entries") + void isAscaVulnerabilityIgnoredHandlesNullInputs() { + IgnoreManager manager = IgnoreManager.getInstance(project); + assertFalse(manager.isAscaVulnerabilityIgnored(null, List.of(), "path")); + assertFalse(manager.isAscaVulnerabilityIgnored(new Vulnerability(), null, "path")); + } + + @Test + @DisplayName("removeIgnoreEntriesForFileIfEmpty removes ASCA entries whose only file reference matches") + void removeIgnoreEntriesForFileIfEmptyRemovesMatchingAscaEntry() { + IgnoreManager manager = IgnoreManager.getInstance(project); + String filePath = tempDir.resolve("Main.java").toString(); + ScanIssue issue = ascaIssueWithVulnerability(filePath, 10, "SQLInjection", "eval(x)"); + manager.addIgnoredEntry(issue, DevAssistConstants.QUICK_FIX); + assertTrue(manager.hasIgnoredEntries(ScanEngine.ASCA)); + + manager.removeIgnoreEntriesForFileIfEmpty(filePath); + + assertFalse(manager.hasIgnoredEntries(ScanEngine.ASCA)); + } + + @Test + @DisplayName("Multiple getInstance calls for the same project return the same cached instance") + void getInstanceCachesPerProject() { + IgnoreManager first = IgnoreManager.getInstance(project); + IgnoreManager second = IgnoreManager.getInstance(project); + assertTrue(first == second); + } +} diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/inspection/DevAssistInspectionTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/inspection/DevAssistInspectionTest.java new file mode 100644 index 00000000..8dd31787 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/inspection/DevAssistInspectionTest.java @@ -0,0 +1,50 @@ +package checkmarx.ast.eclipse.plugin.tests.unit.devassist.inspection; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.checkmarx.eclipse.devassist.inspection.DevAssistInspection; + +/** + * Unit tests for {@link DevAssistInspection}. This is a plain metadata + * holder (Eclipse has no {@code LocalInspectionTool} equivalent to extend), + * so coverage is a straightforward smoke test of its fixed identity values. + */ +class DevAssistInspectionTest { + + private final DevAssistInspection inspection = new DevAssistInspection(); + + @Test + @DisplayName("getInspectionId returns the fixed, non-blank inspection id") + void getInspectionIdReturnsFixedValue() { + assertEquals("com.checkmarx.eclipse.devassist.inspection", inspection.getInspectionId()); + assertFalse(inspection.getInspectionId().isBlank()); + } + + @Test + @DisplayName("getInspectionName returns the fixed display name") + void getInspectionNameReturnsFixedValue() { + assertEquals("Checkmarx Developer Assist", inspection.getInspectionName()); + } + + @Test + @DisplayName("getInspectionGroup returns the fixed group/category") + void getInspectionGroupReturnsFixedValue() { + assertEquals("Checkmarx", inspection.getInspectionGroup()); + } + + @Test + @DisplayName("Every metadata getter is stable across multiple instances") + void metadataIsStableAcrossInstances() { + DevAssistInspection other = new DevAssistInspection(); + + assertEquals(inspection.getInspectionId(), other.getInspectionId()); + assertEquals(inspection.getInspectionName(), other.getInspectionName()); + assertEquals(inspection.getInspectionGroup(), other.getInspectionGroup()); + assertNotNull(inspection.getInspectionId()); + } +} diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/inspection/DevAssistScanSchedulerTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/inspection/DevAssistScanSchedulerTest.java new file mode 100644 index 00000000..06a3bfec --- /dev/null +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/inspection/DevAssistScanSchedulerTest.java @@ -0,0 +1,192 @@ +package checkmarx.ast.eclipse.plugin.tests.unit.devassist.inspection; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.checkmarx.eclipse.devassist.inspection.DevAssistScanScheduler; + +/** + * Unit tests for {@link DevAssistScanScheduler}. Each scheduled + * {@link com.checkmarx.eclipse.devassist.backend.listener.RealTimeScanJob} + * is safe to actually schedule here even though it's a real Eclipse + * background {@code Job}: its {@code run()} bails out immediately with + * {@code Status.CANCEL_STATUS} whenever {@code file.exists()} is false, + * which is Mockito's default for an unstubbed {@code IFile} mock - so a + * pending job firing after a test completes is a guaranteed no-op. + */ +class DevAssistScanSchedulerTest { + + private DevAssistScanScheduler scheduler = new DevAssistScanScheduler(); + + private IFile fileAt(String path) { + IFile file = mock(IFile.class); + when(file.getLocation()).thenReturn(org.eclipse.core.runtime.Path.fromOSString(path)); + when(file.getName()).thenReturn(org.eclipse.core.runtime.Path.fromOSString(path).lastSegment()); + return file; + } + + @Test + @DisplayName("scheduleInspection returns true and tracks a pending scan for a new file") + void scheduleInspectionSchedulesNewFile() { + IFile file = fileAt("/repo/Main.java"); + + boolean scheduled = scheduler.scheduleInspection(file, 5000L); + + assertTrue(scheduled); + assertEquals(1, scheduler.getPendingScansCount()); + + scheduler.cancelScheduledInspection(file); + } + + @Test + @DisplayName("scheduleInspection returns false when a scan is already pending for the same file") + void scheduleInspectionRejectsDuplicatePending() { + IFile file = fileAt("/repo/Main.java"); + scheduler.scheduleInspection(file, 5000L); + + boolean scheduledAgain = scheduler.scheduleInspection(file, 5000L); + + assertFalse(scheduledAgain); + assertEquals(1, scheduler.getPendingScansCount()); + + scheduler.cancelScheduledInspection(file); + } + + @Test + @DisplayName("scheduleInspection returns false for a null file") + void scheduleInspectionRejectsNullFile() { + assertFalse(scheduler.scheduleInspection(null, 5000L)); + assertEquals(0, scheduler.getPendingScansCount()); + } + + @Test + @DisplayName("scheduleInspection(file, ProblemHelper) overload delegates with the default debounce delay") + void scheduleInspectionTwoArgOverloadDelegates() { + IFile file = fileAt("/repo/Main.java"); + + boolean scheduled = scheduler.scheduleInspection(file, (com.checkmarx.eclipse.devassist.problems.ProblemHelper) null); + + assertTrue(scheduled); + assertEquals(1, scheduler.getPendingScansCount()); + + scheduler.cancelScheduledInspection(file); + } + + @Test + @DisplayName("rescheduleInspection schedules a new scan when none is pending yet") + void rescheduleInspectionSchedulesWhenNonePending() { + IFile file = fileAt("/repo/Main.java"); + + boolean rescheduled = scheduler.rescheduleInspection(file, 5000L); + + assertTrue(rescheduled); + assertEquals(1, scheduler.getPendingScansCount()); + + scheduler.cancelScheduledInspection(file); + } + + @Test + @DisplayName("rescheduleInspection on an existing pending job reports success") + void rescheduleInspectionReusesExistingJob() { + // NOTE: rescheduleInspection() calls existingJob.cancel() before + // existingJob.reschedule(delayMs) (which itself cancels again + reschedules + // the SAME job object). Cancelling a still-sleeping (delayed, not yet run) + // Job fires the scheduler's jobCompletionListener#done() callback, which + // removes the file's entry from pendingScans as a side effect - even though + // the job is immediately rescheduled and will still run later. So + // getPendingScansCount() can end up 0 here instead of the "still 1, no + // duplicate" outcome the method's javadoc implies - and because the done() + // notification is dispatched asynchronously by the JobManager, timing is + // not guaranteed, so no pending-count assertion is made here at all + // (asserting a fixed value would be flaky). Only the return value - + // rescheduleInspection() itself succeeding - is asserted. + IFile file = fileAt("/repo/Main.java"); + scheduler.scheduleInspection(file, 5000L); + + boolean rescheduled = scheduler.rescheduleInspection(file, 8000L); + + assertTrue(rescheduled); + + scheduler.cancelScheduledInspection(file); + } + + @Test + @DisplayName("rescheduleInspection returns false for a null file") + void rescheduleInspectionRejectsNullFile() { + assertFalse(scheduler.rescheduleInspection(null, 5000L)); + } + + @Test + @DisplayName("cancelScheduledInspection removes the pending scan and returns true") + void cancelScheduledInspectionRemovesPendingScan() { + IFile file = fileAt("/repo/Main.java"); + scheduler.scheduleInspection(file, 5000L); + + boolean cancelled = scheduler.cancelScheduledInspection(file); + + assertTrue(cancelled); + assertEquals(0, scheduler.getPendingScansCount()); + } + + @Test + @DisplayName("cancelScheduledInspection returns false when there is nothing pending for the file") + void cancelScheduledInspectionReturnsFalseWhenNothingPending() { + IFile file = fileAt("/repo/Main.java"); + + assertFalse(scheduler.cancelScheduledInspection(file)); + } + + @Test + @DisplayName("cancelScheduledInspection returns false for a null file") + void cancelScheduledInspectionRejectsNullFile() { + assertFalse(scheduler.cancelScheduledInspection(null)); + } + + @Test + @DisplayName("Scheduling scans for two different files tracks both independently") + void schedulingTwoFilesTracksBothIndependently() { + IFile file1 = fileAt("/repo/Main.java"); + IFile file2 = fileAt("/repo/Other.java"); + + scheduler.scheduleInspection(file1, 5000L); + scheduler.scheduleInspection(file2, 5000L); + + assertEquals(2, scheduler.getPendingScansCount()); + + scheduler.cancelScheduledInspection(file1); + scheduler.cancelScheduledInspection(file2); + } + + @Test + @DisplayName("triggerInspection is a safe no-op for both a real project and null") + void triggerInspectionIsSafeNoOp() { + IProject project = mock(IProject.class); + when(project.getName()).thenReturn("TestProject"); + + assertDoesNotThrow(() -> scheduler.triggerInspection(project)); + assertDoesNotThrow(() -> scheduler.triggerInspection(null)); + } + + @Test + @DisplayName("getStatistics reports the pending scan count and tracked file paths") + void getStatisticsReportsPendingScans() { + IFile file = fileAt("/repo/Main.java"); + scheduler.scheduleInspection(file, 5000L); + + String stats = scheduler.getStatistics(); + + assertTrue(stats.contains("Pending scans: 1")); + assertTrue(stats.contains("Main.java") || stats.contains("repo")); + + scheduler.cancelScheduledInspection(file); + } +} diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/problems/ProblemBuilderTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/problems/ProblemBuilderTest.java new file mode 100644 index 00000000..4a807d7d --- /dev/null +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/devassist/problems/ProblemBuilderTest.java @@ -0,0 +1,105 @@ +package checkmarx.ast.eclipse.plugin.tests.unit.devassist.problems; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.checkmarx.eclipse.devassist.model.ScanIssue; +import com.checkmarx.eclipse.devassist.problems.ProblemBuilder; +import com.checkmarx.eclipse.devassist.problems.ProblemDescriptor; +import com.checkmarx.eclipse.devassist.problems.ProblemHelper; + +/** + * Unit tests for {@link ProblemBuilder}. Pure logic - builds a + * {@link ProblemDescriptor} from a {@link ScanIssue}, no Eclipse + * workspace/editor interaction involved. + */ +class ProblemBuilderTest { + + private ProblemHelper problemHelper(IFile file) { + IProject project = mock(IProject.class); + return ProblemHelper.builder(file, project).build(); + } + + @Test + @DisplayName("build() wires file, scanIssue and line number through to the descriptor") + void buildWiresBasicFields() { + IFile file = mock(IFile.class); + ScanIssue issue = new ScanIssue(); + issue.setTitle("SQL Injection"); + issue.setSeverity("High"); + issue.setDescription("desc"); + + ProblemDescriptor descriptor = ProblemBuilder.build(problemHelper(file), issue, 42); + + assertSame(file, descriptor.getFile()); + assertSame(issue, descriptor.getScanIssue()); + assertEquals(42, descriptor.getLineNumber()); + assertTrue(descriptor.getFixes().isEmpty()); + } + + @Test + @DisplayName("build() formats an HTML description containing title, severity and description") + void buildFormatsHtmlDescription() { + ScanIssue issue = new ScanIssue(); + issue.setTitle("SQL Injection"); + issue.setSeverity("High"); + issue.setDescription("Untrusted input used in query"); + + ProblemDescriptor descriptor = ProblemBuilder.build(problemHelper(mock(IFile.class)), issue, 1); + + String description = descriptor.getDescription(); + assertTrue(description.startsWith("")); + assertTrue(description.endsWith("")); + assertTrue(description.contains("SQL Injection")); + assertTrue(description.contains("Severity: High")); + assertTrue(description.contains("Untrusted input used in query")); + } + + @Test + @DisplayName("build() HTML-escapes special characters in the title and description") + void buildEscapesHtmlSpecialCharacters() { + ScanIssue issue = new ScanIssue(); + issue.setTitle(""); + issue.setSeverity("Critical"); + issue.setDescription("Value \"a & b\" < c > d"); + + ProblemDescriptor descriptor = ProblemBuilder.build(problemHelper(mock(IFile.class)), issue, 1); + + String description = descriptor.getDescription(); + assertTrue(description.contains("<script>alert('xss')</script>")); + assertTrue(description.contains("Value "a & b" < c > d")); + assertTrue(!description.contains("