diff --git a/openapi.yaml b/openapi.yaml index d5e48b2..ed81656 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -22,7 +22,7 @@ components: properties: source: type: string - enum: [ "github" ] + enum: ["github"] example: "github" issueNumber: type: integer @@ -48,7 +48,7 @@ components: properties: type: type: string - enum: [ "USER" ] + enum: ["USER"] example: "USER" userId: type: string @@ -62,7 +62,7 @@ components: properties: source: type: string - enum: [ "azureDevOps" ] + enum: ["azureDevOps"] example: "azureDevOps" accessToken: type: string @@ -94,7 +94,7 @@ components: properties: type: type: string - enum: [ "USER" ] + enum: ["USER"] example: "USER" userId: type: string @@ -108,7 +108,7 @@ components: properties: source: type: string - enum: [ "discovery" ] + enum: ["discovery"] example: "discovery" description: type: string @@ -119,13 +119,13 @@ components: properties: type: type: string - enum: [ "INITIAL" ] + enum: ["INITIAL"] example: "INITIAL" - type: object properties: type: type: string - enum: [ "USER" ] + enum: ["USER"] example: "USER" userId: type: string @@ -135,7 +135,7 @@ components: properties: source: type: string - enum: [ "manual" ] + enum: ["manual"] example: "manual" description: type: string @@ -145,7 +145,7 @@ components: properties: type: type: string - enum: [ "USER" ] + enum: ["USER"] example: "USER" userId: type: string @@ -155,7 +155,7 @@ components: properties: source: type: string - enum: [ "scheduled" ] + enum: ["scheduled"] example: "scheduled" triggeredBy: type: object @@ -163,7 +163,7 @@ components: properties: type: type: string - enum: [ "USER" ] + enum: ["USER"] example: "USER" userId: type: string @@ -173,7 +173,7 @@ components: properties: source: type: string - enum: [ "proposal" ] + enum: ["proposal"] example: "proposal" description: type: string @@ -184,13 +184,13 @@ components: properties: type: type: string - enum: [ "INITIAL" ] + enum: ["INITIAL"] example: "INITIAL" - type: object properties: type: type: string - enum: [ "USER" ] + enum: ["USER"] example: "USER" userId: type: string @@ -256,7 +256,7 @@ components: properties: source: type: string - enum: [ "manual" ] + enum: ["manual"] description: The source of the test trigger. example: "manual" description: @@ -268,7 +268,7 @@ components: properties: type: type: string - enum: [ "USER" ] + enum: ["USER"] description: The type of entity triggering the test. example: "USER" userId: @@ -307,7 +307,7 @@ components: example: "2024-09-06T13:01:51.686Z" status: type: string - enum: [ "WAITING", "PASSED", "FAILED", "ERROR" ] + enum: ["WAITING", "PASSED", "FAILED", "ERROR"] description: The status of the specific test result, will be WAITING as long as the result is running, FAILED if the execution failed, PASSED if it succeeded, and ERROR if an internal error occurred. errorMessage: type: string @@ -350,7 +350,7 @@ components: example: "https://en.wikipedia.org/" status: type: string - enum: [ "WAITING", "PASSED", "FAILED" ] + enum: ["WAITING", "PASSED", "FAILED"] description: The status of the test report, will be WAITING as long as any result is running, FAILED if the report is done but has any failed result and PASSED if all test results are done and successful context: $ref: "#/components/schemas/TestReportContext" @@ -391,7 +391,7 @@ components: example: "https://app.octomind.dev/testreports/826c15af-644b-4b28-89b4-f50ff34e46b7" testReport: $ref: "#/components/schemas/TestReport" - + TestCasesResponse: type: array items: @@ -427,7 +427,21 @@ components: format: uuid action: type: string - enum: [EXTRACT, ENTER_TEXT, CLICK, SELECT_OPTION, TYPE_TEXT, KEY_PRESS, HOVER,UPLOAD, GO_TO, DRAG_AND_DROP, CLOSE_PAGE, OPEN_EMAIL] + enum: + [ + EXTRACT, + ENTER_TEXT, + CLICK, + SELECT_OPTION, + TYPE_TEXT, + KEY_PRESS, + HOVER, + UPLOAD, + GO_TO, + DRAG_AND_DROP, + CLOSE_PAGE, + OPEN_EMAIL, + ] calledWith: type: string nullable: true @@ -443,7 +457,16 @@ components: format: uuid expectation: type: string - enum: [VISIBLE, NOT_VISIBLE, TO_BE_CHECKED, NOT_TO_BE_CHECKED, TO_HAVE_VALUE, TO_CONTAIN_TEXT, TO_HAVE_STYLE] + enum: + [ + VISIBLE, + NOT_VISIBLE, + TO_BE_CHECKED, + NOT_TO_BE_CHECKED, + TO_HAVE_VALUE, + TO_CONTAIN_TEXT, + TO_HAVE_STYLE, + ] calledWith: type: string nullable: true @@ -740,7 +763,7 @@ components: - name - testTargetId - type - + TestSuiteRequestCommons: type: object properties: @@ -850,13 +873,119 @@ components: description: "Human-readable message of the error that occurred while parsing." example: "Expected object, received string" + Notification: + type: object + properties: + id: + type: string + format: uuid + description: Unique identifier for the event. + example: "826c15af-644b-4b28-89b4-f50ff34e46b7" + testTargetId: + type: string + format: uuid + description: The unique identifier of the test target this event belongs to. + example: "3435918b-3d29-4ebd-8c68-9a540532f45a" + createdAt: + type: string + format: date-time + description: The timestamp when the event was created. + example: "2024-09-06T13:01:51.686Z" + updatedAt: + type: string + format: date-time + description: The timestamp when the event was last updated. + example: "2024-09-06T13:01:51.686Z" + payload: + type: object + description: JSON payload containing event-specific data. + example: { testCaseId: "5deeef76-f37a-418b-8101-7d69529fa561" } + type: + type: string + enum: + [ + "VALIDATION_PASSED", + "VALIDATION_FAILED", + "DISCOVERY_FINISHED", + "PROPOSAL_SUCCESS", + "PROPOSAL_FAILED", + "REPORT_EXECUTION_FINISHED", + ] + description: The type of event that occurred. + example: "VALIDATION_PASSED" + ack: + type: string + enum: ["IN_WEB_APP"] + description: Optional acknowledgment status of the event. + example: "IN_WEB_APP" + nullable: true + required: + - id + - testTargetId + - createdAt + - updatedAt + - payload + - type + + ExternalDiscoveryBody: + type: object + properties: + name: + type: string + description: Name of the discovered test case + example: "Login Form Validation Test" + entryPointUrlPath: + type: string + description: Entry point URL path of the discovered test case + example: "/login" + prerequisiteName: + type: string + description: Prerequisite test case name + example: "Cookie Banner Acceptance" + externalId: + type: string + description: External ID of the discovered test case + example: "ext-test-001" + tagNames: + type: array + items: + type: string + description: Tags to assign to the discovered test case + example: ["authentication", "forms", "validation"] + prompt: + type: string + description: Prompt to generate the discovered test case + example: "Test the login form with valid and invalid credentials" + folderName: + type: string + description: Folder name of the discovered test case + example: "Authentication Tests" + type: + type: string + enum: ["LOGIN", "COOKIE_BANNER"] + description: Type of the discovered test case + example: "LOGIN" + required: + - name + - prompt + + DiscoveryResponse: + type: object + properties: + discoveryId: + type: string + format: uuid + testCaseId: + type: string + format: uuid + paths: /apiKey/v2/test-suites: get: summary: Retrieve test suites description: get a list of all defined test suites. security: - - ApiKeyAuth: [ ] + - ApiKeyAuth: [] tags: - test-suites responses: @@ -869,7 +998,7 @@ paths: post: summary: Create a test suite security: - - ApiKeyAuth: [ ] + - ApiKeyAuth: [] tags: - test-suites requestBody: @@ -896,7 +1025,7 @@ paths: patch: summary: Update a test suite security: - - ApiKeyAuth: [ ] + - ApiKeyAuth: [] tags: - test-suites requestBody: @@ -921,7 +1050,7 @@ paths: delete: summary: Delete a test suite security: - - ApiKeyAuth: [ ] + - ApiKeyAuth: [] tags: - test-suites responses: @@ -942,7 +1071,7 @@ paths: tags: - execute security: - - ApiKeyAuth: [ ] + - ApiKeyAuth: [] requestBody: required: true content: @@ -971,12 +1100,12 @@ paths: delete: summary: Delete an environment security: - - ApiKeyAuth: [ ] + - ApiKeyAuth: [] tags: - environments responses: - '200': - description: Environment deleted successfully + "200": + description: Environment deleted successfully parameters: - in: path name: testTargetId @@ -995,7 +1124,7 @@ paths: patch: summary: Update an environment security: - - ApiKeyAuth: [ ] + - ApiKeyAuth: [] tags: - environments parameters: @@ -1056,7 +1185,7 @@ paths: type: string nullable: true responses: - '200': + "200": description: Environment updated successfully content: application/json: @@ -1068,7 +1197,7 @@ paths: summary: Create an environment description: Create a custom environment. security: - - ApiKeyAuth: [ ] + - ApiKeyAuth: [] tags: - environments parameters: @@ -1131,7 +1260,7 @@ paths: summary: Retrieve environments description: get a list of all defined environments. security: - - ApiKeyAuth: [ ] + - ApiKeyAuth: [] tags: - environments parameters: @@ -1172,7 +1301,7 @@ paths: format: uuid description: ID of the test report to fetch security: - - ApiKeyAuth: [ ] + - ApiKeyAuth: [] responses: "200": description: Test Report information @@ -1238,7 +1367,7 @@ paths: description: The value to compare with to find matches. example: "3435918b-3d29-4ebd-8c68-9a540532f45a" security: - - ApiKeyAuth: [ ] + - ApiKeyAuth: [] responses: "200": description: Test Reports information @@ -1263,7 +1392,6 @@ paths: type: boolean description: If the query in question has another page to retrieve - "400": description: Invalid request parameters content: @@ -1293,7 +1421,7 @@ paths: format: uuid description: The ID of the test target responses: - '200': + "200": description: List of notifications content: application/json: @@ -1320,7 +1448,7 @@ paths: failed: type: boolean context: - $ref: '#/components/schemas/ExecutionContext' + $ref: "#/components/schemas/ExecutionContext" testReportId: type: string format: uuid @@ -1337,19 +1465,19 @@ paths: enum: - IN_WEB_APP nullable: true - '401': + "401": description: Unauthorized - Invalid or missing API key - '404': + "404": description: Test target not found /apiKey/v1/private-location: get: security: - - ApiKeyAuth: [ ] + - ApiKeyAuth: [] summary: Retrieve all private locations description: "gets a list of private location workers" tags: - - private-location + - private-location responses: "200": description: OK @@ -1361,7 +1489,7 @@ paths: /apiKey/v1/private-location/register: put: security: - - ApiKeyAuth: [ ] + - ApiKeyAuth: [] summary: Register a private location description: "registers a private location worker" tags: @@ -1387,7 +1515,7 @@ paths: /apiKey/v1/private-location/unregister: put: security: - - ApiKeyAuth: [ ] + - ApiKeyAuth: [] summary: Unregister a private location description: "Unregisters a private location worker." tags: @@ -1450,7 +1578,7 @@ paths: format: url description: URL of the app to test responses: - '200': + "200": description: Code for the test case content: application/json: @@ -1462,31 +1590,31 @@ paths: description: The code representation of the test case required: - testCode - '404': + "404": description: Test case not found /apiKey/v2/test-targets/{testTargetId}/test-cases: - get: - summary: Retrieve test cases + get: + summary: Retrieve test cases description: Get a list of test cases for a specific test target - security: + security: - ApiKeyAuth: [] tags: - Test Cases parameters: - - name: testTargetId + - name: testTargetId in: path required: true schema: type: string format: uuid - description: The ID of the test target + description: The ID of the test target responses: - '200': + "200": description: List of test cases content: application/json: - schema: + schema: $ref: "#/components/schemas/TestCasesResponse" /apiKey/v2/test-targets/{testTargetId}/test-cases/{testCaseId}: @@ -1513,15 +1641,15 @@ paths: format: uuid description: The ID of the test case responses: - '200': + "200": description: Test case details content: application/json: schema: $ref: "#/components/schemas/TestCaseResponse" - '401': + "401": description: Unauthorized - Invalid or missing API key - '404': + "404": description: Test target or test case not found /apiKey/v2/test-targets/{testTargetId}/config: @@ -1635,7 +1763,7 @@ paths: description: Optional ID of the folder to place the discovery in nullable: true responses: - '200': + "200": description: Discovery created successfully content: application/json: @@ -1653,9 +1781,9 @@ paths: type: string format: uuid description: The ID of the associated test case - '401': + "401": description: Unauthorized - Invalid or missing API key - '404': + "404": description: Test target not found - '400': + "400": description: Invalid request body diff --git a/package.json b/package.json index 446dd92..3a2de97 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@octomind/octomind", - "version": "1.0.4", + "version": "1.0.5", "description": "a command line client for octomind apis", "main": "./dist/index.js", "packageManager": "pnpm@10.13.1", diff --git a/src/api.ts b/src/api.ts index d14e2de..875dd56 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1711,6 +1711,104 @@ export interface components { */ message?: string; }[]; + Notification: { + /** + * Format: uuid + * @description Unique identifier for the event. + * @example 826c15af-644b-4b28-89b4-f50ff34e46b7 + */ + id: string; + /** + * Format: uuid + * @description The unique identifier of the test target this event belongs to. + * @example 3435918b-3d29-4ebd-8c68-9a540532f45a + */ + testTargetId: string; + /** + * Format: date-time + * @description The timestamp when the event was created. + * @example 2024-09-06T13:01:51.686Z + */ + createdAt: string; + /** + * Format: date-time + * @description The timestamp when the event was last updated. + * @example 2024-09-06T13:01:51.686Z + */ + updatedAt: string; + /** + * @description JSON payload containing event-specific data. + * @example { + * "testCaseId": "5deeef76-f37a-418b-8101-7d69529fa561" + * } + */ + payload: Record; + /** + * @description The type of event that occurred. + * @example VALIDATION_PASSED + * @enum {string} + */ + type: "VALIDATION_PASSED" | "VALIDATION_FAILED" | "DISCOVERY_FINISHED" | "PROPOSAL_SUCCESS" | "PROPOSAL_FAILED" | "REPORT_EXECUTION_FINISHED"; + /** + * @description Optional acknowledgment status of the event. + * @example IN_WEB_APP + * @enum {string|null} + */ + ack?: "IN_WEB_APP" | null; + }; + ExternalDiscoveryBody: { + /** + * @description Name of the discovered test case + * @example Login Form Validation Test + */ + name: string; + /** + * @description Entry point URL path of the discovered test case + * @example /login + */ + entryPointUrlPath?: string; + /** + * @description Prerequisite test case name + * @example Cookie Banner Acceptance + */ + prerequisiteName?: string; + /** + * @description External ID of the discovered test case + * @example ext-test-001 + */ + externalId?: string; + /** + * @description Tags to assign to the discovered test case + * @example [ + * "authentication", + * "forms", + * "validation" + * ] + */ + tagNames?: string[]; + /** + * @description Prompt to generate the discovered test case + * @example Test the login form with valid and invalid credentials + */ + prompt: string; + /** + * @description Folder name of the discovered test case + * @example Authentication Tests + */ + folderName?: string; + /** + * @description Type of the discovered test case + * @example LOGIN + * @enum {string} + */ + type?: "LOGIN" | "COOKIE_BANNER"; + }; + DiscoveryResponse: { + /** Format: uuid */ + discoveryId?: string; + /** Format: uuid */ + testCaseId?: string; + }; }; responses: never; parameters: never; diff --git a/src/cli.ts b/src/cli.ts index 1aa5716..286edb7 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,9 +1,12 @@ import { program, Command } from "commander"; import { version } from "./version"; import { + createDiscovery, createEnvironment, deleteEnvironment, executeTests, + getNotifications, + getTestCase, getTestReport, listEnvironments, listPrivateLocations, @@ -28,7 +31,7 @@ export const buildCmd = (): Command => { program .name("octomind-cli") .description( - `Octomind CLI tool. Version: ${version}. see https://octomind.dev/docs/api-reference/`, + `Octomind CLI tool. Version: ${version}. see https://octomind.dev/docs/api-reference/` ) .version(version); @@ -52,7 +55,7 @@ export const buildCmd = (): Command => { if (existingConfig.apiKey && !options.force) { console.log("⚠️ Configuration already exists."); const overwrite = await promptUser( - "Do you want to overwrite it? (y/N): ", + "Do you want to overwrite it? (y/N): " ); if ( @@ -67,7 +70,7 @@ export const buildCmd = (): Command => { let apiKey; if (!options.apiKey) { apiKey = await promptUser( - "Enter your API key. Go to https://octomind.dev/docs/run-tests/execution-curl#create-an-api-key to learn how to generate one: ", + "Enter your API key. Go to https://octomind.dev/docs/run-tests/execution-curl#create-an-api-key to learn how to generate one: " ); if (!apiKey) { console.log("❌ API key is required."); @@ -77,7 +80,7 @@ export const buildCmd = (): Command => { let testTargetId; if (!options.testTargetId) { testTargetId = await promptUser( - "Enter test target id (optional, press Enter to skip): ", + "Enter test target id (optional, press Enter to skip): " ); } @@ -93,11 +96,11 @@ export const buildCmd = (): Command => { } catch (error) { console.error( "❌ Error during initialization:", - (error as Error).message, + (error as Error).message ); process.exit(1); } - }, + } ); createCommandWithCommonOptions("debug") @@ -105,26 +108,27 @@ export const buildCmd = (): Command => { .requiredOption("-u, --url ", "url the tests should run against") .option( "-i, --id ", - "id of the test case you want to run, if not provided will run all test cases in the test target", + "id of the test case you want to run, if not provided will run all test cases in the test target" ) .option( "-e, --environmentId ", - "id of the environment you want to run against, if not provided will run all test cases against the default environment", + "id of the environment you want to run against, if not provided will run all test cases against the default environment" ) .option( "-a, --testTargetId ", - "id of the test target of the test case", + "id of the test target of the test case" ) .option( "--headless", - "if we should run headless without the UI of playwright and the browser", + "if we should run headless without the UI of playwright and the browser" ) - .action((_, options) => - runDebugtopus({ - ...options, - testTargetId: resolveTestTargetId(options.testTargetId), - }), - ); + .action(async (options, command) => { + const resolvedTestTargetId = await resolveTestTargetId( + options.testTargetId + ); + command.setOptionValue("testTargetId", resolvedTestTargetId); + void runDebugtopus(options); + }); createCommandWithCommonOptions("execute") .description("Execute test cases") @@ -136,7 +140,7 @@ export const buildCmd = (): Command => { .option( "-v, --variables-to-overwrite ", "JSON object of variables to overwrite", - toJSON, + toJSON ) .action(async (options) => { const testTargetId = await resolveTestTargetId(options.testTargetId); @@ -178,12 +182,13 @@ export const buildCmd = (): Command => { createCommandWithCommonOptions("list-environments") .description("List all environments") .option("-t, --test-target-id ", "Test target ID") - .action((_, options) => - listEnvironments({ - ...options, - testTargetId: resolveTestTargetId(options.testTargetId), - }), - ); + .action(async (options, command) => { + const resolvedTestTargetId = await resolveTestTargetId( + options.testTargetId + ); + command.setOptionValue("testTargetId", resolvedTestTargetId); + void listEnvironments(options); + }); createCommandWithCommonOptions("create-environment") .description("Create a new environment") @@ -194,16 +199,16 @@ export const buildCmd = (): Command => { .option("--test-account-password ", "Test account password") .option( "--test-account-otp-initializer-key ", - "Test account OTP initializer key", + "Test account OTP initializer key" ) .option("--basic-auth-username ", "Basic auth username") .option("--basic-auth-password ", "Basic auth password") .option("--private-location-name ", "Private location name") - .action((_, options) => + .action(async (_, options) => createEnvironment({ ...options, - testTargetId: resolveTestTargetId(options.testTargetId), - }), + testTargetId: await resolveTestTargetId(options.testTargetId), + }) ); createCommandWithCommonOptions("update-environment") @@ -216,16 +221,16 @@ export const buildCmd = (): Command => { .option("--test-account-password ", "Test account password") .option( "--test-account-otp-initializer-key ", - "Test account OTP initializer key", + "Test account OTP initializer key" ) .option("--basic-auth-username ", "Basic auth username") .option("--basic-auth-password ", "Basic auth password") .option("--private-location-name ", "Private location name") - .action((_, options) => + .action(async (_, options) => updateEnvironment({ ...options, - testTargetId: resolveTestTargetId(options.testTargetId), - }), + testTargetId: await resolveTestTargetId(options.testTargetId), + }) ); createCommandWithCommonOptions("delete-environment") @@ -233,5 +238,50 @@ export const buildCmd = (): Command => { .requiredOption("-e, --environment-id ", "Environment ID") .option("-t, --test-target-id ", "Test target ID") .action(deleteEnvironment); + + createCommandWithCommonOptions("notifications") + .description("Get notifications for a test target") + .option("-t, --test-target-id ", "Test target ID") + .action(async (options, command) => { + const resolvedTestTargetId = await resolveTestTargetId( + options.testTargetId + ); + command.setOptionValue("testTargetId", resolvedTestTargetId); + void getNotifications(options); + }); + + createCommandWithCommonOptions("test-case") + .description("Get details of a specific test case") + .requiredOption("-c, --test-case-id ", "Test case ID") + .option("-t, --test-target-id ", "Test target ID") + .action(async (options, command) => { + const resolvedTestTargetId = await resolveTestTargetId( + options.testTargetId + ); + command.setOptionValue("testTargetId", resolvedTestTargetId); + void getTestCase(options); + }); + + createCommandWithCommonOptions("create-discovery") + .description("Create a new test case discovery") + .requiredOption("-n, --name ", "Discovery name") + .requiredOption("-p, --prompt ", "Discovery prompt") + .option("-t, --test-target-id ", "Test target ID") + .option("-e, --entry-point-url-path ", "Entry point URL path") + .option("--prerequisite-id ", "Prerequisite test case ID") + .option("--external-id ", "External identifier") + .option( + "--assigned-tag-ids ", + "Comma-separated list of tag IDs", + splitter + ) + .option("--folder-id ", "Folder ID") + .action(async (options, command) => { + const resolvedTestTargetId = await resolveTestTargetId( + options.testTargetId + ); + command.setOptionValue("testTargetId", resolvedTestTargetId); + void createDiscovery(options); + }); return program; }; diff --git a/src/helpers.ts b/src/helpers.ts index efaf28c..03935bf 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -14,7 +14,7 @@ export function promptUser(question: string): Promise { } export const resolveTestTargetId = async ( - providedTestTargetId?: string, + providedTestTargetId?: string ): Promise => { if (providedTestTargetId) { return providedTestTargetId; @@ -23,7 +23,7 @@ export const resolveTestTargetId = async ( const config = await loadConfig(); if (!config.testTargetId) { throw new Error( - "testTargetId is required. Please provide it as a parameter or configure it first by running 'octomind init'", + "testTargetId is required. Please provide it as a parameter or configure it first by running 'octomind init'" ); } return config.testTargetId; diff --git a/src/tools.ts b/src/tools.ts index 75fb836..2115e89 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -24,7 +24,18 @@ export type UpdateEnvironmentOptions = paths["/apiKey/v2/test-targets/{testTargetId}/environments/{environmentId}"]["patch"]["requestBody"]["content"]["application/json"]; export type EnvironmentResponse = components["schemas"]["EnvironmentResponse"]; -export type ErrorResponse = components["schemas"]["ZodResponse"] | undefined; +export type ErrorResponse = + | components["schemas"]["ZodResponse"] + | string + | undefined; +export type getNotificationsParams = + paths["/apiKey/v2/test-targets/{testTargetId}/notifications"]["get"]["parameters"]["path"]; +export type Notification = components["schemas"]["Notification"]; +export type getTestCaseParams = + paths["/apiKey/v2/test-targets/{testTargetId}/test-cases/{testCaseId}"]["get"]["parameters"]["path"]; +export type createDiscoveryBody = + components["schemas"]["ExternalDiscoveryBody"]; +export type DiscoveryResponse = components["schemas"]["DiscoveryResponse"]; const BASE_URL = process.env.OCTOMIND_API_URL || "https://app.octomind.dev/api"; @@ -35,12 +46,28 @@ const authMiddleware: Middleware = { const { apiKey } = await loadConfig(); if (!apiKey) { throw new Error( - "API key is required. Please configure it first by running 'octomind init'", + "API key is required. Please configure it first by running 'octomind init'" ); } request.headers.set("x-api-key", apiKey); return request; }, + async onResponse({ response }) { + const { body, ...resOptions } = response; + if (!response.ok) { + const res = new Response(body, resOptions); + const errorBody = await res.json(); + return new Response( + `${response.status}, ${response.statusText}: ${errorBody ? JSON.stringify(errorBody, null, 2) : ""}`, + { ...resOptions, status: response.status } + ); + } + return response; + }, + onError({ error }) { + console.error(error); + process.exit(1); + }, }; client.use(authMiddleware); @@ -52,7 +79,7 @@ const handleError = (error: ErrorResponse) => { }; export const listEnvironments = async ( - options: listEnvironmentsOptions & { json?: boolean }, + options: listEnvironmentsOptions & { json?: boolean } ): Promise => { const { data, error } = await client.GET( "/apiKey/v2/test-targets/{testTargetId}/environments", @@ -60,7 +87,7 @@ export const listEnvironments = async ( params: { path: { testTargetId: options.testTargetId }, }, - }, + } ); handleError(error); @@ -82,7 +109,7 @@ export const listEnvironments = async ( }; export const getEnvironments = async ( - options: listEnvironmentsOptions & { json?: boolean }, + options: listEnvironmentsOptions & { json?: boolean } ): Promise => { const { data, error } = await client.GET( "/apiKey/v2/test-targets/{testTargetId}/environments", @@ -90,7 +117,7 @@ export const getEnvironments = async ( params: { path: { testTargetId: options.testTargetId }, }, - }, + } ); handleError(error); @@ -111,7 +138,7 @@ const outputResult = (result: unknown): void => { }; export const executeTests = async ( - options: executeTestsBody & { json?: boolean; description?: string }, + options: executeTestsBody & { json?: boolean; description?: string } ): Promise => { const { data, error } = await client.POST("/apiKey/v2/execute", { body: { @@ -159,7 +186,7 @@ export const executeTests = async ( }; export const getTestReport = async ( - options: getTestReportParams & { json?: boolean }, + options: getTestReportParams & { json?: boolean } ): Promise => { const { data, error } = await client.GET( "/apiKey/v2/test-targets/{testTargetId}/test-reports/{testReportId}", @@ -170,7 +197,7 @@ export const getTestReport = async ( testReportId: options.testReportId, }, }, - }, + } ); handleError(error); @@ -217,7 +244,7 @@ export const registerLocation = async (options: { password: options.password, }, }, - }, + } ); handleError(error); @@ -242,7 +269,7 @@ export const unregisterLocation = async (options: { body: { name: options.name, }, - }, + } ); handleError(error); @@ -255,7 +282,7 @@ export const unregisterLocation = async (options: { console.log( "Unregistration result:", - response.success ? "Success" : "Failed", + response.success ? "Success" : "Failed" ); }; @@ -289,7 +316,7 @@ export const createEnvironment = async ( basicAuthPassword?: string; privateLocationName?: string; testAccountOtpInitializerKey?: string; - }, + } ): Promise => { const { data, error } = await client.POST( "/apiKey/v2/test-targets/{testTargetId}/environments", @@ -312,7 +339,7 @@ export const createEnvironment = async ( privateLocationName: options.privateLocationName, additionalHeaderFields: options.additionalHeaderFields, }, - }, + } ); handleError(error); @@ -342,7 +369,7 @@ export const updateEnvironment = async ( basicAuthPassword?: string; privateLocationName?: string; testAccountOtpInitializerKey?: string; - }, + } ): Promise => { const { data, error } = await client.PATCH( "/apiKey/v2/test-targets/{testTargetId}/environments/{environmentId}", @@ -367,7 +394,7 @@ export const updateEnvironment = async ( }, privateLocationName: options.privateLocationName, }, - }, + } ); handleError(error); @@ -400,7 +427,7 @@ export const deleteEnvironment = async (options: { environmentId: options.environmentId, }, }, - }, + } ); handleError(error); @@ -413,6 +440,150 @@ export const deleteEnvironment = async (options: { console.log("Environment deleted successfully!"); }; +export const getNotifications = async ( + options: getNotificationsParams & { json?: boolean } +): Promise => { + const { data, error } = await client.GET( + "/apiKey/v2/test-targets/{testTargetId}/notifications", + { + params: { + path: { + testTargetId: options.testTargetId, + }, + }, + } + ); + + handleError(error); + const response = data as Notification[]; + if (options.json) { + outputResult(response); + return; + } + + console.log("Notifications:"); + response.forEach((notification) => { + console.log(`\nID: ${notification.id}`); + console.log(`Type: ${notification.type}`); + console.log(`Created At: ${notification.createdAt}`); + if (notification.payload?.testReportId) { + console.log(`Test Report ID: ${notification.payload.testReportId}`); + } + if (notification.payload?.testCaseId) { + console.log(`Test Case ID: ${notification.payload.testCaseId}`); + } + if (notification.payload?.failed !== undefined) { + console.log(`Failed: ${notification.payload.failed}`); + } + if (notification.ack) { + console.log(`Acknowledged: ${notification.ack}`); + } + }); +}; + +export const getTestCase = async ( + options: getTestCaseParams & { json?: boolean } +): Promise => { + console.log(options); + const { data, error } = await client.GET( + "/apiKey/v2/test-targets/{testTargetId}/test-cases/{testCaseId}", + { + params: { + path: { + testTargetId: options.testTargetId, + testCaseId: options.testCaseId, + }, + }, + } + ); + + handleError(error); + + const response = data as TestCaseResponse; + + if (options.json) { + outputResult(response); + return; + } + + console.log("Test Case Details:"); + console.log(`ID: ${response.id}`); + console.log(`Description: ${response.description}`); + console.log(`Status: ${response.status}`); + console.log(`Run Status: ${response.runStatus}`); + console.log(`Created At: ${response.createdAt}`); + console.log(`Updated At: ${response.updatedAt}`); + + if (response.elements && response.elements.length > 0) { + console.log("\nElements:"); + response.elements.forEach((element, index) => { + console.log(`\nElement ${index + 1}:`); + if (element.interaction) { + console.log(` Action: ${element.interaction.action}`); + if (element.interaction.calledWith) { + console.log(` Called With: ${element.interaction.calledWith}`); + } + } + if (element.assertion) { + console.log(` Expectation: ${element.assertion.expectation}`); + if (element.assertion.calledWith) { + console.log(` Called With: ${element.assertion.calledWith}`); + } + } + console.log(" Selectors:"); + element.selectors?.forEach((selector) => { + console.log(` - ${selector.selectorType}: ${selector.selector}`); + if (selector.options?.name) { + console.log(` Name: ${selector.options.name}`); + } + }); + }); + } +}; + +export const createDiscovery = async ( + options: createDiscoveryBody & { json?: boolean; testTargetId: string } +): Promise => { + const requestBody = { + name: options.name, + prompt: options.prompt, + ...(options.entryPointUrlPath && { + entryPointUrlPath: options.entryPointUrlPath, + }), + ...(options.prerequisiteName && { + prerequisiteName: options.prerequisiteName, + }), + ...(options.externalId && { externalId: options.externalId }), + ...(options.tagNames && { tagNames: options.tagNames }), + ...(options.folderName && { folderName: options.folderName }), + ...(options.type && { type: options.type }), + }; + + const { data, error } = await client.POST( + "/apiKey/v2/test-targets/{testTargetId}/discoveries", + { + params: { + path: { + testTargetId: options.testTargetId, + }, + }, + body: requestBody, + } + ); + + handleError(error); + + const response = data as DiscoveryResponse; + if (options.json) { + outputResult(response); + return; + } + + console.log("Discovery created successfully!"); + console.log(`Discovery ID: ${response.discoveryId}`); + console.log(`Test Case ID: ${response.testCaseId}`); +}; + export const getPlaywrightConfig = async (options: { testTargetId: string; environmentId?: string; @@ -436,10 +607,9 @@ export const getPlaywrightConfig = async (options: { }, }, parseAs: "text", - }, + } ); - handleError(error); if (!data) { @@ -473,7 +643,7 @@ export const getPlaywrightCode = async (options: { executionUrl: options.executionUrl, }, }, - }, + } ); handleError(error); @@ -502,7 +672,7 @@ export const getTestCases = async (options: { testTargetId: options.testTargetId, }, }, - }, + } ); handleError(error);