diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index ced06f58..05185562 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -43,7 +43,6 @@ jobs: with: name: screenshots path: /Users/runner/work/ast-vscode-extension/ast-vscode-extension/test-resources/screenshots/*.png - unit-tests: strategy: max-parallel: 1 @@ -64,7 +63,7 @@ jobs: - name: Run unit tests with coverage run: npm run unit-coverage - integration-tests: + ui-tests: strategy: max-parallel: 1 matrix: diff --git a/.gitignore b/.gitignore index b8fe1525..f63837f2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ test-resources .idea/ .DS_Store .npmrc +coverage/ diff --git a/package-lock.json b/package-lock.json index c28b836b..c3aaaf89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@types/chai": "4.3.11", "@types/mocha": "10.0.6", "@types/node": "^22.9.0", + "@types/sinon": "^17.0.3", "@types/vscode": "^1.50.0", "@typescript-eslint/eslint-plugin": "^7.0.1", "@typescript-eslint/parser": "^7.2.0", @@ -1184,6 +1185,21 @@ "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==", "dev": true }, + "node_modules/@types/sinon": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", + "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", + "dev": true, + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true + }, "node_modules/@types/vscode": { "version": "1.66.0", "dev": true, @@ -6649,7 +6665,6 @@ "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.2.tgz", "integrity": "sha512-euuToqM+PjO4UgXeLETsfQiuoyPXlqFezr6YZDFwHR3t4qaX0fZUe1MfPMznTL5f8BWrVS89KduLdMUsxFCO6g==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.1", "@sinonjs/fake-timers": "^13.0.2", diff --git a/package.json b/package.json index 2cfa1ace..75144812 100644 --- a/package.json +++ b/package.json @@ -917,6 +917,7 @@ "@types/chai": "4.3.11", "@types/mocha": "10.0.6", "@types/node": "^22.9.0", + "@types/sinon": "^17.0.3", "@types/vscode": "^1.50.0", "@typescript-eslint/eslint-plugin": "^7.0.1", "@typescript-eslint/parser": "^7.2.0", diff --git a/src/unit/authValidate.test.ts b/src/unit/authValidate.test.ts new file mode 100644 index 00000000..167bfa25 --- /dev/null +++ b/src/unit/authValidate.test.ts @@ -0,0 +1,40 @@ +import { expect } from "chai"; +import "./mocks/vscode-mock"; +import "./mocks/cxWrapper-mock"; +import { cx } from "../cx"; +import { Logs } from "../models/logs"; + +describe("Cx - authValidate", () => { + let logs: Logs; + + + beforeEach(() => { + const mockOutputChannel = { + append: () => {}, + appendLine: () => {}, + clear: () => {}, + show: () => {}, + hide: () => {}, + dispose: () => {}, + replace: () => {}, + name: "Test" + }; + + logs = { + info: () => {}, + error: () => {}, + output: mockOutputChannel, + log: () => {}, + warn: () => {}, + show: () => {} + } as Logs; + }); + + it("should return true when authentication is successful", async () => { + // Using valid API key from vscode mock + const result = await cx.authValidate(logs); + expect(result).to.be.true; + }); + + +}); \ No newline at end of file diff --git a/src/unit/commonCommand.test.ts b/src/unit/commonCommand.test.ts new file mode 100644 index 00000000..c42082e5 --- /dev/null +++ b/src/unit/commonCommand.test.ts @@ -0,0 +1,48 @@ +import { expect } from "chai"; +import "./mocks/vscode-mock"; +import { CommonCommand } from "../commands/commonCommand"; +import { Logs } from "../models/logs"; +import * as vscode from "vscode"; +import { getCommandsExecuted, clearCommandsExecuted } from "./mocks/vscode-mock"; + +describe("CommonCommand", () => { + let commonCommand: CommonCommand; + let logs: Logs; + let mockContext: vscode.ExtensionContext; + + beforeEach(() => { + clearCommandsExecuted(); + + const mockOutputChannel = { + append: () => {}, + appendLine: () => {}, + clear: () => {}, + show: () => {}, + hide: () => {}, + dispose: () => {}, + replace: () => {}, + name: "Test" + }; + + logs = { + info: () => {}, + error: () => {}, + output: mockOutputChannel, + log: () => {}, + warn: () => {}, + show: () => {} + } as Logs; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockContext = { subscriptions: [] } as any; + + commonCommand = new CommonCommand(mockContext, logs); + }); + + describe("executeCheckSettings", () => { + it("should execute setContext command with correct parameters", () => { + commonCommand.executeCheckSettings(); + expect(getCommandsExecuted()).to.include("setContext"); + }); + }); +}); \ No newline at end of file diff --git a/src/unit/details.test.ts b/src/unit/details.test.ts new file mode 100644 index 00000000..e55a7541 --- /dev/null +++ b/src/unit/details.test.ts @@ -0,0 +1,232 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import "./mocks/vscode-mock"; +import * as vscode from "vscode"; +import { expect } from "chai"; +import * as sinon from "sinon"; +import { Details } from "../utils/interface/details"; +import { AstResult } from "../models/results"; +import { constants } from "../utils/common/constants"; +import { messages } from "../utils/common/messages"; +import CxMask from "@checkmarxdev/ast-cli-javascript-wrapper/dist/main/mask/CxMask"; + +describe("Details", () => { + let details: Details; + let mockContext: vscode.ExtensionContext; + let mockResult: AstResult; + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + mockContext = { + subscriptions: [], + extensionUri: vscode.Uri.parse("file:///mock"), + extensionPath: "/mock", + } as any; + + mockResult = { + label: "Test_Result", + type: "sast", + severity: "HIGH", + state: "NEW", + description: "Test description", + data: { + value: "test value", + remediation: "test remediation", + ruleDescription: "test rule description" + }, + getKicsValues: () => "test kics values", + getTitle: () => "

Test Title

", + getHtmlDetails: () => "Test Details", + scaContent: () => "test sca content", + scaNode: { + packageIdentifier: "test-package" + } + } as any as AstResult; + + details = new Details(mockResult, mockContext, true); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe("header", () => { + it("should generate correct header HTML", () => { + const severityPath = vscode.Uri.parse("file:///mock/severity.png"); + const html = details.header(severityPath); + + expect(html).to.include("Test Result"); // Checks if underscore is replaced + expect(html).to.include(severityPath.toString()); + expect(html).to.include("header-container"); + }); + }); + + describe("changes", () => { + it("should generate changes section with triage", () => { + const html = details.changes("test-class"); + + expect(html).to.include("history-container-loader"); + expect(html).to.include("select_severity"); + expect(html).to.include("select_state"); + }); + }); + + describe("triage", () => { + it("should generate triage section for SAST", () => { + const html = details.triage("test-class"); + + expect(html).to.include("select_severity"); + expect(html).to.include("select_state"); + expect(html).to.include("Update"); + expect(html).to.include("comment_box"); + }); + + it("should generate triage section for SCA without comment and update button", () => { + mockResult.type = constants.sca; + const html = details.triage("test-class"); + + expect(html).to.include("select_severity"); + expect(html).to.include("select_state"); + expect(html).to.not.include("Update"); + expect(html).to.not.include("comment_box"); + }); + }); + + describe("generalTab", () => { + it("should generate general tab content", () => { + const cxPath = vscode.Uri.parse("file:///mock/cx.png"); + const html = details.generalTab(cxPath); + + expect(html).to.include("Test description"); + expect(html).to.include("test kics values"); + expect(html).to.include("Test Title"); + expect(html).to.include("Test Details"); + }); + }); + + describe("secretDetectiongeneralTab", () => { + it("should generate secret detection general tab", () => { + const html = details.secretDetectiongeneralTab(); + expect(html).to.include("Test description"); + }); + }); + + describe("scaView", () => { + it("should generate SCA view content", () => { + const paths = { + severityPath: vscode.Uri.parse("file:///mock/severity.png"), + scaAtackVector: vscode.Uri.parse("file:///mock/attack.png"), + scaComplexity: vscode.Uri.parse("file:///mock/complexity.png"), + scaAuthentication: vscode.Uri.parse("file:///mock/auth.png"), + scaConfidentiality: vscode.Uri.parse("file:///mock/confidentiality.png"), + scaIntegrity: vscode.Uri.parse("file:///mock/integrity.png"), + scaAvailability: vscode.Uri.parse("file:///mock/availability.png"), + scaUpgrade: vscode.Uri.parse("file:///mock/upgrade.png"), + scaUrl: vscode.Uri.parse("file:///mock/url.png") + }; + + const html = details.scaView( + paths.severityPath, + paths.scaAtackVector, + paths.scaComplexity, + paths.scaAuthentication, + paths.scaConfidentiality, + paths.scaIntegrity, + paths.scaAvailability, + paths.scaUpgrade, + paths.scaUrl + ); + + expect(html).to.include("test-package"); + expect(html).to.include("test sca content"); + }); + }); + + describe("secretDetectionDetailsRemediationTab", () => { + it("should show remediation content when available", () => { + const html = details.secretDetectionDetailsRemediationTab(); + expect(html).to.include("test remediation"); + }); + + it("should show no remediation message when content unavailable", () => { + mockResult.data.remediation = undefined; + const html = details.secretDetectionDetailsRemediationTab(); + expect(html).to.include(messages.noRemediationExamplesTab); + }); + }); + + describe("secretDetectionDetailsDescriptionTab", () => { + it("should show description content when available", () => { + const html = details.secretDetectionDetailsDescriptionTab(); + expect(html).to.include("test rule description"); + }); + + it("should show no description message when content unavailable", () => { + mockResult.data.ruleDescription = undefined; + const html = details.secretDetectionDetailsDescriptionTab(); + expect(html).to.include(messages.noDescriptionTab); + }); + }); + + describe("generateMaskedSection", () => { + it("should generate HTML for masked secrets", () => { + const masked: CxMask = { + maskedSecrets: [ + { + secret: "password123", + masked: "********", + line: 42 + } + ] + } as CxMask; + + details.masked = masked; + const html = details.generateMaskedSection(); + + expect(html).to.include("password123"); + expect(html).to.include("********"); + expect(html).to.include("Line: 42"); + }); + + + + it("should show no secrets message when no secrets are masked", () => { + const html = details.generateMaskedSection(); + expect(html).to.include("No secrets were detected and masked"); + }); + }); + + describe("tab", () => { + it("should generate tabs structure with provided content", () => { + const html = details.tab( + "tab1 content", + "tab2 content", + "tab3 content", + "Tab 1", + "Tab 2", + "Tab 3", + "Tab 4", + "tab4 content", + "Tab 6", + "tab6 content" + ); + + expect(html).to.include("tab1 content"); + expect(html).to.include("tab2 content"); + expect(html).to.include("tab3 content"); + expect(html).to.include("tab4 content"); + expect(html).to.include("tab6 content"); + expect(html).to.include("Tab 1"); + expect(html).to.include("Tab 2"); + expect(html).to.include("Tab 3"); + expect(html).to.include("Tab 4"); + expect(html).to.include("Tab 6"); + }); + + it("should handle empty tab content and labels", () => { + const html = details.tab("", "", "", "", "", "", "", "", "", ""); + expect(html.trim()).to.equal(""); + }); + }); +}); \ No newline at end of file diff --git a/src/unit/filterCommand.test.ts b/src/unit/filterCommand.test.ts new file mode 100644 index 00000000..b2a54219 --- /dev/null +++ b/src/unit/filterCommand.test.ts @@ -0,0 +1,76 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import "./mocks/vscode-mock"; // Must be first +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as vscode from "vscode"; +import { FilterCommand } from "../commands/filterCommand"; +import { Logs } from "../models/logs"; +// import { SeverityLevel, StateLevel, constants } from "../utils/common/constants"; +import { commands } from "../utils/common/commands"; + +describe("FilterCommand", () => { + let filterCommand: FilterCommand; + let mockContext: vscode.ExtensionContext; + let logs: Logs; + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + mockContext = { + subscriptions: [], + globalState: { + get: sinon.stub(), + update: sinon.stub().resolves(), + }, + extensionUri: vscode.Uri.file("/mock/path"), + extensionPath: "/mock/path" + } as any; + + const mockOutputChannel = { + append: () => {}, + appendLine: () => {}, + clear: () => {}, + show: () => {}, + hide: () => {}, + dispose: () => {}, + name: "Mock Output", + replace: () => {} + } as vscode.OutputChannel; + + logs = new Logs(mockOutputChannel); + sinon.stub(logs, "info"); + sinon.stub(logs, "error"); + sinon.stub(logs, "log"); + + filterCommand = new FilterCommand(mockContext, logs); + }); + + afterEach(() => { + sandbox.restore(); + }); + + + + + describe("registerFilters", () => { + it("should register all filter commands", () => { + const registerCommandStub = sandbox.stub(vscode.commands, "registerCommand"); + + filterCommand.registerFilters(); + + const registeredCommands = registerCommandStub.args.map(call => call[0]); + expect(registeredCommands).to.include(commands.filterCritical); + expect(registeredCommands).to.include(commands.filterHigh); + expect(registeredCommands).to.include(commands.filterMedium); + expect(registeredCommands).to.include(commands.filterLow); + expect(registeredCommands).to.include(commands.filterInfo); + expect(registeredCommands).to.include(commands.filterNotExploitable); + expect(registeredCommands).to.include(commands.filterProposed); + expect(registeredCommands).to.include(commands.filterConfirmed); + expect(registeredCommands).to.include(commands.filterToVerify); + expect(registeredCommands).to.include(commands.filterUrgent); + expect(registeredCommands).to.include(commands.filterNotIgnored); + expect(registeredCommands).to.include(commands.filterIgnored); + }); + }); +}); \ No newline at end of file diff --git a/src/unit/getBaseAstConfiguration.test.ts b/src/unit/getBaseAstConfiguration.test.ts new file mode 100644 index 00000000..ad5b3075 --- /dev/null +++ b/src/unit/getBaseAstConfiguration.test.ts @@ -0,0 +1,12 @@ +import { expect } from "chai"; +import "./mocks/vscode-mock"; +import "./mocks/cxWrapper-mock"; +import { cx } from "../cx"; + +describe("Cx - getBaseAstConfiguration", () => { + it("should return configuration with additional parameters", async () => { + const config = cx.getBaseAstConfiguration(); + expect(config).to.not.be.undefined; + expect(config.additionalParameters).to.equal("valid-api-key"); + }); +}); \ No newline at end of file diff --git a/src/unit/getProject.test.ts b/src/unit/getProject.test.ts new file mode 100644 index 00000000..2cdb3167 --- /dev/null +++ b/src/unit/getProject.test.ts @@ -0,0 +1,26 @@ +import { expect } from "chai"; +import "./mocks/vscode-mock"; +import "./mocks/cxWrapper-mock"; +import { cx } from "../cx"; + +describe("Cx - getProject", () => { + it("should return project object when projectId is provided", async () => { + const projectId = "test-project-id"; + const result = await cx.getProject(projectId); + + expect(result).to.deep.equal({ + id: "test-project-id", + name: "Test Project", + createdAt: "2023-04-19T10:07:37.628413+01:00", + updatedAt: "2023-04-19T09:08:27.151913Z", + groups: [], + tags: {}, + criticality: 3 + }); + }); + + it("should return undefined when projectId is not provided", async () => { + const result = await cx.getProject(undefined); + expect(result).to.be.undefined; + }); +}); \ No newline at end of file diff --git a/src/unit/getResults.test.ts b/src/unit/getResults.test.ts new file mode 100644 index 00000000..378ff164 --- /dev/null +++ b/src/unit/getResults.test.ts @@ -0,0 +1,28 @@ +import { expect } from "chai"; +import "./mocks/vscode-mock"; +import "./mocks/cxWrapper-mock"; +import { cx } from "../cx"; + +describe("Cx - getResults", () => { + it("should get results when scanId is provided", async () => { + const scanId = "valid-scan-id"; + await cx.getResults(scanId); + // Since getResults doesn't return anything, we just verify it doesn't throw + }); + + it("should return undefined when scanId is not provided", async () => { + const result = await cx.getResults(undefined); + expect(result).to.be.undefined; + }); + + it("should throw error when getting results fails", async () => { + const scanId = "invalid-scan-id"; + + try { + await cx.getResults(scanId); + expect.fail("Expected error was not thrown"); + } catch (error) { + expect(error.message).to.equal("Failed to get results"); + } + }); +}); \ No newline at end of file diff --git a/src/unit/gptView.test.ts b/src/unit/gptView.test.ts new file mode 100644 index 00000000..354c6931 --- /dev/null +++ b/src/unit/gptView.test.ts @@ -0,0 +1,145 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import "./mocks/vscode-mock"; // Ensure mock is loaded first +import * as vscode from "vscode"; +import { expect } from "chai"; +import * as sinon from "sinon"; +import { GptView } from "../views/gptView/gptView"; +import { GptResult } from "../models/gptResult"; +import { constants } from "../utils/common/constants"; + +describe("GptView", () => { + let context: vscode.ExtensionContext; + let extensionUri: vscode.Uri; + let gptView: GptView; + let gptResult: GptResult; + let mockWebview: vscode.Webview; + + beforeEach(() => { + context = { + subscriptions: [], + extensionUri: vscode.Uri.parse("file:///mock"), + extensionPath: "/mock", + } as any; + + extensionUri = vscode.Uri.parse("file:///mock"); + + // ✅ Fix: Provide valid mock objects for GptResult + gptResult = new GptResult( + { + fileName: "mockFile.js", + type: "sast", + id: "mock-result-id", + severity: "High", + label: "Mock Label", + kicsNode: { data: { filename: "mockFile.js", line: 5 } }, + } as any, + { + files: [{ file_name: "mockFile.js", line: 5 }], + severity: "High", + query_name: "Mock Query", + } as any + ); + + gptView = new GptView(extensionUri, gptResult, context, false); + + // Mock webview with proper asWebviewUri implementation + mockWebview = { + asWebviewUri: (uri: vscode.Uri) => ({ + ...uri, + toString: () => `mock-uri://${uri.path}` + }), + options: {}, + html: "", + onDidReceiveMessage: sinon.stub(), + postMessage: sinon.stub(), + } as any; + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should initialize GptView correctly", () => { + expect(gptView).to.be.instanceOf(GptView); + expect(gptView.getLoad()).to.be.false; + expect(gptView.getResult()).to.deep.equal(gptResult); + }); + + it("should set and get the result", () => { + const newResult = new GptResult( + { + fileName: "newMockFile.js", + type: "sast", + id: "new-mock-result-id", + severity: "Medium", + label: "New Mock Label", + } as any, + { + files: [{ file_name: "newMockFile.js", line: 10 }], + severity: "Medium", + query_name: "New Mock Query", + } as any + ); + + gptView.setResult(newResult); + expect(gptView.getResult()).to.equal(newResult); + }); + + it("should set and get loadChanges state", () => { + gptView.setLoad(true); + expect(gptView.getLoad()).to.be.true; + }); + + it("should set webview and generate HTML content", async () => { + const mockWebviewView = { + webview: mockWebview, + } as any; + + await gptView.resolveWebviewView(mockWebviewView, {} as any, {} as any); + + expect(mockWebview.options.enableScripts).to.be.true; + expect(mockWebview.options.localResourceRoots).to.deep.equal([extensionUri]); + expect(mockWebview.html).to.be.a("string"); + expect(mockWebview.html).to.include(constants.aiSecurityChampion); + }); + + it("should get AskKicsIcon and KicsUserIcon URIs", async () => { + const mockWebviewView = { + webview: mockWebview, + } as any; + + await gptView.resolveWebviewView(mockWebviewView, {} as any, {} as any); + + const kicsIcon = gptView.getAskKicsIcon(); + const userIcon = gptView.getAskKicsUserIcon(); + + expect(kicsIcon).to.not.be.undefined; + expect(userIcon).to.not.be.undefined; + expect(kicsIcon.toString()).to.include("kics.png"); + expect(userIcon.toString()).to.include("userKics.png"); + }); + + it("should return valid webview instance", () => { + expect(gptView.getWebView()).to.be.undefined; // Initially undefined + }); + + it("should generate masked secrets section correctly", () => { + const maskedData = { + maskedSecrets: [ + { secret: "password123", masked: "******", line: 10 }, + ], + } as any; + + gptView = new GptView(extensionUri, gptResult, context, false, undefined, maskedData); + const html = gptView.generateMaskedSection(); + + expect(html).to.include("password123"); + expect(html).to.include("******"); + expect(html).to.include("Line: 10"); + }); + + it("should return default message if no secrets are masked", () => { + const html = gptView.generateMaskedSection(); + expect(html).to.include("No secrets were detected and masked"); + }); +}); diff --git a/src/unit/groupByCommand.test.ts b/src/unit/groupByCommand.test.ts new file mode 100644 index 00000000..cadffef4 --- /dev/null +++ b/src/unit/groupByCommand.test.ts @@ -0,0 +1,87 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import "./mocks/vscode-mock"; +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as vscode from "vscode"; +import { GroupByCommand } from "../commands/groupByCommand"; +import { Logs } from "../models/logs"; +import { commands } from "../utils/common/commands"; +import { GroupBy, constants } from "../utils/common/constants"; + +describe("GroupByCommand", () => { + let groupByCommand: GroupByCommand; + let mockContext: vscode.ExtensionContext; + let logs: Logs; + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + mockContext = { + subscriptions: [], + globalState: { + get: sinon.stub(), + update: sinon.stub().resolves(), + }, + extensionUri: vscode.Uri.file("/mock/path"), + extensionPath: "/mock/path" + } as any; + + const mockOutputChannel = { + append: () => {}, + appendLine: () => {}, + clear: () => {}, + show: () => {}, + hide: () => {}, + dispose: () => {}, + name: "Mock Output", + replace: () => {} + } as vscode.OutputChannel; + + logs = new Logs(mockOutputChannel); + sinon.stub(logs, "info"); + sinon.stub(logs, "error"); + sinon.stub(logs, "log"); + + groupByCommand = new GroupByCommand(mockContext, logs); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe("registerGroupBy", () => { + it("should register all group by commands", () => { + const registerCommandStub = sandbox.stub(vscode.commands, "registerCommand"); + + groupByCommand.registerGroupBy(); + + const registeredCommands = registerCommandStub.args.map(call => call[0]); + expect(registeredCommands).to.include(commands.groupByQueryName); + expect(registeredCommands).to.include(commands.groupByLanguage); + expect(registeredCommands).to.include(commands.groupBySeverity); + expect(registeredCommands).to.include(commands.groupByStatus); + expect(registeredCommands).to.include(commands.groupByState); + expect(registeredCommands).to.include(commands.groupByFile); + expect(registeredCommands).to.include(commands.groupByDirectDependency); + }); + }); + + + + describe("group", () => { + it("should update group by value and refresh tree", async () => { + const executeCommandStub = sandbox.stub(vscode.commands, "executeCommand").resolves(); + + await groupByCommand["group"]( + logs, + mockContext, + GroupBy.severity, + constants.severityGroup + ); + + const updateStub = mockContext.globalState.update as sinon.SinonStub; + expect(updateStub.args).to.deep.include([constants.severityGroup, true]); + expect(executeCommandStub.calledWith(commands.refreshTree)).to.be.true; + }); + }); +}); diff --git a/src/unit/kicsRealtimeCommand.test.ts b/src/unit/kicsRealtimeCommand.test.ts new file mode 100644 index 00000000..6d8a23f3 --- /dev/null +++ b/src/unit/kicsRealtimeCommand.test.ts @@ -0,0 +1,61 @@ +import { expect } from "chai"; +import "./mocks/vscode-mock"; +import { KICSRealtimeCommand } from "../commands/kicsRealtimeCommand"; +import { Logs } from "../models/logs"; +import * as vscode from "vscode"; +import { clearCommandsExecuted } from "./mocks/vscode-mock"; +import { KicsProvider } from "../kics/kicsRealtimeProvider"; + +describe("KICSRealtimeCommand", () => { + let kicsCommand: KICSRealtimeCommand; + let logs: Logs; + let mockContext: vscode.ExtensionContext; + let mockKicsProvider: KicsProvider; + + + beforeEach(() => { + clearCommandsExecuted(); + + const mockOutputChannel = { + append: () => {}, + appendLine: () => {}, + clear: () => {}, + show: () => {}, + hide: () => {}, + dispose: () => {}, + replace: () => {}, + name: "Test" + }; + + logs = { + info: () => {}, + error: () => {}, + output: mockOutputChannel, + log: () => {}, + warn: () => {}, + show: () => {} + } as Logs; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockContext = { subscriptions: [] } as any; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockKicsProvider = { runKicsIfEnabled: async () => {} } as any; + + kicsCommand = new KICSRealtimeCommand(mockContext, mockKicsProvider, logs); + }); + + describe("registerKicsScans", () => { + it("should register kics scan command", () => { + kicsCommand.registerKicsScans(); + expect(mockContext.subscriptions.length).to.be.greaterThan(0); + }); + }); + + describe("registerSettings", () => { + it("should register kics settings command", () => { + kicsCommand.registerSettings(); + expect(mockContext.subscriptions.length).to.be.greaterThan(0); + }); + }); +}); \ No newline at end of file diff --git a/src/unit/mocks/cxWrapper-mock.ts b/src/unit/mocks/cxWrapper-mock.ts index 1e8bbc9c..51d0c208 100644 --- a/src/unit/mocks/cxWrapper-mock.ts +++ b/src/unit/mocks/cxWrapper-mock.ts @@ -1,6 +1,15 @@ import mockRequire from "mock-require"; +import { CxParamType } from "@checkmarxdev/ast-cli-javascript-wrapper/dist/main/wrapper/CxParamType"; +import { CxConfig } from "@checkmarxdev/ast-cli-javascript-wrapper/dist/main/wrapper/CxConfig"; + mockRequire("@checkmarxdev/ast-cli-javascript-wrapper", { CxWrapper: class { + config: CxConfig; + + constructor(config?: CxConfig) { + this.config = config || new CxConfig(); + } + async scanShow(scanId: string) { if (scanId === "1") { return { @@ -26,5 +35,79 @@ mockRequire("@checkmarxdev/ast-cli-javascript-wrapper", { }; } } + + async projectShow(projectId: string) { + if (projectId === "test-project-id") { + return { + payload: [ + { + id: "test-project-id", + name: "Test Project", + createdAt: "2023-04-19T10:07:37.628413+01:00", + updatedAt: "2023-04-19T09:08:27.151913Z", + groups: [], + tags: {}, + criticality: 3 + } + ], + exitCode: 0 + }; + } else { + return { + status: "Project not found", + payload: [], + exitCode: 1 + }; + } + } + + async scanCreate(params: Map) { + if (params.get(CxParamType.PROJECT_NAME) === "test-project" && + params.get(CxParamType.BRANCH) === "main") { + return { + payload: [ + { + id: "scan-123", + status: "Created", + projectId: "test-project-id", + branch: "main", + createdAt: "2023-04-19T10:07:37.628413+01:00" + } + ], + exitCode: 0 + }; + } else { + return { + status: "Failed to create scan", + payload: undefined, + exitCode: 1 + }; + } + } + + async getResults(scanId: string, fileExtension: string, fileName: string, filePath: string, agent: string) { + if (scanId === "valid-scan-id") { + return { + payload: "Results retrieved successfully", + exitCode: 0 + }; + } else { + throw new Error("Failed to get results"); + } + } + + async authValidate() { + if (this.config?.apiKey === "valid-api-key") { + return { + exitCode: 0, + status: "Authenticated successfully" + }; + } else { + return { + exitCode: 1, + status: "Authentication failed" + }; + } + } }, -}); +}); \ No newline at end of file diff --git a/src/unit/mocks/vscode-mock.ts b/src/unit/mocks/vscode-mock.ts index c1ba54c8..353003fd 100644 --- a/src/unit/mocks/vscode-mock.ts +++ b/src/unit/mocks/vscode-mock.ts @@ -1,6 +1,11 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/naming-convention */ import mockRequire from "mock-require"; import { constants } from "../../utils/common/constants"; -import sinon from "sinon"; +import * as sinon from "sinon"; + +let commandsExecuted: string[] = []; const mockDiagnosticCollection = { set: sinon.stub(), @@ -14,14 +19,17 @@ function resetMocks() { mockDiagnosticCollection.clear.reset(); } -const mockVscode = { +const mock = { workspace: { getConfiguration: (section: string) => { if (section === "checkmarxOne") { return { get: (key: string) => { if (key === constants.apiKey) { - return constants.apiKey; + return "valid-api-key"; + } + if (key === "additionalParams") { + return "valid-api-key"; } return undefined; }, @@ -30,12 +38,49 @@ const mockVscode = { return undefined; }, workspaceFolders: [{ uri: { fsPath: "/mock/path" } }], + openTextDocument: () => Promise.resolve({ + // Mock document properties + }), + findFiles: () => Promise.resolve([]) }, - ProgressLocation: { - Notification: "Notification", + window: { + showErrorMessage: () => Promise.resolve(), + showInformationMessage: () => Promise.resolve(), + createOutputChannel: () => ({ + append: () => {}, + appendLine: () => {}, + clear: () => {}, + show: () => {}, + hide: () => {}, + dispose: () => {}, + replace: () => {}, + name: "Test" + }), + createWebviewPanel: () => ({ + webview: { + html: "", + asWebviewUri: (uri: any) => uri, + onDidReceiveMessage: () => ({ dispose: () => {} }), + postMessage: () => Promise.resolve() + }, + reveal: () => {}, + dispose: () => {}, + onDidDispose: () => ({ dispose: () => {} }) + }) }, - // Add these for ASCA diagnostics + + commands: { + executeCommand: (command: string) => { + commandsExecuted.push(command); + return Promise.resolve(); + }, + getCommands: () => Promise.resolve([]), + registerCommand: (command: string, callback: (...args: any[]) => any) => { + return { dispose: () => {} }; + } + }, + languages: { createDiagnosticCollection: () => mockDiagnosticCollection }, @@ -43,11 +88,14 @@ const mockVscode = { DiagnosticSeverity: { Error: 0, Warning: 1, - Information: 2 + Information: 2, + Hint: 3 }, Position: class Position { constructor(public line: number, public character: number) {} + translate() { return this; } + with() { return this; } }, Range: class Range { @@ -55,18 +103,64 @@ const mockVscode = { public start: { line: number; character: number }, public end: { line: number; character: number } ) {} + with() { return this; } }, Diagnostic: class Diagnostic { source: string | undefined; + code?: string | number; + relatedInformation?: any[]; + tags?: any[]; + constructor( public range: { start: { line: number; character: number }; end: { line: number; character: number } }, public message: string, public severity: number ) {} + }, + + ProgressLocation: { + Notification: "Notification", + }, + + Uri: { + file: (path: string) => ({ + fsPath: path, + scheme: 'file', + path: path + }), + parse: (path: string) => ({ + fsPath: path, + scheme: 'file', + path: path + }), + joinPath: (uri: any, ...pathSegments: string[]) => ({ + fsPath: pathSegments.join('/'), + scheme: 'file', + path: pathSegments.join('/') + }) + }, + + // 🔹 **תוספת של TreeItem** + TreeItem: class { + label: string; + collapsibleState: any; + + constructor(label: string, collapsibleState: any) { + this.label = label; + this.collapsibleState = collapsibleState; + } + }, + + TreeItemCollapsibleState: { + None: 0, + Collapsed: 1, + Expanded: 2 } }; -mockRequire("vscode", mockVscode); +mockRequire("vscode", mock); -export { mockDiagnosticCollection, mockVscode, resetMocks }; +export { mockDiagnosticCollection, resetMocks }; +export const getCommandsExecuted = () => commandsExecuted; +export const clearCommandsExecuted = () => { commandsExecuted = []; }; diff --git a/src/unit/pickerCommand.test.ts b/src/unit/pickerCommand.test.ts new file mode 100644 index 00000000..4540732b --- /dev/null +++ b/src/unit/pickerCommand.test.ts @@ -0,0 +1,140 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import "./mocks/vscode-mock"; +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as vscode from "vscode"; +import { PickerCommand } from "../commands/pickerCommand"; +import { Logs } from "../models/logs"; +import { commands } from "../utils/common/commands"; +// import { projectPicker, branchPicker, scanPicker, scanInput } from "../utils/pickers/pickers"; +import { AstResultsProvider } from "../views/resultsView/astResultsProvider"; + +describe("PickerCommand", () => { + let pickerCommand: PickerCommand; + let mockContext: vscode.ExtensionContext; + let logs: Logs; + let sandbox: sinon.SinonSandbox; + let resultsProvider: AstResultsProvider; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + mockContext = { + subscriptions: [], + globalState: { + get: sinon.stub(), + update: sinon.stub().resolves(), + }, + extensionUri: vscode.Uri.file("/mock/path"), + extensionPath: "/mock/path" + } as any; + + const mockOutputChannel = { + append: () => {}, + appendLine: () => {}, + clear: () => {}, + show: () => {}, + hide: () => {}, + dispose: () => {}, + name: "Mock Output", + replace: () => {} + } as vscode.OutputChannel; + + logs = new Logs(mockOutputChannel); + sinon.stub(logs, "info"); + sinon.stub(logs, "error"); + sinon.stub(logs, "log"); + + resultsProvider = {} as AstResultsProvider; + pickerCommand = new PickerCommand(mockContext, logs, resultsProvider); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe("registerPickerCommands", () => { + it("should register all picker commands", () => { + const registerCommandStub = sandbox.stub(vscode.commands, "registerCommand"); + + pickerCommand.registerPickerCommands(); + + expect(registerCommandStub.calledWith(commands.generalPick)).to.be.true; + expect(registerCommandStub.calledWith(commands.projectPick)).to.be.true; + expect(registerCommandStub.calledWith(commands.branchPick)).to.be.true; + expect(registerCommandStub.calledWith(commands.scanPick)).to.be.true; + expect(registerCommandStub.calledWith(commands.scanInput)).to.be.true; + }); + }); + + describe("projectPicker", () => { + // it("should call projectPicker function", async () => { + // const projectPickerStub = sandbox.stub().resolves(); + // sandbox.stub(vscode.commands, "registerCommand").callsFake((command, callback) => { + // if (command === commands.projectPick) { + // callback(); + // } + // return {} as vscode.Disposable; + // }); + + // // sandbox.stub(projectPicker, "default").returns(Promise.resolve()); + + // await pickerCommand.registerPickerCommands(); + + // expect(projectPickerStub.calledOnce).to.be.true; + // }); + }); + + describe("branchPicker", () => { + // it("should call branchPicker function", async () => { + // const branchPickerStub = sandbox.stub().resolves(); + // sandbox.stub(vscode.commands, "registerCommand").callsFake((command, callback) => { + // if (command === commands.branchPick) { + // callback(); + // } + // return {} as vscode.Disposable; + // }); + + // // sandbox.stub(branchPicker).resolves(); + + // await pickerCommand.registerPickerCommands(); + + // expect(branchPickerStub.calledOnce).to.be.true; + // }); + }); + + describe("scanPicker", () => { + // it("should call scanPicker function", async () => { + // const scanPickerStub = sandbox.stub().resolves(); + // sandbox.stub(vscode.commands, "registerCommand").callsFake((command, callback) => { + // if (command === commands.scanPick) { + // callback(); + // } + // return {} as vscode.Disposable; + // }); + + // // sandbox.stub(scanPicker).resolves(); + + // await pickerCommand.registerPickerCommands(); + + // expect(scanPickerStub.calledOnce).to.be.true; + // }); + }); + + describe("scanInput", () => { + // it("should call scanInput function", async () => { + // const scanInputStub = sandbox.stub().resolves(); + // sandbox.stub(vscode.commands, "registerCommand").callsFake((command, callback) => { + // if (command === commands.scanInput) { + // callback(); + // } + // return {} as vscode.Disposable; + // }); + + // // sandbox.stub(scanInput).resolves(); + + // await pickerCommand.registerPickerCommands(); + + // expect(scanInputStub.calledOnce).to.be.true; + // }); + }); +}); diff --git a/src/unit/scanCommand.test.ts b/src/unit/scanCommand.test.ts new file mode 100644 index 00000000..0d1116cb --- /dev/null +++ b/src/unit/scanCommand.test.ts @@ -0,0 +1,64 @@ +import { expect } from "chai"; +import "./mocks/vscode-mock"; +import { ScanCommand } from "../commands/scanCommand"; +import { Logs } from "../models/logs"; +import * as vscode from "vscode"; +import { clearCommandsExecuted } from "./mocks/vscode-mock"; + +describe("ScanCommand", () => { + let scanCommand: ScanCommand; + let logs: Logs; + let mockContext: vscode.ExtensionContext; + let mockStatusBar: vscode.StatusBarItem; + let scanStarted = false; + + beforeEach(() => { + clearCommandsExecuted(); + scanStarted = false; + + const mockOutputChannel = { + append: () => {}, + appendLine: () => {}, + clear: () => {}, + show: () => {}, + hide: () => {}, + dispose: () => {}, + replace: () => {}, + name: "Test" + }; + + logs = { + info: () => {}, + error: () => {}, + output: mockOutputChannel, + log: () => {}, + warn: () => {}, + show: () => {} + } as Logs; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockContext = { subscriptions: [] } as any; + + mockStatusBar = { + show: () => {}, + hide: () => {}, + dispose: () => {} + } as vscode.StatusBarItem; + + scanCommand = new ScanCommand(mockContext, mockStatusBar, logs); + }); + + describe("registerIdeScans", () => { + it("should register scan commands", () => { + scanCommand.registerIdeScans(); + expect(mockContext.subscriptions.length).to.equal(3); // createScan, cancelScan, pollScan + }); + }); + + describe("executePollScan", () => { + it("should execute poll scan command", () => { + scanCommand.executePollScan(); + expect(scanStarted).to.be.false; + }); + }); +}); \ No newline at end of file diff --git a/src/unit/scanCreate.test.ts b/src/unit/scanCreate.test.ts new file mode 100644 index 00000000..1326b903 --- /dev/null +++ b/src/unit/scanCreate.test.ts @@ -0,0 +1,32 @@ +import { expect } from "chai"; +import "./mocks/vscode-mock"; +import "./mocks/cxWrapper-mock"; +import { cx } from "../cx"; + +describe("Cx - scanCreate", () => { + it("should create scan when all parameters are provided", async () => { + const projectName = "test-project"; + const branchName = "main"; + const sourcePath = "/test/path"; + + const result = await cx.scanCreate(projectName, branchName, sourcePath); + + expect(result).to.deep.equal({ + id: "scan-123", + status: "Created", + projectId: "test-project-id", + branch: "main", + createdAt: "2023-04-19T10:07:37.628413+01:00" + }); + }); + + it("should return undefined when projectName is not provided", async () => { + const result = await cx.scanCreate(undefined, "main", "/test/path"); + expect(result).to.be.undefined; + }); + + it("should return undefined when branchName is not provided", async () => { + const result = await cx.scanCreate("test-project", undefined, "/test/path"); + expect(result).to.be.undefined; + }); +}); \ No newline at end of file diff --git a/src/unit/treeCommand.test.ts b/src/unit/treeCommand.test.ts new file mode 100644 index 00000000..cca94b2c --- /dev/null +++ b/src/unit/treeCommand.test.ts @@ -0,0 +1,64 @@ +import { expect } from "chai"; +import "./mocks/vscode-mock"; +import { TreeCommand } from "../commands/treeCommand"; +import { Logs } from "../models/logs"; +import * as vscode from "vscode"; +import { clearCommandsExecuted } from "./mocks/vscode-mock"; +import { AstResultsProvider } from "../views/resultsView/astResultsProvider"; +import { SCAResultsProvider } from "../views/scaView/scaResultsProvider"; + +describe("TreeCommand", () => { + let treeCommand: TreeCommand; + let logs: Logs; + let mockContext: vscode.ExtensionContext; + let mockAstProvider: AstResultsProvider; + let mockScaProvider: SCAResultsProvider; + + beforeEach(() => { + clearCommandsExecuted(); + + const mockOutputChannel = { + append: () => {}, + appendLine: () => {}, + clear: () => {}, + show: () => {}, + hide: () => {}, + dispose: () => {}, + replace: () => {}, + name: "Test" + }; + + logs = { + info: () => {}, + error: () => {}, + output: mockOutputChannel, + log: () => {}, + warn: () => {}, + show: () => {} + } as Logs; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockContext = { subscriptions: [] } as any; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockAstProvider = { refreshData: async () => {}, clean: async () => {} } as any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockScaProvider = { refreshData: async () => {}, clean: async () => {} } as any; + + treeCommand = new TreeCommand(mockContext, mockAstProvider, mockScaProvider, logs); + }); + + describe("registerRefreshCommands", () => { + it("should register refresh commands", () => { + treeCommand.registerRefreshCommands(); + expect(mockContext.subscriptions.length).to.be.greaterThan(0); + }); + }); + + describe("registerClearCommands", () => { + it("should register clear commands", () => { + treeCommand.registerClearCommands(); + expect(mockContext.subscriptions.length).to.be.greaterThan(0); + }); + }); +}); \ No newline at end of file diff --git a/src/unit/webViewCommand.test.ts b/src/unit/webViewCommand.test.ts new file mode 100644 index 00000000..4f2ab87a --- /dev/null +++ b/src/unit/webViewCommand.test.ts @@ -0,0 +1,102 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import "./mocks/vscode-mock"; // Must be first +import * as vscode from "vscode"; +import { WebViewCommand } from "../commands/webViewCommand"; +import { Logs } from "../models/logs"; +import { AstResultsProvider } from "../views/resultsView/astResultsProvider"; +import { expect } from "chai"; +import * as sinon from "sinon"; +import { commands } from "../utils/common/commands"; + +describe("WebViewCommand", () => { + let webViewCommand: WebViewCommand; + let context: vscode.ExtensionContext; + let logs: Logs; + let resultsProvider: AstResultsProvider; + + beforeEach(() => { + context = { + subscriptions: [], + extensionUri: vscode.Uri.parse("file:///mock"), + extensionPath: "/mock", + } as any; + logs = sinon.createStubInstance(Logs); + resultsProvider = sinon.createStubInstance(AstResultsProvider); + + webViewCommand = new WebViewCommand(context, logs, resultsProvider); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should register new details command", async () => { + const registerCommandStub = sinon.stub(vscode.commands, "registerCommand"); + + webViewCommand.registerNewDetails(); + + expect(registerCommandStub.calledWith(commands.newDetails)).to.be.true; // ✅ Fixed reference + }); + + it("should register GPT command", async () => { + const registerCommandStub = sinon.stub(vscode.commands, "registerCommand"); + + webViewCommand.registerGpt(); + + expect(registerCommandStub.calledWith(commands.gpt)).to.be.true; // ✅ Fixed reference + }); + + // it("should create a WebviewPanel when registering new details", async () => { + // const createWebviewPanelStub = sinon.stub(vscode.window, "createWebviewPanel"); + // const mockPanel = { + // webview: { + // html: "", + // onDidReceiveMessage: sinon.stub(), + // postMessage: sinon.stub(), + // }, + // dispose: sinon.stub(), + // onDidDispose: sinon.stub(), + // }; + // createWebviewPanelStub.returns(mockPanel as any); + + // webViewCommand.registerNewDetails(); + + // await vscode.commands.executeCommand(commands.newDetails, { + // severity: "High", + // label: "Mock Issue", + // }); + + // expect(createWebviewPanelStub.called).to.be.true; + // }); + + // it("should handle messages sent to Webview", async () => { + // const mockPanel = { + // webview: { + // onDidReceiveMessage: sinon.stub(), + // postMessage: sinon.stub(), + // }, + // dispose: sinon.stub(), + // onDidDispose: sinon.stub(), + // }; + + // (webViewCommand as any).detailsPanel = mockPanel as any; + + // await vscode.commands.executeCommand(commands.newDetails, { + // severity: "High", + // label: "Mock Issue", + // }); + + // expect(mockPanel.webview.onDidReceiveMessage.called).to.be.true; + // }); + + it("should dispose WebviewPanel on close", () => { + const disposeStub = sinon.stub(); + (webViewCommand as any).detailsPanel = { + dispose: disposeStub, + } as any; + + (webViewCommand as any).detailsPanel.dispose(); + + expect(disposeStub.called).to.be.true; + }); +});