/**
* @vitest-environment node
*/
import { vi } from 'vitest';
import { asProjectKey } from '../../types/branded';
import type { IQualityMetricsRepository } from '../../domain/aggregates/quality-metrics/quality-metrics.repository';
import type { QualityMetrics } from '../../domain/aggregates/quality-metrics/quality-metrics.aggregate';
import type { Logger } from '../../utils/logging/logger';
// 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 mockFindByProjectWithFilter = vi.fn();
const mockQualityMetricsRepository = {
findByProject: mockFindByProject,
findByProjectWithFilter: mockFindByProjectWithFilter,
} as unknown as IQualityMetricsRepository;
const mockCreateQualityMetricsRepository = vi.fn(() => mockQualityMetricsRepository);
const mockRepositoryFactory = vi.fn(() => ({
createQualityMetricsRepository: mockCreateQualityMetricsRepository,
}));
vi.mock('../../infrastructure/factories/repository.factory', () => ({
RepositoryFactory: mockRepositoryFactory,
}));
// Import the modules under test AFTER mocking
const { handleDeepsourceQualityMetrics, createQualityMetricsHandlerWithRepo } = await import(
'../../handlers/quality-metrics'
);
describe('Quality Metrics 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('createQualityMetricsHandlerWithRepo', () => {
it('should create a handler that uses injected dependencies', async () => {
// Mock domain quality metrics
const projectKey = asProjectKey('test-project');
const mockMetrics = [
{
id: 'test-project:AGGREGATE:LCV',
projectKey,
repositoryId: 'repo123',
configuration: {
name: 'Line Coverage',
shortcode: 'LCV',
description: 'Percentage of lines covered by tests',
positiveDirection: 'UPWARD',
unit: '%',
minValueAllowed: 0,
maxValueAllowed: 100,
isReported: true,
isThresholdEnforced: true,
metricKey: 'AGGREGATE',
threshold: {
value: 80,
description: 'Minimum line coverage threshold',
},
},
currentValue: {
value: 85.5,
displayValue: '85.5%',
unit: '%',
measuredAt: new Date(),
},
thresholdStatus: 'PASSING',
isCompliant: true,
} as QualityMetrics,
];
// Set up the mock to return the metrics
mockFindByProject.mockResolvedValue(mockMetrics);
const handler = createQualityMetricsHandlerWithRepo({
qualityMetricsRepository: mockQualityMetricsRepository,
logger: mockLogger as unknown as Logger,
});
// Call the handler
const result = await handler({ projectKey: 'test-project' });
// Verify repository was used
expect(mockFindByProject).toHaveBeenCalledWith(projectKey);
// Verify the response
const parsedContent = JSON.parse(result.content[0].text);
expect(parsedContent.metrics).toHaveLength(1);
expect(parsedContent.metrics[0].name).toBe('Line Coverage');
expect(parsedContent.metrics[0].shortcode).toBe('LCV');
expect(parsedContent.metrics[0].items[0].latestValue).toBe(85.5);
expect(parsedContent.metrics[0].items[0].threshold).toBe(80);
});
it('should filter metrics by shortcode when specified', async () => {
// Mock domain quality metrics with multiple shortcodes
const projectKey = asProjectKey('test-project');
const mockMetrics = [
{
id: 'test-project:AGGREGATE:LCV',
projectKey,
repositoryId: 'repo123',
configuration: {
name: 'Line Coverage',
shortcode: 'LCV',
description: 'Percentage of lines covered by tests',
positiveDirection: 'UPWARD',
unit: '%',
minValueAllowed: 0,
maxValueAllowed: 100,
isReported: true,
isThresholdEnforced: true,
metricKey: 'AGGREGATE',
threshold: {
value: 80,
description: 'Minimum line coverage threshold',
},
},
currentValue: {
value: 85.5,
displayValue: '85.5%',
unit: '%',
measuredAt: new Date(),
},
thresholdStatus: 'PASSING',
isCompliant: true,
} as QualityMetrics,
{
id: 'test-project:AGGREGATE:BCV',
projectKey,
repositoryId: 'repo123',
configuration: {
name: 'Branch Coverage',
shortcode: 'BCV',
description: 'Percentage of branches covered by tests',
positiveDirection: 'UPWARD',
unit: '%',
minValueAllowed: 0,
maxValueAllowed: 100,
isReported: true,
isThresholdEnforced: false,
metricKey: 'AGGREGATE',
threshold: null,
},
currentValue: {
value: 75.0,
displayValue: '75.0%',
unit: '%',
measuredAt: new Date(),
},
thresholdStatus: 'NOT_SET',
isCompliant: true,
} as QualityMetrics,
];
// Set up the mock to return only the LCV metric when filtering
mockFindByProjectWithFilter.mockResolvedValue([mockMetrics[0]]);
const handler = createQualityMetricsHandlerWithRepo({
qualityMetricsRepository: mockQualityMetricsRepository,
logger: mockLogger as unknown as Logger,
});
// Call the handler with shortcode filter
const result = await handler({
projectKey: 'test-project',
shortcodeIn: ['LCV'],
});
// Verify repository was used with filter
expect(mockFindByProjectWithFilter).toHaveBeenCalledWith(projectKey, ['LCV']);
// Verify only LCV metric is returned
const parsedContent = JSON.parse(result.content[0].text);
expect(parsedContent.metrics).toHaveLength(1);
expect(parsedContent.metrics[0].shortcode).toBe('LCV');
});
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 = createQualityMetricsHandlerWithRepo({
qualityMetricsRepository: mockQualityMetricsRepository,
logger: mockLogger as unknown as Logger,
});
// Call the handler
const result = await handler({ projectKey: 'test-project' });
// Verify error was logged using injected logger
expect(mockLogger.error).toHaveBeenCalledWith(
'Error in handleQualityMetrics',
expect.any(Object)
);
// Verify error response
expect(result.isError).toBe(true);
const parsedContent = JSON.parse(result.content[0].text);
// Should include MCP error format
expect(parsedContent.error).toBe('An error occurred while processing your request');
expect(parsedContent.code).toBeDefined();
expect(parsedContent.category).toBeDefined();
expect(parsedContent.timestamp).toBeDefined();
});
});
describe('handleDeepsourceQualityMetrics', () => {
it('should return error response if DEEPSOURCE_API_KEY is not set', async () => {
// Unset API key
delete process.env.DEEPSOURCE_API_KEY;
// Call the handler and expect error response
const result = await handleDeepsourceQualityMetrics({ projectKey: 'test-project' });
expect(result.isError).toBe(true);
const errorData = JSON.parse(result.content[0].text);
expect(errorData.error).toBe('An error occurred while processing your request');
expect(errorData.code).toBe(-32008); // Configuration error code
expect(errorData.category).toBe('server_error');
expect(errorData.details?.environmentVariable).toBe('DEEPSOURCE_API_KEY');
});
it('should return quality metrics successfully', async () => {
// Mock domain quality metrics
const projectKey = asProjectKey('test-project');
const mockMetrics = [
{
id: 'test-project:AGGREGATE:LCV',
projectKey,
repositoryId: 'repo123',
configuration: {
name: 'Line Coverage',
shortcode: 'LCV',
description: 'Percentage of lines covered by tests',
positiveDirection: 'UPWARD',
unit: '%',
minValueAllowed: 0,
maxValueAllowed: 100,
isReported: true,
isThresholdEnforced: true,
metricKey: 'AGGREGATE',
threshold: {
value: 80,
description: 'Minimum line coverage threshold',
},
},
currentValue: {
value: 85.5,
displayValue: '85.5%',
unit: '%',
measuredAt: new Date(),
},
thresholdStatus: 'PASSING',
isCompliant: true,
} as QualityMetrics,
];
// Set up the mock to return the metrics
mockFindByProject.mockResolvedValue(mockMetrics);
// Call the handler
const result = await handleDeepsourceQualityMetrics({ projectKey: 'test-project' });
// Verify factory was created with the API key
expect(mockRepositoryFactory).toHaveBeenCalledWith({ apiKey: 'test-api-key' });
// Verify createQualityMetricsRepository was called
expect(mockCreateQualityMetricsRepository).toHaveBeenCalled();
// Verify findByProject was called
expect(mockFindByProject).toHaveBeenCalledWith(projectKey);
// Verify logging behavior - check that key operations were logged
expect(mockLogger.info).toHaveBeenCalledWith(
'Fetching quality metrics 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.metrics).toHaveLength(1);
expect(parsedContent.metrics[0].name).toBe('Line Coverage');
expect(parsedContent.metrics[0].shortcode).toBe('LCV');
expect(parsedContent.metrics[0].items[0].latestValue).toBe(85.5);
});
it('should handle empty metrics list', async () => {
// Set up the mock to return an empty array
mockFindByProject.mockResolvedValue([]);
// Call the handler
const result = await handleDeepsourceQualityMetrics({ projectKey: 'test-project' });
// Verify that the factory and repository methods were called
expect(mockRepositoryFactory).toHaveBeenCalledWith({ apiKey: 'test-api-key' });
expect(mockCreateQualityMetricsRepository).toHaveBeenCalled();
expect(mockFindByProject).toHaveBeenCalled();
// Verify logging behavior
expect(mockLogger.info).toHaveBeenCalledWith(
'Fetching quality metrics from repository',
expect.any(Object)
);
// Verify the response structure
expect(result).toHaveProperty('content');
expect(result.content).toHaveLength(1);
// Parse and verify the content is an empty array
const parsedContent = JSON.parse(result.content[0].text);
expect(parsedContent.metrics).toEqual([]);
});
it('should return error response 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 error response
const result = await handleDeepsourceQualityMetrics({ projectKey: 'test-project' });
expect(result.isError).toBe(true);
const errorData = JSON.parse(result.content[0].text);
expect(errorData.error).toBe('An error occurred while processing your request');
expect(errorData.code).toBeDefined();
expect(errorData.category).toBeDefined();
});
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 error response
const result = await handleDeepsourceQualityMetrics({ projectKey: 'test-project' });
expect(result.isError).toBe(true);
const errorData = JSON.parse(result.content[0].text);
expect(errorData.error).toBe('An error occurred while processing your request');
expect(errorData.code).toBeDefined();
expect(errorData.category).toBeDefined();
});
});
});