diff --git a/README.md b/README.md index 165ff82..264b6df 100644 --- a/README.md +++ b/README.md @@ -209,9 +209,11 @@ qasphere api │ ├── list --project-code # List runs │ ├── clone --project-code --run-id --title # Clone run │ ├── close --project-code --run-id # Close run -│ └── test-cases -│ ├── list --project-code --run-id # List test cases in run -│ └── get --project-code --run-id --tcase-id # Get test case in run +│ ├── test-cases +│ │ ├── list --project-code --run-id # List test cases in run +│ │ └── get --project-code --run-id --tcase-id # Get test case in run +│ └── logs +│ └── create --project-code --run-id --comment # Create run log ├── settings │ ├── list-statuses # List result statuses │ └── update-statuses --statuses # Update custom statuses diff --git a/skills/qas-cli/SKILL.md b/skills/qas-cli/SKILL.md index 584113f..1fbe601 100644 --- a/skills/qas-cli/SKILL.md +++ b/skills/qas-cli/SKILL.md @@ -151,9 +151,11 @@ qasphere api │ ├── list --project-code # List runs │ ├── clone --project-code --run-id --title # Clone run │ ├── close --project-code --run-id # Close run -│ └── test-cases -│ ├── list --project-code --run-id # List test cases in run -│ └── get --project-code --run-id --tcase-id # Get test case in run +│ ├── test-cases +│ │ ├── list --project-code --run-id # List test cases in run +│ │ └── get --project-code --run-id --tcase-id # Get test case in run +│ └── logs +│ └── create --project-code --run-id --comment # Create run log ├── settings │ ├── list-statuses # List result statuses │ └── update-statuses --statuses # Update custom statuses diff --git a/src/commands/api/manifests/runs.ts b/src/commands/api/manifests/runs.ts index 6e59625..0147291 100644 --- a/src/commands/api/manifests/runs.ts +++ b/src/commands/api/manifests/runs.ts @@ -1,6 +1,7 @@ import { z } from 'zod' import { CreateRunRequestSchema, + CreateRunLogRequestSchema, QueryPlansSchema, CloneRunRequestSchema, type ListRunTCasesRequest, @@ -31,6 +32,7 @@ const help = { tags: 'Comma-separated tag IDs to filter by.', priorities: 'Comma-separated priorities to filter by (e.g., "low,high").', include: 'Include additional fields. Use "folder" to include folder details.', + comment: 'Log message body (supports HTML).', create: { describe: 'Create a new test run.', epilog: apiDocsEpilog('run', 'create-new-run'), @@ -102,6 +104,21 @@ const help = { }, ], }, + logsCreate: { + describe: 'Create a log entry on a test run.', + epilog: apiDocsEpilog('run', 'create-run-log'), + examples: [ + { + usage: '$0 api runs logs create --project-code PRJ --run-id 1 --comment "Deploy finished"', + description: 'Create a run log with --comment', + }, + { + usage: + '$0 api runs logs create --project-code PRJ --run-id 1 --body \'{"comment": "Deploy finished"}\'', + description: 'Create a run log using --body', + }, + ], + }, } as const const runIdParam = { @@ -332,4 +349,44 @@ const tcasesGet: ApiEndpointSpec = { }, } -export const runSpecs: ApiEndpointSpec[] = [create, list, clone, close, tcasesList, tcasesGet] +const logsCreate: ApiEndpointSpec = { + id: 'runs.logs.create', + commandPath: ['runs', 'logs', 'create'], + describe: help.logsCreate.describe, + bodyMode: 'json', + pathParams: [projectCodeParam, runIdParam], + fieldOptions: [ + { + name: 'comment', + type: 'string', + describe: help.comment, + schema: CreateRunLogRequestSchema.shape.comment, + }, + ], + check: (argv) => { + return argv.body !== undefined || argv['body-file'] !== undefined || argv.comment !== undefined + ? true + : 'Either --body, --body-file, or --comment is required' + }, + epilog: help.logsCreate.epilog, + examples: help.logsCreate.examples, + execute: async (api, { pathParams, body }) => { + printJson( + await api.runs.createLog( + pathParams['project-code'], + pathParams['run-id'], + body as Parameters[2] + ) + ) + }, +} + +export const runSpecs: ApiEndpointSpec[] = [ + create, + list, + clone, + close, + tcasesList, + tcasesGet, + logsCreate, +] diff --git a/src/tests/api/runs/logs-create.spec.ts b/src/tests/api/runs/logs-create.spec.ts new file mode 100644 index 0000000..f6b693c --- /dev/null +++ b/src/tests/api/runs/logs-create.spec.ts @@ -0,0 +1,112 @@ +import { HttpResponse, http, type PathParams } from 'msw' +import { beforeEach, describe, expect } from 'vitest' +import type { CreateRunLogRequest } from '../../../api/runs' +import { + test, + baseURL, + token, + useMockServer, + runCli, + createFolder, + createTCase, + createRun, + expectValidationError, + testRejectsInvalidIdentifier, + testBodyInput, +} from '../test-helper' + +const runCommand = (...args: string[]) => + runCli('api', 'runs', 'logs', 'create', ...args) + +describe('mocked', () => { + let lastBody: CreateRunLogRequest | null = null + let lastParams: PathParams = {} + + useMockServer( + http.post( + `${baseURL}/api/public/v0/project/:projectCode/run/:runId/log`, + async ({ request, params }) => { + expect(request.headers.get('Authorization')).toEqual(`ApiKey ${token}`) + lastParams = params + lastBody = (await request.json()) as CreateRunLogRequest + return HttpResponse.json({ id: 'log-1' }) + } + ) + ) + + beforeEach(() => { + lastBody = null + lastParams = {} + }) + + test('creates a run log with --comment', async ({ project }) => { + const result = await runCommand( + '--project-code', + project.code, + '--run-id', + '42', + '--comment', + 'Deploy finished' + ) + expect(lastParams.projectCode).toBe(project.code) + expect(lastParams.runId).toBe('42') + expect(lastBody).toEqual({ comment: 'Deploy finished' }) + expect(result).toEqual({ id: 'log-1' }) + }) + + const validBody = { comment: 'Body comment' } + + testBodyInput( + runCommand, + () => lastBody, + (h) => { + const requiredArgs = ['--project-code', 'PRJ', '--run-id', '1'] + h.testInlineBody(validBody, validBody, requiredArgs) + h.testBodyFile(validBody, validBody, requiredArgs) + h.testFieldOverride({ + body: validBody, + flags: ['--comment', 'Overridden'], + expectedRequest: { comment: 'Overridden' }, + requiredArgs, + }) + h.testInvalidJson(requiredArgs) + } + ) +}) + +describe('validation errors', () => { + test('requires --comment, --body, or --body-file', async () => { + await expectValidationError( + () => runCommand('--project-code', 'PRJ', '--run-id', '1'), + /Either --body, --body-file, or --comment is required/ + ) + }) + + testRejectsInvalidIdentifier(runCommand, 'project-code', 'code', [ + '--run-id', + '1', + '--comment', + 'msg', + ]) +}) + +test('creates a run log on live server', { tags: ['live'] }, async ({ project }) => { + const folder = await createFolder(project.code) + const folderId = folder.ids[0][0] + const tcase = await createTCase(project.code, folderId) + const run = await createRun(project.code, [tcase.id]) + const result = await runCli<{ id: string }>( + 'api', + 'runs', + 'logs', + 'create', + '--project-code', + project.code, + '--run-id', + String(run.id), + '--comment', + 'CLI live test log' + ) + expect(result).toHaveProperty('id') + expect(typeof result.id).toBe('string') +})