mirror of
https://github.com/imjasonh/govulncheck-action
synced 2026-07-09 09:07:50 +00:00
Updates the project to use ESM modules and replaces ncc with esbuild. This fixes build failures caused by newer @actions/core versions which use strict ESM exports that ncc cannot resolve. Also updates tests to use ESM and dependency injection. Signed-off-by: Jason Hall <imjasonh@gmail.com>
283 lines
7.8 KiB
JavaScript
283 lines
7.8 KiB
JavaScript
import { run } from '../index.js';
|
|
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
|
|
|
|
describe('GitHub Action Integration', () => {
|
|
let mockGovulncheck;
|
|
let mockParser;
|
|
let mockAnnotator;
|
|
let mockCore;
|
|
let originalChdir;
|
|
|
|
beforeEach(() => {
|
|
// Save original process.chdir
|
|
originalChdir = process.chdir;
|
|
process.chdir = jest.fn();
|
|
|
|
// Reset all mocks
|
|
jest.clearAllMocks();
|
|
|
|
// Create mock core
|
|
const mockSummary = {
|
|
addHeading: jest.fn().mockReturnThis(),
|
|
addEOL: jest.fn().mockReturnThis(),
|
|
addRaw: jest.fn().mockReturnThis(),
|
|
addList: jest.fn().mockReturnThis(),
|
|
addTable: jest.fn().mockReturnThis(),
|
|
addCodeBlock: jest.fn().mockReturnThis(),
|
|
addDetails: jest.fn().mockReturnThis(),
|
|
addLink: jest.fn().mockReturnThis(),
|
|
addSeparator: jest.fn().mockReturnThis(),
|
|
write: jest.fn().mockResolvedValue(),
|
|
};
|
|
|
|
mockCore = {
|
|
getInput: jest.fn().mockReturnValue('.'),
|
|
info: jest.fn(),
|
|
warning: jest.fn(),
|
|
setOutput: jest.fn(),
|
|
setFailed: jest.fn(),
|
|
summary: mockSummary,
|
|
};
|
|
|
|
// Create mock dependencies
|
|
mockGovulncheck = {
|
|
install: jest.fn().mockResolvedValue(),
|
|
run: jest.fn().mockResolvedValue({
|
|
output: '',
|
|
errorOutput: '',
|
|
exitCode: 0,
|
|
}),
|
|
};
|
|
|
|
mockParser = {
|
|
parse: jest.fn().mockReturnValue([]),
|
|
extractUniqueModules: jest.fn().mockReturnValue([]),
|
|
extractCallSites: jest.fn().mockReturnValue([]),
|
|
};
|
|
|
|
mockAnnotator = {
|
|
createAnnotations: jest.fn().mockResolvedValue(),
|
|
};
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Restore original process.chdir
|
|
process.chdir = originalChdir;
|
|
});
|
|
|
|
it('should complete successfully with no vulnerabilities', async () => {
|
|
const result = await run({
|
|
core: mockCore,
|
|
govulncheck: mockGovulncheck,
|
|
parser: mockParser,
|
|
annotator: mockAnnotator,
|
|
});
|
|
|
|
expect(mockCore.info).toHaveBeenCalledWith('Running govulncheck...');
|
|
expect(mockCore.info).toHaveBeenCalledWith('Raw govulncheck output length: 0 characters');
|
|
expect(mockCore.info).toHaveBeenCalledWith('Raw output: ');
|
|
|
|
expect(mockGovulncheck.install).toHaveBeenCalled();
|
|
expect(mockGovulncheck.run).toHaveBeenCalled();
|
|
|
|
expect(mockCore.setOutput).toHaveBeenCalledWith('vulnerabilities-found', 'false');
|
|
expect(mockCore.setOutput).toHaveBeenCalledWith('vulnerability-count', '0');
|
|
|
|
expect(result).toEqual({
|
|
vulnerabilities: [],
|
|
hasVulnerabilities: false,
|
|
});
|
|
});
|
|
|
|
it('should handle vulnerabilities found', async () => {
|
|
const mockVulnerabilities = [
|
|
{
|
|
finding: {
|
|
osv: 'GO-2023-1234',
|
|
trace: [{ module: 'example.com/vulnerable' }],
|
|
},
|
|
},
|
|
{
|
|
finding: {
|
|
osv: 'GO-2023-5678',
|
|
trace: [{ module: 'another.com/package' }],
|
|
},
|
|
},
|
|
];
|
|
|
|
mockParser.parse.mockReturnValue(mockVulnerabilities);
|
|
|
|
const result = await run({
|
|
core: mockCore,
|
|
govulncheck: mockGovulncheck,
|
|
parser: mockParser,
|
|
annotator: mockAnnotator,
|
|
});
|
|
|
|
expect(mockCore.warning).toHaveBeenCalledWith('Found 2 vulnerabilities');
|
|
expect(mockCore.setOutput).toHaveBeenCalledWith('vulnerabilities-found', 'true');
|
|
expect(mockCore.setOutput).toHaveBeenCalledWith('vulnerability-count', '2');
|
|
|
|
expect(mockAnnotator.createAnnotations).toHaveBeenCalledWith(
|
|
mockVulnerabilities,
|
|
mockParser,
|
|
'.'
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
vulnerabilities: mockVulnerabilities,
|
|
hasVulnerabilities: true,
|
|
});
|
|
});
|
|
|
|
it('should change to working directory if specified', async () => {
|
|
mockCore.getInput.mockReturnValue('./subfolder');
|
|
|
|
await run({
|
|
core: mockCore,
|
|
govulncheck: mockGovulncheck,
|
|
parser: mockParser,
|
|
annotator: mockAnnotator,
|
|
});
|
|
|
|
expect(process.chdir).toHaveBeenCalledWith('./subfolder');
|
|
});
|
|
|
|
it('should not change directory for default value', async () => {
|
|
mockCore.getInput.mockReturnValue('.');
|
|
|
|
await run({
|
|
core: mockCore,
|
|
govulncheck: mockGovulncheck,
|
|
parser: mockParser,
|
|
annotator: mockAnnotator,
|
|
});
|
|
|
|
expect(process.chdir).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle stderr warnings', async () => {
|
|
mockGovulncheck.run.mockResolvedValue({
|
|
output: '',
|
|
errorOutput: 'warning: using database from 2023-01-01',
|
|
exitCode: 0,
|
|
});
|
|
|
|
await run({
|
|
core: mockCore,
|
|
govulncheck: mockGovulncheck,
|
|
parser: mockParser,
|
|
annotator: mockAnnotator,
|
|
});
|
|
|
|
expect(mockCore.warning).toHaveBeenCalledWith(
|
|
'govulncheck stderr: warning: using database from 2023-01-01'
|
|
);
|
|
});
|
|
|
|
it('should fail when go.sum is missing', async () => {
|
|
mockGovulncheck.run.mockResolvedValue({
|
|
output: '{"config": {}}',
|
|
errorOutput: 'missing go.sum entry for module providing package golang.org/x/net/html',
|
|
exitCode: 1,
|
|
});
|
|
|
|
await expect(
|
|
run({
|
|
core: mockCore,
|
|
govulncheck: mockGovulncheck,
|
|
parser: mockParser,
|
|
annotator: mockAnnotator,
|
|
})
|
|
).rejects.toThrow('govulncheck failed due to missing dependencies');
|
|
});
|
|
|
|
it('should fail when imports cannot be resolved', async () => {
|
|
mockGovulncheck.run.mockResolvedValue({
|
|
output: '{"config": {}}',
|
|
errorOutput: 'could not import golang.org/x/net/html (invalid package name: "")',
|
|
exitCode: 1,
|
|
});
|
|
|
|
await expect(
|
|
run({
|
|
core: mockCore,
|
|
govulncheck: mockGovulncheck,
|
|
parser: mockParser,
|
|
annotator: mockAnnotator,
|
|
})
|
|
).rejects.toThrow('govulncheck failed due to missing dependencies');
|
|
});
|
|
|
|
it('should handle errors and set action as failed', async () => {
|
|
const error = new Error('Failed to install govulncheck');
|
|
mockGovulncheck.install.mockRejectedValue(error);
|
|
|
|
await expect(
|
|
run({
|
|
core: mockCore,
|
|
govulncheck: mockGovulncheck,
|
|
parser: mockParser,
|
|
annotator: mockAnnotator,
|
|
})
|
|
).rejects.toThrow('Failed to install govulncheck');
|
|
|
|
expect(mockCore.setFailed).toHaveBeenCalledWith('Failed to install govulncheck');
|
|
});
|
|
|
|
it('should handle parsing errors', async () => {
|
|
const error = new Error('Invalid JSON output');
|
|
mockParser.parse.mockImplementation(() => {
|
|
throw error;
|
|
});
|
|
|
|
await expect(
|
|
run({
|
|
core: mockCore,
|
|
govulncheck: mockGovulncheck,
|
|
parser: mockParser,
|
|
annotator: mockAnnotator,
|
|
})
|
|
).rejects.toThrow('Invalid JSON output');
|
|
|
|
expect(mockCore.setFailed).toHaveBeenCalledWith('Invalid JSON output');
|
|
});
|
|
|
|
it('should handle annotation errors gracefully', async () => {
|
|
const mockVulnerabilities = [{ finding: { osv: 'GO-2023-1234' } }];
|
|
mockParser.parse.mockReturnValue(mockVulnerabilities);
|
|
mockAnnotator.createAnnotations.mockRejectedValue(new Error('Annotation failed'));
|
|
|
|
await expect(
|
|
run({
|
|
core: mockCore,
|
|
govulncheck: mockGovulncheck,
|
|
parser: mockParser,
|
|
annotator: mockAnnotator,
|
|
})
|
|
).rejects.toThrow('Annotation failed');
|
|
|
|
expect(mockCore.setFailed).toHaveBeenCalledWith('Annotation failed');
|
|
});
|
|
|
|
it('should truncate output logging when output is large', async () => {
|
|
const largeOutput = 'x'.repeat(6000);
|
|
mockGovulncheck.run.mockResolvedValue({
|
|
output: largeOutput,
|
|
errorOutput: '',
|
|
exitCode: 0,
|
|
});
|
|
|
|
await run({
|
|
core: mockCore,
|
|
govulncheck: mockGovulncheck,
|
|
parser: mockParser,
|
|
annotator: mockAnnotator,
|
|
});
|
|
|
|
expect(mockCore.info).toHaveBeenCalledWith('Raw govulncheck output length: 6000 characters');
|
|
expect(mockCore.info).toHaveBeenCalledWith(
|
|
`Raw output (first 1000 chars): ${'x'.repeat(1000)}...`
|
|
);
|
|
});
|
|
});
|