Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 105 additions & 3 deletions src/__tests__/cli-commands.test.ts
Original file line number Diff line number Diff line change
@@ -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()
Expand Down Expand Up @@ -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')
})
})
})
19 changes: 18 additions & 1 deletion src/__tests__/fixtures.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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'
}]
11 changes: 10 additions & 1 deletion src/api-client.ts
Original file line number Diff line number Diff line change
@@ -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()
Expand Down Expand Up @@ -61,3 +61,12 @@ export const getAllChecklistItems = async (): Promise<APIChecklistItem[]> => {
}
return response.body as APIChecklistItem[]
}

export const getAllChecks = async (): Promise<APICheckItem[]> => {
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[]
}
29 changes: 28 additions & 1 deletion src/cli-commands.ts
Original file line number Diff line number Diff line change
@@ -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()

Expand Down Expand Up @@ -83,3 +83,30 @@ export const printChecklists = async (): Promise<CommandResult> => {
success
}
}

export const printChecks = async (): Promise<CommandResult> => {
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
}
}
14 changes: 13 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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')
Expand Down
22 changes: 22 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down