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)}...` ); }); });