UNPKG

deepsource-mcp-server

Version:
514 lines (418 loc) 17.4 kB
/** * @fileoverview Tests for QualityMetricsRepository */ import { describe, it, expect, beforeEach } from 'vitest'; import { QualityMetricsRepository } from '../quality-metrics.repository.js'; import { DeepSourceClient } from '../../../deepsource.js'; import { RepositoryMetric, MetricShortcode, MetricDirection, MetricThresholdStatus, MetricKey, } from '../../../models/metrics.js'; import { asProjectKey } from '../../../types/branded.js'; import { QualityMetrics } from '../../../domain/aggregates/quality-metrics/quality-metrics.aggregate.js'; import { DeepSourceProject } from '../../../models/projects.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('QualityMetricsRepository', () => { let repository: QualityMetricsRepository; let mockClient: anyed<DeepSourceClient>; let mockApiMetrics: RepositoryMetric[]; beforeEach(() => { // Reset all mocks vi.clearAllMocks(); // Create mock DeepSourceClient mockClient = { listProjects: vi.fn(), getQualityMetrics: vi.fn(), setMetricThreshold: vi.fn(), updateMetricSetting: vi.fn(), listRuns: vi.fn(), } as unknown as anyed<DeepSourceClient>; // Create test data mockApiMetrics = [ { name: 'Line Coverage', shortcode: MetricShortcode.LCV, description: 'Percentage of lines covered by tests', positiveDirection: MetricDirection.UPWARD, unit: '%', minValueAllowed: 0, maxValueAllowed: 100, isReported: true, isThresholdEnforced: true, items: [ { id: 'lcv-aggregate', key: 'AGGREGATE', threshold: 80, latestValue: 85.5, latestValueDisplay: '85.5%', thresholdStatus: MetricThresholdStatus.PASSING, }, { id: 'lcv-python', key: 'PYTHON', threshold: 75, latestValue: 70, latestValueDisplay: '70%', thresholdStatus: MetricThresholdStatus.FAILING, }, ], }, { name: 'Branch Coverage', shortcode: MetricShortcode.BCV, description: 'Percentage of branches covered by tests', positiveDirection: MetricDirection.UPWARD, unit: '%', minValueAllowed: 0, maxValueAllowed: 100, isReported: true, isThresholdEnforced: false, items: [ { id: 'bcv-aggregate', key: 'AGGREGATE', threshold: 70, latestValue: 75, latestValueDisplay: '75%', thresholdStatus: MetricThresholdStatus.PASSING, }, ], }, ]; // Setup default mock behavior mockClient.listProjects.mockResolvedValue([ { key: asProjectKey('test-project'), name: 'Test Project', repository: { url: 'https://github.com/test/test-project', provider: 'GITHUB', login: 'test', isPrivate: false, isActivated: true, }, } as DeepSourceProject, ]); // Mock listRuns to provide repository ID mockClient.listRuns.mockResolvedValue({ items: [ { id: 'run-1', repository: { id: 'repo-123' }, }, ], pageInfo: { hasNextPage: false }, totalCount: 1, } as { items: Array<{ id: string; repository: { id: string } }>; pageInfo: { hasNextPage: boolean }; totalCount: number; }); mockClient.getQualityMetrics.mockResolvedValue(mockApiMetrics); // Create repository instance repository = new QualityMetricsRepository(mockClient); }); describe('findById', () => { it('should find metrics by composite ID', async () => { const id = 'test-project:AGGREGATE:LCV'; const metrics = await repository.findById(id); expect(metrics).not.toBeNull(); expect(metrics?.configuration.shortcode).toBe(MetricShortcode.LCV); expect(metrics?.configuration.metricKey).toBe('AGGREGATE'); expect(mockClient.getQualityMetrics).toHaveBeenCalledWith('test-project', { shortcodeIn: [MetricShortcode.LCV], }); }); it('should return null for invalid composite ID format', async () => { const id = 'invalid-id-format'; const metrics = await repository.findById(id); expect(metrics).toBeNull(); expect(mockClient.getQualityMetrics).not.toHaveBeenCalled(); }); }); describe('findByProject', () => { it('should return all metrics for a project', async () => { const projectKey = asProjectKey('test-project'); const metrics = await repository.findByProject(projectKey); expect(metrics).toHaveLength(3); // 2 from LCV, 1 from BCV expect(metrics[0]).toBeInstanceOf(QualityMetrics); expect(metrics.map((m) => m.configuration.shortcode)).toContain(MetricShortcode.LCV); expect(metrics.map((m) => m.configuration.shortcode)).toContain(MetricShortcode.BCV); expect(mockClient.getQualityMetrics).toHaveBeenCalledWith(projectKey); }); it('should return empty array when no metrics exist', async () => { mockClient.getQualityMetrics.mockResolvedValue([]); const metrics = await repository.findByProject(asProjectKey('test-project')); expect(metrics).toEqual([]); }); it('should throw error when project not found', async () => { mockClient.listProjects.mockResolvedValue([]); await expect(repository.findByProject(asProjectKey('test-project'))).rejects.toThrow( 'Project not found: test-project' ); }); }); describe('findByProjectWithFilter', () => { it('should return filtered metrics for a project', async () => { const projectKey = asProjectKey('test-project'); const shortcodeIn = [MetricShortcode.LCV]; // Mock API to return only LCV metrics when filtered mockClient.getQualityMetrics.mockResolvedValue([mockApiMetrics[0]]); const metrics = await repository.findByProjectWithFilter(projectKey, shortcodeIn); expect(metrics).toHaveLength(2); // Only LCV metrics expect(metrics.every((m) => m.configuration.shortcode === MetricShortcode.LCV)).toBe(true); expect(mockClient.getQualityMetrics).toHaveBeenCalledWith(projectKey, { shortcodeIn }); }); it('should handle multiple shortcode filters', async () => { const projectKey = asProjectKey('test-project'); const shortcodeIn = [MetricShortcode.LCV, MetricShortcode.BCV]; const metrics = await repository.findByProjectWithFilter(projectKey, shortcodeIn); expect(metrics).toHaveLength(3); // 2 LCV + 1 BCV expect(mockClient.getQualityMetrics).toHaveBeenCalledWith(projectKey, { shortcodeIn }); }); it('should return empty array when filtered metrics do not exist', async () => { const projectKey = asProjectKey('test-project'); const shortcodeIn = [MetricShortcode.DDP]; mockClient.getQualityMetrics.mockResolvedValue([]); const metrics = await repository.findByProjectWithFilter(projectKey, shortcodeIn); expect(metrics).toEqual([]); expect(mockClient.getQualityMetrics).toHaveBeenCalledWith(projectKey, { shortcodeIn }); }); it('should throw and log error when API call fails', async () => { const projectKey = asProjectKey('test-project'); const shortcodeIn = [MetricShortcode.LCV]; const error = new Error('API Error'); mockClient.getQualityMetrics.mockRejectedValue(error); await expect(repository.findByProjectWithFilter(projectKey, shortcodeIn)).rejects.toThrow( 'API Error' ); expect(mockClient.getQualityMetrics).toHaveBeenCalledWith(projectKey, { shortcodeIn }); }); it('should log debug information for successful calls', async () => { const projectKey = asProjectKey('test-project'); const shortcodeIn = [MetricShortcode.BCV]; // Return only BCV metrics mockClient.getQualityMetrics.mockResolvedValue([mockApiMetrics[1]]); const metrics = await repository.findByProjectWithFilter(projectKey, shortcodeIn); expect(metrics).toHaveLength(1); expect(metrics[0].configuration.shortcode).toBe(MetricShortcode.BCV); }); }); describe('findByProjectAndMetric', () => { it('should find specific metric by shortcode and key', async () => { const projectKey = asProjectKey('test-project'); const metrics = await repository.findByProjectAndMetric( projectKey, MetricShortcode.LCV, 'PYTHON' ); expect(metrics).not.toBeNull(); expect(metrics?.configuration.shortcode).toBe(MetricShortcode.LCV); expect(metrics?.configuration.metricKey).toBe('PYTHON'); expect(metrics?.currentValue?.value).toBe(70); expect(metrics?.thresholdStatus).toBe('FAILING'); }); it('should return first item when no metric key specified', async () => { const projectKey = asProjectKey('test-project'); const metrics = await repository.findByProjectAndMetric(projectKey, MetricShortcode.LCV); expect(metrics).not.toBeNull(); expect(metrics?.configuration.metricKey).toBe('AGGREGATE'); }); it('should return null when metric not found', async () => { const projectKey = asProjectKey('test-project'); const metrics = await repository.findByProjectAndMetric(projectKey, MetricShortcode.DDP); expect(metrics).toBeNull(); }); it('should return null when metric key not found', async () => { const projectKey = asProjectKey('test-project'); const metrics = await repository.findByProjectAndMetric( projectKey, MetricShortcode.LCV, 'JAVASCRIPT' ); expect(metrics).toBeNull(); }); it('should return null when metric has no items', async () => { mockClient.getQualityMetrics.mockResolvedValue([ { ...mockApiMetrics[0], items: [], }, ]); const metrics = await repository.findByProjectAndMetric( asProjectKey('test-project'), MetricShortcode.LCV ); expect(metrics).toBeNull(); }); }); describe('findFailingMetrics', () => { it('should return only failing metrics', async () => { const projectKey = asProjectKey('test-project'); const failingMetrics = await repository.findFailingMetrics(projectKey); expect(failingMetrics).toHaveLength(1); expect(failingMetrics[0].configuration.shortcode).toBe(MetricShortcode.LCV); expect(failingMetrics[0].configuration.metricKey).toBe('PYTHON'); expect(failingMetrics[0].thresholdStatus).toBe('FAILING'); }); it('should return empty array when no failing metrics', async () => { // Mock all metrics as passing by ensuring values are above thresholds const passingMetrics = mockApiMetrics.map((metric) => ({ ...metric, items: metric.items.map((item) => ({ ...item, latestValue: item.threshold ? item.threshold + 5 : 85, // Ensure value > threshold thresholdStatus: 'PASSING' as const, })), })); mockClient.getQualityMetrics.mockResolvedValue(passingMetrics); const failingMetrics = await repository.findFailingMetrics(asProjectKey('test-project')); expect(failingMetrics).toEqual([]); }); }); describe('findReportedMetrics', () => { it('should return only reported metrics', async () => { const projectKey = asProjectKey('test-project'); const reportedMetrics = await repository.findReportedMetrics(projectKey); expect(reportedMetrics).toHaveLength(3); // All test metrics are reported expect(reportedMetrics.every((m) => m.configuration.isReported)).toBe(true); }); it('should filter out non-reported metrics', async () => { // Mock some metrics as not reported mockClient.getQualityMetrics.mockResolvedValue([ mockApiMetrics[0], // Reported { ...mockApiMetrics[1], isReported: false }, // Not reported ]); const reportedMetrics = await repository.findReportedMetrics(asProjectKey('test-project')); expect(reportedMetrics).toHaveLength(2); // Only LCV items }); }); describe('findByCompositeId', () => { it('should find metrics by composite ID components', async () => { const id = { projectKey: asProjectKey('test-project'), metricKey: MetricKey.AGGREGATE, shortcode: MetricShortcode.BCV, }; const metrics = await repository.findByCompositeId(id); expect(metrics).not.toBeNull(); expect(metrics?.configuration.shortcode).toBe(MetricShortcode.BCV); expect(metrics?.configuration.metricKey).toBe('AGGREGATE'); }); }); describe('countByProject', () => { it('should count all metrics for a project', async () => { const count = await repository.countByProject(asProjectKey('test-project')); expect(count).toBe(3); }); it('should return 0 when no metrics exist', async () => { mockClient.getQualityMetrics.mockResolvedValue([]); const count = await repository.countByProject(asProjectKey('test-project')); expect(count).toBe(0); }); }); describe('countFailingByProject', () => { it('should count failing metrics for a project', async () => { const count = await repository.countFailingByProject(asProjectKey('test-project')); expect(count).toBe(1); }); }); describe('exists', () => { it('should return true when metric exists', async () => { const exists = await repository.exists(asProjectKey('test-project'), MetricShortcode.LCV); expect(exists).toBe(true); }); it('should return false when metric does not exist', async () => { const exists = await repository.exists(asProjectKey('test-project'), MetricShortcode.DDP); expect(exists).toBe(false); }); }); describe('save', () => { it('should update threshold and settings', async () => { const projectKey = asProjectKey('test-project'); const metrics = await repository.findByProjectAndMetric(projectKey, MetricShortcode.LCV); expect(metrics).not.toBeNull(); if (!metrics) return; mockClient.setMetricThreshold.mockResolvedValue({ ok: true }); mockClient.updateMetricSetting.mockResolvedValue({ ok: true }); await repository.save(metrics); expect(mockClient.setMetricThreshold).toHaveBeenCalledWith({ repositoryId: 'repo-123', metricShortcode: MetricShortcode.LCV, metricKey: MetricKey.AGGREGATE, thresholdValue: 80, }); expect(mockClient.updateMetricSetting).toHaveBeenCalledWith({ repositoryId: 'repo-123', metricShortcode: MetricShortcode.LCV, isReported: true, isThresholdEnforced: true, }); }); it('should handle null threshold', async () => { const projectKey = asProjectKey('test-project'); const metrics = await repository.findByProjectAndMetric(projectKey, MetricShortcode.LCV); expect(metrics).not.toBeNull(); if (!metrics) return; metrics.updateThreshold(null); await repository.save(metrics); expect(mockClient.setMetricThreshold).toHaveBeenCalledWith( expect.objectContaining({ thresholdValue: null, }) ); }); it('should propagate errors from client', async () => { const projectKey = asProjectKey('test-project'); const metrics = await repository.findByProjectAndMetric(projectKey, MetricShortcode.LCV); expect(metrics).not.toBeNull(); if (!metrics) return; mockClient.setMetricThreshold.mockRejectedValue(new Error('API Error')); await expect(repository.save(metrics)).rejects.toThrow('API Error'); }); }); describe('delete', () => { it('should throw error indicating operation not supported', async () => { await expect(repository.delete('some-id')).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); // Update mock data if (mockApiMetrics[0]?.items?.[0]) { mockApiMetrics[0].items[0].latestValue = 90; } // Second call should get fresh data const metrics = await repository.findByProject(projectKey); expect(mockClient.getQualityMetrics).toHaveBeenCalledTimes(2); expect(metrics[0]?.currentValue?.value).toBe(90); }); it('should not cache results between different method calls', async () => { const projectKey = asProjectKey('test-project'); await repository.findByProject(projectKey); await repository.findByProjectAndMetric(projectKey, MetricShortcode.LCV); await repository.countByProject(projectKey); await repository.findFailingMetrics(projectKey); expect(mockClient.getQualityMetrics).toHaveBeenCalledTimes(4); }); }); });