diff --git a/src/snyk/cli/process.ts b/src/snyk/cli/process.ts index d49553ff0..793335386 100644 --- a/src/snyk/cli/process.ts +++ b/src/snyk/cli/process.ts @@ -1,11 +1,11 @@ import { ChildProcessWithoutNullStreams, spawn } from 'child_process'; +import { OAuthToken } from '../base/services/authenticationService'; import { Configuration, IConfiguration } from '../common/configuration/configuration'; import { ILog } from '../common/logger/interfaces'; import { getVsCodeProxy } from '../common/proxy'; import { IVSCodeWorkspace } from '../common/vscode/workspace'; import { CLI_INTEGRATION_NAME } from './contants/integration'; import { CliError } from './services/cliService'; -import { OAuthToken } from '../base/services/authenticationService'; export class CliProcess { private runningProcess: ChildProcessWithoutNullStreams | null; @@ -25,6 +25,7 @@ export class CliProcess { return new Promise((resolve, reject) => { let output = ''; + // file deepcode ignore ArrayMethodOnNonArray: readonly string[] is an array of strings this.logger.info(`Running "${cliPath} ${args.join(' ')}".`); this.runningProcess = spawn(cliPath, args, { env: { ...process.env, ...processEnv }, cwd }); diff --git a/src/snyk/common/commands/commandController.ts b/src/snyk/common/commands/commandController.ts index 60d9c1b24..a58ea1799 100644 --- a/src/snyk/common/commands/commandController.ts +++ b/src/snyk/common/commands/commandController.ts @@ -3,7 +3,7 @@ import _ from 'lodash'; import path from 'path'; import { IAuthenticationService } from '../../base/services/authenticationService'; import { ScanModeService } from '../../base/services/scanModeService'; -import { createDCIgnore } from '../../snykCode/utils/ignoreFileUtils'; +import { createDCIgnore as createDCIgnoreUtil } from '../../snykCode/utils/ignoreFileUtils'; import { IssueUtils } from '../../snykCode/utils/issueUtils'; import { CodeIssueCommandArg } from '../../snykCode/views/interfaces'; import { IacIssueCommandArg } from '../../snykIac/views/interfaces'; @@ -89,11 +89,11 @@ export class CommandController { const paths = this.workspace.getWorkspaceFolders(); const promises = []; for (const p of paths) { - promises.push(createDCIgnore(p, custom, this.workspace, this.window, uriAdapter)); + promises.push(createDCIgnoreUtil(p, custom, this.workspace, this.window, uriAdapter)); } await Promise.all(promises); } else { - await createDCIgnore(path, custom, this.workspace, this.window, uriAdapter); + await createDCIgnoreUtil(path, custom, this.workspace, this.window, uriAdapter); } } diff --git a/src/snyk/common/commands/types.ts b/src/snyk/common/commands/types.ts index 7e59c0206..2fee394d9 100644 --- a/src/snyk/common/commands/types.ts +++ b/src/snyk/common/commands/types.ts @@ -1,7 +1,7 @@ import { completeFileSuggestionType } from '../../snykCode/interfaces'; import { CodeIssueCommandArg } from '../../snykCode/views/interfaces'; import { IacIssueCommandArg } from '../../snykIac/views/interfaces'; -import { OssIssueCommandArgLanguageServer } from '../../snykOss/interfaces'; +import { OssIssueCommandArg } from '../../snykOss/interfaces'; import { CodeIssueData, Issue } from '../languageServer/types'; export enum OpenCommandIssueType { @@ -11,12 +11,12 @@ export enum OpenCommandIssueType { } export type OpenIssueCommandArg = { - issue: CodeIssueCommandArg | IacIssueCommandArg | OssIssueCommandArgLanguageServer; + issue: CodeIssueCommandArg | IacIssueCommandArg | OssIssueCommandArg; issueType: OpenCommandIssueType; }; export const isCodeIssue = ( - _issue: completeFileSuggestionType | Issue | OssIssueCommandArgLanguageServer, + _issue: completeFileSuggestionType | Issue | OssIssueCommandArg, issueType: OpenCommandIssueType, ): _issue is Issue => { return issueType === OpenCommandIssueType.CodeIssue; diff --git a/src/snyk/common/editor/codeActionsProvider.ts b/src/snyk/common/editor/codeActionsProvider.ts index 74a38b4b9..8181c39b3 100644 --- a/src/snyk/common/editor/codeActionsProvider.ts +++ b/src/snyk/common/editor/codeActionsProvider.ts @@ -2,19 +2,26 @@ import { IAnalytics, SupportedQuickFixProperties } from '../../common/analytics/ import { IDE_NAME } from '../../common/constants/general'; import { Issue } from '../../common/languageServer/types'; import { ICodeActionKindAdapter } from '../../common/vscode/codeAction'; -import { CodeAction, CodeActionKind, CodeActionProvider, Range, TextDocument } from '../../common/vscode/types'; +import { + CodeAction, + CodeActionContext, + CodeActionKind, + CodeActionProvider, + Range, + TextDocument, +} from '../../common/vscode/types'; import { ProductResult } from '../services/productService'; export abstract class CodeActionsProvider implements CodeActionProvider { protected readonly providedCodeActionKinds = [this.codeActionKindAdapter.getQuickFix()]; constructor( - private readonly issues: ProductResult, + protected readonly issues: ProductResult, private readonly codeActionKindAdapter: ICodeActionKindAdapter, - private readonly analytics: IAnalytics, + protected readonly analytics: IAnalytics, ) {} - abstract getActions(folderPath: string, document: TextDocument, issue: Issue, issueRange: Range): CodeAction[]; + abstract getActions(folderPath: string, document: TextDocument, issue: Issue, issueRange?: Range): CodeAction[]; abstract getAnalyticsActionTypes(): [string, ...string[]] & [SupportedQuickFixProperties, ...SupportedQuickFixProperties[]]; @@ -25,7 +32,11 @@ export abstract class CodeActionsProvider implements CodeActionProvider { return this.providedCodeActionKinds; } - public provideCodeActions(document: TextDocument, clickedRange: Range): CodeAction[] | undefined { + public provideCodeActions( + document: TextDocument, + clickedRange: Range, + _context: CodeActionContext, + ): CodeAction[] | undefined { if (this.issues.size === 0) { return undefined; } @@ -57,7 +68,7 @@ export abstract class CodeActionsProvider implements CodeActionProvider { return undefined; } - private findIssueWithRange( + protected findIssueWithRange( result: Issue[], document: TextDocument, clickedRange: Range, diff --git a/src/snyk/common/languageServer/lsExecutable.ts b/src/snyk/common/languageServer/lsExecutable.ts index 8a7c0ca8e..341e1e04a 100644 --- a/src/snyk/common/languageServer/lsExecutable.ts +++ b/src/snyk/common/languageServer/lsExecutable.ts @@ -43,10 +43,10 @@ export class LsExecutable { return customPath; } - const platform = this.getCurrentWithArch(); + const platform = LsExecutable.getCurrentWithArch(); const homeDir = Platform.getHomeDir(); - const lsFilename = this.getFilename(platform); + const lsFilename = LsExecutable.getFilename(platform); const defaultPath = this.defaultPaths[platform]; const lsDir = path.join(homeDir, defaultPath, 'snyk-ls'); return path.join(lsDir, lsFilename); diff --git a/src/snyk/extension.ts b/src/snyk/extension.ts index e3af60c8c..669c942b2 100644 --- a/src/snyk/extension.ts +++ b/src/snyk/extension.ts @@ -219,6 +219,8 @@ class SnykExtension extends SnykLib implements IExtension { extensionContext, configuration, ossSuggestionProvider, + new CodeActionAdapter(), + this.codeActionKindAdapter, this.viewManagerService, vsCodeWorkspace, this.workspaceTrust, diff --git a/src/snyk/snykCode/constants/analysis.ts b/src/snyk/snykCode/constants/analysis.ts index a9dc2baf4..814b590c9 100644 --- a/src/snyk/snykCode/constants/analysis.ts +++ b/src/snyk/snykCode/constants/analysis.ts @@ -24,6 +24,7 @@ export const ISSUES_MARKERS_DECORATION_TYPE: { [key: string]: string } = { export const DIAGNOSTICS_CODE_SECURITY_COLLECTION_NAME = 'Snyk Code Security'; export const DIAGNOSTICS_CODE_QUALITY_COLLECTION_NAME = 'Snyk Code Quality'; export const DIAGNOSTICS_OSS_COLLECTION_NAME = 'Snyk Open Source Security'; +export const DIAGNOSTICS_OSS_COLLECTION_NAME_LS = 'Snyk Open Source'; export const WEBVIEW_PANEL_SECURITY_TITLE = 'Snyk Code Vulnerability'; export const WEBVIEW_PANEL_QUALITY_TITLE = 'Snyk Code Issue'; diff --git a/src/snyk/snykIac/views/suggestion/iacSuggestionWebviewScript.ts b/src/snyk/snykIac/views/suggestion/iacSuggestionWebviewScript.ts index bcab7587e..1ca18f758 100644 --- a/src/snyk/snykIac/views/suggestion/iacSuggestionWebviewScript.ts +++ b/src/snyk/snykIac/views/suggestion/iacSuggestionWebviewScript.ts @@ -143,6 +143,7 @@ } } + // file deepcode ignore InsufficientPostmessageValidation: Content Security Policy applied in provider window.addEventListener('message', event => { const { type, args } = event.data; switch (type) { diff --git a/src/snyk/snykOss/interfaces.ts b/src/snyk/snykOss/interfaces.ts index 907ba23a0..70056124a 100644 --- a/src/snyk/snykOss/interfaces.ts +++ b/src/snyk/snykOss/interfaces.ts @@ -7,7 +7,7 @@ export interface IOssSuggestionWebviewProvider extends IWebViewProvider & { +export type OssIssueCommandArg = Issue & { matchingIdVulnerabilities: Issue[]; overviewHtml: string; }; diff --git a/src/snyk/snykOss/ossService.ts b/src/snyk/snykOss/ossService.ts index b2fa759bb..b0394567f 100644 --- a/src/snyk/snykOss/ossService.ts +++ b/src/snyk/snykOss/ossService.ts @@ -7,16 +7,20 @@ import { OssIssueData, Scan, ScanProduct } from '../common/languageServer/types' import { ILog } from '../common/logger/interfaces'; import { ProductService } from '../common/services/productService'; import { IViewManagerService } from '../common/services/viewManagerService'; +import { ICodeActionAdapter, ICodeActionKindAdapter } from '../common/vscode/codeAction'; import { ExtensionContext } from '../common/vscode/extensionContext'; import { IVSCodeLanguages } from '../common/vscode/languages'; import { IVSCodeWorkspace } from '../common/vscode/workspace'; import { IOssSuggestionWebviewProvider } from './interfaces'; +import { OssCodeActionsProvider } from './providers/ossCodeActionsProvider'; export class OssService extends ProductService { constructor( extensionContext: ExtensionContext, config: IConfiguration, suggestionProvider: IOssSuggestionWebviewProvider, + codeActionAdapter: ICodeActionAdapter, + codeActionKindAdapter: ICodeActionKindAdapter, viewManagerService: IViewManagerService, workspace: IVSCodeWorkspace, workspaceTrust: IWorkspaceTrust, @@ -36,6 +40,10 @@ export class OssService extends ProductService { languages, logger, ); + + this.registerCodeActionsProvider( + new OssCodeActionsProvider(languages, codeActionAdapter, codeActionKindAdapter, this.result, analytics), + ); } subscribeToLsScanMessages(): Subscription { diff --git a/src/snyk/snykOss/providers/ossCodeActionsProvider.ts b/src/snyk/snykOss/providers/ossCodeActionsProvider.ts new file mode 100644 index 000000000..0ed6d9632 --- /dev/null +++ b/src/snyk/snykOss/providers/ossCodeActionsProvider.ts @@ -0,0 +1,160 @@ +import { CodeAction, Range, TextDocument, Uri } from 'vscode'; +import { IAnalytics, SupportedQuickFixProperties } from '../../common/analytics/itly'; +import { OpenCommandIssueType, OpenIssueCommandArg } from '../../common/commands/types'; +import { SNYK_OPEN_ISSUE_COMMAND } from '../../common/constants/commands'; +import { IDE_NAME } from '../../common/constants/general'; +import { CodeActionsProvider } from '../../common/editor/codeActionsProvider'; +import { Issue, IssueSeverity, OssIssueData } from '../../common/languageServer/types'; +import { ProductResult } from '../../common/services/productService'; +import { ICodeActionAdapter, ICodeActionKindAdapter } from '../../common/vscode/codeAction'; +import { IVSCodeLanguages } from '../../common/vscode/languages'; +import { CodeActionContext } from '../../common/vscode/types'; +import { DIAGNOSTICS_OSS_COLLECTION_NAME_LS } from '../../snykCode/constants/analysis'; + +export class OssCodeActionsProvider extends CodeActionsProvider { + constructor( + private readonly languages: IVSCodeLanguages, + private readonly codeActionAdapter: ICodeActionAdapter, + codeActionKindAdapter: ICodeActionKindAdapter, + issues: Readonly>, + analytics: IAnalytics, + ) { + super(issues, codeActionKindAdapter, analytics); + } + + override provideCodeActions( + document: TextDocument, + _clickedRange: Range, + context: CodeActionContext, + ): CodeAction[] | undefined { + // is there a better way to get the folder path? + const folderPath = document.uri.fsPath.split('/').slice(0, -1).join('/'); + if (!folderPath) { + return; + } + + const vulnerabilities = this.getVulnerabilities(folderPath, context); + if (!vulnerabilities) { + return; + } + + const mostSevereVulnerability = this.getMostSevereVulnerability(vulnerabilities); + if (!mostSevereVulnerability) { + return; + } + + const codeActions = this.getActions( + folderPath, + document, + mostSevereVulnerability, + this.getIssueRange(mostSevereVulnerability), + ); + const analyticsType = this.getAnalyticsActionTypes(); + + this.analytics.logQuickFixIsDisplayed({ + quickFixType: analyticsType, + ide: IDE_NAME, + }); + + return codeActions; + } + + getActions( + _folderPath: string, + _document: TextDocument, + mostSevereVulnerability: Issue, + _issueRange?: Range, + ): CodeAction[] { + const openIssueAction = this.createMostSevereVulnerabilityAction(mostSevereVulnerability); + + // returns list of actions, all new actions should be added to this list + return [openIssueAction]; + } + + getAnalyticsActionTypes(): [string, ...string[]] & [SupportedQuickFixProperties, ...SupportedQuickFixProperties[]] { + return ['Show Suggestion']; + } + + // noop + getIssueRange(_issue: Issue): Range { + return this.languages.createRange(0, 0, 0, 0); + } + + private createMostSevereVulnerabilityAction(mostSevereVulnerability: Issue): CodeAction { + // create the CodeAction + const openIssueAction = this.codeActionAdapter.create( + `Show the most severe vulnerability [${mostSevereVulnerability.id}] (Snyk)`, + this.providedCodeActionKinds[0], + ); + + openIssueAction.command = { + command: SNYK_OPEN_ISSUE_COMMAND, + title: SNYK_OPEN_ISSUE_COMMAND, + arguments: [ + { + issueType: OpenCommandIssueType.OssVulnerability, + issue: mostSevereVulnerability, + } as OpenIssueCommandArg, + ], + }; + + return openIssueAction; + } + + private issueSeverityToRanking(severity: IssueSeverity): number { + switch (severity) { + case IssueSeverity.Critical: + return 3; + case IssueSeverity.High: + return 2; + case IssueSeverity.Medium: + return 1; + default: + return 0; + } + } + + private getVulnerabilities(folderPath: string, context: CodeActionContext): Issue[] | undefined { + // get all OSS vulnerabilities for the folder + const ossResult = this.issues.get(folderPath); + if (!ossResult || ossResult instanceof Error) { + return; + } + + // get all OSS diagnostics; these contain the relevant vulnerabilities + const ossDiagnostics = context.diagnostics.filter(d => d.source === DIAGNOSTICS_OSS_COLLECTION_NAME_LS); + if (!ossDiagnostics.length) { + return; + } + + // find the corresponding Issue objects from ossDiagnostics + const vulnerabilities: Issue[] = []; + for (const diagnostic of ossDiagnostics) { + const vulnerability = ossResult.find( + ossIssue => ossIssue.id === (diagnostic.code as { value: string | number; target: Uri }).value, + ); + if (!vulnerability) { + continue; + } + vulnerabilities.push(vulnerability); + } + + return vulnerabilities; + } + + private getMostSevereVulnerability(vulnerabilities: Issue[]): Issue | undefined { + // iterate vulnerabilities and get the most severe one + // if there are multiple of the same severity, get the first one + let highestSeverity = this.issueSeverityToRanking(IssueSeverity.Low); + let mostSevereVulnerability: Issue | undefined; + + for (const vulnerability of vulnerabilities) { + if (this.issueSeverityToRanking(vulnerability.severity) > highestSeverity) { + highestSeverity = this.issueSeverityToRanking(vulnerability.severity); + mostSevereVulnerability = vulnerability; + } + } + + return mostSevereVulnerability; + } +} diff --git a/src/snyk/snykOss/providers/ossVulnerabilityCountProvider.ts b/src/snyk/snykOss/providers/ossVulnerabilityCountProvider.ts index 3d5a2ba03..0ecd40aa7 100644 --- a/src/snyk/snykOss/providers/ossVulnerabilityCountProvider.ts +++ b/src/snyk/snykOss/providers/ossVulnerabilityCountProvider.ts @@ -2,7 +2,7 @@ import { CliError } from '../../cli/services/cliService'; import { Language, languageToString } from '../../common/types'; import { ILanguageClientAdapter } from '../../common/vscode/languageClient'; import { ITextDocumentAdapter } from '../../common/vscode/textdocument'; -import { InlineValueText, LanguageClient, LSPTextDocument } from '../../common/vscode/types'; +import { InlineValueText, LSPTextDocument } from '../../common/vscode/types'; import { IUriAdapter } from '../../common/vscode/uri'; import { convertIssue, isResultCliError, OssFileResult, OssResultBody } from '../interfaces'; import { OssService } from '../ossService'; diff --git a/src/snyk/snykOss/providers/ossVulnerabilityTreeProvider.ts b/src/snyk/snykOss/providers/ossVulnerabilityTreeProvider.ts index f89fd927a..1b04c0419 100644 --- a/src/snyk/snykOss/providers/ossVulnerabilityTreeProvider.ts +++ b/src/snyk/snykOss/providers/ossVulnerabilityTreeProvider.ts @@ -14,7 +14,7 @@ import { ProductIssueTreeProvider } from '../../common/views/issueTreeProvider'; import { TreeNode } from '../../common/views/treeNode'; import { IVSCodeLanguages } from '../../common/vscode/languages'; import { messages } from '../constants/messages'; -import { OssIssueCommandArgLanguageServer } from '../interfaces'; +import { OssIssueCommandArg } from '../interfaces'; export default class OssIssueTreeProvider extends ProductIssueTreeProvider { constructor( @@ -201,10 +201,7 @@ export default class OssIssueTreeProvider extends ProductIssueTreeProvider, - filteredVulns: Issue[], - ): OssIssueCommandArgLanguageServer { + getOssIssueCommandArg(vuln: Issue, filteredVulns: Issue[]): OssIssueCommandArg { const matchingIdVulnerabilities = filteredVulns.filter(v => v.id === vuln.id); let overviewHtml = ''; diff --git a/src/test/unit/common/editor/codeActionsProvider.test.ts b/src/test/unit/common/editor/codeActionsProvider.test.ts index c30574e7a..e6a2fe81b 100644 --- a/src/test/unit/common/editor/codeActionsProvider.test.ts +++ b/src/test/unit/common/editor/codeActionsProvider.test.ts @@ -6,7 +6,7 @@ import { CodeActionsProvider } from '../../../../snyk/common/editor/codeActionsP import { Issue } from '../../../../snyk/common/languageServer/types'; import { WorkspaceFolderResult } from '../../../../snyk/common/services/productService'; import { ICodeActionKindAdapter } from '../../../../snyk/common/vscode/codeAction'; -import { CodeAction, Range, TextDocument } from '../../../../snyk/common/vscode/types'; +import { CodeAction, CodeActionContext, Range, TextDocument } from '../../../../snyk/common/vscode/types'; type ProductData = { issueType: string; @@ -84,7 +84,7 @@ suite('Code Actions Provider', () => { } as unknown as TextDocument; // act - const codeActions = issuesActionsProvider.provideCodeActions(document, {} as Range); + const codeActions = issuesActionsProvider.provideCodeActions(document, {} as Range, {} as CodeActionContext); // verify strictEqual(codeActions?.length, 2); @@ -101,7 +101,7 @@ suite('Code Actions Provider', () => { } as unknown as TextDocument; // act - issuesActionsProvider.provideCodeActions(document, {} as Range); + issuesActionsProvider.provideCodeActions(document, {} as Range, {} as CodeActionContext); // verify strictEqual(logQuickFixIsDisplayed.calledOnce, true); diff --git a/src/test/unit/snykCode/codeActions/codeIssuesActionsProvider.test.ts b/src/test/unit/snykCode/codeActions/codeIssuesActionsProvider.test.ts index 0a5870c19..d36ae1504 100644 --- a/src/test/unit/snykCode/codeActions/codeIssuesActionsProvider.test.ts +++ b/src/test/unit/snykCode/codeActions/codeIssuesActionsProvider.test.ts @@ -6,7 +6,7 @@ import { CodeIssueData, Issue } from '../../../../snyk/common/languageServer/typ import { WorkspaceFolderResult } from '../../../../snyk/common/services/productService'; import { ICodeActionAdapter, ICodeActionKindAdapter } from '../../../../snyk/common/vscode/codeAction'; import { IVSCodeLanguages } from '../../../../snyk/common/vscode/languages'; -import { CodeActionKind, Range, TextDocument } from '../../../../snyk/common/vscode/types'; +import { CodeActionContext, CodeActionKind, Range, TextDocument } from '../../../../snyk/common/vscode/types'; import { SnykCodeActionsProvider } from '../../../../snyk/snykCode/codeActions/codeIssuesActionsProvider'; import { IssueUtils } from '../../../../snyk/snykCode/utils/issueUtils'; @@ -68,7 +68,7 @@ suite('Snyk Code actions provider', () => { } as unknown as TextDocument; // act - const codeActions = issuesActionsProvider.provideCodeActions(document, {} as Range); + const codeActions = issuesActionsProvider.provideCodeActions(document, {} as Range, {} as CodeActionContext); // verify strictEqual(codeActions?.length, 3); @@ -86,7 +86,7 @@ suite('Snyk Code actions provider', () => { } as unknown as TextDocument; // act - issuesActionsProvider.provideCodeActions(document, {} as Range); + issuesActionsProvider.provideCodeActions(document, {} as Range, {} as CodeActionContext); // verify strictEqual(logQuickFixIsDisplayed.calledOnce, true); diff --git a/src/test/unit/snykIac/codeActions/iacCodeActionsProvider.test.ts b/src/test/unit/snykIac/codeActions/iacCodeActionsProvider.test.ts index 8d59650a0..201207332 100644 --- a/src/test/unit/snykIac/codeActions/iacCodeActionsProvider.test.ts +++ b/src/test/unit/snykIac/codeActions/iacCodeActionsProvider.test.ts @@ -6,7 +6,7 @@ import { IacIssueData, Issue } from '../../../../snyk/common/languageServer/type import { WorkspaceFolderResult } from '../../../../snyk/common/services/productService'; import { ICodeActionAdapter, ICodeActionKindAdapter } from '../../../../snyk/common/vscode/codeAction'; import { IVSCodeLanguages } from '../../../../snyk/common/vscode/languages'; -import { CodeActionKind, Range, TextDocument } from '../../../../snyk/common/vscode/types'; +import { CodeActionContext, CodeActionKind, Range, TextDocument } from '../../../../snyk/common/vscode/types'; import { IacCodeActionsProvider } from '../../../../snyk/snykIac/codeActions/iacCodeActionsProvider'; import { IacIssue } from '../../../../snyk/snykIac/issue'; @@ -66,7 +66,7 @@ suite('IaC code actions provider', () => { } as unknown as TextDocument; // act - const codeActions = issuesActionsProvider.provideCodeActions(document, {} as Range); + const codeActions = issuesActionsProvider.provideCodeActions(document, {} as Range, {} as CodeActionContext); // verify strictEqual(codeActions?.length, 1); @@ -82,7 +82,7 @@ suite('IaC code actions provider', () => { } as unknown as TextDocument; // act - issuesActionsProvider.provideCodeActions(document, {} as Range); + issuesActionsProvider.provideCodeActions(document, {} as Range, {} as CodeActionContext); // verify strictEqual(logQuickFixIsDisplayed.calledOnce, true); diff --git a/src/test/unit/snykOss/ossService.test.ts b/src/test/unit/snykOss/ossService.test.ts index 1dc50a3fe..46f3e13db 100644 --- a/src/test/unit/snykOss/ossService.test.ts +++ b/src/test/unit/snykOss/ossService.test.ts @@ -7,11 +7,12 @@ import { ILanguageServer } from '../../../snyk/common/languageServer/languageSer import { OssIssueData, ScanProduct, ScanStatus } from '../../../snyk/common/languageServer/types'; import { IProductService } from '../../../snyk/common/services/productService'; import { IViewManagerService } from '../../../snyk/common/services/viewManagerService'; +import { ICodeActionAdapter, ICodeActionKindAdapter } from '../../../snyk/common/vscode/codeAction'; import { ExtensionContext } from '../../../snyk/common/vscode/extensionContext'; import { IVSCodeLanguages } from '../../../snyk/common/vscode/languages'; import { IVSCodeWorkspace } from '../../../snyk/common/vscode/workspace'; +import { IOssSuggestionWebviewProvider } from '../../../snyk/snykOss/interfaces'; import { OssService } from '../../../snyk/snykOss/ossService'; -import { OssDetailPanelProvider } from '../../../snyk/snykOss/providers/ossDetailPanelProvider'; import { LanguageServerMock } from '../mocks/languageServer.mock'; import { LoggerMock } from '../mocks/logger.mock'; @@ -31,7 +32,9 @@ suite('OSS Service', () => { service = new OssService( {} as ExtensionContext, {} as IConfiguration, - {} as OssDetailPanelProvider, + {} as IOssSuggestionWebviewProvider, + {} as ICodeActionAdapter, + { getQuickFix: sinon.fake() } as ICodeActionKindAdapter, viewManagerService, { getWorkspaceFolders: () => [''], diff --git a/src/test/unit/snykOss/providers/ossCodeActionsProvider.test.ts b/src/test/unit/snykOss/providers/ossCodeActionsProvider.test.ts new file mode 100644 index 000000000..5a6ad2c26 --- /dev/null +++ b/src/test/unit/snykOss/providers/ossCodeActionsProvider.test.ts @@ -0,0 +1,118 @@ +import assert from 'assert'; +import sinon from 'sinon'; +import { CodeAction, CodeActionContext, CodeActionKind, Range, TextDocument } from 'vscode'; +import { IAnalytics } from '../../../../snyk/common/analytics/itly'; +import { OpenCommandIssueType, OpenIssueCommandArg } from '../../../../snyk/common/commands/types'; +import { SNYK_OPEN_ISSUE_COMMAND } from '../../../../snyk/common/constants/commands'; +import { Issue, IssueSeverity, OssIssueData } from '../../../../snyk/common/languageServer/types'; +import { WorkspaceFolderResult } from '../../../../snyk/common/services/productService'; +import { ICodeActionAdapter, ICodeActionKindAdapter } from '../../../../snyk/common/vscode/codeAction'; +import { IVSCodeLanguages } from '../../../../snyk/common/vscode/languages'; +import { OssCodeActionsProvider } from '../../../../snyk/snykOss/providers/ossCodeActionsProvider'; + +suite('OSS code actions provider', () => { + let ossActionsProvider: OssCodeActionsProvider; + let logQuickFixIsDisplayed: sinon.SinonSpy; + let rangeMock: Range; + + setup(() => { + const ossResults = new Map>(); + ossResults.set('folderName', [ + { + filePath: '//folderName//package.json', + additionalData: {}, + } as unknown as Issue, + ]); + + logQuickFixIsDisplayed = sinon.fake(); + const analytics = { + logQuickFixIsDisplayed, + } as unknown as IAnalytics; + + const codeActionAdapter = { + create: (_: string, _kind?: CodeActionKind) => ({ + command: {}, + }), + } as ICodeActionAdapter; + + const codeActionKindAdapter = { + getQuickFix: sinon.fake(), + } as ICodeActionKindAdapter; + + rangeMock = { + contains: () => true, + } as unknown as Range; + + ossActionsProvider = new OssCodeActionsProvider( + {} as IVSCodeLanguages, + codeActionAdapter, + codeActionKindAdapter, + ossResults, + analytics, + ); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Provides the most severe vulnerability CodeAction', () => { + // arrange + const document = { + uri: { + fsPath: '//folderName//package.json', + }, + } as unknown as TextDocument; + + const clickedRange = {} as Range; + const context = {} as CodeActionContext; + + const vulnerabilities = [ + { + id: 'vulnerability1', + severity: IssueSeverity.High, + }, + { + id: 'vulnerability2', + severity: IssueSeverity.Medium, + }, + { + id: 'vulnerability3', + severity: IssueSeverity.Critical, + }, + ] as Issue[]; + + const mostSevereVulnerability = { + id: 'vulnerability3', + severity: IssueSeverity.Critical, + } as Issue; + + const codeActions = [ + { + command: { + command: SNYK_OPEN_ISSUE_COMMAND, + title: SNYK_OPEN_ISSUE_COMMAND, + arguments: [ + { + issueType: OpenCommandIssueType.OssVulnerability, + issue: mostSevereVulnerability, + } as OpenIssueCommandArg, + ], + }, + }, + ] as CodeAction[]; + + sinon.stub(ossActionsProvider, 'getIssueRange').returns(rangeMock); + // stubbing private methods workaround is to cast to any + sinon.stub(ossActionsProvider, 'getVulnerabilities').returns(vulnerabilities); + sinon.stub(ossActionsProvider, 'getMostSevereVulnerability').returns(mostSevereVulnerability); + sinon.stub(ossActionsProvider, 'getActions').returns(codeActions); + + // act + const result = ossActionsProvider.provideCodeActions(document, clickedRange, context); + + // assert + sinon.assert.calledOnce(logQuickFixIsDisplayed); + assert.deepStrictEqual(result, codeActions); + }); +});