From 48e58de875ba4bb4a11bbbacc97a4d8b91d7bcde Mon Sep 17 00:00:00 2001 From: Ilona Shishov Date: Wed, 10 Jul 2024 14:46:11 +0300 Subject: [PATCH] test: improve coverage Signed-off-by: Ilona Shishov --- test/exhortServices.test.ts | 63 ++++++++- test/imageAnalysis.test.ts | 4 +- test/rhda.test.ts | 20 +-- test/sarif/convert.test.ts | 247 +++++++++++++++++++++++++++++++++--- test/sarif/results.test.ts | 86 +++++++++++++ test/sarif/upload.test.ts | 55 +++++++- test/utils.test.ts | 26 ++-- 7 files changed, 449 insertions(+), 52 deletions(-) diff --git a/test/exhortServices.test.ts b/test/exhortServices.test.ts index e6673d7..3f88177 100644 --- a/test/exhortServices.test.ts +++ b/test/exhortServices.test.ts @@ -1,16 +1,27 @@ -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; import { execSync } from 'child_process'; +import exhort from '@rhecosystemappeng/exhort-javascript-api'; import { UTM_SOURCE } from '../src/constants'; -import { imageAnalysisService } from '../src/exhortServices'; +import { imageAnalysisService, stackAnalysisService } from '../src/exhortServices'; vi.mock('child_process', () => ({ execSync: vi.fn(), })); + +vi.mock('@rhecosystemappeng/exhort-javascript-api', async (importOriginal) => { + const actual: any = await importOriginal(); + return { + ...actual, + default: { + ...actual.default, + stackAnalysis: vi.fn(), + }, + }; +}); describe('imageAnalysisService', () => { const options = { - // RHDA_TOKEN: string; RHDA_SOURCE: UTM_SOURCE, EXHORT_SYFT_PATH: '/path/to/syft', EXHORT_SYFT_CONFIG_PATH: '/path/to/syft_config', @@ -20,7 +31,11 @@ describe('imageAnalysisService', () => { EXHORT_PODMAN_PATH: '/path/to/podman', EXHORT_IMAGE_PLATFORM: 'platform', }; - const images = [{ image: 'image1', platform: 'platform1' }]; + const images = [{ image: 'image1', platform: 'platform1' }, { image: 'image2', platform: '' }]; + + beforeEach(() => { + vi.clearAllMocks(); + }); it('should execute image analysis and return HTML result', async () => { const mockExecSyncResult = 'Mock HTML result'; @@ -29,7 +44,7 @@ describe('imageAnalysisService', () => { const result = await imageAnalysisService(images, options); const expectedJarPath = `${process.cwd()}/javaApiAdapter/exhort-java-api-adapter-1.0-SNAPSHOT-jar-with-dependencies.jar`; - const expectedCommand = `java -DRHDA_SOURCE=github-actions -DEXHORT_SYFT_PATH=/path/to/syft -DEXHORT_SYFT_CONFIG_PATH=/path/to/syft_config -DEXHORT_SKOPEO_PATH=/path/to/skopeo -DEXHORT_SKOPEO_CONFIG_PATH=/path/to/skopeo_config -DEXHORT_DOCKER_PATH=/path/to/docker -DEXHORT_PODMAN_PATH=/path/to/podman -DEXHORT_IMAGE_PLATFORM=platform -jar ${expectedJarPath} json image1^^platform1`; + const expectedCommand = `java -DRHDA_SOURCE=github-actions -DEXHORT_SYFT_PATH=/path/to/syft -DEXHORT_SYFT_CONFIG_PATH=/path/to/syft_config -DEXHORT_SKOPEO_PATH=/path/to/skopeo -DEXHORT_SKOPEO_CONFIG_PATH=/path/to/skopeo_config -DEXHORT_DOCKER_PATH=/path/to/docker -DEXHORT_PODMAN_PATH=/path/to/podman -DEXHORT_IMAGE_PLATFORM=platform -jar ${expectedJarPath} json image1^^platform1 image2`; expect(execSync).toHaveBeenCalledWith(expectedCommand, { maxBuffer: 1000 * 1000 * 10, }); @@ -47,4 +62,40 @@ describe('imageAnalysisService', () => { }); }); -// TODO: stackAnalysisService Unit Test \ No newline at end of file +describe('stackAnalysisService', () => { + const mockPathToManifest = 'path/to/manifest'; + const mockOptions = { someOption: 'someValue' }; + const mockReport: exhort.AnalysisReport = { analysis: 'report' } as exhort.AnalysisReport; + const mockError: Error = new Error('Analysis failed'); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return the stack analysis report in JSON format', async () => { + vi.mocked(exhort.stackAnalysis).mockResolvedValue(mockReport); + + const result = await stackAnalysisService(mockPathToManifest, mockOptions); + + expect(result).toEqual(mockReport); + expect(exhort.stackAnalysis).toHaveBeenCalledWith( + mockPathToManifest, + false, + mockOptions + ); + }); + + it('should throw an error if the stack analysis fails', async () => { + vi.mocked(exhort.stackAnalysis).mockRejectedValue(mockError); + + await expect(stackAnalysisService(mockPathToManifest, mockOptions)).rejects.toThrow( + mockError + ); + + expect(exhort.stackAnalysis).toHaveBeenCalledWith( + mockPathToManifest, + false, + mockOptions + ); + }); +}); \ No newline at end of file diff --git a/test/imageAnalysis.test.ts b/test/imageAnalysis.test.ts index 6a4550b..09243c5 100644 --- a/test/imageAnalysis.test.ts +++ b/test/imageAnalysis.test.ts @@ -13,11 +13,11 @@ vi.mock('../src/exhortServices', () => ({ })); describe('executeDockerImageAnalysis', () => { - const mockFileContent = 'ARG TEST_ARG=14\nFROM node:${TEST_ARG}\nFROM --platform=linux python:3.8 as app\nFROM scratch\n# hello world'; + const mockFileContent = 'ARG TEST_ARG1=node\nARG TEST_ARG2=14\nFROM ${TEST_ARG1}:$TEST_ARG2\nFROM --platform=linux python:3.8 as app\nFROM scratch\n# hello world'; const filePath = '/mock/path/to/Dockerfile'; beforeEach(() => { - vi.resetModules(); + vi.clearAllMocks(); vi.mocked(fs.readFileSync).mockReturnValue(mockFileContent); }); diff --git a/test/rhda.test.ts b/test/rhda.test.ts index 8bd7f57..fb8b38c 100644 --- a/test/rhda.test.ts +++ b/test/rhda.test.ts @@ -32,9 +32,9 @@ describe('generateRHDAReport', () => { const rhdaReportJsonFilePath = `${process.cwd()}/report.json`; beforeEach(() => { - vi.resetModules(); + vi.clearAllMocks(); - vi.spyOn(ghCore, 'getInput').mockImplementation((name) => { + vi.mocked(ghCore.getInput).mockImplementation((name) => { const inputs = { [Inputs.MATCH_MANIFEST_VERSION]: 'true', [Inputs.USE_PYTHON_VIRTUAL_ENVIRONMENT]: 'false', @@ -54,20 +54,15 @@ describe('generateRHDAReport', () => { return inputs[name]; }); - vi.spyOn(exhortServices, 'stackAnalysisService').mockResolvedValue(rhdaReportJson); + vi.mocked(exhortServices.stackAnalysisService).mockResolvedValue(rhdaReportJson); - vi.spyOn(imageAnalysis, 'executeDockerImageAnalysis').mockResolvedValue(rhdaReportJson); - - vi.spyOn(utils, 'writeToFile').mockResolvedValue(undefined); - - vi.spyOn(ghCore, 'info').mockImplementation(() => {}); - - vi.spyOn(ghCore, 'setOutput').mockImplementation(() => {}); + vi.mocked(imageAnalysis.executeDockerImageAnalysis).mockResolvedValue(rhdaReportJson); }); it('should generate the RHDA report for non-Docker ecosystem', async () => { const result = await generateRHDAReport(manifestFilePath, MAVEN); + expect(ghCore.info).toBeCalledWith(`⏳ Analysing dependencies...`); expect(exhortServices.stackAnalysisService).toHaveBeenCalledWith(manifestFilePath, { RHDA_SOURCE: UTM_SOURCE, MATCH_MANIFEST_VERSIONS: 'true', @@ -88,7 +83,9 @@ describe('generateRHDAReport', () => { JSON.stringify(rhdaReportJson, null, 4), rhdaReportJsonFilePath, ); + expect(ghCore.info).toBeCalledWith(`✍️ Setting output "${Outputs.RHDA_REPORT_JSON}" to ${rhdaReportJsonFilePath}`); expect(ghCore.setOutput).toHaveBeenCalledWith(Outputs.RHDA_REPORT_JSON, rhdaReportJson); + expect(ghCore.info).toBeCalledWith(`✅ Successfully generated Red Had Dependency Analytics report`); expect(result).toEqual({ rhdaReportJson: rhdaReportJson, rhdaReportJsonFilePath: rhdaReportJsonFilePath, @@ -98,12 +95,15 @@ describe('generateRHDAReport', () => { it('should generate the RHDA report for Docker ecosystem', async () => { const result = await generateRHDAReport(manifestFilePath, DOCKER); + expect(ghCore.info).toBeCalledWith(`⏳ Analysing dependencies...`); expect(imageAnalysis.executeDockerImageAnalysis).toHaveBeenCalledWith(manifestFilePath); expect(utils.writeToFile).toHaveBeenCalledWith( JSON.stringify(rhdaReportJson, null, 4), rhdaReportJsonFilePath, ); + expect(ghCore.info).toBeCalledWith(`✍️ Setting output "${Outputs.RHDA_REPORT_JSON}" to ${rhdaReportJsonFilePath}`); expect(ghCore.setOutput).toHaveBeenCalledWith(Outputs.RHDA_REPORT_JSON, rhdaReportJson); + expect(ghCore.info).toBeCalledWith(`✅ Successfully generated Red Had Dependency Analytics report`); expect(result).toEqual({ rhdaReportJson: rhdaReportJson, rhdaReportJsonFilePath: rhdaReportJsonFilePath, diff --git a/test/sarif/convert.test.ts b/test/sarif/convert.test.ts index b5d9eef..3da5943 100644 --- a/test/sarif/convert.test.ts +++ b/test/sarif/convert.test.ts @@ -19,10 +19,6 @@ vi.mock('../../src/sarif/results', () => ({ rhdaToResult: vi.fn(), })); -vi.mock('../utils.js', () => ({ - isDefined: vi.fn((obj, key) => obj[key] !== undefined), -})); - describe('resolveDependencyFromReference', () => { it('should correctly resolve dependency from reference', () => { const ref = 'pkg:npm/lodash@4.17.20'; @@ -171,7 +167,7 @@ describe('generateSarif', () => { ecosystem: 'npm', providerId: 'provider1', sourceId: 'source1', - issues: [expectedIssueData], + issues: [expectedIssueData], transitives: null, recommendationRef: '' }; @@ -185,7 +181,7 @@ describe('generateSarif', () => { ecosystem: 'npm', providerId: 'provider1', sourceId: 'source1', - issues: [expectedIssueData], + issues: [expectedIssueData], transitives: [expectedTransitiveDependencyData], recommendationRef: '' }; @@ -204,7 +200,7 @@ describe('generateSarif', () => { expect(vulSeverity).toBe('error'); }); - it('should generate SARIF from RHDA report JSON for MAVEN ecosystem', async () => { + it('should generate SARIF from RHDA report JSON with only transitive vulnerabilities', async () => { const rhdaReportJson = { scanned: { total: 2, @@ -224,6 +220,106 @@ describe('generateSarif', () => { medium: 0, low: 0 }, + dependencies: [ + { + ref: 'pkg:npm/lodash@4.17.20', + issues: null, + transitive: [transitiveVulnerability], + }, + ], + }, + }, + }, + }, + }; + const ecosystem = 'npm'; + const manifestData = ` + { + "dependencies": { + "lodash": "4.17.20", + } + } + `; + + vi.mocked(fs.readFileSync).mockReturnValue(manifestData); + + const { sarifObject, vulSeverity } = await convert.generateSarif( + rhdaReportJson, + manifestFilePath, + ecosystem, + ); + + const expectedIssueData: types.IIssue = { + id: 'CVE-123', + severity: 'CRITICAL', + title: 'Templates do not properly consider backticks.', + cves: ['CVE-123'], + cvss: {cvss: 'CVSS:3.1'}, + remediation: {trustedContent: null} + }; + + const expectedTransitiveDependencyData: types.IDependencyData = { + imageRef: undefined, + depRef: 'pkg:npm/lodash@4.17.20', + depGroup: undefined, + depName: 'lodash', + depVersion: '4.17.20', + ecosystem: 'npm', + providerId: 'provider1', + sourceId: 'source1', + issues: [expectedIssueData], + transitives: null, + recommendationRef: '' + }; + + const expectedDependencyData: types.IDependencyData = { + imageRef: undefined, + depRef: 'pkg:npm/lodash@4.17.20', + depGroup: undefined, + depName: 'lodash', + depVersion: '4.17.20', + ecosystem: 'npm', + providerId: 'provider1', + sourceId: 'source1', + issues: null, + transitives: [expectedTransitiveDependencyData], + recommendationRef: '' + }; + + expect(fs.readFileSync).toHaveBeenCalledWith(manifestFilePath, 'utf-8'); + + expect(result.rhdaToResult).toHaveBeenCalledWith( + expectedDependencyData, + manifestFilePath, + 4, + true + ); + + expect(sarifObject).toStrictEqual(mockSarif); + + expect(vulSeverity).toBe('error'); + }); + + it('should generate SARIF from RHDA report JSON for MAVEN ecosystem', async () => { + const rhdaReportJson = { + scanned: { + total: 2, + direct: 1, + transitive: 1 + }, + providers: { + provider1: { + status: { + ok: true + }, + sources: { + source1: { + summary: { + critical: 0, + high: 1, + medium: 0, + low: 0 + }, dependencies: [ { ref: 'pkg:maven/log4j/log4j@1.2.17', @@ -300,9 +396,9 @@ describe('generateSarif', () => { sources: { source1: { summary: { - critical: 1, + critical: 0, high: 0, - medium: 0, + medium: 1, low: 0 }, dependencies: [ @@ -334,6 +430,11 @@ describe('generateSarif', () => { groupArg = 'log4j' } + ext + { + fakeArg = 'fake' + } + repositories { mavenCentral() } @@ -386,7 +487,7 @@ describe('generateSarif', () => { 1, expectedDependencyData1, manifestFilePath, - 17, + 22, false ); @@ -394,18 +495,18 @@ describe('generateSarif', () => { 2, expectedDependencyData2, manifestFilePath, - 18, + 23, false ); expect(sarifObject).toStrictEqual(mockSarif); - expect(vulSeverity).toBe('error'); + expect(vulSeverity).toBe('warning'); }); it('should generate SARIF from RHDA report JSON for Docker ecosystem', async () => { const rhdaReportJson = { - 'node:14': { + 'node:latest': { providers: { provider1: { status: { @@ -414,10 +515,10 @@ describe('generateSarif', () => { sources: { source1: { summary: { - critical: 1, + critical: 0, high: 0, medium: 0, - low: 0 + low: 1 }, dependencies: [ { @@ -434,8 +535,8 @@ describe('generateSarif', () => { }; const ecosystem = 'docker'; const manifestData = ` - ARG TEST_ARG=14 - FROM node:\${TEST_ARG} + ARG TEST_ARG=node + FROM \${TEST_ARG} FROM --platform=linux python:3.8 as app FROM scratch # hello world @@ -453,7 +554,7 @@ describe('generateSarif', () => { expect(fs.readFileSync).toHaveBeenCalledWith(manifestFilePath, 'utf-8'); const expectedDependencyData: types.IDependencyData = { - imageRef: 'node:14', + imageRef: 'node:latest', depRef: 'pkg:npm/log4j@1.2.17', depGroup: undefined, depName: 'log4j', @@ -473,7 +574,7 @@ describe('generateSarif', () => { ); expect(sarifObject).toStrictEqual(mockSarif); - expect(vulSeverity).toBe('error'); + expect(vulSeverity).toBe('warning'); }); it('should generate SARIF from RHDA report JSON with no vulnerabilities', async () => { @@ -546,6 +647,58 @@ describe('generateSarif', () => { expect(vulSeverity).toBe('none'); }); + it('should handle RHDA report JSON where no ref is defined', async () => { + const rhdaReportJson = { + scanned: { + total: 2, + direct: 1, + transitive: 1 + }, + providers: { + provider1: { + status: { + ok: true + }, + sources: { + source1: { + summary: { + critical: 1, + high: 0, + medium: 0, + low: 0 + }, + dependencies: [{}], + }, + }, + }, + }, + }; + const ecosystem = 'npm'; + const manifestData = ` + { + "dependencies": { + "lodash": "1.2.3", + } + } + `; + + vi.mocked(fs.readFileSync).mockReturnValue(manifestData); + + const { sarifObject, vulSeverity } = await convert.generateSarif( + rhdaReportJson, + manifestFilePath, + ecosystem, + ); + + expect(fs.readFileSync).toHaveBeenCalledWith(manifestFilePath, 'utf-8'); + + expect(result.rhdaToResult).toHaveBeenCalledTimes(0); + + expect(sarifObject).toStrictEqual(mockSarif); + + expect(vulSeverity).toBe('error'); + }); + it('should handle RHDA report JSON where a vulnerability provider has failed', async () => { const rhdaReportJson = { scanned: { @@ -589,4 +742,60 @@ describe('generateSarif', () => { expect(vulSeverity).toBe('none'); }); + + it('should fail to generate SARIF from RHDA report JSON when SARIF schema is not defined', async () => { + const rhdaReportJson = { + scanned: { + total: 2, + direct: 1, + transitive: 1 + }, + providers: { + provider1: { + status: { + ok: true + }, + sources: { + source1: { + summary: { + critical: 1, + high: 0, + medium: 0, + low: 0 + }, + dependencies: [ + { + ref: 'pkg:npm/lodash@4.17.20', + issues: [directVulnerability], + transitive: [transitiveVulnerability], + }, + ], + }, + }, + }, + }, + }; + const ecosystem = 'npm'; + const manifestData = ` + { + "dependencies": { + "lodash": "4.17.20", + } + } + `; + + vi.mocked(fs.readFileSync).mockReturnValue(manifestData); + vi.spyOn(constants, 'SARIF_SCHEMA_URL', 'get').mockReturnValue('' as any) + + try { + await convert.generateSarif( + rhdaReportJson, + manifestFilePath, + ecosystem, + ); + throw new Error('Expected error to be thrown') + } catch (error) { + expect(error.message).toEqual(`No $schema key for SARIF file, cannot proceed.`) + } + }); }); \ No newline at end of file diff --git a/test/sarif/results.test.ts b/test/sarif/results.test.ts index 61d4656..e982c73 100644 --- a/test/sarif/results.test.ts +++ b/test/sarif/results.test.ts @@ -111,6 +111,92 @@ describe('rhdaToResult', () => { expect(results).toStrictEqual(expectedResult) }); + it('should return correct SARIF result for a image dependency with issues', () => { + const refHasIssues = true; + + const TransitiveDependencyData: types.IDependencyData = { + imageRef: 'image:tag', + depRef: 'pkg:ecosystem/artifact1', + depGroup: '', + depName: 'artifact1', + depVersion: '', + ecosystem: 'ecosystem', + providerId: 'providerId', + sourceId: 'sourceId', + issues: [issueData], + transitives: null, + recommendationRef: 'pkg:ecosystem/groupId/artifact@recommendedversion' + }; + + const dependencyData: types.IDependencyData = { + imageRef: 'image:tag', + depRef: 'pkg:ecosystem/groupId/artifact@version', + depGroup: 'groupId', + depName: 'groupId/artifact', + depVersion: 'version', + ecosystem: 'ecosystem', + providerId: 'providerId', + sourceId: 'sourceId', + issues: [issueData], + transitives: [TransitiveDependencyData], + recommendationRef: 'pkg:ecosystem/groupId/artifact@recommendedversion' + }; + + const expectedResult = [ + [{ + "ruleId":issueData.id, + "message":{ + "text":`This line introduces a "${issueData.title}" vulnerability with ` + + `${issueData.severity} severity.\n` + + `Vulnerability data provider is ${dependencyData.providerId}.\n` + + `Vulnerability data source is ${dependencyData.sourceId}.\n` + + `Vulnerable dependency is ${dependencyData.depGroup}/${dependencyData.depName} version ${dependencyData.depVersion}.\n` + + `Recommended remediation version: ${resolveVersionFromReference(issueData.remediation.trustedContent ? issueData.remediation.trustedContent.ref : '')}` + }, + "locations":[ + { + "physicalLocation":{ + "artifactLocation":{ + "uri":manifestFilePath + }, + "region":{ + "startLine":startLine + } + } + } + ] + }, + { + "ruleId":issueData.id, + "message":{ + "text":`This line introduces a "${issueData.title}" vulnerability with ` + + `${issueData.severity} severity.\n` + + `Vulnerability data provider is ${TransitiveDependencyData.providerId}.\n` + + `Vulnerability data source is ${TransitiveDependencyData.sourceId}.\n` + + `Vulnerable transitive dependency is ${TransitiveDependencyData.depName}.\n` + + `Recommended remediation version: ${resolveVersionFromReference(issueData.remediation.trustedContent ? issueData.remediation.trustedContent.ref : '')}` + }, + "locations":[ + { + "physicalLocation":{ + "artifactLocation":{ + "uri":manifestFilePath + }, + "region":{ + "startLine":startLine + } + } + } + ] + }], + ['example rule', 'example rule'] + ] + + const results = rhdaToResult(dependencyData, manifestFilePath, startLine, refHasIssues) + + expect(results).toStrictEqual(expectedResult) + }); + it('should return correct SARIF result for a dependency without issues and with recommendation', () => { const refHasIssues = false; diff --git a/test/sarif/upload.test.ts b/test/sarif/upload.test.ts index dc69090..caea18c 100644 --- a/test/sarif/upload.test.ts +++ b/test/sarif/upload.test.ts @@ -37,7 +37,7 @@ describe('uploadSarifFile', () => { vi.mocked(utils.getEnvVar).mockReturnValue('https://github.com'); }); - it('should upload SARIF file successfully and print security tab link', async () => { + it('should upload SARIF file successfully and print security tab link with branch', async () => { const printSecurityTabLink = true; const ocktokitResponse = { data: { id: 'sarif-id', processing_status: 'complete' }, @@ -64,7 +64,7 @@ describe('uploadSarifFile', () => { { owner: uploadToRepo.owner, repo: uploadToRepo.repo, - ref, + ref: ref, commit_sha: sha, sarif: sarifZipped, started_at: analysisStartTime, @@ -87,6 +87,57 @@ describe('uploadSarifFile', () => { expect(ghCore.info).toHaveBeenNthCalledWith(4, `👀 Review the Code Scanning results in the Security tab: https://github.com/owner/repo/security/code-scanning?${new URLSearchParams('query=is:open sort:created-desc branch:main')}`); }); + it('should upload SARIF file successfully and print security tab link without branch', async () => { + const refWoBranch = 'refs/heads'; + const printSecurityTabLink = true; + const ocktokitResponse = { + data: { id: 'sarif-id', processing_status: 'complete' }, + } + const octokitMock = { + request: vi.fn().mockResolvedValue(ocktokitResponse), + }; + (Octokit as any).mockImplementation(() => octokitMock as any); + + await uploadSarifFile( + ghToken, + sarifPath, + analysisStartTime, + sha, + refWoBranch, + uploadToRepo, + printSecurityTabLink + ); + + expect(utils.zipFile).toHaveBeenCalledWith(sarifPath); + expect(ghCore.info).toHaveBeenNthCalledWith(1, `⬆️ Uploading SARIF to ${uploadToRepo.owner}/${uploadToRepo.repo}`); + expect(octokitMock.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/code-scanning/sarifs', + { + owner: uploadToRepo.owner, + repo: uploadToRepo.repo, + ref: refWoBranch, + commit_sha: sha, + sarif: sarifZipped, + started_at: analysisStartTime, + tool_name: 'Red Hat Dependency Analytics', + } + ); + expect(ghCore.startGroup).toHaveBeenNthCalledWith(1, `⏳ Waiting for SARIF to upload...`); + expect(octokitMock.request).toHaveBeenCalledWith( + 'GET /repos/{owner}/{repo}/code-scanning/sarifs/{sarif_id}', + { + owner: github.context.repo.owner, + repo: github.context.repo.repo, + sarif_id: ocktokitResponse.data.id, + }, + ); + expect(ghCore.endGroup).toHaveBeenCalled(); + expect(ghCore.info).toHaveBeenNthCalledWith(2, `Upload is ${ocktokitResponse.data.processing_status}`); + expect(ghCore.info).toHaveBeenNthCalledWith(3, `✅ Successfully uploaded SARIF file`); + expect(utils.getEnvVar).toHaveBeenCalledWith('GITHUB_SERVER_URL'); + expect(ghCore.info).toHaveBeenNthCalledWith(4, `👀 Review the Code Scanning results in the Security tab: https://github.com/owner/repo/security/code-scanning?${new URLSearchParams('query=is:open sort:created-desc')}`); + }); + it('should upload SARIF file successfully and not print security tab link', async () => { const printSecurityTabLink = false; const ocktokitResponse = { diff --git a/test/utils.test.ts b/test/utils.test.ts index 9e89e6e..7d31074 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -17,7 +17,7 @@ vi.mock('@actions/core', () => ({ describe('getOS', () => { beforeEach(() => { - vi.resetModules(); + vi.clearAllMocks(); }); afterEach(() => { @@ -62,7 +62,7 @@ describe('getOS', () => { describe('getGhToken', () => { beforeEach(() => { - vi.resetModules(); + vi.clearAllMocks(); }); afterEach(() => { @@ -91,7 +91,7 @@ describe('getGhToken', () => { describe('getGitExecutable', () => { beforeEach(() => { - vi.resetModules(); + vi.clearAllMocks(); }); afterEach(() => { @@ -116,7 +116,7 @@ describe('getGitExecutable', () => { describe('getEnvVar', () => { beforeEach(() => { - vi.resetModules(); + vi.clearAllMocks(); }); afterEach(() => { @@ -151,7 +151,7 @@ vi.mock('fs', () => ({ describe('writeToFile', () => { beforeEach(() => { - vi.resetModules(); + vi.clearAllMocks(); }); it('should write data to the specified file path', async () => { @@ -167,7 +167,7 @@ describe('writeToFile', () => { describe('escapeWindowsPathForActionsOutput', () => { beforeEach(() => { - vi.resetModules(); + vi.clearAllMocks(); }); it('should escape backslashes in a Windows path', () => { @@ -205,7 +205,7 @@ vi.mock('zlib', () => ({ describe('zipFile', () => { beforeEach(() => { - vi.resetModules(); + vi.clearAllMocks(); }); it('should zip the file contents and return as base64', async () => { @@ -214,9 +214,9 @@ describe('zipFile', () => { const zippedContent = 'zipped content'; const mockZipped = Buffer.from(zippedContent).toString('base64'); - vi.spyOn(fs, 'readFileSync').mockReturnValue(mockContent); + vi.mocked(fs.readFileSync).mockReturnValue(mockContent); - vi.spyOn(zlib, 'gzipSync').mockReturnValue(Buffer.from(zippedContent)); + vi.mocked(zlib.gzipSync).mockReturnValue(Buffer.from(zippedContent)); const result = await utils.zipFile(mockFile); @@ -229,7 +229,7 @@ describe('zipFile', () => { describe('isDefined', () => { beforeEach(() => { - vi.resetModules(); + vi.clearAllMocks(); }); it('should return true when all keys are defined in the object (with key request)', () => { @@ -296,7 +296,7 @@ vi.mock('@actions/exec', () => ({ describe('execCommand', () => { beforeEach(() => { - vi.resetModules(); + vi.clearAllMocks(); }); it('should execute a command and return the result', async () => { @@ -307,7 +307,7 @@ describe('execCommand', () => { const mockExitCode = 0; const mockStdout = 'hello\n'; const mockStderr = ''; - vi.spyOn(ghExec, 'exec').mockImplementation( + vi.mocked(ghExec.exec).mockImplementation( (commandLine: string, args?: string[] | undefined, options?: ghExec.ExecOptions | undefined) => { options?.listeners?.stdout!(Buffer.from(mockStdout)); options?.listeners?.stderr!(Buffer.from(mockStderr)); @@ -332,7 +332,7 @@ describe('getCommitSha', () => { const mockExitCode = 0; const mockStdout = `${mockSha}\n`; const mockStderr = ''; - vi.spyOn(ghExec, 'exec').mockImplementation( + vi.mocked(ghExec.exec).mockImplementation( (commandLine: string, args?: string[] | undefined, options?: ghExec.ExecOptions | undefined) => { options?.listeners?.stdout!(Buffer.from(mockStdout)); options?.listeners?.stderr!(Buffer.from(mockStderr));