diff --git a/src/__tests__/cli-commands.test.ts b/src/__tests__/cli-commands.test.ts index 5547976..155bf8c 100644 --- a/src/__tests__/cli-commands.test.ts +++ b/src/__tests__/cli-commands.test.ts @@ -1,9 +1,9 @@ /* eslint-env jest */ -import { getVersion, runDoctor, addProjectWithGithubOrgs, printChecklists } from '../cli-commands.js' +import { getVersion, runDoctor, addProjectWithGithubOrgs, printChecklists, printChecks } from '../cli-commands.js' import { getPackageJson } from '../utils.js' -import { APIHealthResponse, APIProjectDetails, APIGithubOrgDetails, APIErrorResponse, APIChecklistItem } from '../types.js' -import { mockApiHealthResponse, mockAPIProjectResponse, mockAPIGithubOrgResponse, mockAPIChecklistResponse } from './fixtures.js' +import { APIHealthResponse, APIProjectDetails, APIGithubOrgDetails, APIErrorResponse, APIChecklistItem, APICheckItem } from '../types.js' +import { mockApiHealthResponse, mockAPIProjectResponse, mockAPIGithubOrgResponse, mockAPIChecklistResponse, mockAPICheckResponse } from './fixtures.js' import nock from 'nock' const pkg = getPackageJson() @@ -278,4 +278,106 @@ describe('CLI Commands', () => { expect(result.messages[0]).toBe('No compliance checklists found') }) }) + + describe('printChecks', () => { + let mockChecks: APICheckItem[] + + beforeEach(() => { + nock.cleanAll() + mockChecks = [...mockAPICheckResponse] + }) + + it('should retrieve and format check items successfully', async () => { + // Mock API call + nock('http://localhost:3000') + .get('/api/v1/compliance-check') + .reply(200, mockChecks) + + // Execute the function + const result = await printChecks() + + // Verify the result + expect(result.success).toBe(true) + expect(result.messages[0]).toBe('Compliance checks available:') + expect(result.messages[1]).toContain(mockChecks[0].code_name) + expect(result.messages[1]).toContain(mockChecks[0].description) + expect(result.messages[1]).toContain(mockChecks[0].details_url) + expect(result.messages).toHaveLength(2) // Header + 1 check item + expect(nock.isDone()).toBe(true) // Verify all mocked endpoints were called + }) + + it('should handle multiple check items', async () => { + // Add a second check item + const secondCheck = { + ...mockChecks[0], + id: 456, + title: 'Second Check', + code_name: 'secondCheck', + description: 'Another check description', + details_url: 'https://openpathfinder.com/docs/checks/secondCheck' + } + mockChecks.push(secondCheck) + + // Mock API call + nock('http://localhost:3000') + .get('/api/v1/compliance-check') + .reply(200, mockChecks) + + // Execute the function + const result = await printChecks() + + // Verify the result + expect(result.success).toBe(true) + expect(result.messages[0]).toBe('Compliance checks available:') + expect(result.messages[1]).toContain(mockChecks[0].code_name) + expect(result.messages[2]).toContain(mockChecks[1].code_name) + expect(result.messages).toHaveLength(3) // Header + 2 check items + }) + + it('should handle API errors gracefully', async () => { + // Mock API error + nock('http://localhost:3000') + .get('/api/v1/compliance-check') + .reply(500, { errors: [{ message: 'Internal server error' }] } as APIErrorResponse) + + // Execute the function + const result = await printChecks() + + // Verify the result + expect(result.success).toBe(false) + expect(result.messages[0]).toContain('❌ Failed to retrieve compliance check items') + expect(result.messages).toHaveLength(1) + }) + + it('should handle network errors gracefully', async () => { + // Mock network error + nock('http://localhost:3000') + .get('/api/v1/compliance-check') + .replyWithError('Network error') + + // Execute the function + const result = await printChecks() + + // Verify the result + expect(result.success).toBe(false) + expect(result.messages[0]).toContain('❌ Failed to retrieve compliance check items') + expect(result.messages[0]).toContain('Network error') + expect(result.messages).toHaveLength(1) + }) + + it('should handle empty check response', async () => { + // Mock empty response + nock('http://localhost:3000') + .get('/api/v1/compliance-check') + .reply(200, []) + + // Execute the function + const result = await printChecks() + + // Verify the result + expect(result.success).toBe(true) + expect(result.messages).toHaveLength(1) // Only the header message + expect(result.messages[0]).toBe('No compliance checks found') + }) + }) }) diff --git a/src/__tests__/fixtures.ts b/src/__tests__/fixtures.ts index 1abe9a9..72751f8 100644 --- a/src/__tests__/fixtures.ts +++ b/src/__tests__/fixtures.ts @@ -1,4 +1,4 @@ -import { APIHealthResponse, APIProjectDetails, APIGithubOrgDetails, APIChecklistItem } from '../types.js' +import { APIHealthResponse, APIProjectDetails, APIGithubOrgDetails, APIChecklistItem, APICheckItem } from '../types.js' export const mockApiHealthResponse: APIHealthResponse = { status: 'ok', @@ -104,3 +104,20 @@ export const mockAPIChecklistResponse: APIChecklistItem[] = [{ created_at: new Date().toISOString(), updated_at: new Date().toISOString() }] + +export const mockAPICheckResponse: APICheckItem[] = [{ + id: 53, + title: 'Refresh dependencies with annual releases', + description: 'Ensure dependencies are refreshed through a new release at least once annually', + default_section_number: '5', + default_section_name: 'vulnerability management', + code_name: 'annualDependencyRefresh', + default_priority_group: 'P14', + is_c_scrm: true, + implementation_status: 'completed', + implementation_type: 'manual', + implementation_details_reference: 'https://github.com/OpenPathfinder/visionBoard/issues/112', + details_url: 'https://openpathfinder.com/docs/checks/annualDependencyRefresh', + created_at: '2025-02-21T18:53:00.485Z', + updated_at: '2025-02-21T18:53:00.485Z' +}] diff --git a/src/api-client.ts b/src/api-client.ts index 12af157..d0485fd 100644 --- a/src/api-client.ts +++ b/src/api-client.ts @@ -1,6 +1,6 @@ import { getConfig } from './utils.js' import { got } from 'got' -import { APIHealthResponse, APIProjectDetails, APIGithubOrgDetails, APIChecklistItem } from './types.js' +import { APIHealthResponse, APIProjectDetails, APIGithubOrgDetails, APIChecklistItem, APICheckItem } from './types.js' export const apiClient = () => { const config = getConfig() @@ -61,3 +61,12 @@ export const getAllChecklistItems = async (): Promise => { } return response.body as APIChecklistItem[] } + +export const getAllChecks = async (): Promise => { + const client = apiClient() + const response = await client.get('compliance-check', { responseType: 'json' }) + if (response.statusCode !== 200) { + throw new Error(`Failed to get the data from the API: ${response.statusCode} ${response.body}`) + } + return response.body as APICheckItem[] +} diff --git a/src/cli-commands.ts b/src/cli-commands.ts index 93888ec..acd17b5 100644 --- a/src/cli-commands.ts +++ b/src/cli-commands.ts @@ -1,6 +1,6 @@ import { CommandResult } from './types.js' import { isApiAvailable, isApiCompatible, getPackageJson } from './utils.js' -import { getAPIDetails, createProject, addGithubOrgToProject, getAllChecklistItems } from './api-client.js' +import { getAPIDetails, createProject, addGithubOrgToProject, getAllChecklistItems, getAllChecks } from './api-client.js' const pkg = getPackageJson() @@ -83,3 +83,30 @@ export const printChecklists = async (): Promise => { success } } + +export const printChecks = async (): Promise => { + const messages: string[] = [] + let success = true + try { + const checks = await getAllChecks() + if (checks.length === 0) { + messages.push('No compliance checks found') + return { + messages, + success + } + } + messages.push('Compliance checks available:') + checks.forEach((check) => { + messages.push(`- ${check.code_name}: ${check.description}. ${check.details_url}`) + }) + } catch (error) { + messages.push(`❌ Failed to retrieve compliance check items: ${error instanceof Error ? error.message : 'Unknown error'}`) + success = false + } + + return { + messages, + success + } +} diff --git a/src/index.ts b/src/index.ts index f32d48d..034ef77 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ import { Command } from 'commander' // @ts-ignore import { stringToArray } from '@ulisesgascon/string-to-array' import { handleCommandResult } from './utils.js' -import { getVersion, runDoctor, addProjectWithGithubOrgs, printChecklists } from './cli-commands.js' +import { getVersion, runDoctor, addProjectWithGithubOrgs, printChecklists, printChecks } from './cli-commands.js' const program = new Command() @@ -35,6 +35,18 @@ checklist handleCommandResult(result) }) +const check = program + .command('compliance-check') + .description('Compliance check management') + +check + .command('list') + .description('Print all available compliance checks') + .action(async () => { + const result = await printChecks() + handleCommandResult(result) + }) + const project = program .command('project') .description('Project management') diff --git a/src/types.ts b/src/types.ts index 9987120..d366bdc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -119,6 +119,28 @@ export interface APIChecklistItem { updated_at: string } +type PriorityGroup = 'P0' | 'P1' | 'P2' | 'P3' | 'P4' | 'P5' | 'P6' | 'P7' | 'P8' | 'P9' | 'P10' | 'P11' | 'P12' | 'P13' | 'P14' | 'R0' | 'R1' | 'R2' | 'R3' | 'R4' | 'R5' | 'R6' | 'R7' | 'R8' | 'R9' | 'R10' | 'R11' | 'R12' | 'R13' | 'R14'; + +/** + * Check Schema + */ +export type APICheckItem = { + id: number; + title: string; + description: string; + default_section_number: string; + default_section_name: string; + code_name: string; + default_priority_group: PriorityGroup; + is_c_scrm: boolean; + implementation_status: 'pending' | 'completed'; + implementation_type: string | null; + implementation_details_reference: string | null; + details_url: string; + created_at: string; + updated_at: string; +}; + /** * Error object as defined in the OpenAPI schema */