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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ This way even entityIds like environmentIds or testCaseIds will be autocompleted

# octomind

Octomind cli tool. Version: 4.4.0. Additional documentation see https://octomind.dev/docs/api-reference/
Octomind cli tool. Version: 4.5.0. Additional documentation see https://octomind.dev/docs/api-reference/

**Usage:** `octomind [options] [command]`

Expand Down Expand Up @@ -184,7 +184,7 @@ Delete an environment

## debug

run test cases against local build
run test cases against local build. can also be authenticated with a bearer token so no need to provide an api key

**Usage:** `debug [options]`

Expand All @@ -204,6 +204,7 @@ run test cases against local build
| `--browser [CHROMIUM, FIREFOX, SAFARI]` | Browser type | No | CHROMIUM |
| `--breakpoint [DESKTOP, MOBILE, TABLET]` | Breakpoint | No | DESKTOP |
| `--run-status [ON, OFF]` | only run test cases that are either ON or OFF | No | |
| `-b, --bearer-token [token]` | Bearer token for authentication (instead of api key) | No | |

## execute

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@octomind/octomind",
"version": "4.4.0",
"version": "4.5.0",
"description": "a command line client for octomind apis",
"main": "./dist/index.js",
"packageManager": "pnpm@10.28.1+sha512.7d7dbbca9e99447b7c3bf7a73286afaaf6be99251eb9498baefa7d406892f67b879adb3a1d7e687fc4ccc1a388c7175fbaae567a26ab44d1067b54fcb0d6a316",
Expand All @@ -25,6 +25,7 @@
"gendoc": "tsx scripts/generate-docs.ts > README.md",
"lint": "pnpm apigen && npx genversion -des src/version.ts && biome check",
"tsc": "tsc --project tsconfig.build.json",
"typecheck": "tsc --project tsconfig.build.json --noEmit",
"dev:local": "pnpm apigen && OCTOMIND_CONFIG_FILE=octomind.dev.local.json OCTOMIND_API_URL=http://localhost:3000/api tsx src/index.ts",
"dev:staging": "pnpm apigen && OCTOMIND_CONFIG_FILE=octomind.dev.staging.json OCTOMIND_API_URL=https://preview.octomind.dev/api tsx src/index.ts",
"dev:prod": "pnpm apigen && OCTOMIND_CONFIG_FILE=octomind.dev.prod.json OCTOMIND_API_URL=https://app.octomind.dev/api tsx src/index.ts",
Expand Down
8 changes: 7 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,9 @@ export const buildCmd = async (): Promise<CompletableCommand> => {
.completer(testTargetIdCompleter)
.completer(testCaseIdCompleter)
.completer(optionsCompleter)
.description("run test cases against local build")
.description(
"run test cases against local build. can also be authenticated with a bearer token so no need to provide an api key",
)
.helpGroup("execute")
.requiredOption("-u, --url <url>", "url the tests should run against")
.option(
Expand Down Expand Up @@ -147,6 +149,10 @@ export const buildCmd = async (): Promise<CompletableCommand> => {
"--run-status [ON, OFF]",
"only run test cases that are either ON or OFF",
)
.option(
"-b, --bearer-token [token]",
"Bearer token for authentication (instead of api key)",
)
.action(addTestTargetWrapper(runDebugtopus));

createCommandWithCommonOptions(program, "execute")
Expand Down
3 changes: 3 additions & 0 deletions src/debugtopus/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export type DebugtopusOptions = {
browser?: "CHROMIUM" | "FIREFOX" | "SAFARI";
breakpoint?: "DESKTOP" | "MOBILE" | "TABLET";
runStatus?: "ON" | "OFF";
bearerToken?: string;
};

const getPackageRootLevel = (appDir: string): string => {
Expand Down Expand Up @@ -224,6 +225,7 @@ export const runDebugtopus = async (options: DebugtopusOptions) => {
testTargetId: options.testTargetId,
url: options.url,
environmentId: options.environmentId,
bearerToken: options.bearerToken,
};

let testCasesWithCode: TestCaseCodeWithId[] = [];
Expand Down Expand Up @@ -293,6 +295,7 @@ export const runDebugtopus = async (options: DebugtopusOptions) => {
bypassProxy: options.bypassProxy,
browser: options.browser,
breakpoint: options.breakpoint,
bearerToken: options.bearerToken,
});

if (!config) {
Expand Down
31 changes: 30 additions & 1 deletion src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,31 @@ import {

export const logger = getLogger("octomind");

const validLogLevels: LogLevel[] = [
"trace",
"debug",
"info",
"warning",
"error",
"fatal",
];

export const getLogLevel = (
level: string | undefined,
defaultLevel: LogLevel = "warning",
): LogLevel => {
if (!level) {
return defaultLevel;
}

const normalizedLevel = level.toLowerCase();
if (validLogLevels.includes(normalizedLevel as LogLevel)) {
return normalizedLevel as LogLevel;
}

return defaultLevel;
};

const ansiColors = {
red: "\x1B[31m",
green: "\x1B[32m",
Expand Down Expand Up @@ -51,6 +76,10 @@ export const configureLogger = async (): Promise<void> =>
sinks: ["console"],
lowestLevel: "warning",
},
{ category: "octomind", lowestLevel: "debug", sinks: ["console"] },
{
category: "octomind",
lowestLevel: getLogLevel(process.env.LOG_LEVEL),
sinks: ["console"],
},
],
});
22 changes: 20 additions & 2 deletions src/tools/environments.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { components, paths } from "../api"; // generated by openapi-typescript
import { logger } from "../logger";
import { client, handleError, ListOptions, logJson } from "./client";
import { BASE_URL, client, handleError, ListOptions, logJson } from "./client";

export type GetEnvironmentsOptions =
paths["/apiKey/v3/test-targets/{testTargetId}/environments"]["get"]["parameters"]["path"];
paths["/apiKey/v3/test-targets/{testTargetId}/environments"]["get"]["parameters"]["path"] & {
bearerToken?: string;
};
export type PostEnvironmentOptions =
paths["/apiKey/v3/test-targets/{testTargetId}/environments"]["post"]["requestBody"]["content"]["application/json"] & {
testTargetId: string;
Expand Down Expand Up @@ -39,6 +41,22 @@ export const listEnvironments = async (
export const getEnvironments = async (
options: GetEnvironmentsOptions,
): Promise<EnvironmentResponse[]> => {
if (options.bearerToken) {
logger.debug("Using bearer token for environments");
const res = await fetch(
`${BASE_URL}/bearer/v1/test-targets/${options.testTargetId}/environments`,
{
headers: {
Authorization: `Bearer ${options.bearerToken}`,
},
},
);
if (res.ok) {
const environments = await res.json();
return environments;
}
throw new Error(`no environments found. error: ${res.statusText}`);
}
const { data, error } = await client.GET(
"/apiKey/v3/test-targets/{testTargetId}/environments",
{
Expand Down
68 changes: 67 additions & 1 deletion src/tools/playwright.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { client, handleError } from "./client";
import { logger } from "../logger";
import { BASE_URL, client, handleError } from "./client";

export const getPlaywrightConfig = async (options: {
testTargetId: string;
Expand All @@ -9,7 +10,39 @@ export const getPlaywrightConfig = async (options: {
bypassProxy?: boolean;
browser?: "CHROMIUM" | "FIREFOX" | "SAFARI";
breakpoint?: "DESKTOP" | "MOBILE" | "TABLET";
bearerToken?: string;
}): Promise<string> => {
if (options.bearerToken) {
logger.debug("Using bearer token for config");
const params = {
environmentId: options.environmentId,
url: options.url,
outputDir: options.outputDir,
headless: options.headless?.toString(),
bypassProxy: options.bypassProxy?.toString(),
browser: options.browser,
breakpoint: options.breakpoint,
};

const filteredParams = Object.fromEntries(
Object.entries(params).filter(([, value]) => value !== undefined),
) as Record<string, string>;

const searchParams = new URLSearchParams(filteredParams);

const response = await fetch(
`${BASE_URL}/bearer/v1/test-targets/${options.testTargetId}/config?${searchParams.toString()}`,
{
headers: {
Authorization: `Bearer ${options.bearerToken}`,
},
},
);
if (response.ok) {
return await response.text();
}
throw new Error(`no config found. error: ${response.statusText}`);
}
const { data, error } = await client.GET(
"/apiKey/v3/test-targets/{testTargetId}/config",
{
Expand Down Expand Up @@ -40,12 +73,45 @@ export const getPlaywrightConfig = async (options: {
return data;
};

type PlaywrightCodeResponse = {
testCode: string;
};

export const getPlaywrightCode = async (options: {
testTargetId: string;
testCaseId: string;
environmentId?: string;
executionUrl: string;
bearerToken?: string;
}): Promise<string> => {
if (options.bearerToken) {
logger.debug("Using bearer token for test code");
const params = {
source: "debugtopus",
executionUrl: options.executionUrl,
environmentId: options.environmentId,
};

const filteredParams = Object.fromEntries(
Object.entries(params).filter(([, value]) => value !== undefined),
) as Record<string, string>;

const searchParams = new URLSearchParams(filteredParams);

const response = await fetch(
`${BASE_URL}/bearer/v1/test-targets/${options.testTargetId}/test-cases/${options.testCaseId}/code?${searchParams.toString()}`,
{
headers: {
Authorization: `Bearer ${options.bearerToken}`,
},
},
);
if (response.ok) {
const res = (await response.json()) as PlaywrightCodeResponse;
return res.testCode;
}
throw new Error(`no test code found. error: ${response.statusText}`);
}
const { data, error } = await client.GET(
"/apiKey/v3/test-targets/{testTargetId}/test-cases/{testCaseId}/code",
{
Expand Down
27 changes: 26 additions & 1 deletion src/tools/test-cases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { components, paths } from "../api"; // generated by openapi-typescr
import { findOctomindFolder } from "../helpers";
import { logger } from "../logger";
import { getUrl } from "../url";
import { client, handleError, ListOptions, logJson } from "./client";
import { BASE_URL, client, handleError, ListOptions, logJson } from "./client";
import { getEnvironments } from "./environments";
import { buildFilename, readTestCasesFromDir } from "./sync/yaml";

Expand Down Expand Up @@ -121,10 +121,35 @@ export type GetTestCasesOptions = {
testTargetId: string;
status?: string;
runStatus?: string;
bearerToken?: string;
};
export const getTestCases = async (
options: GetTestCasesOptions,
): Promise<TestCasesResponse> => {
if (options.bearerToken) {
const url = new URL(
`${BASE_URL}/bearer/v1/test-targets/${options.testTargetId}/test-cases`,
);
if (options.status || options.runStatus) {
url.searchParams.set(
"filter",
JSON.stringify({
status: options.status,
runStatus: options.runStatus,
}),
);
}
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${options.bearerToken}`,
},
});
if (res.ok) {
const response = await res.json();
return response;
}
throw new Error(`no test cases found. error: ${res.statusText}`);
}
const { data, error } = await client.GET(
"/apiKey/v3/test-targets/{testTargetId}/test-cases",
{
Expand Down
50 changes: 50 additions & 0 deletions tests/logger.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, expect, it, vi } from "vitest";

vi.unmock("../src/logger");

import { getLogLevel } from "../src/logger";

describe("logger", () => {
describe("getLogLevel", () => {
it("should return default level when no level is provided", () => {
expect(getLogLevel(undefined)).toBe("warning");
});

it("should return custom default level when provided", () => {
expect(getLogLevel(undefined, "info")).toBe("info");
});

it("should return valid log level when provided", () => {
expect(getLogLevel("debug")).toBe("debug");
expect(getLogLevel("info")).toBe("info");
expect(getLogLevel("warning")).toBe("warning");
expect(getLogLevel("error")).toBe("error");
expect(getLogLevel("fatal")).toBe("fatal");
expect(getLogLevel("trace")).toBe("trace");
});

it("should handle uppercase log levels", () => {
expect(getLogLevel("DEBUG")).toBe("debug");
expect(getLogLevel("INFO")).toBe("info");
expect(getLogLevel("WARNING")).toBe("warning");
expect(getLogLevel("ERROR")).toBe("error");
});

it("should handle mixed case log levels", () => {
expect(getLogLevel("DeBuG")).toBe("debug");
expect(getLogLevel("WaRnInG")).toBe("warning");
});

it("should return default level for invalid log levels", () => {
expect(getLogLevel("invalid")).toBe("warning");
expect(getLogLevel("")).toBe("warning");
expect(getLogLevel("verbose")).toBe("warning");
expect(getLogLevel("critical")).toBe("warning");
});

it("should return custom default level for invalid log levels", () => {
expect(getLogLevel("invalid", "error")).toBe("error");
expect(getLogLevel("", "debug")).toBe("debug");
});
});
});
Loading