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