diff --git a/src/cli.ts b/src/cli.ts index 8129724..b41ab6a 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -33,7 +33,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); @@ -57,7 +57,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 ( @@ -72,7 +72,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."); @@ -82,7 +82,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): " ); } @@ -98,11 +98,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") @@ -110,28 +110,28 @@ 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" ) .option( "--persist", - "if we should write playwright config and files to current directory, you can then run 'npx playwright test' to run them again", + "if we should write playwright config and files to current directory, you can then run 'npx playwright test' to run them again" ) .option("--grep ", "filter test cases by substring") .action(async (options, command) => { const resolvedTestTargetId = await resolveTestTargetId( - options.testTargetId, + options.testTargetId ); command.setOptionValue("testTargetId", resolvedTestTargetId); void runDebugtopus(options); @@ -147,7 +147,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); @@ -191,7 +191,7 @@ export const buildCmd = (): Command => { .option("-t, --test-target-id ", "Test target ID") .action(async (options, command) => { const resolvedTestTargetId = await resolveTestTargetId( - options.testTargetId, + options.testTargetId ); command.setOptionValue("testTargetId", resolvedTestTargetId); void listEnvironments(options); @@ -206,7 +206,7 @@ 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") @@ -215,7 +215,7 @@ export const buildCmd = (): Command => { createEnvironment({ ...options, testTargetId: await resolveTestTargetId(options.testTargetId), - }), + }) ); createCommandWithCommonOptions("update-environment") @@ -228,7 +228,7 @@ 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") @@ -237,7 +237,7 @@ export const buildCmd = (): Command => { updateEnvironment({ ...options, testTargetId: await resolveTestTargetId(options.testTargetId), - }), + }) ); createCommandWithCommonOptions("delete-environment") @@ -246,25 +246,30 @@ export const buildCmd = (): Command => { .option("-t, --test-target-id ", "Test target ID") .action(deleteEnvironment); - - program.command("start-private-location") + program + .command("start-private-location") .description("Start a private location worker") .option("-n, --name ", "Location name") .option("-u, --username ", "Proxy user") .option("-p, --password ", "Proxy password") - .option("-l, --host-network", "Use host network (default: false). If set you can use localhost directly", false) + .option( + "-l, --host-network", + "Use host network (default: false). If set you can use localhost directly", + false + ) .action(startPrivateLocationWorker); - program.command("stop-private-location") + program + .command("stop-private-location") .description("Stop a private location worker") - .action(stopPLW) + .action(stopPLW); 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, + options.testTargetId ); command.setOptionValue("testTargetId", resolvedTestTargetId); void getNotifications(options); @@ -276,7 +281,7 @@ export const buildCmd = (): Command => { .option("-t, --test-target-id ", "Test target ID") .action(async (options, command) => { const resolvedTestTargetId = await resolveTestTargetId( - options.testTargetId, + options.testTargetId ); command.setOptionValue("testTargetId", resolvedTestTargetId); void getTestCase(options); @@ -293,12 +298,12 @@ export const buildCmd = (): Command => { .option( "--assigned-tag-ids ", "Comma-separated list of tag IDs", - splitter, + splitter ) .option("--folder-id ", "Folder ID") .action(async (options, command) => { const resolvedTestTargetId = await resolveTestTargetId( - options.testTargetId, + options.testTargetId ); command.setOptionValue("testTargetId", resolvedTestTargetId); void createDiscovery(options); diff --git a/src/tools.ts b/src/tools.ts deleted file mode 100644 index 9888050..0000000 --- a/src/tools.ts +++ /dev/null @@ -1,692 +0,0 @@ -import type { paths, components } from "./api"; // generated by openapi-typescript -import createClient, { Middleware } from "openapi-fetch"; -import { loadConfig } from "./config"; - -export type listEnvironmentsOptions = - paths["/apiKey/v2/test-targets/{testTargetId}/environments"]["get"]["parameters"]["path"]; -export type executeTestsBody = - components["schemas"]["TestTargetExecutionRequest"]; -export type getTestReportParams = - paths["/apiKey/v2/test-targets/{testTargetId}/test-reports/{testReportId}"]["get"]["parameters"]["path"]; -export type TestReport = components["schemas"]["TestReport"]; -export type SuccessResponse = components["schemas"]["SuccessResponse"]; -export type PrivateLocationInfo = components["schemas"]["PrivateLocationInfo"]; -export type PostEnvironmentOptions = - paths["/apiKey/v2/test-targets/{testTargetId}/environments"]["post"]["requestBody"]["content"]["application/json"] & { - testTargetId: string; - }; - -export type TestCaseResponse = components["schemas"]["TestCaseResponse"]; - -export type TestCasesResponse = components["schemas"]["TestCasesResponse"]; - -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"] - | 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"; - -const client = createClient({ baseUrl: BASE_URL }); - -const authMiddleware: Middleware = { - async onRequest({ request }) { - const { apiKey } = await loadConfig(); - if (!apiKey) { - throw new Error( - "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.text(); - return new Response( - `${response.status}, ${response.statusText}: ${errorBody ? errorBody : ""}`, - { ...resOptions, status: response.status }, - ); - } - return response; - }, - onError({ error }) { - console.error(error); - process.exit(1); - }, -}; -client.use(authMiddleware); - -const handleError = (error: ErrorResponse) => { - if (error) { - console.error(error); - process.exit(1); - } -}; - -export const listEnvironments = async ( - options: listEnvironmentsOptions & { json?: boolean }, -): Promise => { - const { data, error } = await client.GET( - "/apiKey/v2/test-targets/{testTargetId}/environments", - { - params: { - path: { testTargetId: options.testTargetId }, - }, - }, - ); - - handleError(error); - - if (options.json) { - outputResult(data); - return; - } - - console.log("Environments:"); - if (data) { - data.forEach((environment) => { - console.log(`- Name: ${environment.name}`); - console.log(` ID: ${environment.id}`); - console.log(` Discovery URL: ${environment.discoveryUrl}`); - console.log(` Updated At: ${environment.updatedAt}`); - }); - } -}; - -export const getEnvironments = async ( - options: listEnvironmentsOptions & { json?: boolean }, -): Promise => { - const { data, error } = await client.GET( - "/apiKey/v2/test-targets/{testTargetId}/environments", - { - params: { - path: { testTargetId: options.testTargetId }, - }, - }, - ); - - handleError(error); - - if (!data) { - throw new Error("no environments found"); - } - - if (options.json) { - outputResult(data); - } - - return data; -}; - -const outputResult = (result: unknown): void => { - console.log(JSON.stringify(result, null, 2)); -}; - -export const executeTests = async ( - options: executeTestsBody & { json?: boolean; description?: string }, -): Promise => { - const { data, error } = await client.POST("/apiKey/v2/execute", { - body: { - testTargetId: options.testTargetId, - url: options.url, - context: { - source: "manual", - description: options.description || "CLI execution", - triggeredBy: { - type: "USER", - userId: "cli-user", - }, - }, - environmentName: options.environmentName, - tags: options.tags, - variablesToOverwrite: options.variablesToOverwrite, - }, - }); - - handleError(error); - - if (options.json) { - outputResult(data); - return; - } - - if (data) { - console.log("Test execution started successfully!"); - console.log("Test Report URL:", data.testReportUrl); - console.log("Report Status:", data.testReport?.status); - - if (data.testReport?.testResults?.length ?? 0 > 0) { - console.log("\nTest Results:"); - data.testReport?.testResults?.forEach((result) => { - console.log(`- Test ${result.testCaseId}: ${result.status}`); - if (result.errorMessage) { - console.log(` Error: ${result.errorMessage}`); - } - if (result.traceUrl) { - console.log(` Trace: ${result.traceUrl}`); - } - }); - } - } -}; - -export const getTestReport = async ( - options: getTestReportParams & { json?: boolean }, -): Promise => { - const { data, error } = await client.GET( - "/apiKey/v2/test-targets/{testTargetId}/test-reports/{testReportId}", - { - params: { - path: { - testTargetId: options.testTargetId, - testReportId: options.testReportId, - }, - }, - }, - ); - - handleError(error); - - if (options.json) { - outputResult(data); - return; - } - - const response = data as TestReport; - console.log("Test Report Details:"); - console.log("Status:", response.status); - console.log("Execution URL:", response.executionUrl); - - if ((response.testResults ?? []).length > 0) { - console.log("\nTest Results:"); - (response.testResults ?? []).forEach((result) => { - console.log(`- Test ${result.testCaseId}: ${result.status}`); - if (result.errorMessage) { - console.log(` Error: ${result.errorMessage}`); - } - if (result.traceUrl) { - console.log(` Trace: ${result.traceUrl}`); - } - }); - } -}; - -export const registerLocation = async (options: { - address: string; - name: string; - username: string; - password: string; - json?: boolean; -}): Promise => { - const { data, error } = await client.PUT( - "/apiKey/v1/private-location/register", - { - body: { - name: options.name, - registrationData: { - address: options.address, - username: options.username, - password: options.password, - }, - }, - }, - ); - - handleError(error); - - const response = data as SuccessResponse; - - if (options.json) { - outputResult(response); - return; - } - - console.log("Registration result:", response.success ? "Success" : "Failed"); -}; - -export const unregisterLocation = async (options: { - name: string; - json?: boolean; -}): Promise => { - const { data, error } = await client.PUT( - "/apiKey/v1/private-location/unregister", - { - body: { - name: options.name, - }, - }, - ); - - handleError(error); - const response = data as SuccessResponse; - - if (options.json) { - outputResult(response); - return; - } - - console.log( - "Unregistration result:", - response.success ? "Success" : "Failed", - ); -}; - -export const listPrivateLocations = async (options: { - json?: boolean; -}): Promise => { - const { data, error } = await client.GET("/apiKey/v1/private-location"); - - handleError(error); - - const response = data as PrivateLocationInfo; - if (options.json) { - outputResult(response); - return; - } - - console.log("Private Locations:"); - response.forEach((location) => { - console.log(`- Name: ${location.name}`); - console.log(` Status: ${location.status}`); - console.log(` Address: ${location.address}`); - }); -}; - -export const createEnvironment = async ( - options: PostEnvironmentOptions & { - json?: boolean; - testAccountUsername?: string; - testAccountPassword?: string; - basicAuthUsername?: string; - basicAuthPassword?: string; - privateLocationName?: string; - testAccountOtpInitializerKey?: string; - }, -): Promise => { - const { data, error } = await client.POST( - "/apiKey/v2/test-targets/{testTargetId}/environments", - { - params: { - path: { testTargetId: options.testTargetId }, - }, - body: { - name: options.name, - discoveryUrl: options.discoveryUrl, - testAccount: { - username: options.testAccountUsername, - password: options.testAccountPassword, - }, - basicAuth: { - username: options.basicAuthUsername, - password: options.basicAuthPassword, - }, - testAccountOtpInitializerKey: options.testAccountOtpInitializerKey, - privateLocationName: options.privateLocationName, - additionalHeaderFields: options.additionalHeaderFields, - }, - }, - ); - - handleError(error); - - const response = data as EnvironmentResponse; - - if (options.json) { - outputResult(response); - return; - } - - console.log("Environment created successfully!"); - console.log(`- Name: ${response.name}`); - console.log(` ID: ${response.id}`); - console.log(` Discovery URL: ${response.discoveryUrl}`); - console.log(` Updated At: ${response.updatedAt}`); -}; - -export const updateEnvironment = async ( - options: UpdateEnvironmentOptions & { - testTargetId: string; - environmentId: string; - json?: boolean; - testAccountUsername?: string; - testAccountPassword?: string; - basicAuthUsername?: string; - basicAuthPassword?: string; - privateLocationName?: string; - testAccountOtpInitializerKey?: string; - }, -): Promise => { - const { data, error } = await client.PATCH( - "/apiKey/v2/test-targets/{testTargetId}/environments/{environmentId}", - { - params: { - path: { - testTargetId: options.testTargetId, - environmentId: options.environmentId, - }, - }, - body: { - name: options.name, - discoveryUrl: options.discoveryUrl, - testAccount: { - username: options.testAccountUsername, - password: options.testAccountPassword, - }, - otpInitializerKey: options.testAccountOtpInitializerKey, - basicAuth: { - username: options.basicAuthUsername, - password: options.basicAuthPassword, - }, - privateLocationName: options.privateLocationName, - }, - }, - ); - - handleError(error); - - const response = data as EnvironmentResponse; - - if (options.json) { - outputResult(response); - return; - } - - console.log("Environment updated successfully!"); - console.log(`- Name: ${response.name}`); - console.log(` ID: ${response.id}`); - console.log(` Discovery URL: ${response.discoveryUrl}`); - console.log(` Updated At: ${response.updatedAt}`); -}; - -export const deleteEnvironment = async (options: { - testTargetId: string; - environmentId: string; - json?: boolean; -}): Promise => { - const { error } = await client.DELETE( - "/apiKey/v2/test-targets/{testTargetId}/environments/{environmentId}", - { - params: { - path: { - testTargetId: options.testTargetId, - environmentId: options.environmentId, - }, - }, - }, - ); - - handleError(error); - - if (options.json) { - outputResult({ success: true }); - return; - } - - 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 => { - 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; - url: string; - outputDir: string; - headless?: boolean; - json?: boolean; -}): Promise => { - const { data, error } = await client.GET( - "/apiKey/v2/test-targets/{testTargetId}/config", - { - params: { - path: { - testTargetId: options.testTargetId, - }, - query: { - environmentId: options.environmentId, - url: options.url, - outputDir: options.outputDir, - headless: options.headless ? "true" : "false", - }, - }, - parseAs: "text", - }, - ); - - handleError(error); - - if (!data) { - throw new Error("no config found"); - } - - if (options.json) { - outputResult(data); - } - - return data; -}; - -export const getPlaywrightCode = async (options: { - testTargetId: string; - testCaseId: string; - environmentId?: string; - executionUrl: string; - json?: boolean; -}): Promise => { - const { data, error } = await client.GET( - "/apiKey/v2/test-targets/{testTargetId}/test-cases/{testCaseId}/code", - { - params: { - path: { - testTargetId: options.testTargetId, - testCaseId: options.testCaseId, - }, - query: { - environmentId: options.environmentId, - executionUrl: options.executionUrl, - }, - }, - }, - ); - - handleError(error); - - if (!data) { - console.log({ data, error }); - throw new Error("no test code found"); - } - - if (options.json) { - outputResult(data); - } - - return data.testCode; -}; - -export const getTestCases = async (options: { - testTargetId: string; - status?: string; - json?: boolean; -}): Promise => { - const { data, error } = await client.GET( - "/apiKey/v2/test-targets/{testTargetId}/test-cases", - { - params: { - path: { - testTargetId: options.testTargetId, - }, - query: { - filter: JSON.stringify({ status: options.status }), - }, - }, - }, - ); - - handleError(error); - - if (!data) { - throw new Error("no test cases found"); - } - - if (options.json) { - outputResult(data); - } - - return data; -}; diff --git a/src/tools/client.ts b/src/tools/client.ts new file mode 100644 index 0000000..03ff9ff --- /dev/null +++ b/src/tools/client.ts @@ -0,0 +1,52 @@ +import type { components, paths } from "../api"; // generated by openapi-typescript +import createClient, { Middleware } from "openapi-fetch"; +import { loadConfig } from "../config"; + +const BASE_URL = process.env.OCTOMIND_API_URL || "https://app.octomind.dev/api"; +type ErrorResponse = components["schemas"]["ZodResponse"] | string | undefined; + +const client = createClient({ baseUrl: BASE_URL }); + +const authMiddleware: Middleware = { + async onRequest({ request }) { + const { apiKey } = await loadConfig(); + if (!apiKey) { + throw new Error( + "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.text(); + return new Response( + `${response.status}, ${response.statusText}: ${errorBody ? errorBody : ""}`, + { ...resOptions, status: response.status }, + ); + } + return response; + }, + onError({ error }) { + console.error(error); + process.exit(1); + }, +}; + +client.use(authMiddleware); + +export { client }; + +export const handleError = (error: ErrorResponse) => { + if (error) { + console.error(error); + process.exit(1); + } +}; + +export const outputResult = (result: unknown): void => { + console.log(JSON.stringify(result, null, 2)); +}; diff --git a/src/tools/discoveries.ts b/src/tools/discoveries.ts new file mode 100644 index 0000000..554db7f --- /dev/null +++ b/src/tools/discoveries.ts @@ -0,0 +1,49 @@ +import type { components } from "../api"; // generated by openapi-typescript +import { client, handleError, outputResult } from "./client"; + +export type createDiscoveryBody = + components["schemas"]["ExternalDiscoveryBody"]; +export type DiscoveryResponse = components["schemas"]["DiscoveryResponse"]; + +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}`); +}; diff --git a/src/tools/environments.ts b/src/tools/environments.ts new file mode 100644 index 0000000..a8d4d88 --- /dev/null +++ b/src/tools/environments.ts @@ -0,0 +1,200 @@ +import type { paths, components } from "../api"; // generated by openapi-typescript +import { client, handleError, outputResult } from "./client"; + +export type listEnvironmentsOptions = + paths["/apiKey/v2/test-targets/{testTargetId}/environments"]["get"]["parameters"]["path"]; +export type PostEnvironmentOptions = + paths["/apiKey/v2/test-targets/{testTargetId}/environments"]["post"]["requestBody"]["content"]["application/json"] & { + testTargetId: string; + }; +export type UpdateEnvironmentOptions = + paths["/apiKey/v2/test-targets/{testTargetId}/environments/{environmentId}"]["patch"]["requestBody"]["content"]["application/json"]; +export type EnvironmentResponse = components["schemas"]["EnvironmentResponse"]; + +export const listEnvironments = async ( + options: listEnvironmentsOptions & { json?: boolean }, +): Promise => { + const { data, error } = await client.GET( + "/apiKey/v2/test-targets/{testTargetId}/environments", + { + params: { + path: { testTargetId: options.testTargetId }, + }, + }, + ); + + handleError(error); + + if (options.json) { + outputResult(data); + return; + } + + console.log("Environments:"); + if (data) { + data.forEach((environment) => { + console.log(`- Name: ${environment.name}`); + console.log(` ID: ${environment.id}`); + console.log(` Discovery URL: ${environment.discoveryUrl}`); + console.log(` Updated At: ${environment.updatedAt}`); + }); + } +}; + +export const getEnvironments = async ( + options: listEnvironmentsOptions & { json?: boolean }, +): Promise => { + const { data, error } = await client.GET( + "/apiKey/v2/test-targets/{testTargetId}/environments", + { + params: { + path: { testTargetId: options.testTargetId }, + }, + }, + ); + + handleError(error); + + if (!data) { + throw new Error("no environments found"); + } + + if (options.json) { + outputResult(data); + } + + return data; +}; + +export const createEnvironment = async ( + options: PostEnvironmentOptions & { + json?: boolean; + testAccountUsername?: string; + testAccountPassword?: string; + basicAuthUsername?: string; + basicAuthPassword?: string; + privateLocationName?: string; + testAccountOtpInitializerKey?: string; + }, +): Promise => { + const { data, error } = await client.POST( + "/apiKey/v2/test-targets/{testTargetId}/environments", + { + params: { + path: { testTargetId: options.testTargetId }, + }, + body: { + name: options.name, + discoveryUrl: options.discoveryUrl, + testAccount: { + username: options.testAccountUsername, + password: options.testAccountPassword, + }, + basicAuth: { + username: options.basicAuthUsername, + password: options.basicAuthPassword, + }, + testAccountOtpInitializerKey: options.testAccountOtpInitializerKey, + privateLocationName: options.privateLocationName, + additionalHeaderFields: options.additionalHeaderFields, + }, + }, + ); + + handleError(error); + + const response = data as EnvironmentResponse; + + if (options.json) { + outputResult(response); + return; + } + + console.log("Environment created successfully!"); + console.log(`- Name: ${response.name}`); + console.log(` ID: ${response.id}`); + console.log(` Discovery URL: ${response.discoveryUrl}`); + console.log(` Updated At: ${response.updatedAt}`); +}; + +export const updateEnvironment = async ( + options: UpdateEnvironmentOptions & { + testTargetId: string; + environmentId: string; + json?: boolean; + testAccountUsername?: string; + testAccountPassword?: string; + basicAuthUsername?: string; + basicAuthPassword?: string; + privateLocationName?: string; + testAccountOtpInitializerKey?: string; + }, +): Promise => { + const { data, error } = await client.PATCH( + "/apiKey/v2/test-targets/{testTargetId}/environments/{environmentId}", + { + params: { + path: { + testTargetId: options.testTargetId, + environmentId: options.environmentId, + }, + }, + body: { + name: options.name, + discoveryUrl: options.discoveryUrl, + testAccount: { + username: options.testAccountUsername, + password: options.testAccountPassword, + }, + otpInitializerKey: options.testAccountOtpInitializerKey, + basicAuth: { + username: options.basicAuthUsername, + password: options.basicAuthPassword, + }, + privateLocationName: options.privateLocationName, + }, + }, + ); + + handleError(error); + + const response = data as EnvironmentResponse; + + if (options.json) { + outputResult(response); + return; + } + + console.log("Environment updated successfully!"); + console.log(`- Name: ${response.name}`); + console.log(` ID: ${response.id}`); + console.log(` Discovery URL: ${response.discoveryUrl}`); + console.log(` Updated At: ${response.updatedAt}`); +}; + +export const deleteEnvironment = async (options: { + testTargetId: string; + environmentId: string; + json?: boolean; +}): Promise => { + const { error } = await client.DELETE( + "/apiKey/v2/test-targets/{testTargetId}/environments/{environmentId}", + { + params: { + path: { + testTargetId: options.testTargetId, + environmentId: options.environmentId, + }, + }, + }, + ); + + handleError(error); + + if (options.json) { + outputResult({ success: true }); + return; + } + + console.log("Environment deleted successfully!"); +}; diff --git a/src/tools/index.ts b/src/tools/index.ts new file mode 100644 index 0000000..47be71d --- /dev/null +++ b/src/tools/index.ts @@ -0,0 +1,7 @@ +export * from "./environments"; +export * from "./test-reports"; +export * from "./private-locations"; +export * from "./test-cases"; +export * from "./notifications"; +export * from "./discoveries"; +export * from "./playwright"; diff --git a/src/tools/notifications.ts b/src/tools/notifications.ts new file mode 100644 index 0000000..171e1b4 --- /dev/null +++ b/src/tools/notifications.ts @@ -0,0 +1,47 @@ +import type { paths, components } from "../api"; // generated by openapi-typescript +import { client, handleError, outputResult } from "./client"; + +export type getNotificationsParams = + paths["/apiKey/v2/test-targets/{testTargetId}/notifications"]["get"]["parameters"]["path"]; +export type Notification = components["schemas"]["Notification"]; + +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}`); + } + }); +}; diff --git a/src/tools/playwright.ts b/src/tools/playwright.ts new file mode 100644 index 0000000..1b68153 --- /dev/null +++ b/src/tools/playwright.ts @@ -0,0 +1,77 @@ +import { client, handleError, outputResult } from "./client"; + +export const getPlaywrightConfig = async (options: { + testTargetId: string; + environmentId?: string; + url: string; + outputDir: string; + headless?: boolean; + json?: boolean; +}): Promise => { + const { data, error } = await client.GET( + "/apiKey/v2/test-targets/{testTargetId}/config", + { + params: { + path: { + testTargetId: options.testTargetId, + }, + query: { + environmentId: options.environmentId, + url: options.url, + outputDir: options.outputDir, + headless: options.headless ? "true" : "false", + }, + }, + parseAs: "text", + }, + ); + + handleError(error); + + if (!data) { + throw new Error("no config found"); + } + + if (options.json) { + outputResult(data); + } + + return data; +}; + +export const getPlaywrightCode = async (options: { + testTargetId: string; + testCaseId: string; + environmentId?: string; + executionUrl: string; + json?: boolean; +}): Promise => { + const { data, error } = await client.GET( + "/apiKey/v2/test-targets/{testTargetId}/test-cases/{testCaseId}/code", + { + params: { + path: { + testTargetId: options.testTargetId, + testCaseId: options.testCaseId, + }, + query: { + environmentId: options.environmentId, + executionUrl: options.executionUrl, + }, + }, + }, + ); + + handleError(error); + + if (!data) { + console.log({ data, error }); + throw new Error("no test code found"); + } + + if (options.json) { + outputResult(data); + } + + return data.testCode; +}; diff --git a/src/tools/private-locations.ts b/src/tools/private-locations.ts new file mode 100644 index 0000000..fa6db53 --- /dev/null +++ b/src/tools/private-locations.ts @@ -0,0 +1,86 @@ +import type { components } from "../api"; // generated by openapi-typescript +import { client, handleError, outputResult } from "./client"; + +export type SuccessResponse = components["schemas"]["SuccessResponse"]; +export type PrivateLocationInfo = components["schemas"]["PrivateLocationInfo"]; + +export const registerLocation = async (options: { + address: string; + name: string; + username: string; + password: string; + json?: boolean; +}): Promise => { + const { data, error } = await client.PUT( + "/apiKey/v1/private-location/register", + { + body: { + name: options.name, + registrationData: { + address: options.address, + username: options.username, + password: options.password, + }, + }, + }, + ); + + handleError(error); + + const response = data as SuccessResponse; + + if (options.json) { + outputResult(response); + return; + } + + console.log("Registration result:", response.success ? "Success" : "Failed"); +}; + +export const unregisterLocation = async (options: { + name: string; + json?: boolean; +}): Promise => { + const { data, error } = await client.PUT( + "/apiKey/v1/private-location/unregister", + { + body: { + name: options.name, + }, + }, + ); + + handleError(error); + const response = data as SuccessResponse; + + if (options.json) { + outputResult(response); + return; + } + + console.log( + "Unregistration result:", + response.success ? "Success" : "Failed", + ); +}; + +export const listPrivateLocations = async (options: { + json?: boolean; +}): Promise => { + const { data, error } = await client.GET("/apiKey/v1/private-location"); + + handleError(error); + + const response = data as PrivateLocationInfo; + if (options.json) { + outputResult(response); + return; + } + + console.log("Private Locations:"); + response.forEach((location) => { + console.log(`- Name: ${location.name}`); + console.log(` Status: ${location.status}`); + console.log(` Address: ${location.address}`); + }); +}; diff --git a/src/tools/test-cases.ts b/src/tools/test-cases.ts new file mode 100644 index 0000000..6d29999 --- /dev/null +++ b/src/tools/test-cases.ts @@ -0,0 +1,98 @@ +import type { paths, components } from "../api"; // generated by openapi-typescript +import { client, handleError, outputResult } from "./client"; + +export type TestCaseResponse = components["schemas"]["TestCaseResponse"]; +export type TestCasesResponse = components["schemas"]["TestCasesResponse"]; +export type getTestCaseParams = + paths["/apiKey/v2/test-targets/{testTargetId}/test-cases/{testCaseId}"]["get"]["parameters"]["path"]; + +export const getTestCase = async ( + options: getTestCaseParams & { json?: boolean }, +): Promise => { + 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 getTestCases = async (options: { + testTargetId: string; + status?: string; + json?: boolean; +}): Promise => { + const { data, error } = await client.GET( + "/apiKey/v2/test-targets/{testTargetId}/test-cases", + { + params: { + path: { + testTargetId: options.testTargetId, + }, + query: { + filter: JSON.stringify({ status: options.status }), + }, + }, + }, + ); + + handleError(error); + + if (!data) { + throw new Error("no test cases found"); + } + + if (options.json) { + outputResult(data); + } + + return data; +}; diff --git a/src/tools/test-reports.ts b/src/tools/test-reports.ts new file mode 100644 index 0000000..b38211b --- /dev/null +++ b/src/tools/test-reports.ts @@ -0,0 +1,97 @@ +import type { paths, components } from "../api"; // generated by openapi-typescript +import { client, handleError, outputResult } from "./client"; + +export type executeTestsBody = + components["schemas"]["TestTargetExecutionRequest"]; +export type getTestReportParams = + paths["/apiKey/v2/test-targets/{testTargetId}/test-reports/{testReportId}"]["get"]["parameters"]["path"]; +export type TestReport = components["schemas"]["TestReport"]; + +export const executeTests = async ( + options: executeTestsBody & { json?: boolean; description?: string }, +): Promise => { + const { data, error } = await client.POST("/apiKey/v2/execute", { + body: { + testTargetId: options.testTargetId, + url: options.url, + context: { + source: "manual", + description: options.description || "CLI execution", + triggeredBy: { + type: "USER", + userId: "cli-user", + }, + }, + environmentName: options.environmentName, + tags: options.tags, + variablesToOverwrite: options.variablesToOverwrite, + }, + }); + + handleError(error); + + if (options.json) { + outputResult(data); + return; + } + + if (data) { + console.log("Test execution started successfully!"); + console.log("Test Report URL:", data.testReportUrl); + console.log("Report Status:", data.testReport?.status); + + if (data.testReport?.testResults?.length ?? 0 > 0) { + console.log("\nTest Results:"); + data.testReport?.testResults?.forEach((result) => { + console.log(`- Test ${result.testCaseId}: ${result.status}`); + if (result.errorMessage) { + console.log(` Error: ${result.errorMessage}`); + } + if (result.traceUrl) { + console.log(` Trace: ${result.traceUrl}`); + } + }); + } + } +}; + +export const getTestReport = async ( + options: getTestReportParams & { json?: boolean }, +): Promise => { + const { data, error } = await client.GET( + "/apiKey/v2/test-targets/{testTargetId}/test-reports/{testReportId}", + { + params: { + path: { + testTargetId: options.testTargetId, + testReportId: options.testReportId, + }, + }, + }, + ); + + handleError(error); + + if (options.json) { + outputResult(data); + return; + } + + const response = data as TestReport; + console.log("Test Report Details:"); + console.log("Status:", response.status); + console.log("Execution URL:", response.executionUrl); + + if ((response.testResults ?? []).length > 0) { + console.log("\nTest Results:"); + (response.testResults ?? []).forEach((result) => { + console.log(`- Test ${result.testCaseId}: ${result.status}`); + if (result.errorMessage) { + console.log(` Error: ${result.errorMessage}`); + } + if (result.traceUrl) { + console.log(` Trace: ${result.traceUrl}`); + } + }); + } +};