Skip to main content
Glama
discriminated-unions.test.ts15.9 kB
/** * @vitest-environment node */ import { ErrorCategory } from '../utils/errors/categories.js'; import { MetricThresholdStatus } from '../types/metrics.js'; import { ReportType } from '../deepsource.js'; import { AuthError, NetworkError, ServerError, ClientError, ApiResponse, SuccessResponse, ErrorResponse, PendingRun, RunningRun, SuccessfulRun, FailedRun, TimedOutRun, CancelledRun, SkippedRun, RunState, OwaspTop10Report, SansTop25Report, CodeCoverageReport, IssuesPreventedReport, ComplianceReport, PassingMetric, FailingMetric, MetricState, isErrorOfCategory, isSuccessResponse, isRunInState, isReportOfType, isMetricInState, } from '../types/discriminated-unions.js'; import { asRunId, asBranchName, asCommitOid } from '../types/branded.js'; describe('Discriminated Unions', () => { const runId = asRunId('run-123'); const commitOid = asCommitOid('abcdef123456'); const branchName = asBranchName('main'); describe('ApiError Union', () => { it('should create different error types with correct discriminants', () => { // Auth error const authError: AuthError = { category: ErrorCategory.AUTH, message: 'Invalid API key', token: { present: true, expired: true, }, }; // Network error const networkError: NetworkError = { category: ErrorCategory.NETWORK, message: 'Connection failed', url: 'https://api.deepsource.io', connection: { established: false, timeSpent: 5000, }, }; // Server error const serverError: ServerError = { category: ErrorCategory.SERVER, message: 'Internal server error', statusCode: 500, response: { error: 'unexpected error' }, }; expect(authError.category).toBe(ErrorCategory.AUTH); expect(networkError.category).toBe(ErrorCategory.NETWORK); expect(serverError.category).toBe(ErrorCategory.SERVER); // Type guard should correctly identify error categories expect(isErrorOfCategory(authError, ErrorCategory.AUTH)).toBe(true); expect(isErrorOfCategory(networkError, ErrorCategory.AUTH)).toBe(false); expect(isErrorOfCategory(serverError, ErrorCategory.SERVER)).toBe(true); }); it('should handle errors with metadata', () => { const errorWithMetadata: ClientError = { category: ErrorCategory.CLIENT, message: 'Validation failed', statusCode: 400, metadata: { requestId: '1234', timestamp: new Date().toISOString(), }, validationErrors: { field1: 'Required field', field2: 'Invalid format', }, }; expect(errorWithMetadata.metadata).toBeDefined(); expect(errorWithMetadata.validationErrors).toBeDefined(); expect(errorWithMetadata.validationErrors?.field1).toBe('Required field'); }); }); describe('ApiResponse Union', () => { it('should handle successful responses', () => { const successResponse: SuccessResponse<string> = { success: true, data: 'Response data', }; expect(isSuccessResponse(successResponse)).toBe(true); expect(successResponse.data).toBe('Response data'); }); it('should handle error responses', () => { const errorResponse: ErrorResponse = { success: false, error: { category: ErrorCategory.SERVER, message: 'Server error occurred', }, }; expect(isSuccessResponse(errorResponse)).toBe(false); expect(errorResponse.error.message).toBe('Server error occurred'); }); it('should discriminate based on success property', () => { const handleResponse = (response: ApiResponse<string>): string => { if (isSuccessResponse(response)) { return `Success: ${response.data}`; } else { return `Error: ${response.error.message}`; } }; const successResponse: SuccessResponse<string> = { success: true, data: 'Data received', }; const errorResponse: ErrorResponse = { success: false, error: { category: ErrorCategory.NOT_FOUND, message: 'Resource not found', }, }; expect(handleResponse(successResponse)).toBe('Success: Data received'); expect(handleResponse(errorResponse)).toBe('Error: Resource not found'); }); }); describe('RunState Union', () => { it('should create different run states with correct discriminants', () => { // Pending run const pendingRun: PendingRun = { status: 'PENDING', runId, commitOid, branchName, baseOid: commitOid, createdAt: '2023-05-20T10:00:00Z', updatedAt: '2023-05-20T10:00:00Z', queuePosition: 3, estimatedStartTime: '2023-05-20T10:05:00Z', }; // Running run const runningRun: RunningRun = { status: 'READY', runId, commitOid, branchName, baseOid: commitOid, createdAt: '2023-05-20T10:00:00Z', updatedAt: '2023-05-20T10:05:00Z', progress: 45, currentStage: 'analyzing', estimatedCompletionTime: '2023-05-20T10:15:00Z', }; // Successful run const successfulRun: SuccessfulRun = { status: 'SUCCESS', runId, commitOid, branchName, baseOid: commitOid, createdAt: '2023-05-20T10:00:00Z', updatedAt: '2023-05-20T10:15:00Z', finishedAt: '2023-05-20T10:15:00Z', summary: { occurrencesIntroduced: 5, occurrencesResolved: 10, occurrencesSuppressed: 2, analyzerDistribution: [ { analyzer: 'python', count: 3 }, { analyzer: 'javascript', count: 2 }, ], categoryDistribution: [ { category: 'security', count: 1 }, { category: 'maintainability', count: 4 }, ], }, }; expect(pendingRun.status).toBe('PENDING'); expect(runningRun.status).toBe('READY'); expect(successfulRun.status).toBe('SUCCESS'); // Type guard should correctly identify run states expect(isRunInState<PendingRun>(pendingRun, 'PENDING')).toBe(true); expect(isRunInState<RunningRun>(runningRun, 'READY')).toBe(true); expect(isRunInState<SuccessfulRun>(successfulRun, 'SUCCESS')).toBe(true); expect(isRunInState<FailedRun>(successfulRun, 'FAILURE')).toBe(false); }); it('should handle run state specific properties', () => { // Function that returns different messages based on run state const getRunMessage = (run: RunState): string => { switch (run.status) { case 'PENDING': return `Run ${run.runId} is pending in position ${(run as PendingRun).queuePosition || 'unknown'}`; case 'READY': return `Run ${run.runId} is in progress (${(run as RunningRun).progress || 0}% complete)`; case 'SUCCESS': return `Run ${run.runId} succeeded with ${(run as SuccessfulRun).summary.occurrencesIntroduced} new issues`; case 'FAILURE': return `Run ${run.runId} failed: ${(run as FailedRun).error?.message || 'Unknown error'}`; case 'TIMEOUT': return `Run ${run.runId} timed out after ${(run as TimedOutRun).timeout?.limitSeconds || 'unknown'} seconds`; case 'CANCEL': return `Run ${run.runId} was cancelled by ${(run as CancelledRun).cancelledBy || 'unknown'}`; case 'SKIPPED': return `Run ${run.runId} was skipped: ${(run as SkippedRun).skipReason || 'No reason provided'}`; default: return `Run ${run.runId} has unknown status: ${run.status}`; } }; const pendingRun: PendingRun = { status: 'PENDING', runId, commitOid, branchName, baseOid: commitOid, createdAt: '2023-05-20T10:00:00Z', updatedAt: '2023-05-20T10:00:00Z', queuePosition: 3, }; const failedRun: FailedRun = { status: 'FAILURE', runId, commitOid, branchName, baseOid: commitOid, createdAt: '2023-05-20T10:00:00Z', updatedAt: '2023-05-20T10:05:00Z', finishedAt: '2023-05-20T10:05:00Z', error: { message: 'Permission denied', code: 'ACCESS_DENIED', }, }; expect(getRunMessage(pendingRun)).toBe('Run run-123 is pending in position 3'); expect(getRunMessage(failedRun)).toBe('Run run-123 failed: Permission denied'); }); }); describe('ComplianceReport Union', () => { it('should create different report types with correct discriminants', () => { // OWASP report const owaspReport: OwaspTop10Report = { type: ReportType.OWASP_TOP_10, title: 'OWASP Top 10', currentValue: 85, status: 'PASSING', categories: [ { key: 'A01:2021', title: 'Broken Access Control', issues: { critical: 0, major: 2, minor: 1, total: 3, }, }, { key: 'A02:2021', title: 'Cryptographic Failures', issues: { critical: 1, major: 0, minor: 0, total: 1, }, }, ], }; // Code coverage report const coverageReport: CodeCoverageReport = { type: ReportType.CODE_COVERAGE, title: 'Code Coverage', currentValue: 78.5, status: 'PASSING', coverage: { line: 80.2, branch: 75.6, statement: 81.3, function: 90.0, }, trend: [ { date: '2023-04-20', value: 75.0 }, { date: '2023-05-01', value: 76.2 }, { date: '2023-05-15', value: 78.5 }, ], }; expect(owaspReport.type).toBe(ReportType.OWASP_TOP_10); expect(coverageReport.type).toBe(ReportType.CODE_COVERAGE); // Type guard should correctly identify report types expect(isReportOfType<OwaspTop10Report>(owaspReport, ReportType.OWASP_TOP_10)).toBe(true); expect(isReportOfType<CodeCoverageReport>(coverageReport, ReportType.CODE_COVERAGE)).toBe( true ); expect(isReportOfType<SansTop25Report>(owaspReport, ReportType.SANS_TOP_25)).toBe(false); }); it('should handle report-specific properties', () => { // Function that processes report based on type const getReportSummary = (report: ComplianceReport): string => { switch (report.type) { case ReportType.OWASP_TOP_10: return `OWASP compliance: ${report.currentValue}%, issues in ${(report as OwaspTop10Report).categories.length} categories`; case ReportType.SANS_TOP_25: return `SANS compliance: ${report.currentValue}%`; case ReportType.MISRA_C: return `MISRA-C compliance: ${report.currentValue}%`; case ReportType.CODE_COVERAGE: return `Code coverage: ${report.currentValue}% (Line: ${(report as CodeCoverageReport).coverage.line}%)`; case ReportType.ISSUES_PREVENTED: return `Issues prevented: ${(report as IssuesPreventedReport).prevented.total}`; default: return `Unknown report type: ${report.type}, value: ${report.currentValue}%`; } }; const owaspReport: OwaspTop10Report = { type: ReportType.OWASP_TOP_10, title: 'OWASP Top 10', currentValue: 85, status: 'PASSING', categories: [ { key: 'A01:2021', title: 'Broken Access Control', issues: { critical: 0, major: 2, minor: 1, total: 3, }, }, ], }; const coverageReport: CodeCoverageReport = { type: ReportType.CODE_COVERAGE, title: 'Code Coverage', currentValue: 78.5, status: 'PASSING', coverage: { line: 80.2, branch: 75.6, statement: 81.3, function: 90.0, }, }; expect(getReportSummary(owaspReport)).toBe('OWASP compliance: 85%, issues in 1 categories'); expect(getReportSummary(coverageReport)).toBe('Code coverage: 78.5% (Line: 80.2%)'); }); }); describe('MetricState Union', () => { it('should create different metric states with correct discriminants', () => { // Passing metric const passingMetric: PassingMetric = { shortcode: 'LCV', name: 'Line Coverage', unit: '%', positiveDirection: 'UPWARD', value: 85.2, status: MetricThresholdStatus.PASSING, threshold: 80.0, margin: 5.2, }; // Failing metric const failingMetric: FailingMetric = { shortcode: 'DDP', name: 'Duplicate Code Percentage', unit: '%', positiveDirection: 'DOWNWARD', value: 12.5, status: MetricThresholdStatus.FAILING, threshold: 10.0, gap: 2.5, recommendations: [ 'Extract common code into shared functions', 'Use inheritance or composition to reduce duplication', ], }; expect(passingMetric.status).toBe(MetricThresholdStatus.PASSING); expect(failingMetric.status).toBe(MetricThresholdStatus.FAILING); // Type guard should correctly identify metric states expect(isMetricInState<PassingMetric>(passingMetric, MetricThresholdStatus.PASSING)).toBe( true ); expect(isMetricInState<FailingMetric>(failingMetric, MetricThresholdStatus.FAILING)).toBe( true ); expect(isMetricInState<FailingMetric>(passingMetric, MetricThresholdStatus.FAILING)).toBe( false ); }); it('should handle metric state specific properties', () => { // Function that returns different messages based on metric state const getMetricMessage = (metric: MetricState): string => { switch (metric.status) { case MetricThresholdStatus.PASSING: return `${metric.name} is passing at ${metric.value}${metric.unit} (exceeds threshold by ${(metric as PassingMetric).margin}${metric.unit})`; case MetricThresholdStatus.FAILING: return `${metric.name} is failing at ${metric.value}${metric.unit} (below threshold by ${(metric as FailingMetric).gap}${metric.unit})`; case MetricThresholdStatus.UNKNOWN: return `${metric.name} has no threshold set, current value is ${metric.value}${metric.unit}`; default: return `${metric.name} has unknown status: ${metric.status}, value: ${metric.value}${metric.unit}`; } }; const passingMetric: PassingMetric = { shortcode: 'LCV', name: 'Line Coverage', unit: '%', positiveDirection: 'UPWARD', value: 85.2, status: MetricThresholdStatus.PASSING, threshold: 80.0, margin: 5.2, }; const failingMetric: FailingMetric = { shortcode: 'DDP', name: 'Duplicate Code Percentage', unit: '%', positiveDirection: 'DOWNWARD', value: 12.5, status: MetricThresholdStatus.FAILING, threshold: 10.0, gap: 2.5, }; expect(getMetricMessage(passingMetric)).toBe( 'Line Coverage is passing at 85.2% (exceeds threshold by 5.2%)' ); expect(getMetricMessage(failingMetric)).toBe( 'Duplicate Code Percentage is failing at 12.5% (below threshold by 2.5%)' ); }); }); });

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