/**
* @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');
});
});