diff --git a/README.md b/README.md index 9c27fc82..eac9f70e 100644 --- a/README.md +++ b/README.md @@ -1020,7 +1020,7 @@ USAGE FLAGS -v, --verbose Output verbose logs - --app= The app ID (defaults to current app) + --app= The app ID or name (defaults to current app) --json Output in JSON format --pretty-json Output in colorized JSON format @@ -1081,7 +1081,7 @@ USAGE FLAGS -v, --verbose Output verbose logs - --app= The app ID (defaults to current app) + --app= The app ID or name (defaults to current app) --json Output in JSON format --pretty-json Output in colorized JSON format @@ -1113,7 +1113,7 @@ ARGUMENTS FLAGS -v, --verbose Output verbose logs - --app= The app ID (defaults to current app) + --app= The app ID or name (defaults to current app) --force Skip confirmation prompt --json Output in JSON format --pretty-json Output in colorized JSON format @@ -1181,7 +1181,7 @@ ARGUMENTS FLAGS -v, --verbose Output verbose logs - --app= The app ID (defaults to current app) + --app= The app ID or name (defaults to current app) --capabilities= New capabilities for the key (comma-separated list) --json Output in JSON format --name= New name for the key @@ -1421,8 +1421,8 @@ Delete an annotation from a channel message ``` USAGE - $ ably channels annotations delete CHANNEL SERIAL TYPE [-v] [--json | --pretty-json] [--client-id ] [--count ] - [-n ] + $ ably channels annotations delete CHANNEL SERIAL TYPE [-v] [--json | --pretty-json] [--client-id ] [-n + ] ARGUMENTS CHANNEL The channel name @@ -1434,7 +1434,6 @@ FLAGS -v, --verbose Output verbose logs --client-id= Overrides any default client ID when using API authentication. Use "none" to explicitly set no client ID. Not applicable when using token authentication. - --count= The annotation count (for multiple.v1 types) --json Output in JSON format --pretty-json Output in colorized JSON format @@ -1444,7 +1443,7 @@ DESCRIPTION EXAMPLES $ ably channels annotations delete my-channel "01234567890:0" "reactions:flag.v1" --name thumbsup - $ ably channels annotations delete my-channel "01234567890:0" "reactions:multiple.v1" --name thumbsup --count 2 + $ ably channels annotations delete my-channel "01234567890:0" "reactions:multiple.v1" --name thumbsup $ ably channels annotations delete my-channel "01234567890:0" "reactions:flag.v1" --json @@ -1468,7 +1467,7 @@ ARGUMENTS FLAGS -v, --verbose Output verbose logs --json Output in JSON format - --limit= [default: 50] Maximum number of results to return (default: 50) + --limit= [default: 100] Maximum number of results to return (default: 100) --pretty-json Output in colorized JSON format DESCRIPTION diff --git a/src/commands/auth/keys/create.ts b/src/commands/auth/keys/create.ts index 54a9f275..34577f61 100644 --- a/src/commands/auth/keys/create.ts +++ b/src/commands/auth/keys/create.ts @@ -43,15 +43,7 @@ export default class KeysCreateCommand extends ControlBaseCommand { async run(): Promise { const { flags } = await this.parse(KeysCreateCommand); - const appId = flags.app || this.configManager.getCurrentAppId(); - - if (!appId) { - this.fail( - 'No app specified. Please provide --app flag or switch to an app with "ably apps switch".', - flags, - "keyCreate", - ); - } + const appId = await this.requireAppId(flags); let capabilities; try { diff --git a/src/commands/auth/keys/current.ts b/src/commands/auth/keys/current.ts index 1999ca59..d1c05ff0 100644 --- a/src/commands/auth/keys/current.ts +++ b/src/commands/auth/keys/current.ts @@ -17,7 +17,7 @@ export default class KeysCurrentCommand extends ControlBaseCommand { static flags = { ...ControlBaseCommand.globalFlags, app: Flags.string({ - description: "The app ID (defaults to current app)", + description: "The app ID or name (defaults to current app)", env: "ABLY_APP_ID", }), }; @@ -30,16 +30,8 @@ export default class KeysCurrentCommand extends ControlBaseCommand { return this.handleWebCliMode(flags); } - // Get app ID from flag or current config - const appId = flags.app || this.configManager.getCurrentAppId(); - - if (!appId) { - this.fail( - 'No app specified. Please provide --app flag or switch to an app with "ably apps switch".', - flags, - "KeyCurrent", - ); - } + // Get app ID from flag or current config (resolves app names to IDs) + const appId = await this.requireAppId(flags); // Get the current key for this app const apiKey = this.configManager.getApiKey(appId); @@ -48,7 +40,7 @@ export default class KeysCurrentCommand extends ControlBaseCommand { this.fail( `No API key configured for app ${appId}. Use "ably auth keys switch" to select a key.`, flags, - "KeyCurrent", + "keyCurrent", ); } @@ -105,7 +97,7 @@ export default class KeysCurrentCommand extends ControlBaseCommand { this.fail( "ABLY_API_KEY environment variable is not set", flags, - "KeyCurrent", + "keyCurrent", ); } diff --git a/src/commands/auth/keys/get.ts b/src/commands/auth/keys/get.ts index fe76de6d..d0c969d4 100644 --- a/src/commands/auth/keys/get.ts +++ b/src/commands/auth/keys/get.ts @@ -32,12 +32,14 @@ export default class KeysGetCommand extends ControlBaseCommand { async run(): Promise { const { args, flags } = await this.parse(KeysGetCommand); - // Display authentication information - await this.showAuthInfoIfNeeded(flags); - - let appId = flags.app || this.configManager.getCurrentAppId(); + let appId: string | undefined; const keyIdentifier = args.keyNameOrValue; + // If flags.app is set, resolve it (could be a name or ID) + if (flags.app) { + appId = await this.resolveAppIdFromNameOrId(flags.app, flags); + } + // If keyNameOrValue is in APP_ID.KEY_ID format (one period, no colon), extract appId. // Only attempt this when no appId is already known (from --app flag or current app), // to avoid misinterpreting labels containing periods (e.g. "v1.0") as APP_ID.KEY_ID. @@ -51,13 +53,12 @@ export default class KeysGetCommand extends ControlBaseCommand { } if (!appId) { - this.fail( - 'No app specified. Please provide --app flag, include APP_ID in the key name, or switch to an app with "ably apps switch".', - flags, - "keyGet", - ); + appId = await this.requireAppId(flags); } + // Display authentication information (after app resolution so name→ID is correct) + await this.showAuthInfoIfNeeded(flags); + try { const controlApi = this.createControlApi(flags); const key = await controlApi.getKey(appId, keyIdentifier); diff --git a/src/commands/auth/keys/list.ts b/src/commands/auth/keys/list.ts index 5acf45ae..5cb3fcbc 100644 --- a/src/commands/auth/keys/list.ts +++ b/src/commands/auth/keys/list.ts @@ -17,7 +17,7 @@ export default class KeysListCommand extends ControlBaseCommand { static flags = { ...ControlBaseCommand.globalFlags, app: Flags.string({ - description: "The app ID (defaults to current app)", + description: "The app ID or name (defaults to current app)", env: "ABLY_APP_ID", }), }; @@ -25,19 +25,12 @@ export default class KeysListCommand extends ControlBaseCommand { async run(): Promise { const { flags } = await this.parse(KeysListCommand); - // Display authentication information - await this.showAuthInfoIfNeeded(flags); - - // Get app ID from flag or current config - const appId = flags.app || this.configManager.getCurrentAppId(); + // Get app ID from flag or current config (resolves app names to IDs) + // Must resolve before showAuthInfoIfNeeded so --app names display correctly + const appId = await this.requireAppId(flags); - if (!appId) { - this.fail( - 'No app specified. Please provide --app flag or switch to an app with "ably apps switch".', - flags, - "keyList", - ); - } + // Display authentication information (after app resolution so name→ID is correct) + await this.showAuthInfoIfNeeded(flags); try { const controlApi = this.createControlApi(flags); diff --git a/src/commands/auth/keys/revoke.ts b/src/commands/auth/keys/revoke.ts index fc477bba..0317f7de 100644 --- a/src/commands/auth/keys/revoke.ts +++ b/src/commands/auth/keys/revoke.ts @@ -26,7 +26,7 @@ export default class KeysRevokeCommand extends ControlBaseCommand { static flags = { ...ControlBaseCommand.globalFlags, app: Flags.string({ - description: "The app ID (defaults to current app)", + description: "The app ID or name (defaults to current app)", env: "ABLY_APP_ID", }), force: Flags.boolean({ @@ -38,20 +38,12 @@ export default class KeysRevokeCommand extends ControlBaseCommand { async run(): Promise { const { args, flags } = await this.parse(KeysRevokeCommand); - let appId = flags.app || this.configManager.getCurrentAppId(); let keyId = args.keyName; const parsed = parseKeyIdentifier(args.keyName); - if (parsed.appId) appId = parsed.appId; keyId = parsed.keyId; - if (!appId) { - this.fail( - 'No app specified. Please provide --app flag, include APP_ID in the key name, or switch to an app with "ably apps switch".', - flags, - "keyRevoke", - ); - } + const appId = parsed.appId ?? (await this.requireAppId(flags)); try { const controlApi = this.createControlApi(flags); diff --git a/src/commands/auth/keys/switch.ts b/src/commands/auth/keys/switch.ts index b5747883..3327de63 100644 --- a/src/commands/auth/keys/switch.ts +++ b/src/commands/auth/keys/switch.ts @@ -33,23 +33,16 @@ export default class KeysSwitchCommand extends ControlBaseCommand { async run(): Promise { const { args, flags } = await this.parse(KeysSwitchCommand); - // Get app ID from flag or current config - let appId = flags.app || this.configManager.getCurrentAppId(); let keyId: string | undefined = args.keyNameOrValue; + let extractedAppId: string | undefined; if (args.keyNameOrValue) { const parsed = parseKeyIdentifier(args.keyNameOrValue); - if (parsed.appId) appId = parsed.appId; + if (parsed.appId) extractedAppId = parsed.appId; keyId = parsed.keyId; } - if (!appId) { - this.fail( - 'No app specified. Please provide --app flag, include APP_ID in the key name, or switch to an app with "ably apps switch".', - flags, - "keySwitch", - ); - } + const appId = extractedAppId ?? (await this.requireAppId(flags)); try { const controlApi = this.createControlApi(flags); diff --git a/src/commands/auth/keys/update.ts b/src/commands/auth/keys/update.ts index 62c3c687..adfba580 100644 --- a/src/commands/auth/keys/update.ts +++ b/src/commands/auth/keys/update.ts @@ -24,7 +24,7 @@ export default class KeysUpdateCommand extends ControlBaseCommand { static flags = { ...ControlBaseCommand.globalFlags, app: Flags.string({ - description: "The app ID (defaults to current app)", + description: "The app ID or name (defaults to current app)", env: "ABLY_APP_ID", }), capabilities: Flags.string({ @@ -40,22 +40,7 @@ export default class KeysUpdateCommand extends ControlBaseCommand { async run(): Promise { const { args, flags } = await this.parse(KeysUpdateCommand); - let appId = flags.app || this.configManager.getCurrentAppId(); - let keyId = args.keyName; - - const parsed = parseKeyIdentifier(args.keyName); - if (parsed.appId) appId = parsed.appId; - keyId = parsed.keyId; - - if (!appId) { - this.fail( - 'No app specified. Please provide --app flag, include APP_ID in the key name, or switch to an app with "ably apps switch".', - flags, - "keyUpdate", - ); - } - - // Check if any update flags were provided + // Check if any update flags were provided before doing any API calls if (!flags.name && !flags.capabilities) { this.fail( "No updates specified. Please provide at least one property to update (--name or --capabilities).", @@ -64,6 +49,13 @@ export default class KeysUpdateCommand extends ControlBaseCommand { ); } + let keyId = args.keyName; + + const parsed = parseKeyIdentifier(args.keyName); + keyId = parsed.keyId; + + const appId = parsed.appId ?? (await this.requireAppId(flags)); + try { const controlApi = this.createControlApi(flags); // Get original key details diff --git a/test/helpers/control-api-test-helpers.ts b/test/helpers/control-api-test-helpers.ts index f906d8de..bcaef713 100644 --- a/test/helpers/control-api-test-helpers.ts +++ b/test/helpers/control-api-test-helpers.ts @@ -25,6 +25,24 @@ export function getControlApiContext() { }; } +/** + * Mock the app resolution flow used by `requireAppId` / `resolveAppIdFromNameOrId`. + * Call this **before** other nock mocks in tests that pass `--app`. + * Mocks `GET /v1/me` and `GET /v1/accounts/{accountId}/apps`. + */ +export function mockAppResolution(appId: string): void { + const { accountId } = getControlApiContext(); + nockControl() + .get("/v1/me") + .reply(200, { + account: { id: accountId, name: "Test Account" }, + user: { email: "test@example.com" }, + }); + nockControl() + .get(`/v1/accounts/${accountId}/apps`) + .reply(200, [{ id: appId, name: "Test App", accountId }]); +} + /** Clean up all nock interceptors. Call in afterEach. */ export function controlApiCleanup(): void { nock.cleanAll(); diff --git a/test/unit/commands/auth/keys/create.test.ts b/test/unit/commands/auth/keys/create.test.ts index 109647aa..e0cd071d 100644 --- a/test/unit/commands/auth/keys/create.test.ts +++ b/test/unit/commands/auth/keys/create.test.ts @@ -5,6 +5,8 @@ import { nockControl, controlApiCleanup, CONTROL_HOST, + mockAppResolution, + getControlApiContext, } from "../../../../helpers/control-api-test-helpers.js"; import { getMockConfigManager } from "../../../../helpers/mock-config-manager.js"; import { @@ -32,6 +34,7 @@ describe("auth:keys:create command", () => { describe("functionality", () => { it("should create a key successfully", async () => { const appId = getMockConfigManager().getRegisteredAppId(); + mockAppResolution(appId); // Mock the key creation endpoint nockControl() .post(`/v1/apps/${appId}/keys`, { @@ -62,6 +65,7 @@ describe("auth:keys:create command", () => { it("should create a key with custom capabilities", async () => { const appId = getMockConfigManager().getRegisteredAppId(); + mockAppResolution(appId); // Mock the key creation endpoint with custom capabilities nockControl() .post(`/v1/apps/${appId}/keys`, { @@ -107,6 +111,7 @@ describe("auth:keys:create command", () => { it("should output JSON format when --json flag is used", async () => { const appId = getMockConfigManager().getRegisteredAppId(); + mockAppResolution(appId); const mockKey = { id: mockKeyId, appId, @@ -146,6 +151,7 @@ describe("auth:keys:create command", () => { it("should use ABLY_ACCESS_TOKEN environment variable when provided", async () => { const appId = getMockConfigManager().getRegisteredAppId(); + mockAppResolution(appId); const customToken = "custom_access_token"; process.env.ABLY_ACCESS_TOKEN = customToken; @@ -185,6 +191,7 @@ describe("auth:keys:create command", () => { describe("argument validation", () => { it("should require name parameter", async () => { const appId = getMockConfigManager().getRegisteredAppId(); + mockAppResolution(appId); const { error } = await runCommand( ["auth:keys:create", "--app", appId], import.meta.url, @@ -195,17 +202,28 @@ describe("auth:keys:create command", () => { }); it("should require app parameter when no current app is set", async () => { + const { accountId } = getControlApiContext(); + // Mock the app resolution flow (requireAppId → promptForApp → listApps) + nockControl() + .get("/v1/me") + .reply(200, { + account: { id: accountId, name: "Test Account" }, + user: { email: "test@example.com" }, + }); + nockControl().get(`/v1/accounts/${accountId}/apps`).reply(200, []); + const { error } = await runCommand( ["auth:keys:create", "--name", `"${mockKeyName}"`], import.meta.url, ); expect(error).toBeDefined(); - expect(error?.message).toMatch(/No app specified/); + expect(error?.message).toMatch(/No apps found/); expect(error?.oclif?.exit).toBeGreaterThan(0); }); it("should handle invalid capabilities JSON", async () => { const appId = getMockConfigManager().getRegisteredAppId(); + mockAppResolution(appId); // Mock the key creation endpoint with invalid capabilities nockControl().post(`/v1/apps/${appId}/keys`).reply(400, { error: "Invalid capabilities format", @@ -232,6 +250,7 @@ describe("auth:keys:create command", () => { describe("error handling", () => { it("should handle 401 authentication error", async () => { const appId = getMockConfigManager().getRegisteredAppId(); + mockAppResolution(appId); // Mock authentication failure nockControl() .post(`/v1/apps/${appId}/keys`) @@ -248,6 +267,7 @@ describe("auth:keys:create command", () => { it("should handle 403 forbidden error", async () => { const appId = getMockConfigManager().getRegisteredAppId(); + mockAppResolution(appId); // Mock forbidden response nockControl() .post(`/v1/apps/${appId}/keys`) @@ -264,6 +284,7 @@ describe("auth:keys:create command", () => { it("should handle 404 not found error", async () => { const appId = getMockConfigManager().getRegisteredAppId(); + mockAppResolution(appId); // Mock not found response (app doesn't exist) nockControl() .post(`/v1/apps/${appId}/keys`) @@ -280,6 +301,7 @@ describe("auth:keys:create command", () => { it("should handle 500 server error", async () => { const appId = getMockConfigManager().getRegisteredAppId(); + mockAppResolution(appId); // Mock server error nockControl() .post(`/v1/apps/${appId}/keys`) @@ -296,6 +318,7 @@ describe("auth:keys:create command", () => { it("should handle network errors", async () => { const appId = getMockConfigManager().getRegisteredAppId(); + mockAppResolution(appId); // Mock network error nockControl() .post(`/v1/apps/${appId}/keys`) @@ -312,6 +335,7 @@ describe("auth:keys:create command", () => { it("should handle validation errors from API", async () => { const appId = getMockConfigManager().getRegisteredAppId(); + mockAppResolution(appId); // Mock validation error nockControl().post(`/v1/apps/${appId}/keys`).reply(400, { error: "Validation failed", @@ -329,6 +353,7 @@ describe("auth:keys:create command", () => { it("should handle rate limit errors", async () => { const appId = getMockConfigManager().getRegisteredAppId(); + mockAppResolution(appId); // Mock rate limit error nockControl() .post(`/v1/apps/${appId}/keys`) @@ -347,6 +372,7 @@ describe("auth:keys:create command", () => { describe("capability configurations", () => { it("should create a publish-only key", async () => { const appId = getMockConfigManager().getRegisteredAppId(); + mockAppResolution(appId); // Mock the key creation endpoint with publish-only capabilities nockControl() .post(`/v1/apps/${appId}/keys`, { @@ -384,6 +410,7 @@ describe("auth:keys:create command", () => { it("should create a key with mixed capabilities", async () => { const appId = getMockConfigManager().getRegisteredAppId(); + mockAppResolution(appId); // Mock the key creation endpoint with subscribe-only capabilities nockControl() .post(`/v1/apps/${appId}/keys`, { diff --git a/test/unit/commands/auth/keys/current.test.ts b/test/unit/commands/auth/keys/current.test.ts index ff81eb0d..e4dd831e 100644 --- a/test/unit/commands/auth/keys/current.test.ts +++ b/test/unit/commands/auth/keys/current.test.ts @@ -1,6 +1,10 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect, afterEach } from "vitest"; import { runCommand } from "@oclif/test"; import { getMockConfigManager } from "../../../../helpers/mock-config-manager.js"; +import { + mockAppResolution, + controlApiCleanup, +} from "../../../../helpers/control-api-test-helpers.js"; import { standardHelpTests, standardArgValidationTests, @@ -8,6 +12,10 @@ import { } from "../../../../helpers/standard-tests.js"; describe("auth:keys:current command", () => { + afterEach(() => { + controlApiCleanup(); + }); + describe("functionality", () => { it("should display the current API key", async () => { const mockConfig = getMockConfigManager(); @@ -69,6 +77,7 @@ describe("auth:keys:current command", () => { it("should accept --app flag to specify a different app", async () => { const mockConfig = getMockConfigManager(); const appId = mockConfig.getCurrentAppId()!; + mockAppResolution(appId); const keyId = mockConfig.getKeyId()!; const { stdout } = await runCommand( ["auth:keys:current", "--app", appId], diff --git a/test/unit/commands/auth/keys/get.test.ts b/test/unit/commands/auth/keys/get.test.ts index acb0f98d..5be984d4 100644 --- a/test/unit/commands/auth/keys/get.test.ts +++ b/test/unit/commands/auth/keys/get.test.ts @@ -3,6 +3,7 @@ import { runCommand } from "@oclif/test"; import { nockControl, controlApiCleanup, + mockAppResolution, } from "../../../../helpers/control-api-test-helpers.js"; import { getMockConfigManager } from "../../../../helpers/mock-config-manager.js"; import { @@ -43,6 +44,7 @@ describe("auth:keys:get command", () => { it("should get key details with --app flag", async () => { const appId = getMockConfigManager().getCurrentAppId()!; + mockAppResolution(appId); mockKeysList(appId, [buildMockKey(appId, mockKeyId)]); const { stdout } = await runCommand( @@ -56,6 +58,7 @@ describe("auth:keys:get command", () => { it("should get key details by label name", async () => { const appId = getMockConfigManager().getCurrentAppId()!; + mockAppResolution(appId); mockKeysList(appId, [ buildMockKey(appId, mockKeyId, { name: "Root" }), buildMockKey(appId, "otherkey", { name: "Secondary" }), @@ -72,6 +75,7 @@ describe("auth:keys:get command", () => { it("should get key details by label containing a period (e.g. v1.0)", async () => { const appId = getMockConfigManager().getCurrentAppId()!; + mockAppResolution(appId); mockKeysList(appId, [ buildMockKey(appId, mockKeyId, { name: "v1.0" }), buildMockKey(appId, "otherkey", { name: "Secondary" }), @@ -88,6 +92,7 @@ describe("auth:keys:get command", () => { it("should get key details by key ID only", async () => { const appId = getMockConfigManager().getCurrentAppId()!; + mockAppResolution(appId); mockKeysList(appId, [ buildMockKey(appId, mockKeyId), buildMockKey(appId, "otherkey", { name: "Secondary" }), diff --git a/test/unit/commands/auth/keys/list.test.ts b/test/unit/commands/auth/keys/list.test.ts index 07f3c88d..ba7c8544 100644 --- a/test/unit/commands/auth/keys/list.test.ts +++ b/test/unit/commands/auth/keys/list.test.ts @@ -3,6 +3,8 @@ import { runCommand } from "@oclif/test"; import { nockControl, controlApiCleanup, + mockAppResolution, + getControlApiContext, } from "../../../../helpers/control-api-test-helpers.js"; import { getMockConfigManager } from "../../../../helpers/mock-config-manager.js"; import { @@ -57,6 +59,7 @@ describe("auth:keys:list command", () => { it("should list keys with --app flag", async () => { const appId = getMockConfigManager().getCurrentAppId()!; + mockAppResolution(appId); nockControl() .get(`/v1/apps/${appId}/keys`) .reply(200, [ @@ -80,6 +83,44 @@ describe("auth:keys:list command", () => { expect(stdout).toContain("Key Label: Test Key"); }); + it("should resolve app name to ID when --app is a name", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; + const { accountId } = getControlApiContext(); + const meReply = { + account: { id: accountId, name: "Test Account" }, + user: { email: "test@example.com" }, + }; + const appsReply = [{ id: appId, name: "MyApp", accountId }]; + + // displayAuthInfo calls getApp → listApps (consumes /me + /apps) + nockControl().get("/v1/me").reply(200, meReply); + nockControl().get(`/v1/accounts/${accountId}/apps`).reply(200, appsReply); + // requireAppId → resolveAppIdFromNameOrId also calls listApps + nockControl().get("/v1/me").reply(200, meReply); + nockControl().get(`/v1/accounts/${accountId}/apps`).reply(200, appsReply); + nockControl() + .get(`/v1/apps/${appId}/keys`) + .reply(200, [ + { + id: "key1", + appId, + name: "Key One", + key: `${appId}.key1:secret1`, + capability: { "*": ["publish", "subscribe"] }, + created: Date.now(), + modified: Date.now(), + }, + ]); + + const { stdout } = await runCommand( + ["auth:keys:list", "--app", "MyApp"], + import.meta.url, + ); + + expect(stdout).toContain(`Key Name: ${appId}.key1`); + expect(stdout).toContain("Key Label: Key One"); + }); + it("should show message when no keys found", async () => { const appId = getMockConfigManager().getCurrentAppId()!; nockControl().get(`/v1/apps/${appId}/keys`).reply(200, []); @@ -129,12 +170,22 @@ describe("auth:keys:list command", () => { describe("error handling", () => { it("should error when no app is selected", async () => { const mock = getMockConfigManager(); + const { accountId } = getControlApiContext(); mock.setCurrentAppIdForAccount(undefined); + // Mock the app resolution flow (requireAppId → promptForApp → listApps) + nockControl() + .get("/v1/me") + .reply(200, { + account: { id: accountId, name: "Test Account" }, + user: { email: "test@example.com" }, + }); + nockControl().get(`/v1/accounts/${accountId}/apps`).reply(200, []); + const { error } = await runCommand(["auth:keys:list"], import.meta.url); expect(error).toBeDefined(); - expect(error?.message).toMatch(/No app specified/); + expect(error?.message).toMatch(/No apps found/); }); standardControlApiErrorTests({ diff --git a/test/unit/commands/auth/keys/revoke.test.ts b/test/unit/commands/auth/keys/revoke.test.ts index d41553fb..b62dcec1 100644 --- a/test/unit/commands/auth/keys/revoke.test.ts +++ b/test/unit/commands/auth/keys/revoke.test.ts @@ -3,6 +3,7 @@ import { runCommand } from "@oclif/test"; import { nockControl, controlApiCleanup, + mockAppResolution, } from "../../../../helpers/control-api-test-helpers.js"; import { getMockConfigManager } from "../../../../helpers/mock-config-manager.js"; import { @@ -49,6 +50,7 @@ describe("auth:keys:revoke command", () => { it("should revoke key with --app flag", async () => { const appId = getMockConfigManager().getCurrentAppId()!; + mockAppResolution(appId); mockKeysList(appId, [ buildMockKey(appId, mockKeyId, { capability: { "*": ["publish"] }, diff --git a/test/unit/commands/auth/keys/switch.test.ts b/test/unit/commands/auth/keys/switch.test.ts index 84b395df..5bcd45f7 100644 --- a/test/unit/commands/auth/keys/switch.test.ts +++ b/test/unit/commands/auth/keys/switch.test.ts @@ -3,6 +3,7 @@ import { runCommand } from "@oclif/test"; import { nockControl, controlApiCleanup, + getControlApiContext, } from "../../../../helpers/control-api-test-helpers.js"; import { getMockConfigManager } from "../../../../helpers/mock-config-manager.js"; import { @@ -107,15 +108,25 @@ describe("auth:keys:switch command", () => { it("should handle no app specified when config has no current app", async () => { const mockConfig = getMockConfigManager(); + const { accountId } = getControlApiContext(); mockConfig.setCurrentAppIdForAccount(undefined); + // Mock the app resolution flow (requireAppId → promptForApp → listApps) + nockControl() + .get("/v1/me") + .reply(200, { + account: { id: accountId, name: "Test Account" }, + user: { email: "test@example.com" }, + }); + nockControl().get(`/v1/accounts/${accountId}/apps`).reply(200, []); + const { error } = await runCommand( ["auth:keys:switch", "just-a-key-id"], import.meta.url, ); expect(error).toBeDefined(); - expect(error?.message).toMatch(/No app specified/i); + expect(error?.message).toMatch(/No apps found/i); }); it("should handle 401 authentication error", async () => { diff --git a/test/unit/commands/auth/keys/update.test.ts b/test/unit/commands/auth/keys/update.test.ts index ae4afd3c..7f15ca58 100644 --- a/test/unit/commands/auth/keys/update.test.ts +++ b/test/unit/commands/auth/keys/update.test.ts @@ -3,6 +3,7 @@ import { runCommand } from "@oclif/test"; import { nockControl, controlApiCleanup, + mockAppResolution, } from "../../../../helpers/control-api-test-helpers.js"; import { getMockConfigManager } from "../../../../helpers/mock-config-manager.js"; import { @@ -88,6 +89,7 @@ describe("auth:keys:update command", () => { it("should update key with --app flag", async () => { const appId = getMockConfigManager().getCurrentAppId()!; + mockAppResolution(appId); mockKeysList(appId, [ buildMockKey(appId, mockKeyId, { name: "OldName",