deepsource-mcp-server
Version:
Model Context Protocol server for DeepSource
602 lines (502 loc) • 19.4 kB
text/typescript
/**
* @fileoverview Tests for AnalysisRunRepository
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { AnalysisRunRepository } from '../analysis-run.repository.js';
import { DeepSourceClient } from '../../../deepsource.js';
import { DeepSourceRun } from '../../../models/runs.js';
import type { Project } from '../../../models/projects.js';
import type { PaginatedRunResponse } from '../../../client/runs-client.js';
import {
asRunId,
asProjectKey,
asCommitOid,
asBranchName,
asGraphQLNodeId,
} from '../../../types/branded.js';
import { AnalysisRun } from '../../../domain/aggregates/analysis-run/analysis-run.aggregate.js';
import { PaginatedResponse } from '../../../utils/pagination/types.js';
// Mock the DeepSourceClient
vi.mock('../../../deepsource.js');
// Mock the logger
vi.mock('../../../utils/logging/logger.js', () => ({
createLogger: vi.fn(() => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
})),
}));
describe('AnalysisRunRepository', () => {
let repository: AnalysisRunRepository;
let mockClient: anyed<DeepSourceClient>;
let mockApiRuns: DeepSourceRun[];
beforeEach(() => {
// Reset all mocks
vi.clearAllMocks();
// Create mock DeepSourceClient
mockClient = {
listProjects: vi.fn(),
listRuns: vi.fn(),
} as unknown as anyed<DeepSourceClient>;
// Create test data
mockApiRuns = [
{
id: asGraphQLNodeId('R_run1'),
runUid: asRunId('run-1'),
commitOid: asCommitOid('commit-1'),
branchName: asBranchName('main'),
baseOid: asCommitOid('base-1'),
status: 'SUCCESS',
createdAt: '2024-01-15T10:00:00Z',
updatedAt: '2024-01-15T10:05:00Z',
finishedAt: '2024-01-15T10:10:00Z',
summary: {
occurrencesIntroduced: 10,
occurrencesResolved: 5,
occurrencesSuppressed: 2,
},
repository: {
name: 'test-repo',
id: asGraphQLNodeId('R_repo123'),
},
},
{
id: asGraphQLNodeId('R_run2'),
runUid: asRunId('run-2'),
commitOid: asCommitOid('commit-2'),
branchName: asBranchName('feature'),
baseOid: asCommitOid('base-2'),
status: 'FAILURE',
createdAt: '2024-01-14T10:00:00Z',
updatedAt: '2024-01-14T10:05:00Z',
finishedAt: '2024-01-14T10:15:00Z',
summary: {
occurrencesIntroduced: 20,
occurrencesResolved: 0,
occurrencesSuppressed: 0,
},
repository: {
name: 'test-repo',
id: asGraphQLNodeId('R_repo123'),
},
},
{
id: asGraphQLNodeId('R_run3'),
runUid: asRunId('run-3'),
commitOid: asCommitOid('commit-3'),
branchName: asBranchName('main'),
baseOid: asCommitOid('base-3'),
status: 'READY',
createdAt: '2024-01-13T10:00:00Z',
updatedAt: '2024-01-13T10:05:00Z',
summary: {
occurrencesIntroduced: 0,
occurrencesResolved: 0,
occurrencesSuppressed: 0,
},
repository: {
name: 'test-repo',
id: asGraphQLNodeId('R_repo123'),
},
},
];
// Setup default mock behavior
const mockResponse: PaginatedResponse<DeepSourceRun> = {
items: mockApiRuns,
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: 'start',
endCursor: 'end',
},
};
mockClient.listRuns.mockResolvedValue(mockResponse);
// Create repository instance
repository = new AnalysisRunRepository(mockClient);
});
describe('findById', () => {
it('should find run by ID', async () => {
const runId = asRunId('run-1');
mockClient.listProjects.mockResolvedValue([
{
key: asProjectKey('test-project'),
name: 'Test Project',
repository: {},
} as unknown as Project,
]);
const run = await repository.findById(runId);
expect(run).not.toBeNull();
expect(run?.runId).toBe(runId);
expect(run?.status).toBe('SUCCESS');
expect(mockClient.listProjects).toHaveBeenCalledTimes(1);
expect(mockClient.listRuns).toHaveBeenCalledWith('test-project', {
first: 50,
after: undefined,
});
});
it('should return null when run not found', async () => {
mockClient.listProjects.mockResolvedValue([
{
key: asProjectKey('test-project'),
name: 'Test Project',
repository: {},
} as unknown as Project,
]);
const run = await repository.findById(asRunId('non-existent'));
expect(run).toBeNull();
});
it('should search through multiple projects', async () => {
mockClient.listProjects.mockResolvedValue([
{ key: asProjectKey('project-1'), name: 'Project 1', repository: {} } as unknown as Project,
{ key: asProjectKey('project-2'), name: 'Project 2', repository: {} } as unknown as Project,
]);
// First project returns empty
mockClient.listRuns.mockResolvedValueOnce({
items: [],
pageInfo: { hasNextPage: false, hasPreviousPage: false },
} as PaginatedRunResponse);
// Second project has the run
mockClient.listRuns.mockResolvedValueOnce({
items: [mockApiRuns[0]],
pageInfo: { hasNextPage: false, hasPreviousPage: false },
} as PaginatedRunResponse);
const run = await repository.findById(asRunId('run-1'));
expect(run).not.toBeNull();
expect(mockClient.listRuns).toHaveBeenCalledTimes(2);
});
it('should handle pagination when searching', async () => {
mockClient.listProjects.mockResolvedValue([
{
key: asProjectKey('test-project'),
name: 'Test Project',
repository: {},
} as unknown as Project,
]);
// First page doesn't have the run
mockClient.listRuns.mockResolvedValueOnce({
items: [mockApiRuns[1]],
pageInfo: {
hasNextPage: true,
hasPreviousPage: false,
endCursor: 'cursor-1',
},
} as PaginatedRunResponse);
// Second page has the run
mockClient.listRuns.mockResolvedValueOnce({
items: [mockApiRuns[0]],
pageInfo: { hasNextPage: false, hasPreviousPage: true },
} as PaginatedRunResponse);
const run = await repository.findById(asRunId('run-1'));
expect(run).not.toBeNull();
expect(mockClient.listRuns).toHaveBeenCalledWith('test-project', {
first: 50,
after: 'cursor-1',
});
});
});
describe('findByProject', () => {
it('should return paginated runs for project', async () => {
const projectKey = asProjectKey('test-project');
const result = await repository.findByProject(projectKey, { page: 1, pageSize: 2 });
expect(result.items).toHaveLength(2);
expect(result.page).toBe(1);
expect(result.pageSize).toBe(2);
expect(result.hasNextPage).toBe(true);
expect(result.hasPreviousPage).toBe(false);
expect(mockClient.listRuns).toHaveBeenCalledWith(projectKey, { first: 2 });
});
it('should handle pagination correctly', async () => {
const projectKey = asProjectKey('test-project');
const result = await repository.findByProject(projectKey, { page: 2, pageSize: 2 });
expect(result.items).toHaveLength(1); // Only one item on page 2
expect(result.page).toBe(2);
expect(result.hasNextPage).toBe(false);
expect(result.hasPreviousPage).toBe(true);
expect(mockClient.listRuns).toHaveBeenCalledWith(projectKey, { first: 4 });
});
it('should return empty results when no runs exist', async () => {
mockClient.listRuns.mockResolvedValue({
items: [],
pageInfo: { hasNextPage: false, hasPreviousPage: false },
} as PaginatedRunResponse);
const result = await repository.findByProject(asProjectKey('test-project'), {
page: 1,
pageSize: 10,
});
expect(result.items).toEqual([]);
expect(result.totalCount).toBe(0);
expect(result.totalPages).toBe(0);
});
});
describe('findMostRecent', () => {
it('should find most recent run for project', async () => {
const projectKey = asProjectKey('test-project');
const run = await repository.findMostRecent(projectKey);
expect(run).not.toBeNull();
expect(run?.runId).toBe('run-1');
expect(mockClient.listRuns).toHaveBeenCalledWith(projectKey, {
first: 50,
after: undefined,
});
});
it('should find most recent run for specific branch', async () => {
const projectKey = asProjectKey('test-project');
const branch = asBranchName('feature');
const run = await repository.findMostRecent(projectKey, branch);
expect(run).not.toBeNull();
expect(run?.runId).toBe('run-2');
expect(run?.commitInfo.branch).toBe(branch);
expect(mockClient.listRuns).toHaveBeenCalled();
});
it('should return null when no runs exist', async () => {
mockClient.listRuns.mockResolvedValue({
items: [],
pageInfo: { hasNextPage: false, hasPreviousPage: false },
} as PaginatedRunResponse);
const run = await repository.findMostRecent(asProjectKey('test-project'));
expect(run).toBeNull();
});
});
describe('findByCommit', () => {
it('should find run by commit OID', async () => {
const projectKey = asProjectKey('test-project');
const commitOid = asCommitOid('commit-2');
const run = await repository.findByCommit(projectKey, commitOid);
expect(run).not.toBeNull();
expect(run?.runId).toBe('run-2');
expect(run?.commitInfo.oid).toBe(commitOid);
});
it('should return null when no run found for commit', async () => {
const projectKey = asProjectKey('test-project');
const commitOid = asCommitOid('non-existent-commit');
const run = await repository.findByCommit(projectKey, commitOid);
expect(run).toBeNull();
});
it('should handle pagination when searching for commit', async () => {
const projectKey = asProjectKey('test-project');
const commitOid = asCommitOid('commit-on-page-2');
// First page doesn't have the commit
mockClient.listRuns.mockResolvedValueOnce({
items: mockApiRuns.slice(0, 2),
pageInfo: {
hasNextPage: true,
hasPreviousPage: false,
endCursor: 'cursor-1',
},
} as PaginatedRunResponse);
// Second page has the commit
const runOnPage2 = {
...mockApiRuns[0],
runUid: asRunId('run-page-2'),
commitOid,
};
mockClient.listRuns.mockResolvedValueOnce({
items: [runOnPage2],
pageInfo: { hasNextPage: false, hasPreviousPage: true },
} as PaginatedRunResponse);
const run = await repository.findByCommit(projectKey, commitOid);
expect(run).not.toBeNull();
expect(run?.commitInfo.oid).toBe(commitOid);
expect(mockClient.listRuns).toHaveBeenCalledTimes(2);
});
});
describe('findByStatus', () => {
it('should find runs by status', async () => {
const projectKey = asProjectKey('test-project');
const result = await repository.findByStatus(projectKey, 'SUCCESS', {
page: 1,
pageSize: 10,
});
expect(result.items).toHaveLength(1);
expect(result.items[0].status).toBe('SUCCESS');
expect(result.totalCount).toBe(1);
});
it('should handle RUNNING status mapping', async () => {
const projectKey = asProjectKey('test-project');
const result = await repository.findByStatus(projectKey, 'RUNNING', {
page: 1,
pageSize: 10,
});
expect(result.items).toHaveLength(1);
expect(result.items[0].runId).toBe('run-3'); // READY maps to RUNNING
expect(result.items[0].status).toBe('RUNNING');
});
it('should paginate status results', async () => {
// Add more runs with the same status
const moreRuns = Array.from({ length: 15 }, (_, i) => ({
...mockApiRuns[0],
runUid: asRunId(`run-success-${i}`),
status: 'SUCCESS',
}));
mockClient.listRuns.mockResolvedValue({
items: moreRuns,
pageInfo: { hasNextPage: false, hasPreviousPage: false },
} as PaginatedRunResponse);
const result = await repository.findByStatus(asProjectKey('test-project'), 'SUCCESS', {
page: 2,
pageSize: 10,
});
expect(result.items).toHaveLength(5); // 15 total, page 2 with size 10
expect(result.page).toBe(2);
expect(result.hasNextPage).toBe(false);
expect(result.hasPreviousPage).toBe(true);
});
});
describe('findByDateRange', () => {
it('should find runs within date range', async () => {
const projectKey = asProjectKey('test-project');
const startDate = new Date('2024-01-14T00:00:00Z');
const endDate = new Date('2024-01-15T23:59:59Z');
const result = await repository.findByDateRange(projectKey, startDate, endDate, {
page: 1,
pageSize: 10,
});
expect(result.items).toHaveLength(2); // run-1 and run-2
expect(result.items.map((r) => r.runId)).toContain('run-1');
expect(result.items.map((r) => r.runId)).toContain('run-2');
expect(result.items.map((r) => r.runId)).not.toContain('run-3');
});
it('should stop fetching when runs are before start date', async () => {
const projectKey = asProjectKey('test-project');
const startDate = new Date('2024-01-14T00:00:00Z');
const endDate = new Date('2024-01-15T23:59:59Z');
// First page has runs in range
mockClient.listRuns.mockResolvedValueOnce({
items: [mockApiRuns[0], mockApiRuns[1]],
pageInfo: {
hasNextPage: true,
hasPreviousPage: false,
endCursor: 'cursor-1',
},
} as PaginatedRunResponse);
// Second page has runs before start date
mockClient.listRuns.mockResolvedValueOnce({
items: [mockApiRuns[2]], // This is from 2024-01-13
pageInfo: { hasNextPage: true, hasPreviousPage: true },
} as PaginatedRunResponse);
const result = await repository.findByDateRange(projectKey, startDate, endDate, {
page: 1,
pageSize: 10,
});
expect(result.items).toHaveLength(2);
expect(mockClient.listRuns).toHaveBeenCalledTimes(2);
});
it('should handle empty date range', async () => {
const projectKey = asProjectKey('test-project');
const startDate = new Date('2024-01-20T00:00:00Z');
const endDate = new Date('2024-01-21T23:59:59Z');
const result = await repository.findByDateRange(projectKey, startDate, endDate, {
page: 1,
pageSize: 10,
});
expect(result.items).toHaveLength(0);
expect(result.totalCount).toBe(0);
});
});
describe('countByProject', () => {
it('should count all runs for project', async () => {
const projectKey = asProjectKey('test-project');
const count = await repository.countByProject(projectKey);
expect(count).toBe(3);
expect(mockClient.listRuns).toHaveBeenCalledWith(projectKey, {
first: 100,
after: undefined,
});
});
it('should handle pagination when counting', async () => {
const projectKey = asProjectKey('test-project');
// First page
mockClient.listRuns.mockResolvedValueOnce({
items: Array(100).fill(mockApiRuns[0]),
pageInfo: {
hasNextPage: true,
hasPreviousPage: false,
endCursor: 'cursor-1',
},
} as PaginatedRunResponse);
// Second page
mockClient.listRuns.mockResolvedValueOnce({
items: Array(50).fill(mockApiRuns[0]),
pageInfo: { hasNextPage: false, hasPreviousPage: true },
} as PaginatedRunResponse);
const count = await repository.countByProject(projectKey);
expect(count).toBe(150);
expect(mockClient.listRuns).toHaveBeenCalledTimes(2);
});
});
describe('countByStatus', () => {
it('should count runs by status', async () => {
const projectKey = asProjectKey('test-project');
const count = await repository.countByStatus(projectKey, 'SUCCESS');
expect(count).toBe(1);
});
it('should handle status mapping when counting', async () => {
const projectKey = asProjectKey('test-project');
const count = await repository.countByStatus(projectKey, 'RUNNING');
expect(count).toBe(1); // READY maps to RUNNING
});
it('should return 0 for status with no runs', async () => {
const projectKey = asProjectKey('test-project');
const count = await repository.countByStatus(projectKey, 'TIMEOUT');
expect(count).toBe(0);
});
});
describe('existsForCommit', () => {
it('should return true when run exists for commit', async () => {
const projectKey = asProjectKey('test-project');
const exists = await repository.existsForCommit(projectKey, asCommitOid('commit-1'));
expect(exists).toBe(true);
});
it('should return false when no run exists for commit', async () => {
const projectKey = asProjectKey('test-project');
const exists = await repository.existsForCommit(projectKey, asCommitOid('non-existent'));
expect(exists).toBe(false);
});
});
describe('save', () => {
it('should throw error indicating operation not supported', async () => {
const run = AnalysisRun.create({
runId: asRunId('new-run'),
projectKey: asProjectKey('test-project'),
repositoryId: asGraphQLNodeId('R_repo123'),
commitInfo: {
oid: asCommitOid('new-commit'),
branch: asBranchName('main'),
baseOid: asCommitOid('base-commit'),
},
});
await expect(repository.save(run)).rejects.toThrow(
'Save operation is not supported by DeepSource API'
);
});
});
describe('delete', () => {
it('should throw error indicating operation not supported', async () => {
await expect(repository.delete(asRunId('run-1'))).rejects.toThrow(
'Delete operation is not supported by DeepSource API'
);
});
});
describe('data freshness', () => {
it('should fetch fresh data on every request', async () => {
const projectKey = asProjectKey('test-project');
// First call
await repository.findByProject(projectKey, { page: 1, pageSize: 10 });
// Update mock data
mockApiRuns[0].status = 'FAILURE';
// Second call should get fresh data
await repository.findByProject(projectKey, { page: 1, pageSize: 10 });
expect(mockClient.listRuns).toHaveBeenCalledTimes(2);
});
it('should not cache results between different method calls', async () => {
const projectKey = asProjectKey('test-project');
await repository.findByProject(projectKey, { page: 1, pageSize: 10 });
await repository.findMostRecent(projectKey);
await repository.findByCommit(projectKey, asCommitOid('commit-1'));
await repository.countByProject(projectKey);
// All methods use listRuns
expect(mockClient.listRuns).toHaveBeenCalledTimes(4);
});
});
});