Skip to main content
Glama
handlers-integration.test.ts23.7 kB
/** * @fileoverview Integration tests for handler functions * Tests handler logic without requiring complex HTTP mocking */ import { vi } from 'vitest'; // Mock the DeepSource client before any imports const mockClient = { getDependencyVulnerabilities: vi.fn(), listRuns: vi.fn(), getRecentRunIssues: vi.fn(), getRun: vi.fn(), }; vi.mock('../deepsource.js', () => ({ DeepSourceClient: vi.fn().mockImplementation(() => mockClient), ReportType: { OWASP_TOP_10: 'OWASP_TOP_10', SANS_TOP_25: 'SANS_TOP_25', MISRA_C: 'MISRA_C', CODE_COVERAGE: 'CODE_COVERAGE', CODE_HEALTH_TREND: 'CODE_HEALTH_TREND', ISSUE_DISTRIBUTION: 'ISSUE_DISTRIBUTION', ISSUES_PREVENTED: 'ISSUES_PREVENTED', ISSUES_AUTOFIXED: 'ISSUES_AUTOFIXED', }, ReportStatus: { PASSING: 'PASSING', FAILING: 'FAILING', NOOP: 'NOOP', }, })); // Now import the handlers const { handleDeepsourceDependencyVulnerabilities } = await import( '../handlers/dependency-vulnerabilities.js' ); const { handleDeepsourceProjectRuns } = await import('../handlers/project-runs.js'); const { handleDeepsourceRecentRunIssues } = await import('../handlers/recent-run-issues.js'); const { handleDeepsourceRun } = await import('../handlers/run.js'); describe('Handler Integration Tests', () => { const originalEnv = process.env; beforeEach(() => { vi.clearAllMocks(); process.env = { ...originalEnv, DEEPSOURCE_API_KEY: 'test-api-key' }; }); afterEach(() => { process.env = originalEnv; }); describe('dependency-vulnerabilities handler', () => { it('should handle successful vulnerability response with complete data', async () => { const mockVulnerabilities = { items: [ { id: 'vuln-1', vulnerability: { summary: 'Test vulnerability', identifier: 'CVE-2023-1234', severity: 'HIGH', cvssV3BaseScore: 8.5, cvssV2BaseScore: null, fixedVersions: ['1.2.3'], details: 'Detailed vulnerability description', aliases: ['GHSA-1234'], referenceUrls: ['https://example.com'], }, package: { name: 'test-package', }, packageVersion: { version: '1.0.0', }, }, ], pageInfo: { hasNextPage: true, hasPreviousPage: false, startCursor: 'start123', endCursor: 'end123', }, totalCount: 5, }; mockClient.getDependencyVulnerabilities.mockResolvedValue(mockVulnerabilities); const result = await handleDeepsourceDependencyVulnerabilities({ projectKey: 'test-project', first: 10, }); expect(result.isError).toBeUndefined(); expect(result.content).toHaveLength(1); expect(result.content[0].type).toBe('text'); const parsedContent = JSON.parse(result.content[0].text); expect(parsedContent.vulnerabilities).toHaveLength(1); expect(parsedContent.vulnerabilities[0].id).toBe('vuln-1'); expect(parsedContent.vulnerabilities[0].severity).toBe('HIGH'); expect(parsedContent.vulnerabilities[0].risk_assessment).toBeDefined(); expect(parsedContent.pageInfo.hasNextPage).toBe(true); expect(parsedContent.totalCount).toBe(5); }); it('should handle vulnerability with minimal data', async () => { const mockVulnerabilities = { items: [ { id: 'vuln-minimal', vulnerability: { summary: null, identifier: 'CVE-2023-5678', severity: 'CRITICAL', cvssV3BaseScore: null, cvssV2BaseScore: 7.5, fixedVersions: [], details: null, aliases: [], referenceUrls: [], }, package: { name: 'minimal-package', }, packageVersion: { version: '0.5.0', }, }, ], pageInfo: null, totalCount: 1, }; mockClient.getDependencyVulnerabilities.mockResolvedValue(mockVulnerabilities); const result = await handleDeepsourceDependencyVulnerabilities({ projectKey: 'test-project', }); const parsedContent = JSON.parse(result.content[0].text); const vuln = parsedContent.vulnerabilities[0]; expect(vuln.title).toBe('CVE-2023-5678'); // Falls back to identifier expect(vuln.cvssScore).toBe(7.5); // Uses V2 score expect(vuln.fixedIn).toBeNull(); expect(vuln.description).toBe(''); // Falls back to empty string expect(vuln.risk_assessment.fixed_version_available).toBe(false); }); it('should handle vulnerability with no package name and no fixed versions', async () => { const mockVulnerabilities = { items: [ { id: 'vuln-no-package', vulnerability: { summary: 'Critical vulnerability', identifier: 'CVE-2023-9999', severity: 'CRITICAL', cvssV3BaseScore: 9.8, cvssV2BaseScore: null, fixedVersions: [], // No fixed versions details: 'No fix available yet', aliases: [], referenceUrls: [], }, package: { name: null, // No package name }, packageVersion: { version: '1.0.0', }, }, ], pageInfo: null, totalCount: 1, }; mockClient.getDependencyVulnerabilities.mockResolvedValue(mockVulnerabilities); const result = await handleDeepsourceDependencyVulnerabilities({ projectKey: 'test-project', }); const parsedContent = JSON.parse(result.content[0].text); const vuln = parsedContent.vulnerabilities[0]; // This should trigger the fallback case in getRemediationAdvice (line 226) expect(vuln.risk_assessment.remediation_advice).toBe( 'Review the vulnerability details and take appropriate mitigation measures based on your application context.' ); }); it('should handle API errors gracefully', async () => { mockClient.getDependencyVulnerabilities.mockRejectedValue(new Error('API Error')); await expect( handleDeepsourceDependencyVulnerabilities({ projectKey: 'test-project', }) ).rejects.toThrow('API Error'); }); }); describe('project-runs handler', () => { it.skip('should handle successful runs response', async () => { const mockRuns = { items: [ { id: 'run-graphql-id-1', runUid: 'run-uid-123', commitOid: 'abc123commit', branchName: 'main', baseOid: 'base123commit', status: 'SUCCESS', createdAt: '2023-01-01T00:00:00Z', updatedAt: '2023-01-01T00:05:00Z', finishedAt: '2023-01-01T00:05:00Z', summary: { occurrencesIntroduced: 5, occurrencesResolved: 2, occurrencesSuppressed: 1, occurrenceDistributionByAnalyzer: [ { analyzerShortcode: 'python', introduced: 3, }, ], occurrenceDistributionByCategory: [ { category: 'bug-risk', introduced: 2, }, ], }, repository: { name: 'test-repo', id: 'repo-graphql-id-1', }, }, ], pageInfo: { hasNextPage: false, hasPreviousPage: false, startCursor: null, endCursor: null, }, totalCount: 1, }; mockClient.listRuns.mockResolvedValue(mockRuns); const result = await handleDeepsourceProjectRuns({ projectKey: 'test-project', analyzerIn: ['python'], }); expect(result.isError).toBeUndefined(); const parsedContent = JSON.parse(result.content[0].text); expect(parsedContent.runs).toHaveLength(1); expect(parsedContent.runs[0].id).toBe('run-graphql-id-1'); expect(parsedContent.runs[0].status).toBe('SUCCESS'); expect(parsedContent.totalCount).toBe(1); expect(parsedContent.pageInfo.hasNextPage).toBe(false); }); it.skip('should handle runs API errors', async () => { mockClient.listRuns.mockRejectedValue(new Error('Runs API Error')); await expect( handleDeepsourceProjectRuns({ projectKey: 'test-project', }) ).rejects.toThrow('Runs API Error'); }); }); describe('recent-run-issues handler', () => { it.skip('should handle successful recent issues response', async () => { const mockRecentIssues = { run: { id: 'run-graphql-id-1', runUid: 'run-uid-123', commitOid: 'abc123commit', branchName: 'main', baseOid: 'base123commit', status: 'SUCCESS', createdAt: '2023-01-01T00:00:00Z', updatedAt: '2023-01-01T00:05:00Z', finishedAt: '2023-01-01T00:05:00Z', summary: { occurrencesIntroduced: 5, occurrencesResolved: 2, occurrencesSuppressed: 1, }, repository: { name: 'test-repo', id: 'repo-graphql-id-1', }, }, items: [ { id: 'issue-1', title: 'Test issue', shortcode: 'PY-E1101', category: 'BUG', severity: 'MAJOR', status: 'OPEN', issue_text: 'Instance of class has no attribute', file_path: 'src/test.py', line_number: 42, tags: ['python', 'bug-risk'], }, ], pageInfo: { hasNextPage: true, hasPreviousPage: false, startCursor: 'start123', endCursor: 'end123', }, totalCount: 5, }; mockClient.getRecentRunIssues.mockResolvedValue(mockRecentIssues); const result = await handleDeepsourceRecentRunIssues({ projectKey: 'test-project', branchName: 'main', }); expect(result.isError).toBeUndefined(); const parsedContent = JSON.parse(result.content[0].text); expect(parsedContent.run.id).toBe('run-graphql-id-1'); expect(parsedContent.issues).toHaveLength(1); expect(parsedContent.issues[0].id).toBe('issue-1'); expect(parsedContent.issues[0].title).toBe('Test issue'); expect(parsedContent.totalCount).toBe(5); expect(parsedContent.pageInfo.hasNextPage).toBe(true); }); it.skip('should handle case when no recent run is found', async () => { const mockRecentIssuesNoRun = { run: null, items: [], pageInfo: { hasNextPage: false, hasPreviousPage: false }, totalCount: 0, }; mockClient.getRecentRunIssues.mockResolvedValue(mockRecentIssuesNoRun); await expect( handleDeepsourceRecentRunIssues({ projectKey: 'test-project', branchName: 'feature-branch', }) ).rejects.toThrow( 'No recent analysis run found for branch "feature-branch" in project "test-project"' ); }); it.skip('should handle recent issues API errors', async () => { mockClient.getRecentRunIssues.mockRejectedValue(new Error('Recent Issues Error')); await expect( handleDeepsourceRecentRunIssues({ projectKey: 'test-project', branchName: 'main', }) ).rejects.toThrow('Recent Issues Error'); }); }); describe('run handler', () => { it.skip('should handle successful run response by runUid', async () => { const mockRun = { id: 'run-graphql-id-1', runUid: 'run-uid-123', commitOid: 'abc123commit', branchName: 'main', baseOid: 'base123commit', status: 'SUCCESS', createdAt: '2023-01-01T00:00:00Z', updatedAt: '2023-01-01T00:05:00Z', finishedAt: '2023-01-01T00:05:00Z', summary: { occurrencesIntroduced: 5, occurrencesResolved: 2, occurrencesSuppressed: 1, occurrenceDistributionByAnalyzer: [ { analyzerShortcode: 'python', introduced: 3, }, ], occurrenceDistributionByCategory: [ { category: 'bug-risk', introduced: 2, }, ], }, repository: { name: 'test-repo', id: 'repo-graphql-id-1', }, }; mockClient.getRun.mockResolvedValue(mockRun); const result = await handleDeepsourceRun({ projectKey: 'test-project', runIdentifier: 'run-uid-123', }); expect(result.isError).toBeUndefined(); const parsedContent = JSON.parse(result.content[0].text); expect(parsedContent.run.id).toBe('run-graphql-id-1'); expect(parsedContent.run.status).toBe('SUCCESS'); expect(parsedContent.run.runUid).toBe('run-uid-123'); }); it.skip('should handle successful run response by commitOid', async () => { const mockRun = { id: 'run-graphql-id-2', runUid: 'run-uid-456', commitOid: 'def456commit', branchName: 'feature', baseOid: 'base456commit', status: 'FAILED', createdAt: '2023-01-02T00:00:00Z', updatedAt: '2023-01-02T00:05:00Z', finishedAt: '2023-01-02T00:05:00Z', summary: { occurrencesIntroduced: 10, occurrencesResolved: 0, occurrencesSuppressed: 0, }, repository: { name: 'test-repo', id: 'repo-graphql-id-1', }, }; mockClient.getRun.mockResolvedValue(mockRun); const result = await handleDeepsourceRun({ projectKey: 'test-project', runIdentifier: 'def456commit', isCommitOid: true, }); expect(result.isError).toBeUndefined(); const parsedContent = JSON.parse(result.content[0].text); expect(parsedContent.run.id).toBe('run-graphql-id-2'); expect(parsedContent.run.status).toBe('FAILED'); expect(parsedContent.run.commitOid).toBe('def456commit'); }); it.skip('should handle case when run is not found', async () => { mockClient.getRun.mockResolvedValue(null); await expect( handleDeepsourceRun({ projectKey: 'test-project', runIdentifier: 'nonexistent-run', }) ).rejects.toThrow('Run with runUid "nonexistent-run" not found'); }); it.skip('should handle different run statuses and include proper analysis info', async () => { const testStatuses = ['PENDING', 'FAILURE', 'TIMEOUT', 'CANCEL', 'READY', 'SKIPPED']; for (const status of testStatuses) { const mockRun = { id: `run-${status.toLowerCase()}`, runUid: `run-uid-${status.toLowerCase()}`, commitOid: 'abc123commit', branchName: 'main', baseOid: 'base123commit', status, createdAt: '2023-01-01T00:00:00Z', updatedAt: '2023-01-01T00:05:00Z', finishedAt: '2023-01-01T00:05:00Z', summary: { occurrencesIntroduced: 1, occurrencesResolved: 0, occurrencesSuppressed: 0, }, repository: { name: 'test-repo', id: 'repo-graphql-id-1', }, }; mockClient.getRun.mockResolvedValue(mockRun); const result = await handleDeepsourceRun({ projectKey: 'test-project', runIdentifier: `run-uid-${status.toLowerCase()}`, }); expect(result.isError).toBeUndefined(); const parsedContent = JSON.parse(result.content[0].text); expect(parsedContent.run.status).toBe(status); expect(parsedContent.analysis.status_info).toBeDefined(); expect(parsedContent.analysis.status_info).not.toBe(''); // Verify that the status info contains relevant information if (status === 'PENDING') { expect(parsedContent.analysis.status_info).toContain('queued'); } else if (status === 'FAILURE') { expect(parsedContent.analysis.status_info).toContain('failed'); } } }); it.skip('should handle unknown run status', async () => { const mockRun = { id: 'run-unknown', runUid: 'run-uid-unknown', commitOid: 'abc123commit', branchName: 'main', baseOid: 'base123commit', status: 'UNKNOWN_STATUS', createdAt: '2023-01-01T00:00:00Z', updatedAt: '2023-01-01T00:05:00Z', finishedAt: '2023-01-01T00:05:00Z', summary: { occurrencesIntroduced: 0, occurrencesResolved: 0, occurrencesSuppressed: 0, }, repository: { name: 'test-repo', id: 'repo-graphql-id-1', }, }; mockClient.getRun.mockResolvedValue(mockRun); const result = await handleDeepsourceRun({ projectKey: 'test-project', runIdentifier: 'run-uid-unknown', }); expect(result.isError).toBeUndefined(); const parsedContent = JSON.parse(result.content[0].text); expect(parsedContent.run.status).toBe('UNKNOWN_STATUS'); expect(parsedContent.analysis.status_info).toBe('Unknown status: UNKNOWN_STATUS'); }); it.skip('should handle run API errors', async () => { mockClient.getRun.mockRejectedValue(new Error('Run API Error')); await expect( handleDeepsourceRun({ projectKey: 'test-project', runIdentifier: 'run-123', }) ).rejects.toThrow('Run API Error'); }); }); }); // Test helper functions separately describe('Helper Functions Tests', () => { // Import helper functions by importing the module and accessing its internals // Since the functions are not exported, we'll test them through the main handler it('should test severity level descriptions through vulnerability response', async () => { process.env.DEEPSOURCE_API_KEY = 'test-key'; const testCases = [ { severity: 'CRITICAL', expected: 'Critical - Requires immediate attention' }, { severity: 'HIGH', expected: 'High - Should be addressed promptly' }, { severity: 'MEDIUM', expected: 'Medium - Should be planned for remediation' }, { severity: 'LOW', expected: 'Low - Fix when possible' }, { severity: 'UNKNOWN', expected: 'Unknown severity level: UNKNOWN' }, ]; for (const { severity, expected } of testCases) { const mockVulnerabilities = { items: [ { id: 'test-vuln', vulnerability: { summary: 'Test', identifier: 'CVE-2023-TEST', severity, cvssV3BaseScore: null, cvssV2BaseScore: null, fixedVersions: [], details: null, aliases: [], referenceUrls: [], }, package: { name: 'test-pkg' }, packageVersion: { version: '1.0.0' }, }, ], pageInfo: null, totalCount: 1, }; mockClient.getDependencyVulnerabilities.mockResolvedValue(mockVulnerabilities); const result = await handleDeepsourceDependencyVulnerabilities({ projectKey: 'test-project', }); const parsedContent = JSON.parse(result.content[0].text); const riskAssessment = parsedContent.vulnerabilities[0].risk_assessment.severity_level; expect(riskAssessment).toContain(expected); } }); it('should test CVSS score descriptions through vulnerability response', async () => { process.env.DEEPSOURCE_API_KEY = 'test-key'; const testCases = [ { score: 9.5, expected: 'Critical (9.5/10)' }, { score: 8.0, expected: 'High (8/10)' }, { score: 5.5, expected: 'Medium (5.5/10)' }, { score: 2.0, expected: 'Low (2/10)' }, { score: null, expected: 'No CVSS score available' }, ]; for (const { score, expected } of testCases) { const mockVulnerabilities = { items: [ { id: 'test-vuln', vulnerability: { summary: 'Test', identifier: 'CVE-2023-TEST', severity: 'HIGH', cvssV3BaseScore: score, cvssV2BaseScore: null, fixedVersions: [], details: null, aliases: [], referenceUrls: [], }, package: { name: 'test-pkg' }, packageVersion: { version: '1.0.0' }, }, ], pageInfo: null, totalCount: 1, }; mockClient.getDependencyVulnerabilities.mockResolvedValue(mockVulnerabilities); const result = await handleDeepsourceDependencyVulnerabilities({ projectKey: 'test-project', }); const parsedContent = JSON.parse(result.content[0].text); const cvssDescription = parsedContent.vulnerabilities[0].risk_assessment.cvss_description; expect(cvssDescription).toContain(expected); } }); it('should test remediation advice through vulnerability response', async () => { process.env.DEEPSOURCE_API_KEY = 'test-key'; // Test with fixed version available const mockVulnerabilitiesWithFix = { items: [ { id: 'test-vuln', vulnerability: { summary: 'Test', identifier: 'CVE-2023-TEST', severity: 'HIGH', cvssV3BaseScore: 7.5, cvssV2BaseScore: null, fixedVersions: ['2.0.0'], details: null, aliases: [], referenceUrls: [], }, package: { name: 'vulnerable-pkg' }, packageVersion: { version: '1.0.0' }, }, ], pageInfo: null, totalCount: 1, }; mockClient.getDependencyVulnerabilities.mockResolvedValue(mockVulnerabilitiesWithFix); const result = await handleDeepsourceDependencyVulnerabilities({ projectKey: 'test-project', }); const parsedContent = JSON.parse(result.content[0].text); const advice = parsedContent.vulnerabilities[0].risk_assessment.remediation_advice; expect(advice).toContain('Update vulnerable-pkg to version 2.0.0'); // Test with no fix available const mockVulnerabilitiesNoFix = { items: [ { id: 'test-vuln', vulnerability: { summary: 'Test', identifier: 'CVE-2023-TEST', severity: 'HIGH', cvssV3BaseScore: 7.5, cvssV2BaseScore: null, fixedVersions: [], details: null, aliases: [], referenceUrls: [], }, package: { name: 'unfixable-pkg' }, packageVersion: { version: '1.0.0' }, }, ], pageInfo: null, totalCount: 1, }; mockClient.getDependencyVulnerabilities.mockResolvedValue(mockVulnerabilitiesNoFix); const result2 = await handleDeepsourceDependencyVulnerabilities({ projectKey: 'test-project', }); const parsedContent2 = JSON.parse(result2.content[0].text); const advice2 = parsedContent2.vulnerabilities[0].risk_assessment.remediation_advice; expect(advice2).toContain('Consider replacing unfixable-pkg with a secure alternative'); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sapientpants/deepsource-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server