Skip to content
Merged
115 changes: 102 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
},
"dependencies": {
"@ulisesgascon/string-to-array": "2.0.0",
"ajv": "8.17.1",
"ajv-formats": "3.0.1",
"commander": "14.0.0",
"got": "14.4.7",
"nock": "14.0.5",
Expand Down
114 changes: 111 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, printChecks, printWorkflows } from '../cli-commands.js'
import { getVersion, runDoctor, addProjectWithGithubOrgs, printChecklists, printChecks, printWorkflows, executeWorkflow } from '../cli-commands.js'
import { getPackageJson } from '../utils.js'
import { APIHealthResponse, APIProjectDetails, APIGithubOrgDetails, APIErrorResponse, APIChecklistItem, APICheckItem, APIWorkflowItem } from '../types.js'
import { mockApiHealthResponse, mockAPIProjectResponse, mockAPIGithubOrgResponse, mockAPIChecklistResponse, mockAPICheckResponse, mockAPIWorkflowResponse } from './fixtures.js'
import { APIHealthResponse, APIProjectDetails, APIGithubOrgDetails, APIErrorResponse, APIChecklistItem, APICheckItem, APIWorkflowItem, APIWorkflowRunItem } from '../types.js'
import { mockApiHealthResponse, mockAPIProjectResponse, mockAPIGithubOrgResponse, mockAPIChecklistResponse, mockAPICheckResponse, mockAPIWorkflowResponse, mockAPIWorkflowRunResponse } from './fixtures.js'
import nock from 'nock'

const pkg = getPackageJson()
Expand Down Expand Up @@ -432,6 +432,32 @@ describe('CLI Commands', () => {
expect(result.messages).toHaveLength(3) // Header + 2 workflow items
})

it('should handle disabled workflows', async () => {
// Add a second workflow item
const secondWorkflow = {
...mockWorkflows[0],
id: 'create-stuff',
description: 'Another workflow description',
isEnabled: false
}
mockWorkflows.push(secondWorkflow)

// Mock API call
nock('http://localhost:3000')
.get('/api/v1/workflow')
.reply(200, mockWorkflows)

// Execute the function
const result = await printWorkflows()

// Verify the result
expect(result.success).toBe(true)
expect(result.messages[0]).toBe('Compliance workflows available:')
expect(result.messages[1]).toContain(mockWorkflows[0].id)
expect(result.messages[1]).toContain(mockWorkflows[0].description)
expect(result.messages).toHaveLength(2) // Header + 1 enabled workflow item
})

it('should handle API errors gracefully', async () => {
// Mock API error
nock('http://localhost:3000')
Expand Down Expand Up @@ -478,4 +504,86 @@ describe('CLI Commands', () => {
expect(result.messages[0]).toBe('No compliance workflows found')
})
})

describe('executeWorkflow', () => {
let workflowRunResponse: APIWorkflowRunItem

beforeEach(() => {
nock.cleanAll()

// Setup mock workflow run response
workflowRunResponse = { ...mockAPIWorkflowRunResponse }
})

it('should execute a workflow successfully', async () => {
// Mock API call
nock('http://localhost:3000')
.post('/api/v1/workflow/update-stuff/run', { data: { projectId: 123 } })
.reply(202, workflowRunResponse)

// Execute the function
const result = await executeWorkflow('update-stuff', { projectId: 123 })

// Verify the result
expect(result.success).toBe(true)
expect(result.messages).toHaveLength(5) // 5 messages with details
expect(result.messages[0]).toContain('Workflow executed successfully in 2.50 seconds')
expect(result.messages[1]).toContain('Status: completed')
expect(result.messages[2]).toContain('Started:')
expect(result.messages[3]).toContain('Finished:')
expect(result.messages[4]).toContain('Result:')
expect(nock.isDone()).toBe(true) // Verify all mocked endpoints were called
})

it('Should execute a workflow that was unsuccessful', async () => {
// Mock API call
nock('http://localhost:3000')
.post('/api/v1/workflow/update-stuff/run', { data: { projectId: 123 } })
.reply(202, { ...workflowRunResponse, status: 'failed', result: { message: 'Failed to execute workflow', success: false } })

// Execute the function
const result = await executeWorkflow('update-stuff', { projectId: 123 })

// Verify the result
expect(result.success).toBe(true)
expect(result.messages).toHaveLength(5) // 5 messages with details
expect(result.messages[0]).toContain('Workflow executed unsuccessfully in 2.50 seconds')
expect(result.messages[1]).toContain('Status: failed')
expect(result.messages[2]).toContain('Started:')
expect(result.messages[3]).toContain('Finished:')
expect(result.messages[4]).toContain('Result:')
expect(nock.isDone()).toBe(true) // Verify all mocked endpoints were called
})

it('should handle API errors gracefully', async () => {
// Mock API error
nock('http://localhost:3000')
.post('/api/v1/workflow/invalid-workflow/run')
.reply(404, { errors: [{ message: 'Workflow not found' }] } as APIErrorResponse)

// Execute the function
const result = await executeWorkflow('invalid-workflow', {})

// Verify the result
expect(result.success).toBe(false)
expect(result.messages[0]).toContain('❌ Failed to execute the workflow')
expect(result.messages).toHaveLength(1)
})

it('should handle network errors gracefully', async () => {
// Mock network error
nock('http://localhost:3000')
.post('/api/v1/workflow/update-stuff/run')
.replyWithError('Network error')

// Execute the function
const result = await executeWorkflow('update-stuff', {})

// Verify the result
expect(result.success).toBe(false)
expect(result.messages[0]).toContain('❌ Failed to execute the workflow')
expect(result.messages[0]).toContain('Network error')
expect(result.messages).toHaveLength(1)
})
})
})
16 changes: 14 additions & 2 deletions src/__tests__/fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { APIHealthResponse, APIProjectDetails, APIGithubOrgDetails, APIChecklistItem, APICheckItem, APIWorkflowItem } from '../types.js'
import { APIHealthResponse, APIProjectDetails, APIGithubOrgDetails, APIChecklistItem, APICheckItem, APIWorkflowItem, APIWorkflowRunItem } from '../types.js'

export const mockApiHealthResponse: APIHealthResponse = {
status: 'ok',
Expand Down Expand Up @@ -124,5 +124,17 @@ export const mockAPICheckResponse: APICheckItem[] = [{

export const mockAPIWorkflowResponse: APIWorkflowItem[] = [{
id: 'update-stuff',
description: 'Test workflow description'
description: 'Test workflow description',
isEnabled: true,
isRequiredAdditionalData: false,
operations: null,
schema: null
}]

export const mockAPIWorkflowRunResponse: APIWorkflowRunItem = {
status: 'completed',
started: '2025-06-21T10:05:00.000Z',
finished: '2025-06-21T10:05:02.500Z',
completed: '2025-06-21T10:05:02.500Z',
result: { success: true, message: 'Workflow completed successfully' }
}
15 changes: 14 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, APICheckItem, APIWorkflowItem } from './types.js'
import { APIHealthResponse, APIProjectDetails, APIGithubOrgDetails, APIChecklistItem, APICheckItem, APIWorkflowItem, APIWorkflowRunItem } from './types.js'

export const apiClient = () => {
const config = getConfig()
Expand Down Expand Up @@ -79,3 +79,16 @@ export const getAllWorkflows = async (): Promise<APIWorkflowItem[]> => {
}
return response.body as APIWorkflowItem[]
}

export const runWorkflow = async (workflowId: string, data: any): Promise<APIWorkflowRunItem> => {
const client = apiClient()
const payload = data ? { data } : {}
const response = await client.post(`workflow/${workflowId}/run`, {
json: payload,
responseType: 'json'
})
if (response.statusCode !== 202) {
throw new Error(`Failed to run the workflow: ${response.statusCode} ${response.body}`)
}
return response.body as APIWorkflowRunItem
}
Loading