Skip to main content
Glama
project-runs.test.ts12.9 kB
/** * @vitest-environment node */ import { vi } from 'vitest'; import { asProjectKey, asRunId, asCommitOid, asBranchName, asGraphQLNodeId, } from '../../types/branded'; import type { IAnalysisRunRepository } from '../../domain/aggregates/analysis-run/analysis-run.repository'; import type { AnalysisRun } from '../../domain/aggregates/analysis-run/analysis-run.aggregate'; import type { Logger } from '../../utils/logging/logger'; import { IssueCount } from '../../domain/value-objects/issue-count'; // Create mock logger const mockLogger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), }; // Mock modules before importing the implementation vi.mock('../../utils/logging/logger', () => ({ createLogger: vi.fn(() => mockLogger), })); // Mock the repository and factory const mockFindByProject = vi.fn(); const mockAnalysisRunRepository = { findByProject: mockFindByProject, } as unknown as IAnalysisRunRepository; const mockCreateAnalysisRunRepository = vi.fn(() => mockAnalysisRunRepository); const mockRepositoryFactory = vi.fn(() => ({ createAnalysisRunRepository: mockCreateAnalysisRunRepository, })); vi.mock('../../infrastructure/factories/repository.factory', () => ({ RepositoryFactory: mockRepositoryFactory, })); // Import the modules under test AFTER mocking const { handleDeepsourceProjectRuns, createProjectRunsHandlerWithRepo } = await import( '../../handlers/project-runs' ); describe('Project Runs Handler', () => { // Environment backup let originalEnv: Record<string, string | undefined>; beforeEach(() => { // Backup environment originalEnv = { ...process.env }; process.env.DEEPSOURCE_API_KEY = 'test-api-key'; // Reset mocks vi.clearAllMocks(); }); afterEach(() => { // Restore environment process.env = originalEnv; }); describe('createProjectRunsHandlerWithRepo', () => { it('should create a handler that uses injected dependencies', async () => { // Mock domain analysis runs const projectKey = asProjectKey('test-project'); const mockRuns = [ { runId: asRunId('run-1'), projectKey, repositoryId: asGraphQLNodeId('repo-1'), commitInfo: { oid: asCommitOid('commit-1'), branch: asBranchName('main'), baseOid: asCommitOid('base-1'), }, status: 'SUCCESS' as const, timestamps: { createdAt: new Date('2023-01-01T00:00:00Z'), startedAt: new Date('2023-01-01T00:01:00Z'), finishedAt: new Date('2023-01-01T00:05:00Z'), }, summary: { totalIntroduced: IssueCount.create(5), totalResolved: IssueCount.create(2), totalSuppressed: IssueCount.create(1), byAnalyzer: [], byCategory: [], }, }, { runId: asRunId('run-2'), projectKey, repositoryId: asGraphQLNodeId('repo-1'), commitInfo: { oid: asCommitOid('commit-2'), branch: asBranchName('feature'), baseOid: asCommitOid('base-2'), }, status: 'FAILURE' as const, timestamps: { createdAt: new Date('2023-01-02T00:00:00Z'), finishedAt: new Date('2023-01-02T00:03:00Z'), }, summary: { totalIntroduced: IssueCount.create(3), totalResolved: IssueCount.create(0), totalSuppressed: IssueCount.create(0), byAnalyzer: [], byCategory: [], }, }, ] as AnalysisRun[]; // Set up the mock to return paginated results mockFindByProject.mockResolvedValue({ items: mockRuns, page: 1, pageSize: 20, totalCount: 2, totalPages: 1, hasNextPage: false, hasPreviousPage: false, }); const handler = createProjectRunsHandlerWithRepo({ analysisRunRepository: mockAnalysisRunRepository, logger: mockLogger as unknown as Logger, }); // Call the handler const result = await handler({ projectKey: 'test-project', first: 20, }); // Verify repository was used expect(mockFindByProject).toHaveBeenCalledWith(projectKey, { page: 1, pageSize: 20, }); // Verify the response const parsedContent = JSON.parse(result.content[0].text); expect(parsedContent.runs).toHaveLength(2); expect(parsedContent.runs[0].id).toBe('run-1'); expect(parsedContent.runs[0].status).toBe('SUCCESS'); expect(parsedContent.runs[0].commitOid).toBe('commit-1'); expect(parsedContent.runs[0].branchName).toBe('main'); expect(parsedContent.runs[0].summary.occurrencesIntroduced).toBe(5); expect(parsedContent.runs[0].summary.occurrencesResolved).toBe(2); expect(parsedContent.runs[0].summary.occurrencesSuppressed).toBe(1); expect(parsedContent.runs[1].id).toBe('run-2'); expect(parsedContent.runs[1].status).toBe('FAILURE'); expect(parsedContent.runs[1].branchName).toBe('feature'); expect(parsedContent.totalCount).toBe(2); expect(parsedContent.pageInfo.hasNextPage).toBe(false); expect(parsedContent.pageInfo.hasPreviousPage).toBe(false); expect(parsedContent.usage_examples).toBeDefined(); }); it('should handle empty results', async () => { // Set up the mock to return empty results mockFindByProject.mockResolvedValue({ items: [], page: 1, pageSize: 20, totalCount: 0, totalPages: 0, hasNextPage: false, hasPreviousPage: false, }); const handler = createProjectRunsHandlerWithRepo({ analysisRunRepository: mockAnalysisRunRepository, logger: mockLogger as unknown as Logger, }); // Call the handler const result = await handler({ projectKey: 'empty-project', first: 20, }); // Verify the response const parsedContent = JSON.parse(result.content[0].text); expect(parsedContent.runs).toHaveLength(0); expect(parsedContent.totalCount).toBe(0); expect(parsedContent.pageInfo.hasNextPage).toBe(false); }); it('should handle errors using injected logger', async () => { // Set up the mock to throw an error const testError = new Error('Repository connection failed'); mockFindByProject.mockRejectedValue(testError); const handler = createProjectRunsHandlerWithRepo({ analysisRunRepository: mockAnalysisRunRepository, logger: mockLogger as unknown as Logger, }); // Call the handler const result = await handler({ projectKey: 'test-project', first: 20, }); // Verify error was logged using injected logger expect(mockLogger.error).toHaveBeenCalledWith( 'Error in handleProjectRuns', expect.any(Object) ); // Verify error response expect(result.isError).toBe(true); const parsedContent = JSON.parse(result.content[0].text); expect(parsedContent).toEqual({ error: 'Repository connection failed', details: 'Failed to retrieve project runs', }); }); }); describe('handleDeepsourceProjectRuns', () => { it('should throw an error if DEEPSOURCE_API_KEY is not set', async () => { // Unset API key delete process.env.DEEPSOURCE_API_KEY; // Call the handler and expect it to throw await expect( handleDeepsourceProjectRuns({ projectKey: 'test-project', first: 20, }) ).rejects.toThrow('Configuration error: DeepSource API key is required but not configured'); }); it('should return project runs successfully', async () => { // Mock domain analysis run const projectKey = asProjectKey('test-project'); const mockRun = { runId: asRunId('run-1'), projectKey, repositoryId: asGraphQLNodeId('repo-1'), commitInfo: { oid: asCommitOid('commit-1'), branch: asBranchName('main'), baseOid: asCommitOid('base-1'), }, status: 'SUCCESS' as const, timestamps: { createdAt: new Date('2023-01-01T00:00:00Z'), startedAt: new Date('2023-01-01T00:01:00Z'), finishedAt: new Date('2023-01-01T00:05:00Z'), }, summary: { totalIntroduced: IssueCount.create(3), totalResolved: IssueCount.create(1), totalSuppressed: IssueCount.create(0), byAnalyzer: [], byCategory: [], }, } as AnalysisRun; // Set up the mock to return the run mockFindByProject.mockResolvedValue({ items: [mockRun], page: 1, pageSize: 20, totalCount: 1, totalPages: 1, hasNextPage: false, hasPreviousPage: false, }); // Call the handler const result = await handleDeepsourceProjectRuns({ projectKey: 'test-project', first: 20, }); // Verify factory was created with the API key expect(mockRepositoryFactory).toHaveBeenCalledWith({ apiKey: 'test-api-key' }); // Verify createAnalysisRunRepository was called expect(mockCreateAnalysisRunRepository).toHaveBeenCalled(); // Verify findByProject was called expect(mockFindByProject).toHaveBeenCalledWith(projectKey, { page: 1, pageSize: 20, }); // Verify logging behavior - check that key operations were logged expect(mockLogger.info).toHaveBeenCalledWith( 'Fetching project runs from repository', expect.any(Object) ); // Verify the response structure expect(result).toHaveProperty('content'); expect(result.content).toHaveLength(1); expect(result.content[0]).toHaveProperty('type', 'text'); // Parse and verify the content const parsedContent = JSON.parse(result.content[0].text); expect(parsedContent.runs).toHaveLength(1); expect(parsedContent.runs[0].id).toBe('run-1'); expect(parsedContent.runs[0].status).toBe('SUCCESS'); expect(parsedContent.totalCount).toBe(1); }); it('should throw error when repository fails', async () => { // Set up the mock to throw an error const testError = new Error('Repository connection failed'); mockFindByProject.mockRejectedValue(testError); // Call the handler and expect it to throw await expect( handleDeepsourceProjectRuns({ projectKey: 'test-project', first: 20, }) ).rejects.toThrow('Repository connection failed'); }); it('should handle non-Error type exceptions', async () => { // Set up the mock to throw a non-Error value mockFindByProject.mockRejectedValue('Just a string error'); // Call the handler and expect it to throw await expect( handleDeepsourceProjectRuns({ projectKey: 'test-project', first: 20, }) ).rejects.toThrow('Unknown error'); }); it('should handle pagination parameters', async () => { // Mock empty response mockFindByProject.mockResolvedValue({ items: [], page: 1, pageSize: 10, totalCount: 0, totalPages: 0, hasNextPage: false, hasPreviousPage: false, }); // Call the handler with custom pagination await handleDeepsourceProjectRuns({ projectKey: 'test-project', first: 10, after: 'cursor-123', }); // Verify pagination was handled (converted to page/pageSize) expect(mockFindByProject).toHaveBeenCalledWith(asProjectKey('test-project'), { page: 1, pageSize: 10, }); }); it('should handle analyzer filtering parameter (logged but not yet implemented)', async () => { // Mock empty response mockFindByProject.mockResolvedValue({ items: [], page: 1, pageSize: 20, totalCount: 0, totalPages: 0, hasNextPage: false, hasPreviousPage: false, }); // Call the handler with analyzer filter await handleDeepsourceProjectRuns({ projectKey: 'test-project', analyzerIn: ['eslint', 'typescript'], first: 20, }); // Verify that analyzer filtering was logged (though not yet implemented in repository) expect(mockLogger.info).toHaveBeenCalledWith( 'Fetching project runs from repository', expect.objectContaining({ projectKey: 'test-project', hasAnalyzerFilter: true, }) ); // Repository should still be called without analyzer filter expect(mockFindByProject).toHaveBeenCalledWith(asProjectKey('test-project'), { page: 1, pageSize: 20, }); }); }); });

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