deepsource-mcp-server
Version:
Model Context Protocol server for DeepSource
1,609 lines (1,506 loc) • 78.6 kB
text/typescript
import nock from 'nock';
import { jest } from '@jest/globals';
import { AxiosError } from 'axios';
import { DeepSourceClient } from '../deepsource';
// Mock the DeepSourceClient's methods for specific tests
const originalGetIssues = DeepSourceClient.prototype.getIssues;
const originalListRuns = DeepSourceClient.prototype.listRuns;
const originalGetRun = DeepSourceClient.prototype.getRun;
describe('DeepSourceClient', () => {
const API_KEY = 'test-api-key';
const client = new DeepSourceClient(API_KEY);
beforeEach(() => {
nock.cleanAll();
});
afterAll(() => {
nock.restore();
// Restore the original methods after all tests
DeepSourceClient.prototype.getIssues = originalGetIssues;
DeepSourceClient.prototype.listRuns = originalListRuns;
DeepSourceClient.prototype.getRun = originalGetRun;
});
describe('listProjects', () => {
it('should return a list of projects', async () => {
const mockGraphQLResponse = {
data: {
viewer: {
email: 'test@example.com',
accounts: {
edges: [
{
node: {
login: 'testorg',
repositories: {
edges: [
{
node: {
name: 'Project One',
defaultBranch: 'main',
dsn: 'project1',
isPrivate: false,
isActivated: true,
vcsProvider: 'github',
},
},
{
node: {
name: 'Project Two',
defaultBranch: 'main',
dsn: 'project2',
isPrivate: true,
isActivated: true,
vcsProvider: 'github',
},
},
],
},
},
},
],
},
},
},
};
nock('https://api.deepsource.io')
.post('/graphql/')
.matchHeader('Authorization', `Bearer ${API_KEY}`)
.reply(200, mockGraphQLResponse);
const projects = await client.listProjects();
expect(projects).toHaveLength(2);
expect(projects[0]).toEqual({
key: 'project1',
name: 'Project One',
repository: {
url: 'project1',
provider: 'github',
login: 'testorg',
isPrivate: false,
isActivated: true,
},
});
});
it('should handle API errors', async () => {
nock('https://api.deepsource.io')
.post('/graphql/')
.reply(401, { errors: [{ message: 'Unauthorized' }] });
await expect(client.listProjects()).rejects.toThrow('GraphQL Error: Unauthorized');
});
it('should handle non-GraphQL errors in listProjects', async () => {
nock('https://api.deepsource.io').post('/graphql/').replyWithError('Network error');
const client = new DeepSourceClient(API_KEY);
await expect(client.listProjects()).rejects.toThrow('Network error');
});
});
describe('getIssues', () => {
const projectKey = 'test-project';
// No original post method mocking needed for these tests
it('should return a list of issues for a project', async () => {
// Mock the listProjects call first
const mockProjectsResponse = {
data: {
viewer: {
accounts: {
edges: [
{
node: {
login: 'testorg',
repositories: {
edges: [
{
node: {
name: 'test-repo',
defaultBranch: 'main',
dsn: 'test-project',
isPrivate: false,
isActivated: true,
vcsProvider: 'github',
},
},
],
},
},
},
],
},
},
},
};
const mockIssuesResponse = {
data: {
repository: {
name: 'test-repo',
defaultBranch: 'main',
dsn: 'test-project',
isPrivate: false,
issues: {
pageInfo: {
hasNextPage: true,
hasPreviousPage: false,
startCursor: 'cursor1',
endCursor: 'cursor2',
},
totalCount: 5,
edges: [
{
node: {
id: 'issue1',
issue: {
shortcode: 'SEC001',
title: 'Security Issue',
category: 'security',
severity: 'high',
description: 'Potential security vulnerability',
},
occurrences: {
edges: [
{
node: {
id: 'occ1',
path: 'src/main.ts',
beginLine: 42,
endLine: 42,
beginColumn: 1,
endColumn: 10,
title: 'Security Issue',
},
},
],
},
},
},
],
},
},
},
};
nock('https://api.deepsource.io')
.post('/graphql/')
.matchHeader('Authorization', `Bearer ${API_KEY}`)
.reply(200, mockProjectsResponse)
.post('/graphql/')
.matchHeader('Authorization', `Bearer ${API_KEY}`)
.reply(200, mockIssuesResponse);
const result = await client.getIssues(projectKey);
expect(result.items).toHaveLength(1);
expect(result.items[0]).toEqual({
id: 'occ1',
shortcode: 'SEC001',
title: 'Security Issue',
category: 'security',
severity: 'high',
status: 'OPEN',
issue_text: 'Potential security vulnerability',
file_path: 'src/main.ts',
line_number: 42,
tags: [],
});
expect(result.pageInfo).toEqual({
hasNextPage: true,
hasPreviousPage: false,
startCursor: 'cursor1',
endCursor: 'cursor2',
});
expect(result.totalCount).toBe(5);
});
it('should support pagination parameters', async () => {
// Mock the listProjects call first
const mockProjectsResponse = {
data: {
viewer: {
accounts: {
edges: [
{
node: {
login: 'testorg',
repositories: {
edges: [
{
node: {
name: 'test-repo',
defaultBranch: 'main',
dsn: 'test-project',
isPrivate: false,
isActivated: true,
vcsProvider: 'github',
},
},
],
},
},
},
],
},
},
},
};
const mockIssuesResponse = {
data: {
repository: {
name: 'test-repo',
defaultBranch: 'main',
dsn: 'test-project',
isPrivate: false,
issues: {
pageInfo: {
hasNextPage: false,
hasPreviousPage: true,
startCursor: 'cursor3',
endCursor: 'cursor4',
},
totalCount: 10,
edges: [
{
node: {
id: 'issue2',
issue: {
shortcode: 'PERF002',
title: 'Performance Issue',
category: 'performance',
severity: 'medium',
description: 'Potential performance issue',
},
occurrences: {
edges: [
{
node: {
id: 'occ2',
path: 'src/utils.ts',
beginLine: 100,
endLine: 100,
beginColumn: 5,
endColumn: 15,
title: 'Performance Issue',
},
},
],
},
},
},
],
},
},
},
};
nock('https://api.deepsource.io')
.post('/graphql/')
.matchHeader('Authorization', `Bearer ${API_KEY}`)
.reply(200, mockProjectsResponse)
.post('/graphql/')
.matchHeader('Authorization', `Bearer ${API_KEY}`)
.reply(200, mockIssuesResponse);
const pagination = {
first: 5,
after: 'cursor2',
};
const result = await client.getIssues(projectKey, pagination);
expect(result.items).toHaveLength(1);
expect(result.items[0].id).toBe('occ2');
expect(result.pageInfo).toEqual({
hasNextPage: false,
hasPreviousPage: true,
startCursor: 'cursor3',
endCursor: 'cursor4',
});
expect(result.totalCount).toBe(10);
});
it('should handle API errors when fetching issues', async () => {
nock('https://api.deepsource.io')
.post('/graphql/')
.reply(404, { errors: [{ message: 'Project not found' }] });
await expect(client.getIssues(projectKey)).rejects.toThrow(
'GraphQL Error: Project not found'
);
});
it('should support filtering parameters for issues', async () => {
// Mock the listProjects call first
const mockProjectsResponse = {
data: {
viewer: {
accounts: {
edges: [
{
node: {
login: 'testorg',
repositories: {
edges: [
{
node: {
name: 'test-repo',
defaultBranch: 'main',
dsn: 'test-project',
isPrivate: false,
isActivated: true,
vcsProvider: 'github',
},
},
],
},
},
},
],
},
},
},
};
const mockIssuesResponse = {
data: {
repository: {
name: 'test-repo',
defaultBranch: 'main',
dsn: 'test-project',
isPrivate: false,
issues: {
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: 'cursor1',
endCursor: 'cursor2',
},
totalCount: 1,
edges: [
{
node: {
id: 'issue1',
issue: {
shortcode: 'SEC001',
title: 'Security Issue',
category: 'security',
severity: 'high',
description: 'Potential security vulnerability',
tags: ['security', 'vulnerability'],
},
occurrences: {
edges: [
{
node: {
id: 'occ1',
path: 'src/auth.ts',
beginLine: 42,
endLine: 42,
beginColumn: 1,
endColumn: 10,
title: 'Security Issue',
},
},
],
},
},
},
],
},
},
},
};
// Create a tracked array to record the GraphQL variables
const graphqlVariables: Record<string, unknown>[] = [];
// Intercept the GraphQL requests and capture the variables
nock('https://api.deepsource.io')
.post('/graphql/')
.matchHeader('Authorization', `Bearer ${API_KEY}`)
.reply(() => [
// First call - return projects
200,
mockProjectsResponse,
])
.post('/graphql/')
.matchHeader('Authorization', `Bearer ${API_KEY}`)
.reply((uri, requestBody: Record<string, unknown>) => {
// Second call - capture variables and return issues
graphqlVariables.push(requestBody.variables);
return [200, mockIssuesResponse];
});
const filterParams = {
path: 'src/auth.ts',
analyzerIn: ['python', 'javascript'],
tags: ['security'],
first: 10,
};
const result = await client.getIssues(projectKey, filterParams);
// Verify that the filter parameters were passed to the GraphQL call
expect(graphqlVariables.length).toBe(1);
expect(graphqlVariables[0]).toMatchObject({
path: 'src/auth.ts',
analyzerIn: ['python', 'javascript'],
tags: ['security'],
first: 10,
});
// Verify the result includes tags from the response
expect(result.items).toHaveLength(1);
expect(result.items[0].id).toBe('occ1');
expect(result.items[0].tags).toEqual(['security', 'vulnerability']);
});
it('should return empty result when project not found', async () => {
// Mock empty project response
nock('https://api.deepsource.io')
.post('/graphql/')
.reply(200, { data: { viewer: { accounts: { edges: [] } } } });
const result = await client.getIssues('non-existent-project');
expect(result.items).toEqual([]);
expect(result.pageInfo).toEqual({
hasNextPage: false,
hasPreviousPage: false,
});
expect(result.totalCount).toBe(0);
});
it('should handle NoneType errors in getIssues', async () => {
// First mock to find the project
nock('https://api.deepsource.io')
.post('/graphql/')
.matchHeader('Authorization', `Bearer ${API_KEY}`)
.reply(200, {
data: {
viewer: {
accounts: {
edges: [
{
node: {
login: 'testorg',
repositories: {
edges: [
{
node: {
name: 'test-repo',
defaultBranch: 'main',
dsn: 'test-project',
isPrivate: false,
isActivated: true,
vcsProvider: 'github',
},
},
],
},
},
},
],
},
},
},
});
// Then mock the issues call to return a NoneType error
nock('https://api.deepsource.io')
.post('/graphql/')
.matchHeader('Authorization', `Bearer ${API_KEY}`)
.reply(200, { errors: [{ message: 'NoneType object has no attribute' }] });
const client = new DeepSourceClient(API_KEY);
const result = await client.getIssues('test-project');
expect(result).toEqual({
items: [],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
},
totalCount: 0,
});
});
it('should handle project not found in getIssues', async () => {
// Mock the listProjects call to return no matching projects
nock('https://api.deepsource.io')
.post('/graphql/')
.matchHeader('Authorization', `Bearer ${API_KEY}`)
.reply(200, {
data: {
viewer: {
accounts: {
edges: [
{
node: {
login: 'testorg',
repositories: {
edges: [
{
node: {
name: 'test-repo',
defaultBranch: 'main',
dsn: 'different-project', // This doesn't match our request
isPrivate: false,
isActivated: true,
vcsProvider: 'github',
},
},
],
},
},
},
],
},
},
},
});
const client = new DeepSourceClient(API_KEY);
const result = await client.getIssues('non-existent-project');
expect(result).toEqual({
items: [],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
},
totalCount: 0,
});
});
it('should handle pagination with before and last parameters', async () => {
// Mock the listProjects call first
const mockProjectsResponse = {
data: {
viewer: {
accounts: {
edges: [
{
node: {
login: 'testorg',
repositories: {
edges: [
{
node: {
name: 'test-repo',
defaultBranch: 'main',
dsn: 'test-project',
isPrivate: false,
isActivated: true,
vcsProvider: 'github',
},
},
],
},
},
},
],
},
},
},
};
const mockIssuesResponse = {
data: {
repository: {
name: 'test-repo',
defaultBranch: 'main',
dsn: 'test-project',
isPrivate: false,
issues: {
pageInfo: {
hasNextPage: false,
hasPreviousPage: true,
startCursor: 'cursor5',
endCursor: 'cursor6',
},
totalCount: 15,
edges: [
{
node: {
id: 'issue3',
issue: {
shortcode: 'STYLE003',
title: 'Style Issue',
category: 'style',
severity: 'low',
description: 'Code style issue',
},
occurrences: {
edges: [
{
node: {
id: 'occ3',
path: 'src/styles.ts',
beginLine: 50,
endLine: 50,
beginColumn: 10,
endColumn: 20,
title: 'Style Issue',
},
},
],
},
},
},
],
},
},
},
};
nock('https://api.deepsource.io')
.post('/graphql/')
.matchHeader('Authorization', `Bearer ${API_KEY}`)
.reply(200, mockProjectsResponse)
.post('/graphql/')
.matchHeader('Authorization', `Bearer ${API_KEY}`)
.reply(200, mockIssuesResponse);
const pagination = {
before: 'cursor7',
last: 5,
first: 10, // This should be ignored when before is provided
};
const result = await client.getIssues(projectKey, pagination);
expect(result.items).toHaveLength(1);
expect(result.items[0].id).toBe('occ3');
expect(result.pageInfo).toEqual({
hasNextPage: false,
hasPreviousPage: true,
startCursor: 'cursor5',
endCursor: 'cursor6',
});
expect(result.totalCount).toBe(15);
});
it('should handle pagination with last parameter without before', async () => {
// Mock the listProjects call first
const mockProjectsResponse = {
data: {
viewer: {
accounts: {
edges: [
{
node: {
login: 'testorg',
repositories: {
edges: [
{
node: {
name: 'test-repo',
defaultBranch: 'main',
dsn: 'test-project',
isPrivate: false,
isActivated: true,
vcsProvider: 'github',
},
},
],
},
},
},
],
},
},
},
};
const mockIssuesResponse = {
data: {
repository: {
name: 'test-repo',
defaultBranch: 'main',
dsn: 'test-project',
isPrivate: false,
issues: {
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: 'cursor8',
endCursor: 'cursor9',
},
totalCount: 8,
edges: [
{
node: {
id: 'issue4',
issue: {
shortcode: 'DOC004',
title: 'Documentation Issue',
category: 'documentation',
severity: 'info',
description: 'Missing documentation',
},
occurrences: {
edges: [
{
node: {
id: 'occ4',
path: 'src/utils.ts',
beginLine: 75,
endLine: 75,
beginColumn: 1,
endColumn: 5,
title: 'Documentation Issue',
},
},
],
},
},
},
],
},
},
},
};
// Spy on static method logPaginationWarning
const logPaginationWarningSpy = jest.spyOn(DeepSourceClient, 'logPaginationWarning');
try {
nock('https://api.deepsource.io')
.post('/graphql/')
.matchHeader('Authorization', `Bearer ${API_KEY}`)
.reply(200, mockProjectsResponse)
.post('/graphql/')
.matchHeader('Authorization', `Bearer ${API_KEY}`)
.reply(200, mockIssuesResponse);
const pagination = {
last: 5,
first: undefined,
};
const result = await client.getIssues(projectKey, pagination);
expect(result.items).toHaveLength(1);
expect(result.items[0].id).toBe('occ4');
expect(result.pageInfo).toEqual({
hasNextPage: false,
hasPreviousPage: false,
startCursor: 'cursor8',
endCursor: 'cursor9',
});
expect(result.totalCount).toBe(8);
// Verify that the warning logger was called
expect(logPaginationWarningSpy).toHaveBeenCalled();
} finally {
// Restore original method
logPaginationWarningSpy.mockRestore();
}
});
it('should handle GraphQL errors in repository response', async () => {
// Mock the listProjects call first
const mockProjectsResponse = {
data: {
viewer: {
accounts: {
edges: [
{
node: {
login: 'testorg',
repositories: {
edges: [
{
node: {
name: 'test-repo',
defaultBranch: 'main',
dsn: 'test-project',
isPrivate: false,
isActivated: true,
vcsProvider: 'github',
},
},
],
},
},
},
],
},
},
},
};
// Mock GraphQL errors in the response data
const mockErrorResponse = {
data: null,
errors: [{ message: 'Repository access denied' }, { message: 'Invalid query' }],
};
nock('https://api.deepsource.io')
.post('/graphql/')
.matchHeader('Authorization', `Bearer ${API_KEY}`)
.reply(200, mockProjectsResponse)
.post('/graphql/')
.matchHeader('Authorization', `Bearer ${API_KEY}`)
.reply(200, mockErrorResponse);
await expect(client.getIssues(projectKey)).rejects.toThrow(
'GraphQL Errors: Repository access denied, Invalid query'
);
});
});
describe('getIssue', () => {
const projectKey = 'test-project';
const issueId = 'issue1';
it('should return a specific issue by ID', async () => {
// Mock the getIssues call
const mockIssuesResponse = {
items: [
{
id: 'issue1',
title: 'Test Issue',
shortcode: 'TEST-001',
category: 'BUG',
severity: 'HIGH',
status: 'OPEN',
issue_text: 'This is a test issue',
file_path: 'src/test.ts',
line_number: 10,
tags: ['bug'],
},
{
id: 'issue2',
title: 'Another Issue',
shortcode: 'TEST-002',
category: 'SECURITY',
severity: 'CRITICAL',
status: 'OPEN',
issue_text: 'This is another test issue',
file_path: 'src/main.ts',
line_number: 20,
tags: ['security'],
},
],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
},
totalCount: 2,
};
// Create a mocked version of getIssues for this test
DeepSourceClient.prototype.getIssues = async () => mockIssuesResponse;
const client = new DeepSourceClient('test-api-key');
const issue = await client.getIssue(projectKey, issueId);
expect(issue).not.toBeNull();
expect(issue).toEqual({
id: 'issue1',
title: 'Test Issue',
shortcode: 'TEST-001',
category: 'BUG',
severity: 'HIGH',
status: 'OPEN',
issue_text: 'This is a test issue',
file_path: 'src/test.ts',
line_number: 10,
tags: ['bug'],
});
});
it('should return null for non-existent issue ID', async () => {
// Mock the getIssues call with results
const mockIssuesResponse = {
items: [
{
id: 'issue2',
title: 'Another Issue',
shortcode: 'TEST-002',
category: 'SECURITY',
severity: 'CRITICAL',
status: 'OPEN',
issue_text: 'This is another test issue',
file_path: 'src/main.ts',
line_number: 20,
tags: ['security'],
},
],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
},
totalCount: 1,
};
// Create a mocked version of getIssues for this test
DeepSourceClient.prototype.getIssues = async () => mockIssuesResponse;
const client = new DeepSourceClient('test-api-key');
const issue = await client.getIssue(projectKey, 'non-existent-id');
expect(issue).toBeNull();
});
it('should handle errors from getIssues', async () => {
// Store the original method
const originalGetIssues = DeepSourceClient.prototype.getIssues;
try {
// Create a mocked version of getIssues that throws an error
DeepSourceClient.prototype.getIssues = async () => {
throw new Error('API Error');
};
const client = new DeepSourceClient('test-api-key');
await expect(client.getIssue(projectKey, issueId)).rejects.toThrow('API Error');
} finally {
// Restore the original method
DeepSourceClient.prototype.getIssues = originalGetIssues;
}
});
});
describe('listRuns', () => {
const projectKey = 'test-project';
it('should return a list of runs for a project', async () => {
// Mock the listProjects call first
const mockProjectsResponse = {
data: {
viewer: {
accounts: {
edges: [
{
node: {
login: 'testorg',
repositories: {
edges: [
{
node: {
name: 'test-repo',
defaultBranch: 'main',
dsn: 'test-project',
isPrivate: false,
isActivated: true,
vcsProvider: 'github',
},
},
],
},
},
},
],
},
},
},
};
const mockRunsResponse = {
data: {
repository: {
name: 'test-repo',
id: 'repo1',
analysisRuns: {
pageInfo: {
hasNextPage: true,
hasPreviousPage: false,
startCursor: 'cursor1',
endCursor: 'cursor2',
},
totalCount: 5,
edges: [
{
node: {
id: 'run1',
runUid: '12345678-1234-1234-1234-123456789012',
commitOid: 'abcdef123456',
branchName: 'main',
baseOid: '654321fedcba',
status: 'SUCCESS',
createdAt: '2023-01-01T12:00:00Z',
updatedAt: '2023-01-01T12:30:00Z',
finishedAt: '2023-01-01T12:30:00Z',
summary: {
occurrencesIntroduced: 5,
occurrencesResolved: 2,
occurrencesSuppressed: 1,
occurrenceDistributionByAnalyzer: [
{
analyzerShortcode: 'python',
introduced: 3,
},
{
analyzerShortcode: 'javascript',
introduced: 2,
},
],
occurrenceDistributionByCategory: [
{
category: 'SECURITY',
introduced: 2,
},
{
category: 'PERFORMANCE',
introduced: 3,
},
],
},
repository: {
name: 'test-repo',
id: 'repo1',
},
},
},
],
},
},
},
};
nock('https://api.deepsource.io')
.post('/graphql/')
.matchHeader('Authorization', `Bearer ${API_KEY}`)
.reply(200, mockProjectsResponse)
.post('/graphql/')
.matchHeader('Authorization', `Bearer ${API_KEY}`)
.reply(200, mockRunsResponse);
const result = await client.listRuns(projectKey);
expect(result.items).toHaveLength(1);
expect(result.items[0]).toEqual({
id: 'run1',
runUid: '12345678-1234-1234-1234-123456789012',
commitOid: 'abcdef123456',
branchName: 'main',
baseOid: '654321fedcba',
status: 'SUCCESS',
createdAt: '2023-01-01T12:00:00Z',
updatedAt: '2023-01-01T12:30:00Z',
finishedAt: '2023-01-01T12:30:00Z',
summary: {
occurrencesIntroduced: 5,
occurrencesResolved: 2,
occurrencesSuppressed: 1,
occurrenceDistributionByAnalyzer: [
{
analyzerShortcode: 'python',
introduced: 3,
},
{
analyzerShortcode: 'javascript',
introduced: 2,
},
],
occurrenceDistributionByCategory: [
{
category: 'SECURITY',
introduced: 2,
},
{
category: 'PERFORMANCE',
introduced: 3,
},
],
},
repository: {
name: 'test-repo',
id: 'repo1',
},
});
expect(result.pageInfo).toEqual({
hasNextPage: true,
hasPreviousPage: false,
startCursor: 'cursor1',
endCursor: 'cursor2',
});
expect(result.totalCount).toBe(5);
});
it('should support pagination parameters for listRuns', async () => {
// Mock the listProjects call first
const mockProjectsResponse = {
data: {
viewer: {
accounts: {
edges: [
{
node: {
login: 'testorg',
repositories: {
edges: [
{
node: {
name: 'test-repo',
defaultBranch: 'main',
dsn: 'test-project',
isPrivate: false,
isActivated: true,
vcsProvider: 'github',
},
},
],
},
},
},
],
},
},
},
};
const mockRunsResponse = {
data: {
repository: {
name: 'test-repo',
id: 'repo1',
analysisRuns: {
pageInfo: {
hasNextPage: false,
hasPreviousPage: true,
startCursor: 'cursor3',
endCursor: 'cursor4',
},
totalCount: 10,
edges: [
{
node: {
id: 'run2',
runUid: '87654321-4321-4321-4321-210987654321',
commitOid: '654321abcdef',
branchName: 'feature',
baseOid: 'abcdef654321',
status: 'FAILURE',
createdAt: '2023-01-02T12:00:00Z',
updatedAt: '2023-01-02T12:30:00Z',
finishedAt: '2023-01-02T12:30:00Z',
summary: {
occurrencesIntroduced: 10,
occurrencesResolved: 0,
occurrencesSuppressed: 0,
occurrenceDistributionByAnalyzer: [],
occurrenceDistributionByCategory: [],
},
repository: {
name: 'test-repo',
id: 'repo1',
},
},
},
],
},
},
},
};
nock('https://api.deepsource.io')
.post('/graphql/')
.matchHeader('Authorization', `Bearer ${API_KEY}`)
.reply(200, mockProjectsResponse)
.post('/graphql/')
.matchHeader('Authorization', `Bearer ${API_KEY}`)
.reply(200, mockRunsResponse);
const pagination = {
first: 5,
after: 'cursor2',
};
const result = await client.listRuns(projectKey, pagination);
expect(result.items).toHaveLength(1);
expect(result.items[0].id).toBe('run2');
expect(result.pageInfo).toEqual({
hasNextPage: false,
hasPreviousPage: true,
startCursor: 'cursor3',
endCursor: 'cursor4',
});
expect(result.totalCount).toBe(10);
});
it('should return empty result when project not found for listRuns', async () => {
// Mock empty project response
nock('https://api.deepsource.io')
.post('/graphql/')
.reply(200, { data: { viewer: { accounts: { edges: [] } } } });
const result = await client.listRuns('non-existent-project');
expect(result.items).toEqual([]);
expect(result.pageInfo).toEqual({
hasNextPage: false,
hasPreviousPage: false,
});
expect(result.totalCount).toBe(0);
});
it('should handle NoneType errors in listRuns', async () => {
// First mock to find the project
nock('https://api.deepsource.io')
.post('/graphql/')
.matchHeader('Authorization', `Bearer ${API_KEY}`)
.reply(200, {
data: {
viewer: {
accounts: {
edges: [
{
node: {
login: 'testorg',
repositories: {
edges: [
{
node: {
name: 'test-repo',
defaultBranch: 'main',
dsn: 'test-project',
isPrivate: false,
isActivated: true,
vcsProvider: 'github',
},
},
],
},
},
},
],
},
},
},
});
// Then mock the runs call to return a NoneType error
nock('https://api.deepsource.io')
.post('/graphql/')
.matchHeader('Authorization', `Bearer ${API_KEY}`)
.reply(200, { errors: [{ message: 'NoneType object has no attribute' }] });
const client = new DeepSourceClient(API_KEY);
const result = await client.listRuns('test-project');
expect(result).toEqual({
items: [],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
},
totalCount: 0,
});
});
it('should handle pagination with before and last parameters for listRuns', async () => {
// Mock the listProjects call first
const mockProjectsResponse = {
data: {
viewer: {
accounts: {
edges: [
{
node: {
login: 'testorg',
repositories: {
edges: [
{
node: {
name: 'test-repo',
defaultBranch: 'main',
dsn: 'test-project',
isPrivate: false,
isActivated: true,
vcsProvider: 'github',
},
},
],
},
},
},
],
},
},
},
};
const mockRunsResponse = {
data: {
repository: {
name: 'test-repo',
id: 'repo1',
analysisRuns: {
pageInfo: {
hasNextPage: false,
hasPreviousPage: true,
startCursor: 'cursor5',
endCursor: 'cursor6',
},
totalCount: 15,
edges: [
{
node: {
id: 'run3',
runUid: '99999999-9999-9999-9999-999999999999',
commitOid: '999999abcdef',
branchName: 'feature-branch',
baseOid: 'abcdef999999',
status: 'SUCCESS',
createdAt: '2023-01-03T12:00:00Z',
updatedAt: '2023-01-03T12:30:00Z',
finishedAt: '2023-01-03T12:30:00Z',
summary: {
occurrencesIntroduced: 3,
occurrencesResolved: 0,
occurrencesSuppressed: 0,
occurrenceDistributionByAnalyzer: [],
occurrenceDistributionByCategory: [],
},
repository: {
name: 'test-repo',
id: 'repo1',
},
},
},
],
},
},
},
};
nock('https://api.deepsource.io')
.post('/graphql/')
.matchHeader('Authorization', `Bearer ${API_KEY}`)
.reply(200, mockProjectsResponse)
.post('/graphql/')
.matchHeader('Authorization', `Bearer ${API_KEY}`)
.reply(200, mockRunsResponse);
const pagination = {
before: 'cursor7',
last: 5,
first: 10, // This should be ignored when before is provided
};
const result = await client.listRuns(projectKey, pagination);
expect(result.items).toHaveLength(1);
expect(result.items[0].id).toBe('run3');
expect(result.pageInfo).toEqual({
hasNextPage: false,
hasPreviousPage: true,
startCursor: 'cursor5',
endCursor: 'cursor6',
});
expect(result.totalCount).toBe(15);
});
it('should handle pagination with last parameter without before for listRuns', async () => {
// Mock the listProjects call first
const mockProjectsResponse = {
data: {
viewer: {
accounts: {
edges: [
{
node: {
login: 'testorg',
repositories: {
edges: [
{
node: {
name: 'test-repo',
defaultBranch: 'main',
dsn: 'test-project',
isPrivate: false,
isActivated: true,
vcsProvider: 'github',
},
},
],
},
},
},
],
},
},
},
};
const mockRunsResponse = {
data: {
repository: {
name: 'test-repo',
id: 'repo1',
analysisRuns: {
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: 'cursor8',
endCursor: 'cursor9',
},
totalCount: 8,
edges: [
{
node: {
id: 'run4',
runUid: '11111111-1111-1111-1111-111111111111',
commitOid: '111111abcdef',
branchName: 'hotfix',
baseOid: 'abcdef111111',
status: 'PENDING',
createdAt: '2023-01-04T12:00:00Z',
updatedAt: '2023-01-04T12:30:00Z',
finishedAt: '2023-01-04T12:30:00Z',
summary: {
occurrencesIntroduced: 0,
occurrencesResolved: 0,
occurrencesSuppressed: 0,
occurrenceDistributionByAnalyzer: [],
occurrenceDistributionByCategory: [],
},
repository: {
name: 'test-repo',
id: 'repo1',
},
},
},
],
},
},
},
};
// Spy on static method logPaginationWarning
const logPaginationWarningSpy = jest.spyOn(DeepSourceClient, 'logPaginationWarning');
try {
nock('https://api.deepsource.io')
.post('/graphql/')
.matchHeader('Authorization', `Bearer ${API_KEY}`)
.reply(200, mockProjectsResponse)
.post('/graphql/')
.matchHeader('Authorization', `Bearer ${API_KEY}`)
.reply(200, mockRunsResponse);
const pagination = {
last: 5,
first: undefined,
};
const result = await client.listRuns(projectKey, pagination);
expect(result.items).toHaveLength(1);
expect(result.items[0].id).toBe('run4');
expect(result.pageInfo).toEqual({
hasNextPage: false,
hasPreviousPage: false,
startCursor: 'cursor8',
endCursor: 'cursor9',
});
expect(result.totalCount).toBe(8);
// Verify that the warning logger was called
expect(logPaginationWarningSpy).toHaveBeenCalled();
} finally {
// Restore original method
logPaginationWarningSpy.mockRestore();
}
});
it('should handle GraphQL errors in repository response for listRuns', async () => {
// Mock the listProjects call first
const mockProjectsResponse = {
data: {
viewer: {
accounts: {
edges: [
{
node: {
login: 'testorg',
repositories: {
edges: [
{
node: {
name: 'test-repo',
defaultBranch: 'main',
dsn: 'test-project',
isPrivate: false,
isActivated: true,
vcsProvider: 'github',
},
},
],
},
},
},
],
},
},
},
};
// Mock GraphQL errors in the response data
const mockErrorResponse = {
data: null,
errors: [{ message: 'Repository access denied' }, { message: 'Invalid query' }],
};
nock('https://api.deepsource.io')
.post('/graphql/')
.matchHeader('Authorization', `Bearer ${API_KEY}`)
.reply(200, mockProjectsResponse)
.post('/graphql/')
.matchHeader('Authorization', `Bearer ${API_KEY}`)
.reply(200, mockErrorResponse);
await expect(client.listRuns(projectKey)).rejects.toThrow(
'GraphQL Errors: Repository access denied, Invalid query'
);
});
it('should support filtering parameters for runs', async () => {
// Mock the listProjects call first
const mockProjectsResponse = {
data: {
viewer: {
accounts: {
edges: [
{
node: {
login: 'testorg',
repositories: {
edges: [
{
node: {
name: 'test-repo',
defaultBranch: 'main',
dsn: 'test-project',
isPrivate: false,
isActivated: true,
vcsProvider: 'github',
},
},
],
},
},
},
],
},
},
},
};
const mockRunsResponse = {
data: {
repository: {
name: 'test-repo',
id: 'repo1',
analysisRuns: {
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: 'cursor1',
endCursor: 'cursor2',
},
totalCount: 1,
edges: [
{
n