Skip to content
Open
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
9 changes: 9 additions & 0 deletions src/base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,15 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand {
return { apiKey, appId };
}

// Fall back to ABLY_API_KEY environment variable (for CI/scripting)
const envApiKey = process.env.ABLY_API_KEY;
if (envApiKey) {
const envAppId = envApiKey.split(".")[0] || "";
if (envAppId) {
return { apiKey: envApiKey, appId: envAppId };
}
}

// Get access token for control API
const accessToken =
process.env.ABLY_ACCESS_TOKEN || this.configManager.getAccessToken();
Expand Down
9 changes: 0 additions & 9 deletions src/commands/logs/subscribe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,6 @@ export default class LogsSubscribe extends AblyBaseCommand {
includeUserFriendlyMessages: true,
});

// Get the logs channel
const appConfig = await this.ensureAppAndKey(flags);
if (!appConfig) {
this.fail(
"Unable to determine app configuration",
flags,
"logSubscribe",
);
}
const logsChannelName = `[meta]log`;

// Configure channel options for rewind if specified
Expand Down
28 changes: 17 additions & 11 deletions src/commands/rooms/typing/keystroke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,18 +160,24 @@ export default class TypingKeystroke extends ChatBaseCommand {
}, KEYSTROKE_INTERVAL);
}

this.logCliEvent(
flags,
"typing",
"listening",
"Maintaining typing status...",
);
// If auto-type is enabled, keep the command running to maintain typing state
if (flags["auto-type"]) {
this.logCliEvent(
flags,
"typing",
"listening",
"Maintaining typing status...",
);

// Wait until the user interrupts, duration elapses, or the room fails
await Promise.race([
this.waitAndTrackCleanup(flags, "typing", flags.duration),
failurePromise,
]);
// Wait until the user interrupts, duration elapses, or the room fails
await Promise.race([
this.waitAndTrackCleanup(flags, "typing", flags.duration),
failurePromise,
]);
} else {
// Suppress unhandled rejection if room fails during cleanup
failurePromise.catch(() => {});
}
} catch (error) {
this.fail(error, flags, "roomTypingKeystroke", { room: args.room });
}
Expand Down
68 changes: 68 additions & 0 deletions test/e2e/accounts/accounts-e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {
describe,
it,
beforeEach,
afterEach,
beforeAll,
afterAll,
expect,
} from "vitest";
import {
E2E_ACCESS_TOKEN,
SHOULD_SKIP_CONTROL_E2E,
forceExit,
cleanupTrackedResources,
setupTestFailureHandler,
resetTestTracking,
} from "../../helpers/e2e-test-helper.js";
import { runCommand } from "../../helpers/command-helpers.js";

describe.skipIf(SHOULD_SKIP_CONTROL_E2E)("Accounts E2E Tests", () => {
beforeAll(() => {
process.on("SIGINT", forceExit);
});

afterAll(() => {
process.removeListener("SIGINT", forceExit);
});

beforeEach(() => {
resetTestTracking();
});

afterEach(async () => {
await cleanupTrackedResources();
});

it(
"should list locally configured accounts",
{ timeout: 15000 },
async () => {
setupTestFailureHandler("should list locally configured accounts");

// accounts list reads from local config, not the API directly.
// In E2E environment, there may or may not be configured accounts.
// We just verify the command runs without crashing.
const listResult = await runCommand(["accounts", "list"], {
env: { ABLY_ACCESS_TOKEN: E2E_ACCESS_TOKEN || "" },
});

// The command may exit 0 (accounts found) or non-zero (no accounts configured).
// Either way, it should produce output and not crash.
const combinedOutput = listResult.stdout + listResult.stderr;
expect(combinedOutput.length).toBeGreaterThan(0);
},
);

it("should show help for accounts current", { timeout: 10000 }, async () => {
setupTestFailureHandler("should show help for accounts current");

const helpResult = await runCommand(["accounts", "current", "--help"], {
env: { ABLY_ACCESS_TOKEN: E2E_ACCESS_TOKEN || "" },
});

expect(helpResult.exitCode).toBe(0);
const output = helpResult.stdout + helpResult.stderr;
expect(output).toContain("USAGE");
});
});
224 changes: 224 additions & 0 deletions test/e2e/auth/auth-keys-e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import {
describe,
it,
beforeEach,
afterEach,
beforeAll,
afterAll,
expect,
} from "vitest";
import {
E2E_ACCESS_TOKEN,
SHOULD_SKIP_CONTROL_E2E,
forceExit,
cleanupTrackedResources,
setupTestFailureHandler,
resetTestTracking,
} from "../../helpers/e2e-test-helper.js";
import { runCommand } from "../../helpers/command-helpers.js";
import { parseNdjsonLines } from "../../helpers/ndjson.js";

describe.skipIf(SHOULD_SKIP_CONTROL_E2E)("Auth Keys E2E Tests", () => {
let testAppId: string;

beforeAll(async () => {
process.on("SIGINT", forceExit);

// Create a test app for key operations
const createResult = await runCommand(
["apps", "create", "--name", `e2e-keys-test-${Date.now()}`, "--json"],
{
env: { ABLY_ACCESS_TOKEN: E2E_ACCESS_TOKEN || "" },
},
);

if (createResult.exitCode !== 0) {
throw new Error(`Failed to create test app: ${createResult.stderr}`);
}
const result = parseNdjsonLines(createResult.stdout).find(
(r) => r.type === "result",
) as Record<string, unknown>;
const app = result.app as Record<string, unknown>;
testAppId = (app.id ?? app.appId) as string;
if (!testAppId) {
throw new Error(`No app ID found in result: ${JSON.stringify(result)}`);
}
});

afterAll(async () => {
if (testAppId) {
try {
await runCommand(["apps", "delete", testAppId, "--force"], {
env: { ABLY_ACCESS_TOKEN: E2E_ACCESS_TOKEN || "" },
});
} catch {
// Ignore cleanup errors — the app may already be deleted
}
}
process.removeListener("SIGINT", forceExit);
});

beforeEach(() => {
resetTestTracking();
});

afterEach(async () => {
await cleanupTrackedResources();
});

it("should list API keys for an app", { timeout: 15000 }, async () => {
setupTestFailureHandler("should list API keys for an app");

const listResult = await runCommand(
["auth", "keys", "list", "--app", testAppId, "--json"],
{
env: { ABLY_ACCESS_TOKEN: E2E_ACCESS_TOKEN || "" },
},
);

expect(listResult.exitCode).toBe(0);
const records = parseNdjsonLines(listResult.stdout);
const result = records.find((r) => r.type === "result");
expect(result).toBeDefined();
expect(result).toHaveProperty("success", true);
expect(Array.isArray(result!.keys)).toBe(true);
// Every app has at least one default key
expect((result!.keys as unknown[]).length).toBeGreaterThan(0);
});

it("should create a new API key", { timeout: 15000 }, async () => {
setupTestFailureHandler("should create a new API key");

const keyName = `e2e-test-key-${Date.now()}`;
const createResult = await runCommand(
[
"auth",
"keys",
"create",
"--app",
testAppId,
"--name",
keyName,
"--json",
],
{
env: { ABLY_ACCESS_TOKEN: E2E_ACCESS_TOKEN || "" },
},
);

expect(createResult.exitCode).toBe(0);
const result = parseNdjsonLines(createResult.stdout).find(
(r) => r.type === "result",
) as Record<string, unknown>;
expect(result).toBeDefined();
expect(result).toHaveProperty("success", true);
const key = result.key as Record<string, unknown>;
expect(key).toHaveProperty("name", keyName);
expect(key).toHaveProperty("key");
expect(key).toHaveProperty("keyName");
});

it("should get details for a specific key", { timeout: 20000 }, async () => {
setupTestFailureHandler("should get details for a specific key");

// First create a key to get
const keyName = `e2e-get-key-${Date.now()}`;
const createResult = await runCommand(
[
"auth",
"keys",
"create",
"--app",
testAppId,
"--name",
keyName,
"--json",
],
{
env: { ABLY_ACCESS_TOKEN: E2E_ACCESS_TOKEN || "" },
},
);

const createRecord = parseNdjsonLines(createResult.stdout).find(
(r) => r.type === "result",
) as Record<string, unknown>;
const createdKey = createRecord.key as Record<string, unknown>;
const keyFullName = createdKey.keyName as string;

// Now get that key by its name
const getResult = await runCommand(
["auth", "keys", "get", keyFullName, "--app", testAppId, "--json"],
{
env: { ABLY_ACCESS_TOKEN: E2E_ACCESS_TOKEN || "" },
},
);

expect(getResult.exitCode).toBe(0);
const getRecord = parseNdjsonLines(getResult.stdout).find(
(r) => r.type === "result",
) as Record<string, unknown>;
expect(getRecord).toBeDefined();
expect(getRecord).toHaveProperty("success", true);
const fetchedKey = getRecord.key as Record<string, unknown>;
expect(fetchedKey).toHaveProperty("name", keyName);
expect(fetchedKey).toHaveProperty("keyName", keyFullName);
});

it("should update a key name", { timeout: 20000 }, async () => {
setupTestFailureHandler("should update a key name");

// First create a key to update
const originalName = `e2e-update-key-${Date.now()}`;
const createResult = await runCommand(
[
"auth",
"keys",
"create",
"--app",
testAppId,
"--name",
originalName,
"--json",
],
{
env: { ABLY_ACCESS_TOKEN: E2E_ACCESS_TOKEN || "" },
},
);

const createRecord = parseNdjsonLines(createResult.stdout).find(
(r) => r.type === "result",
) as Record<string, unknown>;
const createdKey = createRecord.key as Record<string, unknown>;
const keyFullName = createdKey.keyName as string;

// Update the key name
const updatedName = `updated-key-${Date.now()}`;
const updateResult = await runCommand(
[
"auth",
"keys",
"update",
keyFullName,
"--app",
testAppId,
"--name",
updatedName,
"--json",
],
{
env: { ABLY_ACCESS_TOKEN: E2E_ACCESS_TOKEN || "" },
},
);

expect(updateResult.exitCode).toBe(0);
const updateRecord = parseNdjsonLines(updateResult.stdout).find(
(r) => r.type === "result",
) as Record<string, unknown>;
expect(updateRecord).toBeDefined();
expect(updateRecord).toHaveProperty("success", true);
const updatedKey = updateRecord.key as Record<string, unknown>;
const nameChange = updatedKey.name as Record<string, unknown>;
expect(nameChange).toHaveProperty("before", originalName);
expect(nameChange).toHaveProperty("after", updatedName);
});
});
Loading
Loading