1
0
Fork 0
mirror of https://github.com/imjasonh/govulncheck-action synced 2026-07-11 18:11:01 +00:00
govulncheck-action/test/index.test.js
Jason Hall 24718a7114
Detect and fail on missing go.sum dependencies
Previously, if go.sum was out of date or missing, govulncheck would fail
to analyze the code but the action would report "no vulnerabilities found"
which is misleading and dangerous.

This change:
- Detects specific error patterns in govulncheck stderr that indicate
  missing dependencies (missing go.sum entry, could not import, invalid package name)
- Throws a clear error message telling users to run 'go mod tidy'
- Prevents false "no vulnerabilities" reports when govulncheck can't analyze the code

Added tests to verify the behavior for both missing go.sum and import errors.

Fixes the issue where vulnerabilities could be missed due to dependency problems.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-07 02:08:43 -04:00

264 lines
No EOL
7.3 KiB
JavaScript

const { run } = require('../index');
const core = require('@actions/core');
// Mock the @actions/core module
jest.mock('@actions/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()
};
return {
getInput: jest.fn(),
info: jest.fn(),
warning: jest.fn(),
setOutput: jest.fn(),
setFailed: jest.fn(),
summary: mockSummary
};
});
describe('GitHub Action Integration', () => {
let mockGovulncheck;
let mockParser;
let mockAnnotator;
let originalChdir;
beforeEach(() => {
// Save original process.chdir
originalChdir = process.chdir;
process.chdir = jest.fn();
// Reset all mocks
jest.clearAllMocks();
// Set default input values
core.getInput.mockReturnValue('.');
// 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({
govulncheck: mockGovulncheck,
parser: mockParser,
annotator: mockAnnotator
});
expect(core.info).toHaveBeenCalledWith('Running govulncheck...');
expect(core.info).toHaveBeenCalledWith('Raw govulncheck output length: 0 characters');
expect(core.info).toHaveBeenCalledWith('Raw output: ');
expect(mockGovulncheck.install).toHaveBeenCalled();
expect(mockGovulncheck.run).toHaveBeenCalled();
expect(core.setOutput).toHaveBeenCalledWith('vulnerabilities-found', 'false');
expect(core.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({
govulncheck: mockGovulncheck,
parser: mockParser,
annotator: mockAnnotator
});
expect(core.warning).toHaveBeenCalledWith('Found 2 vulnerabilities');
expect(core.setOutput).toHaveBeenCalledWith('vulnerabilities-found', 'true');
expect(core.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 () => {
core.getInput.mockReturnValue('./subfolder');
await run({
govulncheck: mockGovulncheck,
parser: mockParser,
annotator: mockAnnotator
});
expect(process.chdir).toHaveBeenCalledWith('./subfolder');
});
it('should not change directory for default value', async () => {
core.getInput.mockReturnValue('.');
await run({
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({
govulncheck: mockGovulncheck,
parser: mockParser,
annotator: mockAnnotator
});
expect(core.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({
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({
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({
govulncheck: mockGovulncheck,
parser: mockParser,
annotator: mockAnnotator
})).rejects.toThrow('Failed to install govulncheck');
expect(core.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({
govulncheck: mockGovulncheck,
parser: mockParser,
annotator: mockAnnotator
})).rejects.toThrow('Invalid JSON output');
expect(core.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({
govulncheck: mockGovulncheck,
parser: mockParser,
annotator: mockAnnotator
})).rejects.toThrow('Annotation failed');
expect(core.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({
govulncheck: mockGovulncheck,
parser: mockParser,
annotator: mockAnnotator
});
expect(core.info).toHaveBeenCalledWith('Raw govulncheck output length: 6000 characters');
expect(core.info).toHaveBeenCalledWith(`Raw output (first 1000 chars): ${'x'.repeat(1000)}...`);
});
});