diff --git a/package.json b/package.json index 7d519002..77c09a7c 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,7 @@ "ora": "^8.2.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "toml": "^3.0.0", + "smol-toml": "^1.5.2", "ws": "^8.16.0", "zod": "^3.24.2" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7305b7a8..e1d5e128 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -79,9 +79,9 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) - toml: - specifier: ^3.0.0 - version: 3.0.0 + smol-toml: + specifier: ^1.5.2 + version: 1.5.2 ws: specifier: ^8.16.0 version: 8.18.1 @@ -599,10 +599,6 @@ packages: resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.27.1': - resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} - engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.28.5': resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} @@ -5401,6 +5397,10 @@ packages: slashes@3.0.12: resolution: {integrity: sha512-Q9VME8WyGkc7pJf6QEkj3wE+2CnvZMI+XJhwdTPR8Z/kWQRXi7boAWLDibRPyHRTUTPx5FaU7MsyrjI3yLB4HA==} + smol-toml@1.5.2: + resolution: {integrity: sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ==} + engines: {node: '>= 18'} + snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} @@ -5624,9 +5624,6 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} - toml@3.0.0: - resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} - totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -6641,7 +6638,7 @@ snapshots: '@babel/code-frame@7.27.1': dependencies: - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 js-tokens: 4.0.0 picocolors: 1.1.1 @@ -6707,8 +6704,6 @@ snapshots: '@babel/helper-validator-identifier@7.25.9': {} - '@babel/helper-validator-identifier@7.27.1': {} - '@babel/helper-validator-identifier@7.28.5': {} '@babel/helper-validator-option@7.25.9': {} @@ -11958,6 +11953,8 @@ snapshots: slashes@3.0.12: {} + smol-toml@1.5.2: {} + snake-case@3.0.4: dependencies: dot-case: 3.0.4 @@ -12196,8 +12193,6 @@ snapshots: toidentifier@1.0.1: {} - toml@3.0.0: {} - totalist@3.0.1: {} tough-cookie@5.1.2: diff --git a/src/base-command.ts b/src/base-command.ts index 8e8c703f..8f18aa56 100644 --- a/src/base-command.ts +++ b/src/base-command.ts @@ -5,7 +5,10 @@ import chalk from "chalk"; import colorJson from "color-json"; import { randomUUID } from "node:crypto"; -import { ConfigManager } from "./services/config-manager.js"; +import { + ConfigManager, + createConfigManager, +} from "./services/config-manager.js"; import { ControlApi } from "./services/control-api.js"; import { InteractiveHelper } from "./services/interactive-helper.js"; import { BaseFlags, CommandConfig, ErrorDetails } from "./types/cli.js"; @@ -170,7 +173,7 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand { constructor(argv: string[], config: CommandConfig) { super(argv, config); - this.configManager = new ConfigManager(); + this.configManager = createConfigManager(); this.interactiveHelper = new InteractiveHelper(this.configManager); // Check if we're running in web CLI mode this.isWebCliMode = isWebCliMode(); diff --git a/src/commands/config/show.ts b/src/commands/config/show.ts index d2a6a896..5b70b6e5 100644 --- a/src/commands/config/show.ts +++ b/src/commands/config/show.ts @@ -1,5 +1,5 @@ import * as fs from "node:fs"; -import * as toml from "toml"; +import { parse } from "smol-toml"; import { AblyBaseCommand } from "../../base-command.js"; @@ -43,7 +43,7 @@ export default class ConfigShow extends AblyBaseCommand { if (this.shouldOutputJson(flags)) { // Parse the TOML and output as JSON try { - const config = toml.parse(contents); + const config = parse(contents); this.log( this.formatJsonOutput( { exists: true, path: configPath, config }, diff --git a/src/commands/mcp/start-server.ts b/src/commands/mcp/start-server.ts index e1744793..2cfb018b 100644 --- a/src/commands/mcp/start-server.ts +++ b/src/commands/mcp/start-server.ts @@ -1,6 +1,6 @@ import { AblyBaseCommand } from "../../base-command.js"; import { AblyMcpServer } from "../../mcp/index.js"; -import { ConfigManager } from "../../services/config-manager.js"; +import { createConfigManager } from "../../services/config-manager.js"; export default class StartMcpServer extends AblyBaseCommand { static description = @@ -20,7 +20,7 @@ export default class StartMcpServer extends AblyBaseCommand { const { flags } = await this.parse(StartMcpServer); // Initialize Config Manager - const configManager = new ConfigManager(); + const configManager = createConfigManager(); try { // Start the server, write to stderr only diff --git a/src/help.ts b/src/help.ts index 2efaaffd..c3e70d40 100644 --- a/src/help.ts +++ b/src/help.ts @@ -2,7 +2,10 @@ import { Command, Help, Config, Interfaces } from "@oclif/core"; import chalk from "chalk"; import stripAnsi from "strip-ansi"; -import { ConfigManager } from "./services/config-manager.js"; +import { + ConfigManager, + createConfigManager, +} from "./services/config-manager.js"; import { displayLogo } from "./utils/logo.js"; import { formatReleaseStatus } from "./utils/version.js"; @@ -27,7 +30,7 @@ export default class CustomHelp extends Help { this.webCliMode = isWebCliMode(); this.interactiveMode = process.env.ABLY_INTERACTIVE_MODE === "true"; this.anonymousMode = process.env.ABLY_ANONYMOUS_USER_MODE === "true"; - this.configManager = new ConfigManager(); + this.configManager = createConfigManager(); } // Override formatHelpOutput to apply stripAnsi when necessary diff --git a/src/services/config-manager.ts b/src/services/config-manager.ts index 37b82a41..c3bce075 100644 --- a/src/services/config-manager.ts +++ b/src/services/config-manager.ts @@ -1,7 +1,8 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import toml from "toml"; +import { parse, stringify } from "smol-toml"; +import isTestMode from "../utils/test-mode.js"; // Updated to include key and app metadata export interface AppConfig { @@ -38,7 +39,89 @@ export interface AblyConfig { }; } -export class ConfigManager { +export interface ConfigManager { + // Account management + getAccessToken(alias?: string): string | undefined; + getCurrentAccount(): AccountConfig | undefined; + getCurrentAccountAlias(): string | undefined; + listAccounts(): { account: AccountConfig; alias: string }[]; + storeAccount( + accessToken: string, + alias?: string, + accountInfo?: { + accountId?: string; + accountName?: string; + tokenId?: string; + userEmail?: string; + }, + ): void; + switchAccount(alias: string): boolean; + removeAccount(alias: string): boolean; + + // App management + getApiKey(appId?: string): string | undefined; + getAppName(appId: string): string | undefined; + getAppConfig(appId: string): AppConfig | undefined; + getCurrentAppId(): string | undefined; + getKeyId(appId?: string): string | undefined; + getKeyName(appId?: string): string | undefined; + setCurrentApp(appId: string): void; + storeAppInfo( + appId: string, + appInfo: { appName: string }, + accountAlias?: string, + ): void; + storeAppKey( + appId: string, + apiKey: string, + metadata?: { appName?: string; keyId?: string; keyName?: string }, + accountAlias?: string, + ): void; + removeApiKey(appId: string): boolean; + + // Help context (AI conversation) + getHelpContext(): + | { + conversation: { + messages: { + content: string; + role: "assistant" | "user"; + }[]; + }; + } + | undefined; + storeHelpContext(question: string, answer: string): void; + clearHelpContext(): void; + + // Config file + getConfigPath(): string; + saveConfig(): void; +} + +// Type declaration for test mocks available on globalThis +declare global { + var __TEST_MOCKS__: + | { configManager?: ConfigManager; [key: string]: unknown } + | undefined; +} + +/** + * Factory function to create a ConfigManager instance. + * In test mode (when ABLY_CLI_TEST_MODE is set and mock is available), + * returns the MockConfigManager from globals. + * Otherwise returns a new TomlConfigManager. + */ +export function createConfigManager(): ConfigManager { + // Check if we're in test mode and have a mock available + if (isTestMode() && globalThis.__TEST_MOCKS__?.configManager) { + return globalThis.__TEST_MOCKS__.configManager; + } + + // Default to TomlConfigManager for production use + return new TomlConfigManager(); +} + +export class TomlConfigManager implements ConfigManager { private config: AblyConfig = { accounts: {}, }; @@ -225,8 +308,8 @@ export class ConfigManager { public saveConfig(): void { try { - // Format the config as TOML - const tomlContent = this.formatToToml(this.config); + // Format the config as TOML using smol-toml stringify + const tomlContent = stringify(this.config); // Write the config to disk fs.writeFileSync(this.configPath, tomlContent, { mode: 0o600 }); // Secure file permissions @@ -382,100 +465,11 @@ export class ConfigManager { } } - // Updated formatToToml method to include app and key metadata - private formatToToml(config: AblyConfig): string { - let result = ""; - - // Write current section - if (config.current) { - result += "[current]\n"; - if (config.current.account) { - result += `account = "${config.current.account}"\n`; - } - - result += "\n"; - } - - // Write help context if it exists - if (config.helpContext) { - result += "[helpContext]\n"; - - // Format the conversation as TOML array of tables - if (config.helpContext.conversation.messages.length > 0) { - result += "\n[[helpContext.conversation.messages]]\n"; - const { messages } = config.helpContext.conversation; - - for (const [i, message] of messages.entries()) { - if (i > 0) result += "\n[[helpContext.conversation.messages]]\n"; - result += `role = "${message.role}"\n`; - result += `content = """${message.content}"""\n`; - } - - result += "\n"; - } - } - - // Write accounts section - for (const [alias, account] of Object.entries(config.accounts)) { - result += `[accounts.${alias}]\n`; - result += `accessToken = "${account.accessToken}"\n`; - - if (account.tokenId) { - result += `tokenId = "${account.tokenId}"\n`; - } - - if (account.userEmail) { - result += `userEmail = "${account.userEmail}"\n`; - } - - if (account.accountId) { - result += `accountId = "${account.accountId}"\n`; - } - - if (account.accountName) { - result += `accountName = "${account.accountName}"\n`; - } - - if (account.currentAppId) { - result += `currentAppId = "${account.currentAppId}"\n`; - } - - // Write apps section for this account - if (account.apps && Object.keys(account.apps).length > 0) { - for (const [appId, appConfig] of Object.entries(account.apps)) { - result += `[accounts.${alias}.apps.${appId}]\n`; - - if (appConfig.apiKey) { - result += `apiKey = "${appConfig.apiKey}"\n`; - } - - if (appConfig.keyId) { - result += `keyId = "${appConfig.keyId}"\n`; - } - - if (appConfig.keyName) { - result += `keyName = "${appConfig.keyName}"\n`; - } - - if (appConfig.appName) { - result += `appName = "${appConfig.appName}"\n`; - } - - result += "\n"; - } - } else { - result += "\n"; - } - } - - return result; - } - private loadConfig(): void { if (fs.existsSync(this.configPath)) { try { const configContent = fs.readFileSync(this.configPath, "utf8"); - this.config = toml.parse(configContent) as AblyConfig; + this.config = parse(configContent) as unknown as AblyConfig; // Ensure config has the expected structure if (!this.config.accounts) { diff --git a/test/e2e/auth/basic-auth.test.ts b/test/e2e/auth/basic-auth.test.ts index c9adc49b..e6f66444 100644 --- a/test/e2e/auth/basic-auth.test.ts +++ b/test/e2e/auth/basic-auth.test.ts @@ -68,10 +68,10 @@ describe("Authentication E2E", () => { expect(initialFiles).toHaveLength(0); // Create a config file by instantiating ConfigManager - const { ConfigManager } = await import( + const { TomlConfigManager } = await import( "../../../src/services/config-manager.js" ); - const configManager = new ConfigManager(); + const configManager = new TomlConfigManager(); // Store test account configManager.storeAccount("test-token", "e2e-test", { @@ -140,10 +140,10 @@ describe("Authentication E2E", () => { it("should create valid TOML config", async () => { setupTestFailureHandler("should create valid TOML config"); - const { ConfigManager } = await import( + const { TomlConfigManager } = await import( "../../../src/services/config-manager.js" ); - const configManager = new ConfigManager(); + const configManager = new TomlConfigManager(); // Store complex configuration configManager.storeAccount("access-token-123", "complex-account", { @@ -184,10 +184,10 @@ describe("Authentication E2E", () => { "should handle special characters in account data", ); - const { ConfigManager } = await import( + const { TomlConfigManager } = await import( "../../../src/services/config-manager.js" ); - const configManager = new ConfigManager(); + const configManager = new TomlConfigManager(); // Store account with special characters configManager.storeAccount("token", "special-chars", { @@ -218,10 +218,10 @@ describe("Authentication E2E", () => { expect(configPath).toContain(configPathSeparator); // Should be able to create and access files - const { ConfigManager } = await import( + const { TomlConfigManager } = await import( "../../../src/services/config-manager.js" ); - const configManager = new ConfigManager(); + const configManager = new TomlConfigManager(); configManager.storeAccount("token", "platform-test", { accountId: "platform_test", @@ -234,10 +234,10 @@ describe("Authentication E2E", () => { it("should handle different line endings", async () => { setupTestFailureHandler("should handle different line endings"); - const { ConfigManager } = await import( + const { TomlConfigManager } = await import( "../../../src/services/config-manager.js" ); - const configManager = new ConfigManager(); + const configManager = new TomlConfigManager(); configManager.storeAccount("token", "lineending-test", { accountId: "lineending_test", @@ -261,10 +261,10 @@ describe("Authentication E2E", () => { // Verify we're using the test config directory expect(process.env.ABLY_CLI_CONFIG_DIR).toBe(tempConfigDir); - const { ConfigManager } = await import( + const { TomlConfigManager } = await import( "../../../src/services/config-manager.js" ); - const configManager = new ConfigManager(); + const configManager = new TomlConfigManager(); // Store test data configManager.storeAccount("isolated-token", "isolated-account", { diff --git a/test/helpers/mock-config-manager.ts b/test/helpers/mock-config-manager.ts new file mode 100644 index 00000000..e0e4e0af --- /dev/null +++ b/test/helpers/mock-config-manager.ts @@ -0,0 +1,508 @@ +/** + * Mock ConfigManager for unit tests. + * + * Usage in tests: + * import { getMockConfigManager } from "../../helpers/mock-config-manager.js"; + * + * // Get values through ConfigManager interface methods + * const mockConfig = getMockConfigManager(); + * const appId = mockConfig.getCurrentAppId()!; + * const apiKey = mockConfig.getApiKey()!; + * const accountId = mockConfig.getCurrentAccount()!.accountId!; + * + * // Manipulate config for error scenarios + * mockConfig.setCurrentAccountAlias(undefined); // Test "no account" error + * mockConfig.clearAccounts(); // Test "no config" scenario + */ + +import type { + AblyConfig, + AccountConfig, + AppConfig, + ConfigManager, +} from "../../src/services/config-manager.js"; + +/** + * Type for test configuration values. + */ +export interface TestConfigValues { + accessToken: string; + accountId: string; + accountName: string; + userEmail: string; + appId: string; + appName: string; + apiKey: string; + keyId: string; + keyName: string; + accountAlias: string; +} + +/** + * Generate a random string of specified length for test isolation. + */ +function randomString(length: number): string { + return Math.random() + .toString(36) + .slice(2, 2 + length); +} + +/** + * Generate random test config values. + * Each call produces unique values to ensure test isolation. + */ +function generateTestConfig(): TestConfigValues { + const appId = randomString(6); + const keyId = `${appId}.key${randomString(6)}`; + const keySecret = `secret${randomString(12)}`; + + return { + accessToken: `token_${randomString(16)}`, + accountId: `acc_${randomString(12)}`, + accountName: `Test Account ${randomString(4)}`, + userEmail: `test_${randomString(6)}@example.com`, + appId, + appName: `Test App ${randomString(4)}`, + apiKey: `${keyId}:${keySecret}`, + keyId, + keyName: `Test Key ${randomString(4)}`, + accountAlias: "default", + }; +} + +/** + * In-memory mock implementation of ConfigManager for testing. + * This allows tests to run without filesystem operations. + */ +export class MockConfigManager implements ConfigManager { + private config: AblyConfig; + private testValues: TestConfigValues; + + constructor(initialConfig?: AblyConfig) { + this.testValues = generateTestConfig(); + this.config = initialConfig ?? this.createDefaultConfig(); + } + + /** + * Get the current test configuration values (internal use only). + * Tests should use ConfigManager interface methods instead. + */ + private getTestValues(): TestConfigValues { + return { ...this.testValues }; + } + + /** + * Get a registered app ID from the mock config. + * This returns an appId that exists in the config's apps list, + * even if currentAppId has been set to undefined. + * Useful for tests that need to set up nock mocks after modifying config state. + */ + public getRegisteredAppId(): string { + const currentAccount = this.getCurrentAccount(); + if (currentAccount?.apps) { + const appIds = Object.keys(currentAccount.apps); + if (appIds.length > 0) { + return appIds[0]; + } + } + // Fallback to testValues appId + return this.testValues.appId; + } + + /** + * Creates a default config that satisfies most test requirements. + */ + private createDefaultConfig(): AblyConfig { + const { + accessToken, + accountId, + accountName, + userEmail, + appId, + appName, + apiKey, + keyId, + keyName, + accountAlias, + } = this.testValues; + + return { + current: { + account: accountAlias, + }, + accounts: { + [accountAlias]: { + accessToken, + accountId, + accountName, + userEmail, + currentAppId: appId, + apps: { + [appId]: { + apiKey, + appName, + keyId, + keyName, + }, + }, + }, + }, + }; + } + + /** + * Reset the config to default values with new randomized test values. + * Useful for test cleanup or starting fresh. + */ + public reset(): void { + this.testValues = generateTestConfig(); + this.config = this.createDefaultConfig(); + } + + /** + * Set a completely custom config. + * Useful for tests that need specific configurations. + */ + public setConfig(config: AblyConfig): void { + this.config = config; + } + + /** + * Get the current raw config (for test assertions). + */ + public getConfig(): AblyConfig { + return this.config; + } + + /** + * Set the current account alias. + */ + public setCurrentAccountAlias(alias: string | undefined): void { + if (!this.config.current) { + this.config.current = {}; + } + this.config.current.account = alias; + } + + /** + * Set the current app ID for the current account. + */ + public setCurrentAppIdForAccount(appId: string | undefined): void { + const currentAccount = this.getCurrentAccount(); + const currentAlias = this.getCurrentAccountAlias(); + if (currentAccount && currentAlias) { + this.config.accounts[currentAlias].currentAppId = appId; + } + } + + /** + * Clear all accounts (useful for testing error scenarios). + */ + public clearAccounts(): void { + this.config.accounts = {}; + if (this.config.current) { + delete this.config.current.account; + } + } + + // ConfigManager interface implementation + + public clearHelpContext(): void { + delete this.config.helpContext; + } + + public getAccessToken(alias?: string): string | undefined { + if (alias) { + return this.config.accounts[alias]?.accessToken; + } + const currentAccount = this.getCurrentAccount(); + return currentAccount?.accessToken; + } + + public getApiKey(appId?: string): string | undefined { + const currentAccount = this.getCurrentAccount(); + if (!currentAccount || !currentAccount.apps) { + return process.env.ABLY_API_KEY; + } + + const targetAppId = appId || this.getCurrentAppId(); + if (!targetAppId) { + return process.env.ABLY_API_KEY; + } + + return currentAccount.apps[targetAppId]?.apiKey || process.env.ABLY_API_KEY; + } + + public getAppName(appId: string): string | undefined { + const currentAccount = this.getCurrentAccount(); + if (!currentAccount || !currentAccount.apps) return undefined; + return currentAccount.apps[appId]?.appName; + } + + public getAppConfig(appId: string): AppConfig | undefined { + const currentAccount = this.getCurrentAccount(); + if (!currentAccount || !currentAccount.apps) return undefined; + const cfg = currentAccount.apps[appId]; + return cfg ? { ...cfg } : undefined; + } + + public getConfigPath(): string { + return "/mock/config/path"; + } + + public getCurrentAccount(): AccountConfig | undefined { + const currentAlias = this.getCurrentAccountAlias(); + if (!currentAlias) return undefined; + return this.config.accounts[currentAlias]; + } + + public getCurrentAccountAlias(): string | undefined { + return this.config.current?.account; + } + + public getCurrentAppId(): string | undefined { + const currentAccount = this.getCurrentAccount(); + if (!currentAccount) return undefined; + return currentAccount.currentAppId; + } + + public getHelpContext(): + | { + conversation: { + messages: { + content: string; + role: "assistant" | "user"; + }[]; + }; + } + | undefined { + return this.config.helpContext; + } + + public getKeyId(appId?: string): string | undefined { + const currentAccount = this.getCurrentAccount(); + if (!currentAccount || !currentAccount.apps) return undefined; + + const targetAppId = appId || this.getCurrentAppId(); + if (!targetAppId) return undefined; + + const appConfig = currentAccount.apps[targetAppId]; + if (!appConfig) return undefined; + + if (appConfig.keyId) { + return appConfig.keyId; + } + + if (appConfig.apiKey) { + return appConfig.apiKey.split(":")[0]; + } + + return undefined; + } + + public getKeyName(appId?: string): string | undefined { + const currentAccount = this.getCurrentAccount(); + if (!currentAccount || !currentAccount.apps) return undefined; + + const targetAppId = appId || this.getCurrentAppId(); + if (!targetAppId) return undefined; + + return currentAccount.apps[targetAppId]?.keyName; + } + + public listAccounts(): { account: AccountConfig; alias: string }[] { + return Object.entries(this.config.accounts).map(([alias, account]) => ({ + account, + alias, + })); + } + + public removeAccount(alias: string): boolean { + if (!this.config.accounts[alias]) { + return false; + } + + delete this.config.accounts[alias]; + + if (this.config.current?.account === alias) { + delete this.config.current.account; + } + + return true; + } + + public removeApiKey(appId: string): boolean { + const currentAccount = this.getCurrentAccount(); + if (!currentAccount || !currentAccount.apps) return false; + + if (currentAccount.apps[appId]) { + delete currentAccount.apps[appId].apiKey; + return true; + } + + return false; + } + + public saveConfig(): void { + // No-op for in-memory implementation + } + + public setCurrentApp(appId: string): void { + const currentAccount = this.getCurrentAccount(); + const currentAlias = this.getCurrentAccountAlias(); + + if (!currentAccount || !currentAlias) { + throw new Error("No current account selected"); + } + + this.config.accounts[currentAlias].currentAppId = appId; + } + + public storeAccount( + accessToken: string, + alias: string = "default", + accountInfo?: { + accountId?: string; + accountName?: string; + tokenId?: string; + userEmail?: string; + }, + ): void { + this.config.accounts[alias] = { + accessToken, + ...accountInfo, + apps: this.config.accounts[alias]?.apps || {}, + currentAppId: this.config.accounts[alias]?.currentAppId, + }; + + if (!this.config.current || !this.config.current.account) { + this.config.current = { account: alias }; + } + } + + public storeAppInfo( + appId: string, + appInfo: { appName: string }, + accountAlias?: string, + ): void { + const alias = accountAlias || this.getCurrentAccountAlias() || "default"; + + if (!this.config.accounts[alias]) { + throw new Error(`Account "${alias}" not found`); + } + + if (!this.config.accounts[alias].apps) { + this.config.accounts[alias].apps = {}; + } + + this.config.accounts[alias].apps[appId] = { + ...this.config.accounts[alias].apps[appId], + ...appInfo, + }; + } + + public storeAppKey( + appId: string, + apiKey: string, + metadata?: { + appName?: string; + keyId?: string; + keyName?: string; + }, + accountAlias?: string, + ): void { + const alias = accountAlias || this.getCurrentAccountAlias() || "default"; + + if (!this.config.accounts[alias]) { + throw new Error(`Account "${alias}" not found`); + } + + if (!this.config.accounts[alias].apps) { + this.config.accounts[alias].apps = {}; + } + + this.config.accounts[alias].apps[appId] = { + ...this.config.accounts[alias].apps[appId], + apiKey, + appName: metadata?.appName, + keyId: metadata?.keyId || apiKey.split(":")[0], + keyName: metadata?.keyName, + }; + } + + public storeHelpContext(question: string, answer: string): void { + if (!this.config.helpContext) { + this.config.helpContext = { + conversation: { + messages: [], + }, + }; + } + + this.config.helpContext.conversation.messages.push( + { + content: question, + role: "user", + }, + { + content: answer, + role: "assistant", + }, + ); + } + + public switchAccount(alias: string): boolean { + if (!this.config.accounts[alias]) { + return false; + } + + if (!this.config.current) { + this.config.current = {}; + } + + this.config.current.account = alias; + return true; + } +} + +/** + * Get the MockConfigManager instance from globals. + * Throws if not in test mode or mock not initialized. + */ +export function getMockConfigManager(): MockConfigManager { + if (!globalThis.__TEST_MOCKS__?.configManager) { + throw new Error( + "MockConfigManager not initialized. Ensure you are running unit tests with the proper setup.", + ); + } + return globalThis.__TEST_MOCKS__.configManager as MockConfigManager; +} + +/** + * Reset the mock config manager to default values. + * Call this in beforeEach or when you need a fresh config. + */ +export function resetMockConfig(): void { + const mock = getMockConfigManager(); + mock.reset(); +} + +/** + * Initialize the mock config manager on globals. + * This is called by the unit test setup file. + */ +export function initializeMockConfigManager(): void { + if (!globalThis.__TEST_MOCKS__) { + globalThis.__TEST_MOCKS__ = { + ablyRestMock: {}, + }; + } + globalThis.__TEST_MOCKS__.configManager = new MockConfigManager(); +} + +/** + * Check if mock config manager is available. + */ +export function hasMockConfigManager(): boolean { + return !!globalThis.__TEST_MOCKS__?.configManager; +} diff --git a/test/setup.ts b/test/setup.ts index fa67251c..94b34f53 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -5,6 +5,9 @@ import { existsSync } from "node:fs"; import { exec } from "node:child_process"; import * as Ably from "ably"; +// Import types for test mocks +import type { MockConfigManager } from "./helpers/mock-config-manager.js"; + // Global type declarations for test mocks declare global { var __TEST_MOCKS__: @@ -17,6 +20,7 @@ declare global { ablySpacesMock?: any; // eslint-disable-next-line @typescript-eslint/no-explicit-any ablyRealtimeMock?: any; + configManager?: MockConfigManager; // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; } diff --git a/test/unit/base/base-command.test.ts b/test/unit/base/base-command.test.ts index d5d2b39e..c518ee19 100644 --- a/test/unit/base/base-command.test.ts +++ b/test/unit/base/base-command.test.ts @@ -9,7 +9,10 @@ import { } from "vitest"; import fs from "node:fs"; import { AblyBaseCommand } from "../../../src/base-command.js"; -import { ConfigManager } from "../../../src/services/config-manager.js"; +import { + ConfigManager, + TomlConfigManager, +} from "../../../src/services/config-manager.js"; import { InteractiveHelper } from "../../../src/services/interactive-helper.js"; import { BaseFlags } from "../../../src/types/cli.js"; import { Config } from "@oclif/core"; @@ -139,12 +142,13 @@ describe("AblyBaseCommand", function () { // Instead of stubbing loadConfig which is private, we'll stub methods that might access the file system vi.spyOn( - ConfigManager.prototype as any, + TomlConfigManager.prototype as any, "ensureConfigDirExists", ).mockImplementation(() => {}); - vi.spyOn(ConfigManager.prototype as any, "saveConfig").mockImplementation( - () => {}, - ); + vi.spyOn( + TomlConfigManager.prototype as any, + "saveConfig", + ).mockImplementation(() => {}); // Note: createStubInstance doesn't need sandbox explicitly. interactiveHelperStub = { diff --git a/test/unit/commands/accounts/login.test.ts b/test/unit/commands/accounts/login.test.ts index 064306fc..91701b20 100644 --- a/test/unit/commands/accounts/login.test.ts +++ b/test/unit/commands/accounts/login.test.ts @@ -1,54 +1,21 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { runCommand } from "@oclif/test"; import nock from "nock"; -import { resolve } from "node:path"; -import { - mkdirSync, - writeFileSync, - existsSync, - rmSync, - readFileSync, -} from "node:fs"; -import { tmpdir } from "node:os"; +import { getMockConfigManager } from "../../../helpers/mock-config-manager.js"; describe("accounts:login command", () => { const mockAccessToken = "test_access_token_12345"; const mockAccountId = "test-account-id"; - let testConfigDir: string; - let originalConfigDir: string; beforeEach(() => { nock.cleanAll(); - - // Create a temporary config directory for testing - testConfigDir = resolve(tmpdir(), `ably-cli-test-login-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - // Store original config dir and set test config dir - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - // Create a minimal config file - const configContent = `[current] -account = "default" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + // Clear accounts so login tests start fresh + const mock = getMockConfigManager(); + mock.clearAccounts(); }); afterEach(() => { nock.cleanAll(); - - // Restore original config directory - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - // Clean up test config directory - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } }); describe("help", () => { @@ -102,18 +69,15 @@ account = "default" expect(result.account).toHaveProperty("name", "Test Account"); expect(result.account.user).toHaveProperty("email", "test@example.com"); - // Verify config file was written correctly - const configContent = readFileSync( - resolve(testConfigDir, "config"), - "utf8", - ); - expect(configContent).toContain("[current]"); - expect(configContent).toContain('account = "default"'); - expect(configContent).toContain("[accounts.default]"); - expect(configContent).toContain(`accessToken = "${mockAccessToken}"`); - expect(configContent).toContain(`accountId = "${mockAccountId}"`); - expect(configContent).toContain('accountName = "Test Account"'); - expect(configContent).toContain('userEmail = "test@example.com"'); + // Verify config was updated correctly via mock + const mock = getMockConfigManager(); + const config = mock.getConfig(); + expect(config.current?.account).toBe("default"); + expect(config.accounts["default"]).toBeDefined(); + expect(config.accounts["default"].accessToken).toBe(mockAccessToken); + expect(config.accounts["default"].accountId).toBe(mockAccountId); + expect(config.accounts["default"].accountName).toBe("Test Account"); + expect(config.accounts["default"].userEmail).toBe("test@example.com"); }); it("should include alias in JSON response when --alias flag is provided", async () => { @@ -141,18 +105,15 @@ account = "default" expect(result).toHaveProperty("success", true); expect(result.account).toHaveProperty("alias", customAlias); - // Verify config file was written with custom alias - const configContent = readFileSync( - resolve(testConfigDir, "config"), - "utf8", - ); - expect(configContent).toContain("[current]"); - expect(configContent).toContain(`account = "${customAlias}"`); - expect(configContent).toContain(`[accounts.${customAlias}]`); - expect(configContent).toContain(`accessToken = "${mockAccessToken}"`); - expect(configContent).toContain(`accountId = "${mockAccountId}"`); - expect(configContent).toContain('accountName = "Test Account"'); - expect(configContent).toContain('userEmail = "test@example.com"'); + // Verify config was written with custom alias via mock + const mock = getMockConfigManager(); + const config = mock.getConfig(); + expect(config.current?.account).toBe(customAlias); + expect(config.accounts[customAlias]).toBeDefined(); + expect(config.accounts[customAlias].accessToken).toBe(mockAccessToken); + expect(config.accounts[customAlias].accountId).toBe(mockAccountId); + expect(config.accounts[customAlias].accountName).toBe("Test Account"); + expect(config.accounts[customAlias].userEmail).toBe("test@example.com"); }); it("should include app info when single app is auto-selected", async () => { @@ -187,19 +148,18 @@ account = "default" expect(result.app).toHaveProperty("name", mockAppName); expect(result.app).toHaveProperty("autoSelected", true); - // Verify config file was written with app info - const configContent = readFileSync( - resolve(testConfigDir, "config"), - "utf8", + // Verify config was written with app info via mock + const mock = getMockConfigManager(); + const config = mock.getConfig(); + expect(config.current?.account).toBe("default"); + expect(config.accounts["default"]).toBeDefined(); + expect(config.accounts["default"].accessToken).toBe(mockAccessToken); + expect(config.accounts["default"].accountId).toBe(mockAccountId); + expect(config.accounts["default"].currentAppId).toBe(mockAppId); + expect(config.accounts["default"].apps?.[mockAppId]).toBeDefined(); + expect(config.accounts["default"].apps?.[mockAppId]?.appName).toBe( + mockAppName, ); - expect(configContent).toContain("[current]"); - expect(configContent).toContain('account = "default"'); - expect(configContent).toContain("[accounts.default]"); - expect(configContent).toContain(`accessToken = "${mockAccessToken}"`); - expect(configContent).toContain(`accountId = "${mockAccountId}"`); - expect(configContent).toContain(`currentAppId = "${mockAppId}"`); - expect(configContent).toContain(`[accounts.default.apps.${mockAppId}]`); - expect(configContent).toContain(`appName = "${mockAppName}"`); }); it("should not include app info when multiple apps exist (no interactive selection in JSON mode)", async () => { @@ -229,18 +189,15 @@ account = "default" expect(result).toHaveProperty("success", true); expect(result).not.toHaveProperty("app"); - // Verify config file was written without app selection - const configContent = readFileSync( - resolve(testConfigDir, "config"), - "utf8", - ); - expect(configContent).toContain("[current]"); - expect(configContent).toContain('account = "default"'); - expect(configContent).toContain("[accounts.default]"); - expect(configContent).toContain(`accessToken = "${mockAccessToken}"`); - expect(configContent).toContain(`accountId = "${mockAccountId}"`); - // Should NOT contain currentAppId when multiple apps exist - expect(configContent).not.toContain("currentAppId"); + // Verify config was written without app selection via mock + const mock = getMockConfigManager(); + const config = mock.getConfig(); + expect(config.current?.account).toBe("default"); + expect(config.accounts["default"]).toBeDefined(); + expect(config.accounts["default"].accessToken).toBe(mockAccessToken); + expect(config.accounts["default"].accountId).toBe(mockAccountId); + // Should NOT have currentAppId when multiple apps exist + expect(config.accounts["default"].currentAppId).toBeUndefined(); }); }); @@ -327,19 +284,15 @@ account = "default" expect(result).toHaveProperty("success", true); expect(result.account).toHaveProperty("id", mockAccountId); - // Verify config file was written with custom control host in mind - // (the account should still be stored correctly) - const configContent = readFileSync( - resolve(testConfigDir, "config"), - "utf8", - ); - expect(configContent).toContain("[current]"); - expect(configContent).toContain('account = "default"'); - expect(configContent).toContain("[accounts.default]"); - expect(configContent).toContain(`accessToken = "${mockAccessToken}"`); - expect(configContent).toContain(`accountId = "${mockAccountId}"`); - expect(configContent).toContain('accountName = "Test Account"'); - expect(configContent).toContain('userEmail = "test@example.com"'); + // Verify config was written correctly via mock + const mock = getMockConfigManager(); + const config = mock.getConfig(); + expect(config.current?.account).toBe("default"); + expect(config.accounts["default"]).toBeDefined(); + expect(config.accounts["default"].accessToken).toBe(mockAccessToken); + expect(config.accounts["default"].accountId).toBe(mockAccountId); + expect(config.accounts["default"].accountName).toBe("Test Account"); + expect(config.accounts["default"].userEmail).toBe("test@example.com"); }); }); }); diff --git a/test/unit/commands/accounts/logout.test.ts b/test/unit/commands/accounts/logout.test.ts index 1892a7a5..307755e7 100644 --- a/test/unit/commands/accounts/logout.test.ts +++ b/test/unit/commands/accounts/logout.test.ts @@ -1,43 +1,8 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, beforeEach } from "vitest"; import { runCommand } from "@oclif/test"; -import { resolve } from "node:path"; -import { - mkdirSync, - writeFileSync, - existsSync, - rmSync, - readFileSync, -} from "node:fs"; -import { tmpdir } from "node:os"; +import { getMockConfigManager } from "../../../helpers/mock-config-manager.js"; describe("accounts:logout command", () => { - let testConfigDir: string; - let originalConfigDir: string; - - beforeEach(() => { - // Create a temporary config directory for testing - testConfigDir = resolve(tmpdir(), `ably-cli-test-logout-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - // Store original config dir and set test config dir - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - }); - - afterEach(() => { - // Restore original config directory - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - // Clean up test config directory - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } - }); - describe("help", () => { it("should display help with --help flag", async () => { const { stdout } = await runCommand( @@ -62,10 +27,9 @@ describe("accounts:logout command", () => { describe("with no logged in accounts", () => { beforeEach(() => { - // Create empty config - const configContent = `[current] -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + // Clear accounts to simulate no logged in state + const mock = getMockConfigManager(); + mock.clearAccounts(); }); it("should output error in JSON format when no account is selected", async () => { @@ -83,17 +47,19 @@ describe("accounts:logout command", () => { describe("with logged in account", () => { beforeEach(() => { - // Create config with a logged in account - const configContent = `[current] -account = "testaccount" - -[accounts.testaccount] -accessToken = "test_token_12345" -accountId = "acc-123" -accountName = "Test Account" -userEmail = "test@example.com" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + // Set up config with a logged in account via mock + const mock = getMockConfigManager(); + mock.setConfig({ + current: { account: "testaccount" }, + accounts: { + testaccount: { + accessToken: "test_token_12345", + accountId: "acc-123", + accountName: "Test Account", + userEmail: "test@example.com", + }, + }, + }); }); it("should successfully logout with --force and --json flags", async () => { @@ -108,14 +74,10 @@ userEmail = "test@example.com" expect(result.account).toHaveProperty("alias", "testaccount"); expect(result).toHaveProperty("remainingAccounts"); - // Verify config file was updated - account should be removed - const configContent = readFileSync( - resolve(testConfigDir, "config"), - "utf8", - ); - expect(configContent).not.toContain("[accounts.testaccount]"); - expect(configContent).not.toContain("test_token_12345"); - expect(configContent).not.toContain("acc-123"); + // Verify config was updated - account should be removed + const mock = getMockConfigManager(); + const config = mock.getConfig(); + expect(config.accounts["testaccount"]).toBeUndefined(); }); it("should logout specific account by alias with --force and --json", async () => { @@ -128,35 +90,34 @@ userEmail = "test@example.com" expect(result).toHaveProperty("success", true); expect(result.account).toHaveProperty("alias", "testaccount"); - // Verify config file was updated - account should be removed - const configContent = readFileSync( - resolve(testConfigDir, "config"), - "utf8", - ); - expect(configContent).not.toContain("[accounts.testaccount]"); - expect(configContent).not.toContain("test_token_12345"); + // Verify config was updated - account should be removed + const mock = getMockConfigManager(); + const config = mock.getConfig(); + expect(config.accounts["testaccount"]).toBeUndefined(); }); }); describe("with multiple logged in accounts", () => { beforeEach(() => { - // Create config with multiple accounts - const configContent = `[current] -account = "primary" - -[accounts.primary] -accessToken = "primary_token" -accountId = "acc-primary" -accountName = "Primary Account" -userEmail = "primary@example.com" - -[accounts.secondary] -accessToken = "secondary_token" -accountId = "acc-secondary" -accountName = "Secondary Account" -userEmail = "secondary@example.com" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + // Set up config with multiple accounts via mock + const mock = getMockConfigManager(); + mock.setConfig({ + current: { account: "primary" }, + accounts: { + primary: { + accessToken: "primary_token", + accountId: "acc-primary", + accountName: "Primary Account", + userEmail: "primary@example.com", + }, + secondary: { + accessToken: "secondary_token", + accountId: "acc-secondary", + accountName: "Secondary Account", + userEmail: "secondary@example.com", + }, + }, + }); }); it("should logout current account and show remaining accounts", async () => { @@ -170,16 +131,15 @@ userEmail = "secondary@example.com" expect(result.account).toHaveProperty("alias", "primary"); expect(result.remainingAccounts).toContain("secondary"); - // Verify config file was updated - primary removed, secondary remains - const configContent = readFileSync( - resolve(testConfigDir, "config"), - "utf8", + // Verify config was updated - primary removed, secondary remains + const mock = getMockConfigManager(); + const config = mock.getConfig(); + expect(config.accounts["primary"]).toBeUndefined(); + expect(config.accounts["secondary"]).toBeDefined(); + expect(config.accounts["secondary"].accessToken).toBe("secondary_token"); + expect(config.accounts["secondary"].accountName).toBe( + "Secondary Account", ); - expect(configContent).not.toContain("[accounts.primary]"); - expect(configContent).not.toContain("primary_token"); - expect(configContent).toContain("[accounts.secondary]"); - expect(configContent).toContain("secondary_token"); - expect(configContent).toContain('accountName = "Secondary Account"'); }); it("should logout specific account when alias is provided", async () => { @@ -193,34 +153,33 @@ userEmail = "secondary@example.com" expect(result.account).toHaveProperty("alias", "secondary"); expect(result.remainingAccounts).toContain("primary"); - // Verify config file was updated - secondary removed, primary remains - const configContent = readFileSync( - resolve(testConfigDir, "config"), - "utf8", - ); - expect(configContent).not.toContain("[accounts.secondary]"); - expect(configContent).not.toContain("secondary_token"); - expect(configContent).toContain("[accounts.primary]"); - expect(configContent).toContain("primary_token"); - expect(configContent).toContain('accountName = "Primary Account"'); + // Verify config was updated - secondary removed, primary remains + const mock = getMockConfigManager(); + const config = mock.getConfig(); + expect(config.accounts["secondary"]).toBeUndefined(); + expect(config.accounts["primary"]).toBeDefined(); + expect(config.accounts["primary"].accessToken).toBe("primary_token"); + expect(config.accounts["primary"].accountName).toBe("Primary Account"); // Current account should still be primary - expect(configContent).toContain('account = "primary"'); + expect(config.current?.account).toBe("primary"); }); }); describe("error handling", () => { beforeEach(() => { - // Create config with a logged in account - const configContent = `[current] -account = "existingaccount" - -[accounts.existingaccount] -accessToken = "test_token" -accountId = "acc-123" -accountName = "Test Account" -userEmail = "test@example.com" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + // Set up config with a logged in account via mock + const mock = getMockConfigManager(); + mock.setConfig({ + current: { account: "existingaccount" }, + accounts: { + existingaccount: { + accessToken: "test_token", + accountId: "acc-123", + accountName: "Test Account", + userEmail: "test@example.com", + }, + }, + }); }); it("should output error in JSON format when account alias does not exist", async () => { diff --git a/test/unit/commands/apps/channel-rules/create.test.ts b/test/unit/commands/apps/channel-rules/create.test.ts index 9ba1d4d7..f9e885a9 100644 --- a/test/unit/commands/apps/channel-rules/create.test.ts +++ b/test/unit/commands/apps/channel-rules/create.test.ts @@ -1,60 +1,21 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, afterEach } from "vitest"; import { runCommand } from "@oclif/test"; import nock from "nock"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; +import { getMockConfigManager } from "../../../../helpers/mock-config-manager.js"; describe("apps:channel-rules:create command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; const mockRuleName = "chat"; const mockRuleId = "chat"; - let testConfigDir: string; - let originalConfigDir: string; - - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); - }); afterEach(() => { nock.cleanAll(); - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } }); describe("successful channel rule creation", () => { it("should create a channel rule successfully", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/namespaces`) + .post(`/v1/apps/${appId}/namespaces`) .reply(201, { id: mockRuleId, persisted: false, @@ -64,13 +25,7 @@ currentAppId = "${mockAppId}" }); const { stdout } = await runCommand( - [ - "apps:channel-rules:create", - "--name", - mockRuleName, - "--app", - mockAppId, - ], + ["apps:channel-rules:create", "--name", mockRuleName, "--app", appId], import.meta.url, ); @@ -79,8 +34,9 @@ currentAppId = "${mockAppId}" }); it("should create a channel rule with persisted flag", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/namespaces`, (body) => { + .post(`/v1/apps/${appId}/namespaces`, (body) => { return body.persisted === true; }) .reply(201, { @@ -97,7 +53,7 @@ currentAppId = "${mockAppId}" "--name", mockRuleName, "--app", - mockAppId, + appId, "--persisted", ], import.meta.url, @@ -108,8 +64,9 @@ currentAppId = "${mockAppId}" }); it("should create a channel rule with push-enabled flag", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/namespaces`, (body) => { + .post(`/v1/apps/${appId}/namespaces`, (body) => { return body.pushEnabled === true; }) .reply(201, { @@ -126,7 +83,7 @@ currentAppId = "${mockAppId}" "--name", mockRuleName, "--app", - mockAppId, + appId, "--push-enabled", ], import.meta.url, @@ -137,6 +94,7 @@ currentAppId = "${mockAppId}" }); it("should output JSON format when --json flag is used", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; const mockRule = { id: mockRuleId, persisted: false, @@ -146,7 +104,7 @@ currentAppId = "${mockAppId}" }; nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/namespaces`) + .post(`/v1/apps/${appId}/namespaces`) .reply(201, mockRule); const { stdout } = await runCommand( @@ -155,7 +113,7 @@ currentAppId = "${mockAppId}" "--name", mockRuleName, "--app", - mockAppId, + appId, "--json", ], import.meta.url, @@ -170,8 +128,9 @@ currentAppId = "${mockAppId}" describe("error handling", () => { it("should require name parameter", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; const { error } = await runCommand( - ["apps:channel-rules:create", "--app", mockAppId], + ["apps:channel-rules:create", "--app", appId], import.meta.url, ); @@ -180,18 +139,13 @@ currentAppId = "${mockAppId}" }); it("should handle 401 authentication error", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/namespaces`) + .post(`/v1/apps/${appId}/namespaces`) .reply(401, { error: "Unauthorized" }); const { error } = await runCommand( - [ - "apps:channel-rules:create", - "--name", - mockRuleName, - "--app", - mockAppId, - ], + ["apps:channel-rules:create", "--name", mockRuleName, "--app", appId], import.meta.url, ); @@ -200,18 +154,13 @@ currentAppId = "${mockAppId}" }); it("should handle 400 validation error", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/namespaces`) + .post(`/v1/apps/${appId}/namespaces`) .reply(400, { error: "Validation failed" }); const { error } = await runCommand( - [ - "apps:channel-rules:create", - "--name", - mockRuleName, - "--app", - mockAppId, - ], + ["apps:channel-rules:create", "--name", mockRuleName, "--app", appId], import.meta.url, ); @@ -220,18 +169,13 @@ currentAppId = "${mockAppId}" }); it("should handle network errors", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/namespaces`) + .post(`/v1/apps/${appId}/namespaces`) .replyWithError("Network error"); const { error } = await runCommand( - [ - "apps:channel-rules:create", - "--name", - mockRuleName, - "--app", - mockAppId, - ], + ["apps:channel-rules:create", "--name", mockRuleName, "--app", appId], import.meta.url, ); diff --git a/test/unit/commands/apps/channel-rules/delete.test.ts b/test/unit/commands/apps/channel-rules/delete.test.ts index 67612ee3..fea08eb9 100644 --- a/test/unit/commands/apps/channel-rules/delete.test.ts +++ b/test/unit/commands/apps/channel-rules/delete.test.ts @@ -1,60 +1,21 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, afterEach } from "vitest"; import { runCommand } from "@oclif/test"; import nock from "nock"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; +import { getMockConfigManager } from "../../../../helpers/mock-config-manager.js"; describe("apps:channel-rules:delete command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; const mockRuleId = "chat"; - let testConfigDir: string; - let originalConfigDir: string; - - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); - }); afterEach(() => { nock.cleanAll(); - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } }); describe("successful channel rule deletion", () => { it("should delete a channel rule with force flag", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; // Mock listing namespaces to find the rule nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/namespaces`) + .get(`/v1/apps/${appId}/namespaces`) .reply(200, [ { id: mockRuleId, @@ -67,17 +28,11 @@ currentAppId = "${mockAppId}" // Mock delete endpoint nock("https://control.ably.net") - .delete(`/v1/apps/${mockAppId}/namespaces/${mockRuleId}`) + .delete(`/v1/apps/${appId}/namespaces/${mockRuleId}`) .reply(204); const { stdout } = await runCommand( - [ - "apps:channel-rules:delete", - mockRuleId, - "--app", - mockAppId, - "--force", - ], + ["apps:channel-rules:delete", mockRuleId, "--app", appId, "--force"], import.meta.url, ); @@ -85,8 +40,9 @@ currentAppId = "${mockAppId}" }); it("should output JSON format when --json flag is used", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/namespaces`) + .get(`/v1/apps/${appId}/namespaces`) .reply(200, [ { id: mockRuleId, @@ -98,7 +54,7 @@ currentAppId = "${mockAppId}" ]); nock("https://control.ably.net") - .delete(`/v1/apps/${mockAppId}/namespaces/${mockRuleId}`) + .delete(`/v1/apps/${appId}/namespaces/${mockRuleId}`) .reply(204); const { stdout } = await runCommand( @@ -106,7 +62,7 @@ currentAppId = "${mockAppId}" "apps:channel-rules:delete", mockRuleId, "--app", - mockAppId, + appId, "--force", "--json", ], @@ -122,8 +78,9 @@ currentAppId = "${mockAppId}" describe("error handling", () => { it("should require nameOrId argument", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; const { error } = await runCommand( - ["apps:channel-rules:delete", "--app", mockAppId], + ["apps:channel-rules:delete", "--app", appId], import.meta.url, ); @@ -132,18 +89,13 @@ currentAppId = "${mockAppId}" }); it("should handle channel rule not found", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/namespaces`) + .get(`/v1/apps/${appId}/namespaces`) .reply(200, []); const { error } = await runCommand( - [ - "apps:channel-rules:delete", - "nonexistent", - "--app", - mockAppId, - "--force", - ], + ["apps:channel-rules:delete", "nonexistent", "--app", appId, "--force"], import.meta.url, ); @@ -152,18 +104,13 @@ currentAppId = "${mockAppId}" }); it("should handle 401 authentication error", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/namespaces`) + .get(`/v1/apps/${appId}/namespaces`) .reply(401, { error: "Unauthorized" }); const { error } = await runCommand( - [ - "apps:channel-rules:delete", - mockRuleId, - "--app", - mockAppId, - "--force", - ], + ["apps:channel-rules:delete", mockRuleId, "--app", appId, "--force"], import.meta.url, ); @@ -172,18 +119,13 @@ currentAppId = "${mockAppId}" }); it("should handle network errors", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/namespaces`) + .get(`/v1/apps/${appId}/namespaces`) .replyWithError("Network error"); const { error } = await runCommand( - [ - "apps:channel-rules:delete", - mockRuleId, - "--app", - mockAppId, - "--force", - ], + ["apps:channel-rules:delete", mockRuleId, "--app", appId, "--force"], import.meta.url, ); diff --git a/test/unit/commands/apps/channel-rules/list.test.ts b/test/unit/commands/apps/channel-rules/list.test.ts index 236b4c69..e4817fd0 100644 --- a/test/unit/commands/apps/channel-rules/list.test.ts +++ b/test/unit/commands/apps/channel-rules/list.test.ts @@ -1,56 +1,16 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, afterEach } from "vitest"; import { runCommand } from "@oclif/test"; import nock from "nock"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; +import { getMockConfigManager } from "../../../../helpers/mock-config-manager.js"; describe("apps:channel-rules:list command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - let testConfigDir: string; - let originalConfigDir: string; - - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); - }); - afterEach(() => { nock.cleanAll(); - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } }); describe("successful channel rules listing", () => { it("should list channel rules successfully", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; const mockRules = [ { id: "chat", @@ -69,7 +29,7 @@ currentAppId = "${mockAppId}" ]; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/namespaces`) + .get(`/v1/apps/${appId}/namespaces`) .reply(200, mockRules); const { stdout } = await runCommand( @@ -83,8 +43,9 @@ currentAppId = "${mockAppId}" }); it("should handle empty rules list", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/namespaces`) + .get(`/v1/apps/${appId}/namespaces`) .reply(200, []); const { stdout } = await runCommand( @@ -96,6 +57,7 @@ currentAppId = "${mockAppId}" }); it("should display rule details correctly", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; const mockRules = [ { id: "chat", @@ -109,7 +71,7 @@ currentAppId = "${mockAppId}" ]; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/namespaces`) + .get(`/v1/apps/${appId}/namespaces`) .reply(200, mockRules); const { stdout } = await runCommand( @@ -126,8 +88,9 @@ currentAppId = "${mockAppId}" describe("error handling", () => { it("should handle 401 authentication error", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/namespaces`) + .get(`/v1/apps/${appId}/namespaces`) .reply(401, { error: "Unauthorized" }); const { error } = await runCommand( @@ -140,8 +103,9 @@ currentAppId = "${mockAppId}" }); it("should handle 404 not found error", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/namespaces`) + .get(`/v1/apps/${appId}/namespaces`) .reply(404, { error: "App not found" }); const { error } = await runCommand( @@ -154,8 +118,9 @@ currentAppId = "${mockAppId}" }); it("should handle network errors", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/namespaces`) + .get(`/v1/apps/${appId}/namespaces`) .replyWithError("Network error"); const { error } = await runCommand( diff --git a/test/unit/commands/apps/channel-rules/update.test.ts b/test/unit/commands/apps/channel-rules/update.test.ts index 0a320a20..d33bbaf8 100644 --- a/test/unit/commands/apps/channel-rules/update.test.ts +++ b/test/unit/commands/apps/channel-rules/update.test.ts @@ -1,60 +1,21 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, afterEach } from "vitest"; import { runCommand } from "@oclif/test"; import nock from "nock"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; +import { getMockConfigManager } from "../../../../helpers/mock-config-manager.js"; describe("apps:channel-rules:update command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; const mockRuleId = "chat"; - let testConfigDir: string; - let originalConfigDir: string; - - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); - }); afterEach(() => { nock.cleanAll(); - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } }); describe("successful channel rule update", () => { it("should update a channel rule with persisted flag", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; // Mock listing namespaces to find the rule nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/namespaces`) + .get(`/v1/apps/${appId}/namespaces`) .reply(200, [ { id: mockRuleId, @@ -67,7 +28,7 @@ currentAppId = "${mockAppId}" // Mock update endpoint nock("https://control.ably.net") - .patch(`/v1/apps/${mockAppId}/namespaces/${mockRuleId}`) + .patch(`/v1/apps/${appId}/namespaces/${mockRuleId}`) .reply(200, { id: mockRuleId, persisted: true, @@ -81,7 +42,7 @@ currentAppId = "${mockAppId}" "apps:channel-rules:update", mockRuleId, "--app", - mockAppId, + appId, "--persisted", ], import.meta.url, @@ -92,8 +53,9 @@ currentAppId = "${mockAppId}" }); it("should update a channel rule with push-enabled flag", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/namespaces`) + .get(`/v1/apps/${appId}/namespaces`) .reply(200, [ { id: mockRuleId, @@ -105,7 +67,7 @@ currentAppId = "${mockAppId}" ]); nock("https://control.ably.net") - .patch(`/v1/apps/${mockAppId}/namespaces/${mockRuleId}`) + .patch(`/v1/apps/${appId}/namespaces/${mockRuleId}`) .reply(200, { id: mockRuleId, persisted: false, @@ -119,7 +81,7 @@ currentAppId = "${mockAppId}" "apps:channel-rules:update", mockRuleId, "--app", - mockAppId, + appId, "--push-enabled", ], import.meta.url, @@ -130,8 +92,9 @@ currentAppId = "${mockAppId}" }); it("should output JSON format when --json flag is used", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/namespaces`) + .get(`/v1/apps/${appId}/namespaces`) .reply(200, [ { id: mockRuleId, @@ -143,7 +106,7 @@ currentAppId = "${mockAppId}" ]); nock("https://control.ably.net") - .patch(`/v1/apps/${mockAppId}/namespaces/${mockRuleId}`) + .patch(`/v1/apps/${appId}/namespaces/${mockRuleId}`) .reply(200, { id: mockRuleId, persisted: true, @@ -157,7 +120,7 @@ currentAppId = "${mockAppId}" "apps:channel-rules:update", mockRuleId, "--app", - mockAppId, + appId, "--persisted", "--json", ], @@ -174,8 +137,9 @@ currentAppId = "${mockAppId}" describe("error handling", () => { it("should require nameOrId argument", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; const { error } = await runCommand( - ["apps:channel-rules:update", "--app", mockAppId, "--persisted"], + ["apps:channel-rules:update", "--app", appId, "--persisted"], import.meta.url, ); @@ -184,8 +148,9 @@ currentAppId = "${mockAppId}" }); it("should require at least one update parameter", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/namespaces`) + .get(`/v1/apps/${appId}/namespaces`) .reply(200, [ { id: mockRuleId, @@ -197,7 +162,7 @@ currentAppId = "${mockAppId}" ]); const { error } = await runCommand( - ["apps:channel-rules:update", mockRuleId, "--app", mockAppId], + ["apps:channel-rules:update", mockRuleId, "--app", appId], import.meta.url, ); @@ -206,8 +171,9 @@ currentAppId = "${mockAppId}" }); it("should handle channel rule not found", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/namespaces`) + .get(`/v1/apps/${appId}/namespaces`) .reply(200, []); const { error } = await runCommand( @@ -215,7 +181,7 @@ currentAppId = "${mockAppId}" "apps:channel-rules:update", "nonexistent", "--app", - mockAppId, + appId, "--persisted", ], import.meta.url, @@ -226,8 +192,9 @@ currentAppId = "${mockAppId}" }); it("should handle 401 authentication error", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/namespaces`) + .get(`/v1/apps/${appId}/namespaces`) .reply(401, { error: "Unauthorized" }); const { error } = await runCommand( @@ -235,7 +202,7 @@ currentAppId = "${mockAppId}" "apps:channel-rules:update", mockRuleId, "--app", - mockAppId, + appId, "--persisted", ], import.meta.url, @@ -246,8 +213,9 @@ currentAppId = "${mockAppId}" }); it("should handle network errors", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/namespaces`) + .get(`/v1/apps/${appId}/namespaces`) .replyWithError("Network error"); const { error } = await runCommand( @@ -255,7 +223,7 @@ currentAppId = "${mockAppId}" "apps:channel-rules:update", mockRuleId, "--app", - mockAppId, + appId, "--persisted", ], import.meta.url, diff --git a/test/unit/commands/apps/create.test.ts b/test/unit/commands/apps/create.test.ts index 65953be7..0dc1cc53 100644 --- a/test/unit/commands/apps/create.test.ts +++ b/test/unit/commands/apps/create.test.ts @@ -1,86 +1,44 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { runCommand } from "@oclif/test"; import nock from "nock"; -import { resolve } from "node:path"; -import { - mkdirSync, - writeFileSync, - readFileSync, - existsSync, - rmSync, -} from "node:fs"; -import { tmpdir } from "node:os"; +import { getMockConfigManager } from "../../../helpers/mock-config-manager.js"; describe("apps:create command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; const mockAppName = "TesttApp"; - let testConfigDir: string; - let originalConfigDir: string; + const newAppId = "new-app-id-12345"; beforeEach(() => { - // Set environment variable for access token - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - // Create a temporary config directory for testing - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - // Store original config dir and set test config dir - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - // Create a minimal config file with a default account - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + nock.cleanAll(); }); afterEach(() => { - // Clean up nock interceptors nock.cleanAll(); - delete process.env.ABLY_ACCESS_TOKEN; - - // Restore original config directory - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - // Clean up test config directory - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } }); describe("successful app creation", () => { it("should create an app successfully", async () => { + const mock = getMockConfigManager(); + const accountId = mock.getCurrentAccount()!.accountId!; + const accountName = mock.getCurrentAccount()!.accountName!; + const userEmail = mock.getCurrentAccount()!.userEmail!; + // Mock the /me endpoint to get account ID nock("https://control.ably.net") .get("/v1/me") .reply(200, { - account: { id: mockAccountId, name: "Test Account" }, - user: { email: "test@example.com" }, + account: { id: accountId, name: accountName }, + user: { email: userEmail }, }); // Mock the app creation endpoint nock("https://control.ably.net") - .post(`/v1/accounts/${mockAccountId}/apps`, { + .post(`/v1/accounts/${accountId}/apps`, { name: mockAppName, tlsOnly: false, }) .reply(201, { - id: mockAppId, - accountId: mockAccountId, + id: newAppId, + accountId: accountId, name: mockAppName, status: "active", created: Date.now(), @@ -95,29 +53,34 @@ userEmail = "test@example.com" ]); expect(stdout).toContain("App created successfully"); - expect(stdout).toContain(mockAppId); + expect(stdout).toContain(newAppId); expect(stdout).toContain(mockAppName); expect(stdout).toContain("Automatically switched to app"); }); it("should create an app with TLS only flag", async () => { + const mock = getMockConfigManager(); + const accountId = mock.getCurrentAccount()!.accountId!; + const accountName = mock.getCurrentAccount()!.accountName!; + const userEmail = mock.getCurrentAccount()!.userEmail!; + // Mock the /me endpoint nock("https://control.ably.net") .get("/v1/me") .reply(200, { - account: { id: mockAccountId, name: "Test Account" }, - user: { email: "test@example.com" }, + account: { id: accountId, name: accountName }, + user: { email: userEmail }, }); // Mock the app creation endpoint with TLS only nock("https://control.ably.net") - .post(`/v1/accounts/${mockAccountId}/apps`, { + .post(`/v1/accounts/${accountId}/apps`, { name: mockAppName, tlsOnly: true, }) .reply(201, { - id: mockAppId, - accountId: mockAccountId, + id: newAppId, + accountId: accountId, name: mockAppName, status: "active", created: Date.now(), @@ -136,9 +99,14 @@ userEmail = "test@example.com" }); it("should output JSON format when --json flag is used", async () => { + const mock = getMockConfigManager(); + const accountId = mock.getCurrentAccount()!.accountId!; + const accountName = mock.getCurrentAccount()!.accountName!; + const userEmail = mock.getCurrentAccount()!.userEmail!; + const mockApp = { - id: mockAppId, - accountId: mockAccountId, + id: newAppId, + accountId: accountId, name: mockAppName, status: "active", created: Date.now(), @@ -150,13 +118,13 @@ userEmail = "test@example.com" nock("https://control.ably.net") .get("/v1/me") .reply(200, { - account: { id: mockAccountId, name: "Test Account" }, - user: { email: "test@example.com" }, + account: { id: accountId, name: accountName }, + user: { email: userEmail }, }); // Mock the app creation endpoint nock("https://control.ably.net") - .post(`/v1/accounts/${mockAccountId}/apps`) + .post(`/v1/accounts/${accountId}/apps`) .reply(201, mockApp); const { stdout } = await runCommand( @@ -166,12 +134,16 @@ userEmail = "test@example.com" const result = JSON.parse(stdout); expect(result).toHaveProperty("app"); - expect(result.app).toHaveProperty("id", mockAppId); + expect(result.app).toHaveProperty("id", newAppId); expect(result.app).toHaveProperty("name", mockAppName); expect(result).toHaveProperty("success", true); }); it("should use custom access token when provided", async () => { + const mock = getMockConfigManager(); + const accountId = mock.getCurrentAccount()!.accountId!; + const accountName = mock.getCurrentAccount()!.accountName!; + const userEmail = mock.getCurrentAccount()!.userEmail!; const customToken = "custom_access_token"; // Mock the /me endpoint with custom token @@ -182,8 +154,8 @@ userEmail = "test@example.com" }) .get("/v1/me") .reply(200, { - account: { id: mockAccountId, name: "Test Account" }, - user: { email: "test@example.com" }, + account: { id: accountId, name: accountName }, + user: { email: userEmail }, }); // Mock the app creation endpoint @@ -192,10 +164,10 @@ userEmail = "test@example.com" authorization: `Bearer ${customToken}`, }, }) - .post(`/v1/accounts/${mockAccountId}/apps`) + .post(`/v1/accounts/${accountId}/apps`) .reply(201, { - id: mockAppId, - accountId: mockAccountId, + id: newAppId, + accountId: accountId, name: mockAppName, status: "active", created: Date.now(), @@ -219,20 +191,25 @@ userEmail = "test@example.com" }); it("should automatically switch to the newly created app", async () => { + const mock = getMockConfigManager(); + const accountId = mock.getCurrentAccount()!.accountId!; + const accountName = mock.getCurrentAccount()!.accountName!; + const userEmail = mock.getCurrentAccount()!.userEmail!; + // Mock the /me endpoint nock("https://control.ably.net") .get("/v1/me") .reply(200, { - account: { id: mockAccountId, name: "Test Account" }, - user: { email: "test@example.com" }, + account: { id: accountId, name: accountName }, + user: { email: userEmail }, }); // Mock the app creation endpoint nock("https://control.ably.net") - .post(`/v1/accounts/${mockAccountId}/apps`) + .post(`/v1/accounts/${accountId}/apps`) .reply(201, { - id: mockAppId, - accountId: mockAccountId, + id: newAppId, + accountId: accountId, name: mockAppName, status: "active", created: Date.now(), @@ -247,14 +224,12 @@ userEmail = "test@example.com" expect(stdout).toContain("App created successfully"); expect(stdout).toContain( - `Automatically switched to app: ${mockAppName} (${mockAppId})`, + `Automatically switched to app: ${mockAppName} (${newAppId})`, ); - // Verify the config file was updated with the new app - const configPath = resolve(testConfigDir, "config"); - const configContent = readFileSync(configPath, "utf8"); - expect(configContent).toContain(`currentAppId = "${mockAppId}"`); - expect(configContent).toContain(`appName = "${mockAppName}"`); + // Verify the mock config was updated with the new app + expect(mock.getCurrentAppId()).toBe(newAppId); + expect(mock.getAppName(newAppId)).toBe(mockAppName); }); }); @@ -275,17 +250,22 @@ userEmail = "test@example.com" }); it("should handle 403 forbidden error", async () => { + const mock = getMockConfigManager(); + const accountId = mock.getCurrentAccount()!.accountId!; + const accountName = mock.getCurrentAccount()!.accountName!; + const userEmail = mock.getCurrentAccount()!.userEmail!; + // Mock the /me endpoint nock("https://control.ably.net") .get("/v1/me") .reply(200, { - account: { id: mockAccountId, name: "Test Account" }, - user: { email: "test@example.com" }, + account: { id: accountId, name: accountName }, + user: { email: userEmail }, }); // Mock forbidden response nock("https://control.ably.net") - .post(`/v1/accounts/${mockAccountId}/apps`) + .post(`/v1/accounts/${accountId}/apps`) .reply(403, { error: "Forbidden" }); const { error } = await runCommand( @@ -298,17 +278,22 @@ userEmail = "test@example.com" }); it("should handle 404 not found error", async () => { + const mock = getMockConfigManager(); + const accountId = mock.getCurrentAccount()!.accountId!; + const accountName = mock.getCurrentAccount()!.accountName!; + const userEmail = mock.getCurrentAccount()!.userEmail!; + // Mock the /me endpoint nock("https://control.ably.net") .get("/v1/me") .reply(200, { - account: { id: mockAccountId, name: "Test Account" }, - user: { email: "test@example.com" }, + account: { id: accountId, name: accountName }, + user: { email: userEmail }, }); // Mock not found response nock("https://control.ably.net") - .post(`/v1/accounts/${mockAccountId}/apps`) + .post(`/v1/accounts/${accountId}/apps`) .reply(404, { error: "Not Found" }); const { error } = await runCommand( @@ -321,17 +306,22 @@ userEmail = "test@example.com" }); it("should handle 500 server error", async () => { + const mock = getMockConfigManager(); + const accountId = mock.getCurrentAccount()!.accountId!; + const accountName = mock.getCurrentAccount()!.accountName!; + const userEmail = mock.getCurrentAccount()!.userEmail!; + // Mock the /me endpoint nock("https://control.ably.net") .get("/v1/me") .reply(200, { - account: { id: mockAccountId, name: "Test Account" }, - user: { email: "test@example.com" }, + account: { id: accountId, name: accountName }, + user: { email: userEmail }, }); // Mock server error nock("https://control.ably.net") - .post(`/v1/accounts/${mockAccountId}/apps`) + .post(`/v1/accounts/${accountId}/apps`) .reply(500, { error: "Internal Server Error" }); const { error } = await runCommand( @@ -366,17 +356,22 @@ userEmail = "test@example.com" }); it("should handle validation errors from API", async () => { + const mock = getMockConfigManager(); + const accountId = mock.getCurrentAccount()!.accountId!; + const accountName = mock.getCurrentAccount()!.accountName!; + const userEmail = mock.getCurrentAccount()!.userEmail!; + // Mock the /me endpoint nock("https://control.ably.net") .get("/v1/me") .reply(200, { - account: { id: mockAccountId, name: "Test Account" }, - user: { email: "test@example.com" }, + account: { id: accountId, name: accountName }, + user: { email: userEmail }, }); // Mock validation error nock("https://control.ably.net") - .post(`/v1/accounts/${mockAccountId}/apps`) + .post(`/v1/accounts/${accountId}/apps`) .reply(400, { error: "Validation failed", details: "App name already exists", diff --git a/test/unit/commands/apps/current.test.ts b/test/unit/commands/apps/current.test.ts index 67ed65f9..e849b11e 100644 --- a/test/unit/commands/apps/current.test.ts +++ b/test/unit/commands/apps/current.test.ts @@ -1,107 +1,42 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect } from "vitest"; import { runCommand } from "@oclif/test"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; +import { getMockConfigManager } from "../../../helpers/mock-config-manager.js"; describe("apps:current command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - let testConfigDir: string; - let originalConfigDir: string; - - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - }); - - afterEach(() => { - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } - }); - describe("successful current app display", () => { it("should display the current app", async () => { - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); - + const mockConfig = getMockConfigManager(); + const appId = mockConfig.getCurrentAppId()!; + const appName = mockConfig.getAppName(appId)!; const { stdout } = await runCommand(["apps:current"], import.meta.url); - expect(stdout).toContain(`App: ${mockAppId}`); + expect(stdout).toContain(`App: ${appName}`); + expect(stdout).toContain(`(${appId})`); }); it("should display account information", async () => { - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); - + const accountName = + getMockConfigManager().getCurrentAccount()!.accountName!; const { stdout } = await runCommand(["apps:current"], import.meta.url); - expect(stdout).toContain("Account: Test Account"); + expect(stdout).toContain(`Account: ${accountName}`); }); it("should display API key info when set", async () => { - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[accounts.default.apps."${mockAppId}"] -apiKey = "testkey:secret" -keyId = "${mockAppId}.testkey" -keyName = "Test Key" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); - + const mockConfig = getMockConfigManager(); + const keyId = mockConfig.getKeyId()!; + const keyName = mockConfig.getKeyName()!; const { stdout } = await runCommand(["apps:current"], import.meta.url); - expect(stdout).toContain(`API Key: ${mockAppId}.testkey`); - expect(stdout).toContain("Key Label: Test Key"); + expect(stdout).toContain(`API Key: ${keyId}`); + expect(stdout).toContain(`Key Label: ${keyName}`); }); }); describe("error handling", () => { it("should error when no account is selected", async () => { - const configContent = `[current] -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + const mock = getMockConfigManager(); + mock.setCurrentAccountAlias(undefined); const { error } = await runCommand(["apps:current"], import.meta.url); @@ -110,16 +45,8 @@ keyName = "Test Key" }); it("should error when no app is selected", async () => { - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + const mock = getMockConfigManager(); + mock.setCurrentAppIdForAccount(undefined); const { error } = await runCommand(["apps:current"], import.meta.url); diff --git a/test/unit/commands/apps/delete.test.ts b/test/unit/commands/apps/delete.test.ts index 44ceca96..8b016f35 100644 --- a/test/unit/commands/apps/delete.test.ts +++ b/test/unit/commands/apps/delete.test.ts @@ -1,67 +1,42 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { runCommand } from "@oclif/test"; import nock from "nock"; -import { mkdirSync, writeFileSync } from "node:fs"; -import { resolve } from "node:path"; -import { tmpdir } from "node:os"; +import { getMockConfigManager } from "../../../helpers/mock-config-manager.js"; describe("apps:delete command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; const mockAppName = "TestApp"; - let testConfigDir: string; - let originalConfigDir: string; beforeEach(() => { - // Set environment variable for access token - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - // Create a temporary config directory for testing - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - // Store original config dir and set test config dir - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - // Create a minimal config file with a default account - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + nock.cleanAll(); }); afterEach(() => { - // Clean up nock interceptors nock.cleanAll(); - delete process.env.ABLY_ACCESS_TOKEN; - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; }); describe("successful app deletion", () => { it("should delete app successfully with --force flag", async () => { + const mock = getMockConfigManager(); + const accountId = mock.getCurrentAccount()!.accountId!; + const accountName = mock.getCurrentAccount()!.accountName!; + const userEmail = mock.getCurrentAccount()!.userEmail!; + const appId = mock.getCurrentAppId()!; + // Mock the /me endpoint for getApp (listApps) nock("https://control.ably.net") .get("/v1/me") .reply(200, { - account: { id: mockAccountId, name: "Test Account" }, - user: { email: "test@example.com" }, + account: { id: accountId, name: accountName }, + user: { email: userEmail }, }); // Mock the app listing endpoint for getApp nock("https://control.ably.net") - .get(`/v1/accounts/${mockAccountId}/apps`) + .get(`/v1/accounts/${accountId}/apps`) .reply(200, [ { - id: mockAppId, - accountId: mockAccountId, + id: appId, + accountId: accountId, name: mockAppName, status: "active", created: Date.now(), @@ -71,12 +46,10 @@ userEmail = "test@example.com" ]); // Mock the app deletion endpoint - nock("https://control.ably.net") - .delete(`/v1/apps/${mockAppId}`) - .reply(204); + nock("https://control.ably.net").delete(`/v1/apps/${appId}`).reply(204); const { stdout } = await runCommand( - ["apps:delete", mockAppId, "--force"], + ["apps:delete", appId, "--force"], import.meta.url, ); @@ -84,9 +57,15 @@ userEmail = "test@example.com" }); it("should output JSON format when --json flag is used", async () => { + const mock = getMockConfigManager(); + const accountId = mock.getCurrentAccount()!.accountId!; + const accountName = mock.getCurrentAccount()!.accountName!; + const userEmail = mock.getCurrentAccount()!.userEmail!; + const appId = mock.getCurrentAppId()!; + const mockApp = { - id: mockAppId, - accountId: mockAccountId, + id: appId, + accountId: accountId, name: mockAppName, status: "active", created: Date.now(), @@ -98,33 +77,36 @@ userEmail = "test@example.com" nock("https://control.ably.net") .get("/v1/me") .reply(200, { - account: { id: mockAccountId, name: "Test Account" }, - user: { email: "test@example.com" }, + account: { id: accountId, name: accountName }, + user: { email: userEmail }, }); // Mock the app listing endpoint for getApp nock("https://control.ably.net") - .get(`/v1/accounts/${mockAccountId}/apps`) + .get(`/v1/accounts/${accountId}/apps`) .reply(200, [mockApp]); // Mock the app deletion endpoint - nock("https://control.ably.net") - .delete(`/v1/apps/${mockAppId}`) - .reply(204); + nock("https://control.ably.net").delete(`/v1/apps/${appId}`).reply(204); const { stdout } = await runCommand( - ["apps:delete", mockAppId, "--force", "--json"], + ["apps:delete", appId, "--force", "--json"], import.meta.url, ); const result = JSON.parse(stdout); expect(result).toHaveProperty("success", true); expect(result).toHaveProperty("app"); - expect(result.app).toHaveProperty("id", mockAppId); + expect(result.app).toHaveProperty("id", appId); expect(result.app).toHaveProperty("name", mockAppName); }); it("should use custom access token when provided", async () => { + const mock = getMockConfigManager(); + const accountId = mock.getCurrentAccount()!.accountId!; + const accountName = mock.getCurrentAccount()!.accountName!; + const userEmail = mock.getCurrentAccount()!.userEmail!; + const appId = mock.getCurrentAppId()!; const customToken = "custom_access_token"; // Mock the /me endpoint with custom token @@ -135,8 +117,8 @@ userEmail = "test@example.com" }) .get("/v1/me") .reply(200, { - account: { id: mockAccountId, name: "Test Account" }, - user: { email: "test@example.com" }, + account: { id: accountId, name: accountName }, + user: { email: userEmail }, }); // Mock the app listing endpoint @@ -145,11 +127,11 @@ userEmail = "test@example.com" authorization: `Bearer ${customToken}`, }, }) - .get(`/v1/accounts/${mockAccountId}/apps`) + .get(`/v1/accounts/${accountId}/apps`) .reply(200, [ { - id: mockAppId, - accountId: mockAccountId, + id: appId, + accountId: accountId, name: mockAppName, status: "active", created: Date.now(), @@ -164,13 +146,13 @@ userEmail = "test@example.com" authorization: `Bearer ${customToken}`, }, }) - .delete(`/v1/apps/${mockAppId}`) + .delete(`/v1/apps/${appId}`) .reply(204); const { stdout } = await runCommand( [ "apps:delete", - mockAppId, + appId, "--force", "--access-token", "custom_access_token", @@ -184,13 +166,16 @@ userEmail = "test@example.com" describe("error handling", () => { it("should handle 401 authentication error", async () => { + const mock = getMockConfigManager(); + const appId = mock.getCurrentAppId()!; + // Mock authentication failure nock("https://control.ably.net") .get("/v1/me") .reply(401, { error: "Unauthorized" }); const { error } = await runCommand( - ["apps:delete", mockAppId, "--force"], + ["apps:delete", appId, "--force"], import.meta.url, ); expect(error).toBeDefined(); @@ -199,21 +184,27 @@ userEmail = "test@example.com" }); it("should handle app not found error", async () => { + const mock = getMockConfigManager(); + const accountId = mock.getCurrentAccount()!.accountId!; + const accountName = mock.getCurrentAccount()!.accountName!; + const userEmail = mock.getCurrentAccount()!.userEmail!; + const appId = mock.getCurrentAppId()!; + // Mock the /me endpoint nock("https://control.ably.net") .get("/v1/me") .reply(200, { - account: { id: mockAccountId, name: "Test Account" }, - user: { email: "test@example.com" }, + account: { id: accountId, name: accountName }, + user: { email: userEmail }, }); // Mock app not found nock("https://control.ably.net") - .get(`/v1/accounts/${mockAccountId}/apps`) + .get(`/v1/accounts/${accountId}/apps`) .reply(200, []); const { error } = await runCommand( - ["apps:delete", mockAppId, "--force"], + ["apps:delete", appId, "--force"], import.meta.url, ); expect(error).toBeDefined(); @@ -222,21 +213,27 @@ userEmail = "test@example.com" }); it("should handle deletion API error", async () => { + const mock = getMockConfigManager(); + const accountId = mock.getCurrentAccount()!.accountId!; + const accountName = mock.getCurrentAccount()!.accountName!; + const userEmail = mock.getCurrentAccount()!.userEmail!; + const appId = mock.getCurrentAppId()!; + // Mock the /me endpoint nock("https://control.ably.net") .get("/v1/me") .reply(200, { - account: { id: mockAccountId, name: "Test Account" }, - user: { email: "test@example.com" }, + account: { id: accountId, name: accountName }, + user: { email: userEmail }, }); // Mock the app listing endpoint nock("https://control.ably.net") - .get(`/v1/accounts/${mockAccountId}/apps`) + .get(`/v1/accounts/${accountId}/apps`) .reply(200, [ { - id: mockAppId, - accountId: mockAccountId, + id: appId, + accountId: accountId, name: mockAppName, status: "active", created: Date.now(), @@ -247,11 +244,11 @@ userEmail = "test@example.com" // Mock deletion failure nock("https://control.ably.net") - .delete(`/v1/apps/${mockAppId}`) + .delete(`/v1/apps/${appId}`) .reply(500, { error: "Internal Server Error" }); const { error } = await runCommand( - ["apps:delete", mockAppId, "--force"], + ["apps:delete", appId, "--force"], import.meta.url, ); expect(error).toBeDefined(); @@ -260,6 +257,10 @@ userEmail = "test@example.com" }); it("should handle missing app ID when no current app is set", async () => { + // Clear the current app from mock config + const mock = getMockConfigManager(); + mock.setCurrentAppIdForAccount(undefined); + const { error } = await runCommand(["apps:delete"], import.meta.url); expect(error).toBeDefined(); expect(error.message).toMatch( @@ -269,13 +270,16 @@ userEmail = "test@example.com" }); it("should handle network errors", async () => { + const mock = getMockConfigManager(); + const appId = mock.getCurrentAppId()!; + // Mock network error nock("https://control.ably.net") .get("/v1/me") .replyWithError("Network error"); const { error } = await runCommand( - ["apps:delete", mockAppId, "--force"], + ["apps:delete", appId, "--force"], import.meta.url, ); expect(error).toBeDefined(); @@ -284,21 +288,27 @@ userEmail = "test@example.com" }); it("should handle errors in JSON format when --json flag is used", async () => { + const mock = getMockConfigManager(); + const accountId = mock.getCurrentAccount()!.accountId!; + const accountName = mock.getCurrentAccount()!.accountName!; + const userEmail = mock.getCurrentAccount()!.userEmail!; + const appId = mock.getCurrentAppId()!; + // Mock the /me endpoint nock("https://control.ably.net") .get("/v1/me") .reply(200, { - account: { id: mockAccountId, name: "Test Account" }, - user: { email: "test@example.com" }, + account: { id: accountId, name: accountName }, + user: { email: userEmail }, }); // Mock the app listing endpoint nock("https://control.ably.net") - .get(`/v1/accounts/${mockAccountId}/apps`) + .get(`/v1/accounts/${accountId}/apps`) .reply(200, [ { - id: mockAppId, - accountId: mockAccountId, + id: appId, + accountId: accountId, name: mockAppName, status: "active", created: Date.now(), @@ -309,11 +319,11 @@ userEmail = "test@example.com" // Mock deletion failure nock("https://control.ably.net") - .delete(`/v1/apps/${mockAppId}`) + .delete(`/v1/apps/${appId}`) .reply(500, { error: "Internal Server Error" }); const { stdout } = await runCommand( - ["apps:delete", mockAppId, "--force", "--json"], + ["apps:delete", appId, "--force", "--json"], import.meta.url, ); @@ -321,25 +331,31 @@ userEmail = "test@example.com" expect(result).toHaveProperty("success", false); expect(result).toHaveProperty("status", "error"); expect(result).toHaveProperty("error"); - expect(result).toHaveProperty("appId", mockAppId); + expect(result).toHaveProperty("appId", appId); }); it("should handle 403 forbidden error", async () => { + const mock = getMockConfigManager(); + const accountId = mock.getCurrentAccount()!.accountId!; + const accountName = mock.getCurrentAccount()!.accountName!; + const userEmail = mock.getCurrentAccount()!.userEmail!; + const appId = mock.getCurrentAppId()!; + // Mock the /me endpoint nock("https://control.ably.net") .get("/v1/me") .reply(200, { - account: { id: mockAccountId, name: "Test Account" }, - user: { email: "test@example.com" }, + account: { id: accountId, name: accountName }, + user: { email: userEmail }, }); // Mock the app listing endpoint nock("https://control.ably.net") - .get(`/v1/accounts/${mockAccountId}/apps`) + .get(`/v1/accounts/${accountId}/apps`) .reply(200, [ { - id: mockAppId, - accountId: mockAccountId, + id: appId, + accountId: accountId, name: mockAppName, status: "active", created: Date.now(), @@ -350,11 +366,11 @@ userEmail = "test@example.com" // Mock forbidden error nock("https://control.ably.net") - .delete(`/v1/apps/${mockAppId}`) + .delete(`/v1/apps/${appId}`) .reply(403, { error: "Forbidden" }); const { error } = await runCommand( - ["apps:delete", mockAppId, "--force"], + ["apps:delete", appId, "--force"], import.meta.url, ); expect(error).toBeDefined(); @@ -363,21 +379,27 @@ userEmail = "test@example.com" }); it("should handle 409 conflict error when app has dependencies", async () => { + const mock = getMockConfigManager(); + const accountId = mock.getCurrentAccount()!.accountId!; + const accountName = mock.getCurrentAccount()!.accountName!; + const userEmail = mock.getCurrentAccount()!.userEmail!; + const appId = mock.getCurrentAppId()!; + // Mock the /me endpoint nock("https://control.ably.net") .get("/v1/me") .reply(200, { - account: { id: mockAccountId, name: "Test Account" }, - user: { email: "test@example.com" }, + account: { id: accountId, name: accountName }, + user: { email: userEmail }, }); // Mock the app listing endpoint nock("https://control.ably.net") - .get(`/v1/accounts/${mockAccountId}/apps`) + .get(`/v1/accounts/${accountId}/apps`) .reply(200, [ { - id: mockAppId, - accountId: mockAccountId, + id: appId, + accountId: accountId, name: mockAppName, status: "active", created: Date.now(), @@ -387,15 +409,13 @@ userEmail = "test@example.com" ]); // Mock conflict error (app has dependencies) - nock("https://control.ably.net") - .delete(`/v1/apps/${mockAppId}`) - .reply(409, { - error: "Conflict", - details: "App has active resources that must be deleted first", - }); + nock("https://control.ably.net").delete(`/v1/apps/${appId}`).reply(409, { + error: "Conflict", + details: "App has active resources that must be deleted first", + }); const { error } = await runCommand( - ["apps:delete", mockAppId, "--force"], + ["apps:delete", appId, "--force"], import.meta.url, ); expect(error).toBeDefined(); @@ -406,26 +426,32 @@ userEmail = "test@example.com" describe("current app handling", () => { it("should use current app when no app ID provided", async () => { + const mock = getMockConfigManager(); + const accountId = mock.getCurrentAccount()!.accountId!; + const accountName = mock.getCurrentAccount()!.accountName!; + const userEmail = mock.getCurrentAccount()!.userEmail!; + const appId = mock.getCurrentAppId()!; + // Set environment variable for current app const originalAppId = process.env.ABLY_APP_ID; - process.env.ABLY_APP_ID = mockAppId; + process.env.ABLY_APP_ID = appId; try { // Mock the /me endpoint nock("https://control.ably.net") .get("/v1/me") .reply(200, { - account: { id: mockAccountId, name: "Test Account" }, - user: { email: "test@example.com" }, + account: { id: accountId, name: accountName }, + user: { email: userEmail }, }); // Mock the app listing endpoint for getApp nock("https://control.ably.net") - .get(`/v1/accounts/${mockAccountId}/apps`) + .get(`/v1/accounts/${accountId}/apps`) .reply(200, [ { - id: mockAppId, - accountId: mockAccountId, + id: appId, + accountId: accountId, name: mockAppName, status: "active", created: Date.now(), @@ -435,9 +461,7 @@ userEmail = "test@example.com" ]); // Mock the app deletion endpoint - nock("https://control.ably.net") - .delete(`/v1/apps/${mockAppId}`) - .reply(204); + nock("https://control.ably.net").delete(`/v1/apps/${appId}`).reply(204); const { stdout } = await runCommand( ["apps:delete", "--force"], diff --git a/test/unit/commands/apps/logs/history.test.ts b/test/unit/commands/apps/logs/history.test.ts index b5b864aa..32dd17e8 100644 --- a/test/unit/commands/apps/logs/history.test.ts +++ b/test/unit/commands/apps/logs/history.test.ts @@ -1,42 +1,14 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { runCommand } from "@oclif/test"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; describe("apps:logs:history command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - const mockApiKey = `${mockAppId}.testkey:testsecret`; - let testConfigDir: string; - let originalConfigDir: string; let mockHistory: ReturnType; let mockChannelGet: ReturnType; beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[apps."${mockAppId}"] -apiKey = "${mockApiKey}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRestMock; + } // Setup global mock for Ably REST client mockHistory = vi.fn().mockResolvedValue({ @@ -48,6 +20,7 @@ apiKey = "${mockApiKey}" }); globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRestMock: { channels: { get: mockChannelGet, @@ -57,21 +30,10 @@ apiKey = "${mockApiKey}" }); afterEach(() => { - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } - - // Clean up global mock - globalThis.__TEST_MOCKS__ = undefined; vi.clearAllMocks(); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRestMock; + } }); describe("successful log history retrieval", () => { diff --git a/test/unit/commands/apps/logs/subscribe.test.ts b/test/unit/commands/apps/logs/subscribe.test.ts index 80d3a80c..39d3c3b9 100644 --- a/test/unit/commands/apps/logs/subscribe.test.ts +++ b/test/unit/commands/apps/logs/subscribe.test.ts @@ -1,54 +1,16 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { runCommand } from "@oclif/test"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; describe("apps:logs:subscribe command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - const mockApiKey = `${mockAppId}.testkey:testsecret`; - let testConfigDir: string; - let originalConfigDir: string; - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[apps."${mockAppId}"] -apiKey = "${mockApiKey}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + } }); afterEach(() => { - delete process.env.ABLY_ACCESS_TOKEN; - globalThis.__TEST_MOCKS__ = undefined; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; } }); @@ -87,6 +49,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: { channels: mockChannels, connection: mockConnection, @@ -130,6 +93,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: { channels: mockChannels, connection: mockConnection, diff --git a/test/unit/commands/apps/set-apns-p12.test.ts b/test/unit/commands/apps/set-apns-p12.test.ts index 1279be32..5ceb4bab 100644 --- a/test/unit/commands/apps/set-apns-p12.test.ts +++ b/test/unit/commands/apps/set-apns-p12.test.ts @@ -4,66 +4,42 @@ import nock from "nock"; import { resolve } from "node:path"; import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; +import { getMockConfigManager } from "../../../helpers/mock-config-manager.js"; describe("apps:set-apns-p12 command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - let testConfigDir: string; - let originalConfigDir: string; + let testTempDir: string; let testCertFile: string; beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + // Create temp directory for test certificate file + testTempDir = resolve(tmpdir(), `ably-cli-test-apns-p12-${Date.now()}`); + mkdirSync(testTempDir, { recursive: true, mode: 0o700 }); // Create a fake certificate file - testCertFile = resolve(testConfigDir, "test-cert.p12"); + testCertFile = resolve(testTempDir, "test-cert.p12"); writeFileSync(testCertFile, "fake-certificate-data"); }); afterEach(() => { nock.cleanAll(); - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); + if (existsSync(testTempDir)) { + rmSync(testTempDir, { recursive: true, force: true }); } }); describe("successful certificate upload", () => { it("should upload APNS P12 certificate successfully", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/push/certificate`) + .post(`/v1/apps/${appId}/push/certificate`) .reply(200, { id: "cert-123", - appId: mockAppId, + appId, }); const { stdout } = await runCommand( - ["apps:set-apns-p12", mockAppId, "--certificate", testCertFile], + ["apps:set-apns-p12", appId, "--certificate", testCertFile], import.meta.url, ); @@ -71,17 +47,18 @@ userEmail = "test@example.com" }); it("should upload certificate with password", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/push/certificate`) + .post(`/v1/apps/${appId}/push/certificate`) .reply(200, { id: "cert-123", - appId: mockAppId, + appId, }); const { stdout } = await runCommand( [ "apps:set-apns-p12", - mockAppId, + appId, "--certificate", testCertFile, "--password", @@ -94,17 +71,18 @@ userEmail = "test@example.com" }); it("should upload certificate for sandbox environment", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/push/certificate`) + .post(`/v1/apps/${appId}/push/certificate`) .reply(200, { id: "cert-123", - appId: mockAppId, + appId, }); const { stdout } = await runCommand( [ "apps:set-apns-p12", - mockAppId, + appId, "--certificate", testCertFile, "--use-for-sandbox", @@ -129,8 +107,9 @@ userEmail = "test@example.com" }); it("should require certificate flag", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; const { error } = await runCommand( - ["apps:set-apns-p12", mockAppId], + ["apps:set-apns-p12", appId], import.meta.url, ); @@ -139,10 +118,11 @@ userEmail = "test@example.com" }); it("should error when certificate file does not exist", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; const { error } = await runCommand( [ "apps:set-apns-p12", - mockAppId, + appId, "--certificate", "/nonexistent/path/cert.p12", ], @@ -154,12 +134,13 @@ userEmail = "test@example.com" }); it("should handle API errors", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/push/certificate`) + .post(`/v1/apps/${appId}/push/certificate`) .reply(400, { error: "Invalid certificate" }); const { error } = await runCommand( - ["apps:set-apns-p12", mockAppId, "--certificate", testCertFile], + ["apps:set-apns-p12", appId, "--certificate", testCertFile], import.meta.url, ); @@ -168,12 +149,13 @@ userEmail = "test@example.com" }); it("should handle 401 authentication error", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/push/certificate`) + .post(`/v1/apps/${appId}/push/certificate`) .reply(401, { error: "Unauthorized" }); const { error } = await runCommand( - ["apps:set-apns-p12", mockAppId, "--certificate", testCertFile], + ["apps:set-apns-p12", appId, "--certificate", testCertFile], import.meta.url, ); diff --git a/test/unit/commands/apps/update.test.ts b/test/unit/commands/apps/update.test.ts index 54451405..d2af59b2 100644 --- a/test/unit/commands/apps/update.test.ts +++ b/test/unit/commands/apps/update.test.ts @@ -1,73 +1,34 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { runCommand } from "@oclif/test"; import nock from "nock"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; +import { getMockConfigManager } from "../../../helpers/mock-config-manager.js"; describe("apps:update command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; const mockAppName = "TestApp"; - let testConfigDir: string; - let originalConfigDir: string; beforeEach(() => { - // Set environment variable for access token - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - // Create a temporary config directory for testing - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - // Store original config dir and set test config dir - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - // Create a minimal config file with a default account - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + nock.cleanAll(); }); afterEach(() => { - // Clean up nock interceptors nock.cleanAll(); - delete process.env.ABLY_ACCESS_TOKEN; - - // Restore original config directory - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - // Clean up test config directory - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } }); describe("successful app update", () => { it("should update an app name successfully", async () => { + const mock = getMockConfigManager(); + const accountId = mock.getCurrentAccount()!.accountId!; + const appId = mock.getCurrentAppId()!; const updatedName = "UpdatedAppName"; // Mock the app update endpoint nock("https://control.ably.net") - .patch(`/v1/apps/${mockAppId}`, { + .patch(`/v1/apps/${appId}`, { name: updatedName, }) .reply(200, { - id: mockAppId, - accountId: mockAccountId, + id: appId, + accountId: accountId, name: updatedName, status: "active", created: Date.now(), @@ -76,24 +37,28 @@ userEmail = "test@example.com" }); const { stdout } = await runCommand( - ["apps:update", mockAppId, "--name", updatedName], + ["apps:update", appId, "--name", updatedName], import.meta.url, ); expect(stdout).toContain("App updated successfully"); - expect(stdout).toContain(mockAppId); + expect(stdout).toContain(appId); expect(stdout).toContain(updatedName); }); it("should update TLS only flag successfully", async () => { + const mock = getMockConfigManager(); + const accountId = mock.getCurrentAccount()!.accountId!; + const appId = mock.getCurrentAppId()!; + // Mock the app update endpoint nock("https://control.ably.net") - .patch(`/v1/apps/${mockAppId}`, { + .patch(`/v1/apps/${appId}`, { tlsOnly: true, }) .reply(200, { - id: mockAppId, - accountId: mockAccountId, + id: appId, + accountId: accountId, name: mockAppName, status: "active", created: Date.now(), @@ -102,7 +67,7 @@ userEmail = "test@example.com" }); const { stdout } = await runCommand( - ["apps:update", mockAppId, "--tls-only"], + ["apps:update", appId, "--tls-only"], import.meta.url, ); @@ -111,17 +76,20 @@ userEmail = "test@example.com" }); it("should update both name and TLS only successfully", async () => { + const mock = getMockConfigManager(); + const accountId = mock.getCurrentAccount()!.accountId!; + const appId = mock.getCurrentAppId()!; const updatedName = "UpdatedAppName"; // Mock the app update endpoint nock("https://control.ably.net") - .patch(`/v1/apps/${mockAppId}`, { + .patch(`/v1/apps/${appId}`, { name: updatedName, tlsOnly: true, }) .reply(200, { - id: mockAppId, - accountId: mockAccountId, + id: appId, + accountId: accountId, name: updatedName, status: "active", created: Date.now(), @@ -130,7 +98,7 @@ userEmail = "test@example.com" }); const { stdout } = await runCommand( - ["apps:update", mockAppId, "--name", updatedName, "--tls-only"], + ["apps:update", appId, "--name", updatedName, "--tls-only"], import.meta.url, ); @@ -140,10 +108,14 @@ userEmail = "test@example.com" }); it("should output JSON format when --json flag is used", async () => { + const mock = getMockConfigManager(); + const accountId = mock.getCurrentAccount()!.accountId!; + const appId = mock.getCurrentAppId()!; const updatedName = "UpdatedAppName"; + const mockApp = { - id: mockAppId, - accountId: mockAccountId, + id: appId, + accountId: accountId, name: updatedName, status: "active", created: Date.now(), @@ -153,22 +125,25 @@ userEmail = "test@example.com" // Mock the app update endpoint nock("https://control.ably.net") - .patch(`/v1/apps/${mockAppId}`) + .patch(`/v1/apps/${appId}`) .reply(200, mockApp); const { stdout } = await runCommand( - ["apps:update", mockAppId, "--name", updatedName, "--json"], + ["apps:update", appId, "--name", updatedName, "--json"], import.meta.url, ); const result = JSON.parse(stdout); expect(result).toHaveProperty("app"); - expect(result.app).toHaveProperty("id", mockAppId); + expect(result.app).toHaveProperty("id", appId); expect(result.app).toHaveProperty("name", updatedName); expect(result).toHaveProperty("success", true); }); it("should use custom access token when provided", async () => { + const mock = getMockConfigManager(); + const accountId = mock.getCurrentAccount()!.accountId!; + const appId = mock.getCurrentAppId()!; const customToken = "custom_access_token"; const updatedName = "UpdatedAppName"; @@ -178,10 +153,10 @@ userEmail = "test@example.com" authorization: `Bearer ${customToken}`, }, }) - .patch(`/v1/apps/${mockAppId}`) + .patch(`/v1/apps/${appId}`) .reply(200, { - id: mockAppId, - accountId: mockAccountId, + id: appId, + accountId: accountId, name: updatedName, status: "active", created: Date.now(), @@ -192,7 +167,7 @@ userEmail = "test@example.com" const { stdout } = await runCommand( [ "apps:update", - mockAppId, + appId, "--name", updatedName, "--access-token", @@ -207,8 +182,11 @@ userEmail = "test@example.com" describe("error handling", () => { it("should require at least one update parameter", async () => { + const mock = getMockConfigManager(); + const appId = mock.getCurrentAppId()!; + const { error } = await runCommand( - ["apps:update", mockAppId], + ["apps:update", appId], import.meta.url, ); @@ -217,8 +195,11 @@ userEmail = "test@example.com" }); it("should handle JSON error output when no update parameter provided", async () => { + const mock = getMockConfigManager(); + const appId = mock.getCurrentAppId()!; + const { stdout } = await runCommand( - ["apps:update", mockAppId, "--json"], + ["apps:update", appId, "--json"], import.meta.url, ); @@ -239,13 +220,16 @@ userEmail = "test@example.com" }); it("should handle 401 authentication error", async () => { + const mock = getMockConfigManager(); + const appId = mock.getCurrentAppId()!; + // Mock authentication failure nock("https://control.ably.net") - .patch(`/v1/apps/${mockAppId}`) + .patch(`/v1/apps/${appId}`) .reply(401, { error: "Unauthorized" }); const { error } = await runCommand( - ["apps:update", mockAppId, "--name", "NewName"], + ["apps:update", appId, "--name", "NewName"], import.meta.url, ); @@ -254,13 +238,16 @@ userEmail = "test@example.com" }); it("should handle 403 forbidden error", async () => { + const mock = getMockConfigManager(); + const appId = mock.getCurrentAppId()!; + // Mock forbidden response nock("https://control.ably.net") - .patch(`/v1/apps/${mockAppId}`) + .patch(`/v1/apps/${appId}`) .reply(403, { error: "Forbidden" }); const { error } = await runCommand( - ["apps:update", mockAppId, "--name", "NewName"], + ["apps:update", appId, "--name", "NewName"], import.meta.url, ); @@ -269,13 +256,16 @@ userEmail = "test@example.com" }); it("should handle 404 not found error", async () => { + const mock = getMockConfigManager(); + const appId = mock.getCurrentAppId()!; + // Mock not found response nock("https://control.ably.net") - .patch(`/v1/apps/${mockAppId}`) + .patch(`/v1/apps/${appId}`) .reply(404, { error: "Not Found" }); const { error } = await runCommand( - ["apps:update", mockAppId, "--name", "NewName"], + ["apps:update", appId, "--name", "NewName"], import.meta.url, ); @@ -284,13 +274,16 @@ userEmail = "test@example.com" }); it("should handle 500 server error", async () => { + const mock = getMockConfigManager(); + const appId = mock.getCurrentAppId()!; + // Mock server error nock("https://control.ably.net") - .patch(`/v1/apps/${mockAppId}`) + .patch(`/v1/apps/${appId}`) .reply(500, { error: "Internal Server Error" }); const { error } = await runCommand( - ["apps:update", mockAppId, "--name", "NewName"], + ["apps:update", appId, "--name", "NewName"], import.meta.url, ); @@ -299,13 +292,16 @@ userEmail = "test@example.com" }); it("should handle network errors", async () => { + const mock = getMockConfigManager(); + const appId = mock.getCurrentAppId()!; + // Mock network error nock("https://control.ably.net") - .patch(`/v1/apps/${mockAppId}`) + .patch(`/v1/apps/${appId}`) .replyWithError("Network error"); const { error } = await runCommand( - ["apps:update", mockAppId, "--name", "NewName"], + ["apps:update", appId, "--name", "NewName"], import.meta.url, ); @@ -314,41 +310,46 @@ userEmail = "test@example.com" }); it("should handle JSON error output for API errors", async () => { + const mock = getMockConfigManager(); + const appId = mock.getCurrentAppId()!; + // Mock server error nock("https://control.ably.net") - .patch(`/v1/apps/${mockAppId}`) + .patch(`/v1/apps/${appId}`) .reply(500, { error: "Internal Server Error" }); const { stdout } = await runCommand( - ["apps:update", mockAppId, "--name", "NewName", "--json"], + ["apps:update", appId, "--name", "NewName", "--json"], import.meta.url, ); const result = JSON.parse(stdout); expect(result).toHaveProperty("success", false); expect(result).toHaveProperty("error"); - expect(result).toHaveProperty("appId", mockAppId); + expect(result).toHaveProperty("appId", appId); }); }); describe("output formatting", () => { it("should display APNS sandbox cert status when available", async () => { + const mock = getMockConfigManager(); + const accountId = mock.getCurrentAccount()!.accountId!; + const appId = mock.getCurrentAppId()!; + // Mock the app update endpoint with APNS cert info - nock("https://control.ably.net") - .patch(`/v1/apps/${mockAppId}`) - .reply(200, { - id: mockAppId, - accountId: mockAccountId, - name: mockAppName, - status: "active", - created: Date.now(), - modified: Date.now(), - tlsOnly: false, - apnsUsesSandboxCert: true, - }); + nock("https://control.ably.net").patch(`/v1/apps/${appId}`).reply(200, { + id: appId, + accountId: accountId, + name: mockAppName, + status: "active", + created: Date.now(), + modified: Date.now(), + tlsOnly: false, + apnsUsesSandboxCert: true, + }); const { stdout } = await runCommand( - ["apps:update", mockAppId, "--name", mockAppName], + ["apps:update", appId, "--name", mockAppName], import.meta.url, ); @@ -356,22 +357,24 @@ userEmail = "test@example.com" }); it("should include APNS info in JSON output when available", async () => { + const mock = getMockConfigManager(); + const accountId = mock.getCurrentAccount()!.accountId!; + const appId = mock.getCurrentAppId()!; + // Mock the app update endpoint with APNS cert info - nock("https://control.ably.net") - .patch(`/v1/apps/${mockAppId}`) - .reply(200, { - id: mockAppId, - accountId: mockAccountId, - name: mockAppName, - status: "active", - created: Date.now(), - modified: Date.now(), - tlsOnly: false, - apnsUsesSandboxCert: false, - }); + nock("https://control.ably.net").patch(`/v1/apps/${appId}`).reply(200, { + id: appId, + accountId: accountId, + name: mockAppName, + status: "active", + created: Date.now(), + modified: Date.now(), + tlsOnly: false, + apnsUsesSandboxCert: false, + }); const { stdout } = await runCommand( - ["apps:update", mockAppId, "--name", mockAppName, "--json"], + ["apps:update", appId, "--name", mockAppName, "--json"], import.meta.url, ); diff --git a/test/unit/commands/auth/issue-ably-token.test.ts b/test/unit/commands/auth/issue-ably-token.test.ts index 4749d1fd..479289f7 100644 --- a/test/unit/commands/auth/issue-ably-token.test.ts +++ b/test/unit/commands/auth/issue-ably-token.test.ts @@ -1,61 +1,24 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { runCommand } from "@oclif/test"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; +import { getMockConfigManager } from "../../../helpers/mock-config-manager.js"; describe("auth:issue-ably-token command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - const mockApiKey = `${mockAppId}.testkey:testsecret`; - let testConfigDir: string; - let originalConfigDir: string; - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[accounts.default.apps."${mockAppId}"] -appName = "Test App" -apiKey = "${mockApiKey}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + // Clean up any test mocks from previous tests + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRestMock; + } }); afterEach(() => { - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRestMock; } - - globalThis.__TEST_MOCKS__ = undefined; }); describe("successful token issuance", () => { it("should issue an Ably token successfully", async () => { + const keyId = getMockConfigManager().getKeyId()!; const mockTokenDetails = { token: "mock-ably-token-12345", issued: Date.now(), @@ -65,7 +28,7 @@ apiKey = "${mockApiKey}" }; const mockTokenRequest = { - keyName: `${mockAppId}.testkey`, + keyName: keyId, ttl: 3600000, capability: '{"*":["*"]}', }; @@ -75,12 +38,12 @@ apiKey = "${mockApiKey}" requestToken: vi.fn().mockResolvedValue(mockTokenDetails), }; - globalThis.__TEST_MOCKS__ = { - ablyRestMock: { + if (globalThis.__TEST_MOCKS__) { + globalThis.__TEST_MOCKS__.ablyRestMock = { auth: mockAuth, close: vi.fn(), - }, - }; + }; + } const { stdout } = await runCommand( ["auth:issue-ably-token"], @@ -109,12 +72,12 @@ apiKey = "${mockApiKey}" requestToken: vi.fn().mockResolvedValue(mockTokenDetails), }; - globalThis.__TEST_MOCKS__ = { - ablyRestMock: { + if (globalThis.__TEST_MOCKS__) { + globalThis.__TEST_MOCKS__.ablyRestMock = { auth: mockAuth, close: vi.fn(), - }, - }; + }; + } const { stdout } = await runCommand( ["auth:issue-ably-token", "--capability", customCapability], @@ -141,12 +104,12 @@ apiKey = "${mockApiKey}" requestToken: vi.fn().mockResolvedValue(mockTokenDetails), }; - globalThis.__TEST_MOCKS__ = { - ablyRestMock: { + if (globalThis.__TEST_MOCKS__) { + globalThis.__TEST_MOCKS__.ablyRestMock = { auth: mockAuth, close: vi.fn(), - }, - }; + }; + } const { stdout } = await runCommand( ["auth:issue-ably-token", "--ttl", "7200"], @@ -175,12 +138,12 @@ apiKey = "${mockApiKey}" requestToken: vi.fn().mockResolvedValue(mockTokenDetails), }; - globalThis.__TEST_MOCKS__ = { - ablyRestMock: { + if (globalThis.__TEST_MOCKS__) { + globalThis.__TEST_MOCKS__.ablyRestMock = { auth: mockAuth, close: vi.fn(), - }, - }; + }; + } const { stdout } = await runCommand( ["auth:issue-ably-token", "--client-id", customClientId], @@ -208,12 +171,12 @@ apiKey = "${mockApiKey}" requestToken: vi.fn().mockResolvedValue(mockTokenDetails), }; - globalThis.__TEST_MOCKS__ = { - ablyRestMock: { + if (globalThis.__TEST_MOCKS__) { + globalThis.__TEST_MOCKS__.ablyRestMock = { auth: mockAuth, close: vi.fn(), - }, - }; + }; + } const { stdout } = await runCommand( ["auth:issue-ably-token", "--client-id", "none"], @@ -242,12 +205,12 @@ apiKey = "${mockApiKey}" requestToken: vi.fn().mockResolvedValue(mockTokenDetails), }; - globalThis.__TEST_MOCKS__ = { - ablyRestMock: { + if (globalThis.__TEST_MOCKS__) { + globalThis.__TEST_MOCKS__.ablyRestMock = { auth: mockAuth, close: vi.fn(), - }, - }; + }; + } const { stdout } = await runCommand( ["auth:issue-ably-token", "--token-only"], @@ -273,12 +236,12 @@ apiKey = "${mockApiKey}" requestToken: vi.fn().mockResolvedValue(mockTokenDetails), }; - globalThis.__TEST_MOCKS__ = { - ablyRestMock: { + if (globalThis.__TEST_MOCKS__) { + globalThis.__TEST_MOCKS__.ablyRestMock = { auth: mockAuth, close: vi.fn(), - }, - }; + }; + } const { stdout } = await runCommand( ["auth:issue-ably-token", "--json"], @@ -307,12 +270,12 @@ apiKey = "${mockApiKey}" requestToken: vi.fn(), }; - globalThis.__TEST_MOCKS__ = { - ablyRestMock: { + if (globalThis.__TEST_MOCKS__) { + globalThis.__TEST_MOCKS__.ablyRestMock = { auth: mockAuth, close: vi.fn(), - }, - }; + }; + } const { error } = await runCommand( ["auth:issue-ably-token"], @@ -324,17 +287,9 @@ apiKey = "${mockApiKey}" }); it("should not produce token output when app configuration is missing", async () => { - // Remove app from config - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + // Use mock config manager to clear app configuration + const mock = getMockConfigManager(); + mock.setCurrentAppIdForAccount(undefined); const { stdout } = await runCommand( ["auth:issue-ably-token"], @@ -348,6 +303,7 @@ userEmail = "test@example.com" describe("command arguments and flags", () => { it("should accept --app flag to specify app", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; const mockTokenDetails = { token: "mock-ably-token-app", issued: Date.now(), @@ -361,15 +317,15 @@ userEmail = "test@example.com" requestToken: vi.fn().mockResolvedValue(mockTokenDetails), }; - globalThis.__TEST_MOCKS__ = { - ablyRestMock: { + if (globalThis.__TEST_MOCKS__) { + globalThis.__TEST_MOCKS__.ablyRestMock = { auth: mockAuth, close: vi.fn(), - }, - }; + }; + } const { stdout } = await runCommand( - ["auth:issue-ably-token", "--app", mockAppId], + ["auth:issue-ably-token", "--app", appId], import.meta.url, ); diff --git a/test/unit/commands/auth/issue-jwt-token.test.ts b/test/unit/commands/auth/issue-jwt-token.test.ts index a52c6e18..2ad108fc 100644 --- a/test/unit/commands/auth/issue-jwt-token.test.ts +++ b/test/unit/commands/auth/issue-jwt-token.test.ts @@ -1,62 +1,14 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect } from "vitest"; import { runCommand } from "@oclif/test"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; import jwt from "jsonwebtoken"; +import { getMockConfigManager } from "../../../helpers/mock-config-manager.js"; describe("auth:issue-jwt-token command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - const mockKeyId = `${mockAppId}.testkey`; - const mockKeySecret = "testsecret"; - const mockApiKey = `${mockKeyId}:${mockKeySecret}`; - let testConfigDir: string; - let originalConfigDir: string; - - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[accounts.default.apps."${mockAppId}"] -appName = "Test App" -apiKey = "${mockApiKey}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); - }); - - afterEach(() => { - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } - }); - describe("successful JWT token issuance", () => { it("should issue a JWT token successfully", async () => { + const mockConfig = getMockConfigManager(); + const appId = mockConfig.getCurrentAppId()!; + const keyId = mockConfig.getKeyId()!; const { stdout } = await runCommand( ["auth:issue-jwt-token"], import.meta.url, @@ -65,13 +17,17 @@ apiKey = "${mockApiKey}" expect(stdout).toContain("Generated Ably JWT Token"); expect(stdout).toContain("Token:"); expect(stdout).toContain("Type: JWT"); - expect(stdout).toContain(`App ID: ${mockAppId}`); - expect(stdout).toContain(`Key ID: ${mockKeyId}`); + expect(stdout).toContain(`App ID: ${appId}`); + expect(stdout).toContain(`Key ID: ${keyId}`); expect(stdout).toContain("Issued:"); expect(stdout).toContain("Expires:"); }); it("should generate a valid JWT token", async () => { + const mockConfig = getMockConfigManager(); + const appId = mockConfig.getCurrentAppId()!; + const apiKey = mockConfig.getApiKey()!; + const mockKeySecret = apiKey.split(":")[1]; const { stdout } = await runCommand( ["auth:issue-jwt-token", "--token-only"], import.meta.url, @@ -84,11 +40,13 @@ apiKey = "${mockApiKey}" const decoded = jwt.verify(token, mockKeySecret, { algorithms: ["HS256"], }); - expect(decoded).toHaveProperty("x-ably-appId", mockAppId); + expect(decoded).toHaveProperty("x-ably-appId", appId); expect(decoded).toHaveProperty("x-ably-capability"); }); it("should issue a token with custom capability", async () => { + const apiKey = getMockConfigManager().getApiKey()!; + const mockKeySecret = apiKey.split(":")[1]; const customCapability = '{"chat:*":["publish","subscribe"]}'; const { stdout } = await runCommand( @@ -112,6 +70,8 @@ apiKey = "${mockApiKey}" }); it("should issue a token with custom TTL", async () => { + const apiKey = getMockConfigManager().getApiKey()!; + const mockKeySecret = apiKey.split(":")[1]; const ttl = 7200; // 2 hours const { stdout } = await runCommand( @@ -129,6 +89,8 @@ apiKey = "${mockApiKey}" }); it("should issue a token with custom client ID", async () => { + const apiKey = getMockConfigManager().getApiKey()!; + const mockKeySecret = apiKey.split(":")[1]; const customClientId = "my-custom-client"; const { stdout } = await runCommand( @@ -145,6 +107,8 @@ apiKey = "${mockApiKey}" }); it("should issue a token with no client ID when 'none' is specified", async () => { + const apiKey = getMockConfigManager().getApiKey()!; + const mockKeySecret = apiKey.split(":")[1]; const { stdout } = await runCommand( ["auth:issue-jwt-token", "--client-id", "none", "--token-only"], import.meta.url, @@ -170,6 +134,9 @@ apiKey = "${mockApiKey}" }); it("should output JSON format when --json flag is used", async () => { + const mockConfig = getMockConfigManager(); + const appId = mockConfig.getCurrentAppId()!; + const keyId = mockConfig.getKeyId()!; const { stdout } = await runCommand( ["auth:issue-jwt-token", "--json"], import.meta.url, @@ -177,14 +144,16 @@ apiKey = "${mockApiKey}" const result = JSON.parse(stdout); expect(result).toHaveProperty("token"); - expect(result).toHaveProperty("appId", mockAppId); - expect(result).toHaveProperty("keyId", mockKeyId); + expect(result).toHaveProperty("appId", appId); + expect(result).toHaveProperty("keyId", keyId); expect(result).toHaveProperty("type", "jwt"); expect(result).toHaveProperty("capability"); expect(result).toHaveProperty("ttl"); }); it("should generate token with default capability of all permissions", async () => { + const apiKey = getMockConfigManager().getApiKey()!; + const mockKeySecret = apiKey.split(":")[1]; const { stdout } = await runCommand( ["auth:issue-jwt-token", "--token-only"], import.meta.url, @@ -200,6 +169,8 @@ apiKey = "${mockApiKey}" }); it("should generate token with default TTL of 1 hour", async () => { + const apiKey = getMockConfigManager().getApiKey()!; + const mockKeySecret = apiKey.split(":")[1]; const { stdout } = await runCommand( ["auth:issue-jwt-token", "--token-only"], import.meta.url, @@ -227,17 +198,9 @@ apiKey = "${mockApiKey}" }); it("should not produce token output when app configuration is missing", async () => { - // Remove app from config - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + // Use mock config manager to clear app configuration + const mock = getMockConfigManager(); + mock.setCurrentAppIdForAccount(undefined); const { stdout } = await runCommand( ["auth:issue-jwt-token"], @@ -251,8 +214,9 @@ userEmail = "test@example.com" describe("command arguments and flags", () => { it("should accept --app flag to specify app", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; const { stdout } = await runCommand( - ["auth:issue-jwt-token", "--app", mockAppId], + ["auth:issue-jwt-token", "--app", appId], import.meta.url, ); diff --git a/test/unit/commands/auth/keys/create.test.ts b/test/unit/commands/auth/keys/create.test.ts index 1aa28096..cea74d14 100644 --- a/test/unit/commands/auth/keys/create.test.ts +++ b/test/unit/commands/auth/keys/create.test.ts @@ -1,65 +1,38 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { runCommand } from "@oclif/test"; import nock from "nock"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; +import { getMockConfigManager } from "../../../../helpers/mock-config-manager.js"; describe("auth:keys:create command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "MockAccount"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; const mockKeyName = "TestKey"; const mockKeyId = "test-key-id"; const mockKeySecret = "test-key-secret"; - let testConfigDir: string; - let originalConfigDir: string; beforeEach(() => { - // Set environment variable for access token - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - // Create a temporary config directory for testing - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - // Store original config dir and set test config dir - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - // Create a minimal config file with a default account - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + nock.cleanAll(); + // Set up config without currentAppId to test "no app" error + const mock = getMockConfigManager(); + mock.setCurrentAppIdForAccount(undefined); }); afterEach(() => { - // Clean up nock interceptors nock.cleanAll(); - delete process.env.ABLY_ACCESS_TOKEN; - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; }); describe("successful key creation", () => { it("should create a key successfully", async () => { + const appId = getMockConfigManager().getRegisteredAppId(); // Mock the key creation endpoint nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/keys`, { + .post(`/v1/apps/${appId}/keys`, { name: mockKeyName, capability: { "*": ["*"] }, }) .reply(201, { id: mockKeyId, - appId: mockAppId, + appId, name: mockKeyName, - key: `${mockAppId}.${mockKeyId}:${mockKeySecret}`, + key: `${appId}.${mockKeyId}:${mockKeySecret}`, capability: { "*": ["*"] }, created: Date.now(), modified: Date.now(), @@ -68,7 +41,7 @@ userEmail = "test@example.com" }); const { stdout } = await runCommand( - ["auth:keys:create", "--name", `"${mockKeyName}"`, "--app", mockAppId], + ["auth:keys:create", "--name", `"${mockKeyName}"`, "--app", appId], import.meta.url, ); @@ -78,9 +51,10 @@ userEmail = "test@example.com" }); it("should create a key with custom capabilities", async () => { + const appId = getMockConfigManager().getRegisteredAppId(); // Mock the key creation endpoint with custom capabilities nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/keys`, { + .post(`/v1/apps/${appId}/keys`, { name: mockKeyName, capability: { channel1: ["publish", "subscribe"], @@ -89,9 +63,9 @@ userEmail = "test@example.com" }) .reply(201, { id: mockKeyId, - appId: mockAppId, + appId, name: mockKeyName, - key: `${mockAppId}.${mockKeyId}:${mockKeySecret}`, + key: `${appId}.${mockKeyId}:${mockKeySecret}`, capability: { channel1: ["publish", "subscribe"], channel2: ["history"], @@ -108,7 +82,7 @@ userEmail = "test@example.com" "--name", `"${mockKeyName}"`, "--app", - mockAppId, + appId, "--capabilities", '{"channel1":["publish","subscribe"],"channel2":["history"]}', ], @@ -122,11 +96,12 @@ userEmail = "test@example.com" }); it("should output JSON format when --json flag is used", async () => { + const appId = getMockConfigManager().getRegisteredAppId(); const mockKey = { id: mockKeyId, - appId: mockAppId, + appId, name: mockKeyName, - key: `${mockAppId}.${mockKeyId}:${mockKeySecret}`, + key: `${appId}.${mockKeyId}:${mockKeySecret}`, capability: { "*": ["*"] }, created: Date.now(), modified: Date.now(), @@ -136,7 +111,7 @@ userEmail = "test@example.com" // Mock the key creation endpoint nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/keys`) + .post(`/v1/apps/${appId}/keys`) .reply(201, mockKey); const { stdout } = await runCommand( @@ -145,7 +120,7 @@ userEmail = "test@example.com" "--name", `"${mockKeyName}"`, "--app", - mockAppId, + appId, "--json", ], import.meta.url, @@ -160,6 +135,7 @@ userEmail = "test@example.com" }); it("should use custom access token when provided", async () => { + const appId = getMockConfigManager().getRegisteredAppId(); const customToken = "custom_access_token"; // Mock the key creation endpoint with custom token @@ -168,12 +144,12 @@ userEmail = "test@example.com" authorization: `Bearer ${customToken}`, }, }) - .post(`/v1/apps/${mockAppId}/keys`) + .post(`/v1/apps/${appId}/keys`) .reply(201, { id: mockKeyId, - appId: mockAppId, + appId, name: mockKeyName, - key: `${mockAppId}.${mockKeyId}:${mockKeySecret}`, + key: `${appId}.${mockKeyId}:${mockKeySecret}`, capability: { "*": ["*"] }, created: Date.now(), modified: Date.now(), @@ -187,7 +163,7 @@ userEmail = "test@example.com" "--name", `"${mockKeyName}"`, "--app", - mockAppId, + appId, "--access-token", "custom_access_token", ], @@ -200,8 +176,9 @@ userEmail = "test@example.com" describe("parameter validation", () => { it("should require name parameter", async () => { + const appId = getMockConfigManager().getRegisteredAppId(); const { error } = await runCommand( - ["auth:keys:create", "--app", mockAppId], + ["auth:keys:create", "--app", appId], import.meta.url, ); expect(error).toBeDefined(); @@ -220,9 +197,10 @@ userEmail = "test@example.com" }); it("should handle invalid capabilities JSON", async () => { + const appId = getMockConfigManager().getRegisteredAppId(); // Mock the key creation endpoint with invalid capabilities nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/keys`) + .post(`/v1/apps/${appId}/keys`) .reply(400, { error: "Invalid capabilities format", }); @@ -233,7 +211,7 @@ userEmail = "test@example.com" "--name", `"${mockKeyName}"`, "--app", - mockAppId, + appId, "--capabilities", "invalid-json", ], @@ -247,13 +225,14 @@ userEmail = "test@example.com" describe("error handling", () => { it("should handle 401 authentication error", async () => { + const appId = getMockConfigManager().getRegisteredAppId(); // Mock authentication failure nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/keys`) + .post(`/v1/apps/${appId}/keys`) .reply(401, { error: "Unauthorized" }); const { error } = await runCommand( - ["auth:keys:create", "--name", `"${mockKeyName}"`, "--app", mockAppId], + ["auth:keys:create", "--name", `"${mockKeyName}"`, "--app", appId], import.meta.url, ); expect(error).toBeDefined(); @@ -262,13 +241,14 @@ userEmail = "test@example.com" }); it("should handle 403 forbidden error", async () => { + const appId = getMockConfigManager().getRegisteredAppId(); // Mock forbidden response nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/keys`) + .post(`/v1/apps/${appId}/keys`) .reply(403, { error: "Forbidden" }); const { error } = await runCommand( - ["auth:keys:create", "--name", `"${mockKeyName}"`, "--app", mockAppId], + ["auth:keys:create", "--name", `"${mockKeyName}"`, "--app", appId], import.meta.url, ); expect(error).toBeDefined(); @@ -277,13 +257,14 @@ userEmail = "test@example.com" }); it("should handle 404 not found error", async () => { + const appId = getMockConfigManager().getRegisteredAppId(); // Mock not found response (app doesn't exist) nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/keys`) + .post(`/v1/apps/${appId}/keys`) .reply(404, { error: "App not found" }); const { error } = await runCommand( - ["auth:keys:create", "--name", `"${mockKeyName}"`, "--app", mockAppId], + ["auth:keys:create", "--name", `"${mockKeyName}"`, "--app", appId], import.meta.url, ); expect(error).toBeDefined(); @@ -292,13 +273,14 @@ userEmail = "test@example.com" }); it("should handle 500 server error", async () => { + const appId = getMockConfigManager().getRegisteredAppId(); // Mock server error nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/keys`) + .post(`/v1/apps/${appId}/keys`) .reply(500, { error: "Internal Server Error" }); const { error } = await runCommand( - ["auth:keys:create", "--name", `"${mockKeyName}"`, "--app", mockAppId], + ["auth:keys:create", "--name", `"${mockKeyName}"`, "--app", appId], import.meta.url, ); expect(error).toBeDefined(); @@ -307,13 +289,14 @@ userEmail = "test@example.com" }); it("should handle network errors", async () => { + const appId = getMockConfigManager().getRegisteredAppId(); // Mock network error nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/keys`) + .post(`/v1/apps/${appId}/keys`) .replyWithError("Network error"); const { error } = await runCommand( - ["auth:keys:create", "--name", `"${mockKeyName}"`, "--app", mockAppId], + ["auth:keys:create", "--name", `"${mockKeyName}"`, "--app", appId], import.meta.url, ); expect(error).toBeDefined(); @@ -322,16 +305,17 @@ userEmail = "test@example.com" }); it("should handle validation errors from API", async () => { + const appId = getMockConfigManager().getRegisteredAppId(); // Mock validation error nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/keys`) + .post(`/v1/apps/${appId}/keys`) .reply(400, { error: "Validation failed", details: "Key name already exists", }); const { error } = await runCommand( - ["auth:keys:create", "--name", `"${mockKeyName}"`, "--app", mockAppId], + ["auth:keys:create", "--name", `"${mockKeyName}"`, "--app", appId], import.meta.url, ); expect(error).toBeDefined(); @@ -340,13 +324,14 @@ userEmail = "test@example.com" }); it("should handle rate limit errors", async () => { + const appId = getMockConfigManager().getRegisteredAppId(); // Mock rate limit error nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/keys`) + .post(`/v1/apps/${appId}/keys`) .reply(429, { error: "Rate limit exceeded" }); const { error } = await runCommand( - ["auth:keys:create", "--name", `"${mockKeyName}"`, "--app", mockAppId], + ["auth:keys:create", "--name", `"${mockKeyName}"`, "--app", appId], import.meta.url, ); expect(error).toBeDefined(); @@ -357,17 +342,18 @@ userEmail = "test@example.com" describe("capability configurations", () => { it("should create a publish-only key", async () => { + const appId = getMockConfigManager().getRegisteredAppId(); // Mock the key creation endpoint with publish-only capabilities nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/keys`, { + .post(`/v1/apps/${appId}/keys`, { name: mockKeyName, capability: { "channel:*": ["publish"] }, }) .reply(201, { id: mockKeyId, - appId: mockAppId, + appId, name: mockKeyName, - key: `${mockAppId}.${mockKeyId}:${mockKeySecret}`, + key: `${appId}.${mockKeyId}:${mockKeySecret}`, capability: { "channel:*": ["publish"] }, created: Date.now(), modified: Date.now(), @@ -381,7 +367,7 @@ userEmail = "test@example.com" "--name", `"${mockKeyName}"`, "--app", - mockAppId, + appId, "--capabilities", '{"channel:*":["publish"]}', ], @@ -393,9 +379,10 @@ userEmail = "test@example.com" }); it("should create a key with mixed capabilities", async () => { + const appId = getMockConfigManager().getRegisteredAppId(); // Mock the key creation endpoint with subscribe-only capabilities nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/keys`, { + .post(`/v1/apps/${appId}/keys`, { name: mockKeyName, capability: { "channel:chat-*": ["subscribe"], @@ -404,9 +391,9 @@ userEmail = "test@example.com" }) .reply(201, { id: mockKeyId, - appId: mockAppId, + appId, name: mockKeyName, - key: `${mockAppId}.${mockKeyId}:${mockKeySecret}`, + key: `${appId}.${mockKeyId}:${mockKeySecret}`, capability: { "channel:chat-*": ["subscribe"], "channel:updates": ["publish"], @@ -423,7 +410,7 @@ userEmail = "test@example.com" "--name", `"${mockKeyName}"`, "--app", - mockAppId, + appId, "--capabilities", '{"channel:chat-*":["subscribe"],"channel:updates":["publish"]}', ], diff --git a/test/unit/commands/auth/keys/current.test.ts b/test/unit/commands/auth/keys/current.test.ts index b196f06c..52c3617b 100644 --- a/test/unit/commands/auth/keys/current.test.ts +++ b/test/unit/commands/auth/keys/current.test.ts @@ -1,114 +1,37 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect } from "vitest"; import { runCommand } from "@oclif/test"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; +import { getMockConfigManager } from "../../../../helpers/mock-config-manager.js"; describe("auth:keys:current command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - const mockApiKey = `${mockAppId}.testkey:testsecret`; - let testConfigDir: string; - let originalConfigDir: string; - - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - }); - - afterEach(() => { - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } - }); - describe("successful key display", () => { it("should display the current API key", async () => { - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[accounts.default.apps."${mockAppId}"] -apiKey = "${mockApiKey}" -keyId = "${mockAppId}.testkey" -keyName = "Test Key" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); - + const mockConfig = getMockConfigManager(); + const keyId = mockConfig.getKeyId()!; + const apiKey = mockConfig.getApiKey()!; const { stdout } = await runCommand( ["auth:keys:current"], import.meta.url, ); - expect(stdout).toContain(`API Key: ${mockAppId}.testkey`); - expect(stdout).toContain(`Key Value: ${mockApiKey}`); + expect(stdout).toContain(`API Key: ${keyId}`); + expect(stdout).toContain(`Key Value: ${apiKey}`); }); it("should display account and app information", async () => { - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[accounts.default.apps."${mockAppId}"] -apiKey = "${mockApiKey}" -keyId = "${mockAppId}.testkey" -keyName = "Test Key" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); - + const mockConfig = getMockConfigManager(); + const accountName = mockConfig.getCurrentAccount()!.accountName!; + const appId = mockConfig.getCurrentAppId()!; + const appName = mockConfig.getAppName(appId)!; const { stdout } = await runCommand( ["auth:keys:current"], import.meta.url, ); - expect(stdout).toContain("Account: Test Account"); - expect(stdout).toContain(`App: ${mockAppId}`); + expect(stdout).toContain(`Account: ${accountName}`); + expect(stdout).toContain(`App: ${appName} (${appId})`); }); it("should output JSON format when --json flag is used", async () => { - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[accounts.default.apps."${mockAppId}"] -apiKey = "${mockApiKey}" -keyId = "${mockAppId}.testkey" -keyName = "Test Key" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); - const { stdout } = await runCommand( ["auth:keys:current", "--json"], import.meta.url, @@ -123,21 +46,6 @@ keyName = "Test Key" describe("error handling", () => { it("should reject unknown flags", async () => { - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[accounts.default.apps."${mockAppId}"] -apiKey = "${mockApiKey}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); - const { error } = await runCommand( ["auth:keys:current", "--unknown-flag-xyz"], import.meta.url, @@ -148,30 +56,15 @@ apiKey = "${mockApiKey}" }); it("should accept --app flag to specify a different app", async () => { - // Test that --app flag is accepted even with a different app ID - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[accounts.default.apps."${mockAppId}"] -apiKey = "${mockApiKey}" -keyId = "${mockAppId}.testkey" -keyName = "Test Key" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); - + const mockConfig = getMockConfigManager(); + const appId = mockConfig.getCurrentAppId()!; + const keyId = mockConfig.getKeyId()!; const { stdout } = await runCommand( - ["auth:keys:current", "--app", mockAppId], + ["auth:keys:current", "--app", appId], import.meta.url, ); - expect(stdout).toContain(`API Key: ${mockAppId}.testkey`); + expect(stdout).toContain(`API Key: ${keyId}`); }); }); }); diff --git a/test/unit/commands/auth/keys/get.test.ts b/test/unit/commands/auth/keys/get.test.ts index 874d6956..9d219393 100644 --- a/test/unit/commands/auth/keys/get.test.ts +++ b/test/unit/commands/auth/keys/get.test.ts @@ -1,115 +1,82 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { runCommand } from "@oclif/test"; import nock from "nock"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; +import { getMockConfigManager } from "../../../../helpers/mock-config-manager.js"; describe("auth:keys:get command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; const mockKeyId = "testkey"; - let testConfigDir: string; - let originalConfigDir: string; beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + nock.cleanAll(); }); afterEach(() => { nock.cleanAll(); - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } }); describe("successful key retrieval", () => { it("should get key details by full key name (APP_ID.KEY_ID)", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/keys/${mockKeyId}`) + .get(`/v1/apps/${appId}/keys/${mockKeyId}`) .reply(200, { id: mockKeyId, - appId: mockAppId, + appId, name: "Test Key", - key: `${mockAppId}.${mockKeyId}:secret`, + key: `${appId}.${mockKeyId}:secret`, capability: { "*": ["publish", "subscribe"] }, created: Date.now(), modified: Date.now(), }); const { stdout } = await runCommand( - ["auth:keys:get", `${mockAppId}.${mockKeyId}`], + ["auth:keys:get", `${appId}.${mockKeyId}`], import.meta.url, ); - expect(stdout).toContain(`Key Name: ${mockAppId}.${mockKeyId}`); + expect(stdout).toContain(`Key Name: ${appId}.${mockKeyId}`); expect(stdout).toContain("Key Label: Test Key"); }); it("should get key details with --app flag", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/keys/${mockKeyId}`) + .get(`/v1/apps/${appId}/keys/${mockKeyId}`) .reply(200, { id: mockKeyId, - appId: mockAppId, + appId, name: "Test Key", - key: `${mockAppId}.${mockKeyId}:secret`, + key: `${appId}.${mockKeyId}:secret`, capability: { "*": ["publish", "subscribe"] }, created: Date.now(), modified: Date.now(), }); const { stdout } = await runCommand( - ["auth:keys:get", mockKeyId, "--app", mockAppId], + ["auth:keys:get", mockKeyId, "--app", appId], import.meta.url, ); - expect(stdout).toContain(`Key Name: ${mockAppId}.${mockKeyId}`); + expect(stdout).toContain(`Key Name: ${appId}.${mockKeyId}`); expect(stdout).toContain("Key Label: Test Key"); }); it("should output JSON format when --json flag is used", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/keys/${mockKeyId}`) + .get(`/v1/apps/${appId}/keys/${mockKeyId}`) .reply(200, { id: mockKeyId, - appId: mockAppId, + appId, name: "Test Key", - key: `${mockAppId}.${mockKeyId}:secret`, + key: `${appId}.${mockKeyId}:secret`, capability: { "*": ["publish", "subscribe"] }, created: Date.now(), modified: Date.now(), }); const { stdout } = await runCommand( - ["auth:keys:get", `${mockAppId}.${mockKeyId}`, "--json"], + ["auth:keys:get", `${appId}.${mockKeyId}`, "--json"], import.meta.url, ); @@ -129,12 +96,13 @@ currentAppId = "${mockAppId}" }); it("should handle 404 key not found", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/keys/nonexistent`) + .get(`/v1/apps/${appId}/keys/nonexistent`) .reply(404, { error: "Key not found" }); const { error } = await runCommand( - ["auth:keys:get", `${mockAppId}.nonexistent`], + ["auth:keys:get", `${appId}.nonexistent`], import.meta.url, ); @@ -143,12 +111,13 @@ currentAppId = "${mockAppId}" }); it("should handle 401 authentication error", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/keys/${mockKeyId}`) + .get(`/v1/apps/${appId}/keys/${mockKeyId}`) .reply(401, { error: "Unauthorized" }); const { error } = await runCommand( - ["auth:keys:get", `${mockAppId}.${mockKeyId}`], + ["auth:keys:get", `${appId}.${mockKeyId}`], import.meta.url, ); diff --git a/test/unit/commands/auth/keys/list.test.ts b/test/unit/commands/auth/keys/list.test.ts index a93e46c6..3c374774 100644 --- a/test/unit/commands/auth/keys/list.test.ts +++ b/test/unit/commands/auth/keys/list.test.ts @@ -1,73 +1,37 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { runCommand } from "@oclif/test"; import nock from "nock"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; +import { getMockConfigManager } from "../../../../helpers/mock-config-manager.js"; describe("auth:keys:list command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - let testConfigDir: string; - let originalConfigDir: string; - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + nock.cleanAll(); }); afterEach(() => { nock.cleanAll(); - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } }); describe("successful key listing", () => { it("should list all keys for the current app", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/keys`) + .get(`/v1/apps/${appId}/keys`) .reply(200, [ { id: "key1", - appId: mockAppId, + appId, name: "Key One", - key: `${mockAppId}.key1:secret1`, + key: `${appId}.key1:secret1`, capability: { "*": ["publish", "subscribe"] }, created: Date.now(), modified: Date.now(), }, { id: "key2", - appId: mockAppId, + appId, name: "Key Two", - key: `${mockAppId}.key2:secret2`, + key: `${appId}.key2:secret2`, capability: { "*": ["subscribe"] }, created: Date.now(), modified: Date.now(), @@ -76,21 +40,22 @@ currentAppId = "${mockAppId}" const { stdout } = await runCommand(["auth:keys:list"], import.meta.url); - expect(stdout).toContain(`Key Name: ${mockAppId}.key1`); + expect(stdout).toContain(`Key Name: ${appId}.key1`); expect(stdout).toContain("Key Label: Key One"); - expect(stdout).toContain(`Key Name: ${mockAppId}.key2`); + expect(stdout).toContain(`Key Name: ${appId}.key2`); expect(stdout).toContain("Key Label: Key Two"); }); it("should list keys with --app flag", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/keys`) + .get(`/v1/apps/${appId}/keys`) .reply(200, [ { id: "key1", - appId: mockAppId, + appId, name: "Test Key", - key: `${mockAppId}.key1:secret`, + key: `${appId}.key1:secret`, capability: { "*": ["publish"] }, created: Date.now(), modified: Date.now(), @@ -98,17 +63,18 @@ currentAppId = "${mockAppId}" ]); const { stdout } = await runCommand( - ["auth:keys:list", "--app", mockAppId], + ["auth:keys:list", "--app", appId], import.meta.url, ); - expect(stdout).toContain(`Key Name: ${mockAppId}.key1`); + expect(stdout).toContain(`Key Name: ${appId}.key1`); expect(stdout).toContain("Key Label: Test Key"); }); it("should show message when no keys found", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/keys`) + .get(`/v1/apps/${appId}/keys`) .reply(200, []); const { stdout } = await runCommand(["auth:keys:list"], import.meta.url); @@ -117,14 +83,15 @@ currentAppId = "${mockAppId}" }); it("should output JSON format when --json flag is used", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/keys`) + .get(`/v1/apps/${appId}/keys`) .reply(200, [ { id: "key1", - appId: mockAppId, + appId, name: "Test Key", - key: `${mockAppId}.key1:secret`, + key: `${appId}.key1:secret`, capability: { "*": ["publish", "subscribe"] }, created: Date.now(), modified: Date.now(), @@ -145,16 +112,8 @@ currentAppId = "${mockAppId}" describe("error handling", () => { it("should error when no app is selected", async () => { - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + const mock = getMockConfigManager(); + mock.setCurrentAppIdForAccount(undefined); const { error } = await runCommand(["auth:keys:list"], import.meta.url); @@ -163,8 +122,9 @@ userEmail = "test@example.com" }); it("should handle 401 authentication error", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/keys`) + .get(`/v1/apps/${appId}/keys`) .reply(401, { error: "Unauthorized" }); const { error } = await runCommand(["auth:keys:list"], import.meta.url); @@ -174,8 +134,9 @@ userEmail = "test@example.com" }); it("should handle network errors", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/keys`) + .get(`/v1/apps/${appId}/keys`) .replyWithError("Network error"); const { error } = await runCommand(["auth:keys:list"], import.meta.url); diff --git a/test/unit/commands/auth/keys/revoke.test.ts b/test/unit/commands/auth/keys/revoke.test.ts index c2f8da57..e4d6de0d 100644 --- a/test/unit/commands/auth/keys/revoke.test.ts +++ b/test/unit/commands/auth/keys/revoke.test.ts @@ -1,65 +1,30 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { runCommand } from "@oclif/test"; import nock from "nock"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; +import { getMockConfigManager } from "../../../../helpers/mock-config-manager.js"; describe("auth:keys:revoke command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; const mockKeyId = "testkey"; - let testConfigDir: string; - let originalConfigDir: string; beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + nock.cleanAll(); }); afterEach(() => { nock.cleanAll(); - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } }); describe("successful key revocation", () => { it("should display key info before revocation", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; // Mock get key details nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/keys/${mockKeyId}`) + .get(`/v1/apps/${appId}/keys/${mockKeyId}`) .reply(200, { id: mockKeyId, - appId: mockAppId, + appId, name: "Test Key", - key: `${mockAppId}.${mockKeyId}:secret`, + key: `${appId}.${mockKeyId}:secret`, capability: { "*": ["publish", "subscribe"] }, created: Date.now(), modified: Date.now(), @@ -67,63 +32,65 @@ currentAppId = "${mockAppId}" // Mock revoke key nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/keys/${mockKeyId}/revoke`) + .post(`/v1/apps/${appId}/keys/${mockKeyId}/revoke`) .reply(200, {}); const { stdout } = await runCommand( - ["auth:keys:revoke", `${mockAppId}.${mockKeyId}`, "--force"], + ["auth:keys:revoke", `${appId}.${mockKeyId}`, "--force"], import.meta.url, ); - expect(stdout).toContain(`Key Name: ${mockAppId}.${mockKeyId}`); + expect(stdout).toContain(`Key Name: ${appId}.${mockKeyId}`); expect(stdout).toContain("Key Label: Test Key"); }); it("should revoke key with --app flag", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/keys/${mockKeyId}`) + .get(`/v1/apps/${appId}/keys/${mockKeyId}`) .reply(200, { id: mockKeyId, - appId: mockAppId, + appId, name: "Test Key", - key: `${mockAppId}.${mockKeyId}:secret`, + key: `${appId}.${mockKeyId}:secret`, capability: { "*": ["publish"] }, created: Date.now(), modified: Date.now(), }); nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/keys/${mockKeyId}/revoke`) + .post(`/v1/apps/${appId}/keys/${mockKeyId}/revoke`) .reply(200, {}); const { stdout } = await runCommand( - ["auth:keys:revoke", mockKeyId, "--app", mockAppId, "--force"], + ["auth:keys:revoke", mockKeyId, "--app", appId, "--force"], import.meta.url, ); - expect(stdout).toContain(`Key Name: ${mockAppId}.${mockKeyId}`); + expect(stdout).toContain(`Key Name: ${appId}.${mockKeyId}`); expect(stdout).toContain("Key Label: Test Key"); }); it("should output JSON format when --json flag is used", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/keys/${mockKeyId}`) + .get(`/v1/apps/${appId}/keys/${mockKeyId}`) .reply(200, { id: mockKeyId, - appId: mockAppId, + appId, name: "Test Key", - key: `${mockAppId}.${mockKeyId}:secret`, + key: `${appId}.${mockKeyId}:secret`, capability: { "*": ["publish", "subscribe"] }, created: Date.now(), modified: Date.now(), }); nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/keys/${mockKeyId}/revoke`) + .post(`/v1/apps/${appId}/keys/${mockKeyId}/revoke`) .reply(200, {}); const { stdout } = await runCommand( - ["auth:keys:revoke", `${mockAppId}.${mockKeyId}`, "--force", "--json"], + ["auth:keys:revoke", `${appId}.${mockKeyId}`, "--force", "--json"], import.meta.url, ); @@ -146,12 +113,13 @@ currentAppId = "${mockAppId}" }); it("should handle 404 key not found", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/keys/nonexistent`) + .get(`/v1/apps/${appId}/keys/nonexistent`) .reply(404, { error: "Key not found" }); const { error } = await runCommand( - ["auth:keys:revoke", `${mockAppId}.nonexistent`, "--force"], + ["auth:keys:revoke", `${appId}.nonexistent`, "--force"], import.meta.url, ); @@ -160,12 +128,13 @@ currentAppId = "${mockAppId}" }); it("should handle 401 authentication error", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/keys/${mockKeyId}`) + .get(`/v1/apps/${appId}/keys/${mockKeyId}`) .reply(401, { error: "Unauthorized" }); const { error } = await runCommand( - ["auth:keys:revoke", `${mockAppId}.${mockKeyId}`, "--force"], + ["auth:keys:revoke", `${appId}.${mockKeyId}`, "--force"], import.meta.url, ); diff --git a/test/unit/commands/auth/keys/update.test.ts b/test/unit/commands/auth/keys/update.test.ts index 5763d216..4d1bc15c 100644 --- a/test/unit/commands/auth/keys/update.test.ts +++ b/test/unit/commands/auth/keys/update.test.ts @@ -1,65 +1,30 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { runCommand } from "@oclif/test"; import nock from "nock"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; +import { getMockConfigManager } from "../../../../helpers/mock-config-manager.js"; describe("auth:keys:update command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; const mockKeyId = "testkey"; - let testConfigDir: string; - let originalConfigDir: string; beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + nock.cleanAll(); }); afterEach(() => { nock.cleanAll(); - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } }); describe("successful key update", () => { it("should update key name", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; // Mock get key details nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/keys/${mockKeyId}`) + .get(`/v1/apps/${appId}/keys/${mockKeyId}`) .reply(200, { id: mockKeyId, - appId: mockAppId, + appId, name: "OldName", - key: `${mockAppId}.${mockKeyId}:secret`, + key: `${appId}.${mockKeyId}:secret`, capability: { "*": ["publish", "subscribe"] }, created: Date.now(), modified: Date.now(), @@ -67,46 +32,47 @@ currentAppId = "${mockAppId}" // Mock update key nock("https://control.ably.net") - .patch(`/v1/apps/${mockAppId}/keys/${mockKeyId}`) + .patch(`/v1/apps/${appId}/keys/${mockKeyId}`) .reply(200, { id: mockKeyId, - appId: mockAppId, + appId, name: "NewName", - key: `${mockAppId}.${mockKeyId}:secret`, + key: `${appId}.${mockKeyId}:secret`, capability: { "*": ["publish", "subscribe"] }, created: Date.now(), modified: Date.now(), }); const { stdout } = await runCommand( - ["auth:keys:update", `${mockAppId}.${mockKeyId}`, "--name=NewName"], + ["auth:keys:update", `${appId}.${mockKeyId}`, "--name=NewName"], import.meta.url, ); - expect(stdout).toContain(`Key Name: ${mockAppId}.${mockKeyId}`); + expect(stdout).toContain(`Key Name: ${appId}.${mockKeyId}`); expect(stdout).toContain(`Key Label: "OldName" → "NewName"`); }); it("should update key capabilities", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/keys/${mockKeyId}`) + .get(`/v1/apps/${appId}/keys/${mockKeyId}`) .reply(200, { id: mockKeyId, - appId: mockAppId, + appId, name: "Test Key", - key: `${mockAppId}.${mockKeyId}:secret`, + key: `${appId}.${mockKeyId}:secret`, capability: { "*": ["publish", "subscribe"] }, created: Date.now(), modified: Date.now(), }); nock("https://control.ably.net") - .patch(`/v1/apps/${mockAppId}/keys/${mockKeyId}`) + .patch(`/v1/apps/${appId}/keys/${mockKeyId}`) .reply(200, { id: mockKeyId, - appId: mockAppId, + appId, name: "Test Key", - key: `${mockAppId}.${mockKeyId}:secret`, + key: `${appId}.${mockKeyId}:secret`, capability: { "*": ["subscribe"] }, created: Date.now(), modified: Date.now(), @@ -115,54 +81,49 @@ currentAppId = "${mockAppId}" const { stdout } = await runCommand( [ "auth:keys:update", - `${mockAppId}.${mockKeyId}`, + `${appId}.${mockKeyId}`, "--capabilities", "subscribe", ], import.meta.url, ); - expect(stdout).toContain(`Key Name: ${mockAppId}.${mockKeyId}`); + expect(stdout).toContain(`Key Name: ${appId}.${mockKeyId}`); expect(stdout).toContain("After: * → subscribe"); }); it("should update key with --app flag", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/keys/${mockKeyId}`) + .get(`/v1/apps/${appId}/keys/${mockKeyId}`) .reply(200, { id: mockKeyId, - appId: mockAppId, + appId, name: "OldName", - key: `${mockAppId}.${mockKeyId}:secret`, + key: `${appId}.${mockKeyId}:secret`, capability: { "*": ["publish"] }, created: Date.now(), modified: Date.now(), }); nock("https://control.ably.net") - .patch(`/v1/apps/${mockAppId}/keys/${mockKeyId}`) + .patch(`/v1/apps/${appId}/keys/${mockKeyId}`) .reply(200, { id: mockKeyId, - appId: mockAppId, + appId, name: "UpdatedName", - key: `${mockAppId}.${mockKeyId}:secret`, + key: `${appId}.${mockKeyId}:secret`, capability: { "*": ["publish"] }, created: Date.now(), modified: Date.now(), }); const { stdout } = await runCommand( - [ - "auth:keys:update", - mockKeyId, - "--app", - mockAppId, - "--name=UpdatedName", - ], + ["auth:keys:update", mockKeyId, "--app", appId, "--name=UpdatedName"], import.meta.url, ); - expect(stdout).toContain(`Key Name: ${mockAppId}.${mockKeyId}`); + expect(stdout).toContain(`Key Name: ${appId}.${mockKeyId}`); expect(stdout).toContain(`Key Label: "OldName" → "UpdatedName"`); }); }); @@ -179,8 +140,9 @@ currentAppId = "${mockAppId}" }); it("should require at least one update parameter", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; const { error } = await runCommand( - ["auth:keys:update", `${mockAppId}.${mockKeyId}`], + ["auth:keys:update", `${appId}.${mockKeyId}`], import.meta.url, ); @@ -189,12 +151,13 @@ currentAppId = "${mockAppId}" }); it("should handle 404 key not found", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/keys/nonexistent`) + .get(`/v1/apps/${appId}/keys/nonexistent`) .reply(404, { error: "Key not found" }); const { error } = await runCommand( - ["auth:keys:update", `${mockAppId}.nonexistent`, "--name=NewName"], + ["auth:keys:update", `${appId}.nonexistent`, "--name=NewName"], import.meta.url, ); @@ -203,12 +166,13 @@ currentAppId = "${mockAppId}" }); it("should handle 401 authentication error", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/keys/${mockKeyId}`) + .get(`/v1/apps/${appId}/keys/${mockKeyId}`) .reply(401, { error: "Unauthorized" }); const { error } = await runCommand( - ["auth:keys:update", `${mockAppId}.${mockKeyId}`, "--name=NewName"], + ["auth:keys:update", `${appId}.${mockKeyId}`, "--name=NewName"], import.meta.url, ); diff --git a/test/unit/commands/auth/revoke-token.test.ts b/test/unit/commands/auth/revoke-token.test.ts index f2267b46..16c6f967 100644 --- a/test/unit/commands/auth/revoke-token.test.ts +++ b/test/unit/commands/auth/revoke-token.test.ts @@ -1,68 +1,29 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { runCommand } from "@oclif/test"; import nock from "nock"; -import { mkdirSync, writeFileSync } from "node:fs"; -import { resolve } from "node:path"; -import { tmpdir } from "node:os"; - -// Define the type for global test mocks -declare global { - var __TEST_MOCKS__: { - ablyRealtimeMock?: { - close: () => void; - }; - }; -} +import { getMockConfigManager } from "../../../helpers/mock-config-manager.js"; describe("auth:revoke-token command", () => { - const mockApiKey = "appid.keyid:secret"; - const mockAppId = "appid"; - const mockKeyName = "appid.keyid"; const mockToken = "test-token-12345"; const mockClientId = "test-client-id"; - let testConfigDir: string; - let originalConfigDir: string; beforeEach(() => { nock.cleanAll(); - // Create a temporary config directory with current app set - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - // Store original config dir and set test config dir - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - // Create a minimal config file with the current app set - const configContent = `[current] -account = "default" -app = "${mockAppId}" - -[accounts.default] -accountId = "test-account" -accountName = "Test Account" -userEmail = "test@example.com" - -[accounts.default.apps.${mockAppId}] -appName = "Test App" -apiKey = "${mockApiKey}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); - // Set up a minimal mock Ably realtime client // The revoke-token command creates one but doesn't actually use it for the HTTP request - globalThis.__TEST_MOCKS__ = { - ablyRealtimeMock: { + if (globalThis.__TEST_MOCKS__) { + globalThis.__TEST_MOCKS__.ablyRealtimeMock = { close: () => {}, - }, - }; + }; + } }); afterEach(() => { nock.cleanAll(); - delete globalThis.__TEST_MOCKS__; - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + } }); describe("help", () => { @@ -113,9 +74,12 @@ apiKey = "${mockApiKey}" describe("token revocation", () => { it("should successfully revoke a token with client-id", async () => { + const mockConfig = getMockConfigManager(); + const keyId = mockConfig.getKeyId()!; + const apiKey = mockConfig.getApiKey()!; // Mock the token revocation endpoint nock("https://rest.ably.io") - .post(`/keys/${mockKeyName}/revokeTokens`, { + .post(`/keys/${keyId}/revokeTokens`, { targets: [`clientId:${mockClientId}`], }) .reply(200, {}); @@ -127,7 +91,7 @@ apiKey = "${mockApiKey}" "--client-id", mockClientId, "--api-key", - mockApiKey, + apiKey, ], import.meta.url, ); @@ -136,15 +100,18 @@ apiKey = "${mockApiKey}" }); it("should use token as client-id when --client-id not provided", async () => { + const mockConfig = getMockConfigManager(); + const keyId = mockConfig.getKeyId()!; + const apiKey = mockConfig.getApiKey()!; // When no client-id is provided, the token is used as the client-id nock("https://rest.ably.io") - .post(`/keys/${mockKeyName}/revokeTokens`, { + .post(`/keys/${keyId}/revokeTokens`, { targets: [`clientId:${mockToken}`], }) .reply(200, {}); const { stdout, stderr } = await runCommand( - ["auth:revoke-token", mockToken, "--api-key", mockApiKey], + ["auth:revoke-token", mockToken, "--api-key", apiKey], import.meta.url, ); @@ -157,8 +124,11 @@ apiKey = "${mockApiKey}" }); it("should output JSON format when --json flag is used", async () => { + const mockConfig = getMockConfigManager(); + const keyId = mockConfig.getKeyId()!; + const apiKey = mockConfig.getApiKey()!; nock("https://rest.ably.io") - .post(`/keys/${mockKeyName}/revokeTokens`, { + .post(`/keys/${keyId}/revokeTokens`, { targets: [`clientId:${mockClientId}`], }) .reply(200, { issuedBefore: 1234567890 }); @@ -170,7 +140,7 @@ apiKey = "${mockApiKey}" "--client-id", mockClientId, "--api-key", - mockApiKey, + apiKey, "--json", ], import.meta.url, @@ -186,9 +156,12 @@ apiKey = "${mockApiKey}" }); it("should handle token not found error with special message", async () => { + const mockConfig = getMockConfigManager(); + const keyId = mockConfig.getKeyId()!; + const apiKey = mockConfig.getApiKey()!; // The command handles token_not_found specifically in the response body nock("https://rest.ably.io") - .post(`/keys/${mockKeyName}/revokeTokens`) + .post(`/keys/${keyId}/revokeTokens`) .reply(404, "token_not_found"); const { stdout } = await runCommand( @@ -198,7 +171,7 @@ apiKey = "${mockApiKey}" "--client-id", mockClientId, "--api-key", - mockApiKey, + apiKey, ], import.meta.url, ); @@ -208,8 +181,11 @@ apiKey = "${mockApiKey}" }); it("should handle authentication error (invalid API key)", async () => { + const mockConfig = getMockConfigManager(); + const keyId = mockConfig.getKeyId()!; + const apiKey = mockConfig.getApiKey()!; nock("https://rest.ably.io") - .post(`/keys/${mockKeyName}/revokeTokens`) + .post(`/keys/${keyId}/revokeTokens`) .reply(401, { error: { message: "Unauthorized" } }); const { error } = await runCommand( @@ -219,7 +195,7 @@ apiKey = "${mockApiKey}" "--client-id", mockClientId, "--api-key", - mockApiKey, + apiKey, ], import.meta.url, ); @@ -229,8 +205,11 @@ apiKey = "${mockApiKey}" }); it("should handle server error", async () => { + const mockConfig = getMockConfigManager(); + const keyId = mockConfig.getKeyId()!; + const apiKey = mockConfig.getApiKey()!; nock("https://rest.ably.io") - .post(`/keys/${mockKeyName}/revokeTokens`) + .post(`/keys/${keyId}/revokeTokens`) .reply(500, { error: "Internal Server Error" }); const { error } = await runCommand( @@ -240,7 +219,7 @@ apiKey = "${mockApiKey}" "--client-id", mockClientId, "--api-key", - mockApiKey, + apiKey, ], import.meta.url, ); @@ -252,8 +231,11 @@ apiKey = "${mockApiKey}" describe("debug mode", () => { it("should show debug information when --debug flag is used", async () => { + const mockConfig = getMockConfigManager(); + const keyId = mockConfig.getKeyId()!; + const apiKey = mockConfig.getApiKey()!; nock("https://rest.ably.io") - .post(`/keys/${mockKeyName}/revokeTokens`) + .post(`/keys/${keyId}/revokeTokens`) .reply(200, {}); const { stdout } = await runCommand( @@ -263,7 +245,7 @@ apiKey = "${mockApiKey}" "--client-id", mockClientId, "--api-key", - mockApiKey, + apiKey, "--debug", ], import.meta.url, @@ -273,8 +255,12 @@ apiKey = "${mockApiKey}" }); it("should mask the API key secret in debug output", async () => { + const mockConfig = getMockConfigManager(); + const keyId = mockConfig.getKeyId()!; + const apiKey = mockConfig.getApiKey()!; + const keySecret = apiKey.split(":")[1]; nock("https://rest.ably.io") - .post(`/keys/${mockKeyName}/revokeTokens`) + .post(`/keys/${keyId}/revokeTokens`) .reply(200, {}); const { stdout } = await runCommand( @@ -284,14 +270,14 @@ apiKey = "${mockApiKey}" "--client-id", mockClientId, "--api-key", - mockApiKey, + apiKey, "--debug", ], import.meta.url, ); // Verify the secret part of the API key is masked - expect(stdout).not.toContain("secret"); + expect(stdout).not.toContain(keySecret); expect(stdout).toContain("***"); }); }); diff --git a/test/unit/commands/bench/benchmarking.test.ts b/test/unit/commands/bench/benchmarking.test.ts index f5ac7fc5..55f905c8 100644 --- a/test/unit/commands/bench/benchmarking.test.ts +++ b/test/unit/commands/bench/benchmarking.test.ts @@ -39,7 +39,9 @@ describe("benchmarking commands", { timeout: 20000 }, () => { on: vi.fn(), }; + // Merge with existing mocks (don't overwrite configManager) globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: { channels: { get: vi.fn().mockReturnValue(mockChannel) }, connection: { @@ -64,7 +66,11 @@ describe("benchmarking commands", { timeout: 20000 }, () => { }); afterEach(() => { - delete globalThis.__TEST_MOCKS__; + // Only delete the mocks we added, not the whole object + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + delete globalThis.__TEST_MOCKS__.ablyRestMock; + } vi.restoreAllMocks(); }); diff --git a/test/unit/commands/channel-rule/create.test.ts b/test/unit/commands/channel-rule/create.test.ts index eeed6f7e..a4cd54e1 100644 --- a/test/unit/commands/channel-rule/create.test.ts +++ b/test/unit/commands/channel-rule/create.test.ts @@ -1,58 +1,18 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, afterEach } from "vitest"; import { runCommand } from "@oclif/test"; import nock from "nock"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; +import { getMockConfigManager } from "../../../helpers/mock-config-manager.js"; describe("channel-rule:create command (alias)", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - let testConfigDir: string; - let originalConfigDir: string; - - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); - }); - afterEach(() => { nock.cleanAll(); - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } }); describe("alias behavior", () => { it("should execute the same as apps:channel-rules:create", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/namespaces`) + .post(`/v1/apps/${appId}/namespaces`) .reply(200, { id: "test-rule", persisted: true, @@ -66,7 +26,7 @@ currentAppId = "${mockAppId}" "channel-rule:create", "--name=test-rule", "--app", - mockAppId, + appId, "--persisted", ], import.meta.url, @@ -76,8 +36,9 @@ currentAppId = "${mockAppId}" }); it("should require name flag", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; const { error } = await runCommand( - ["channel-rule:create", "--app", mockAppId], + ["channel-rule:create", "--app", appId], import.meta.url, ); diff --git a/test/unit/commands/channel-rule/delete.test.ts b/test/unit/commands/channel-rule/delete.test.ts index a75640ff..f962a4d1 100644 --- a/test/unit/commands/channel-rule/delete.test.ts +++ b/test/unit/commands/channel-rule/delete.test.ts @@ -1,59 +1,20 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, afterEach } from "vitest"; import { runCommand } from "@oclif/test"; import nock from "nock"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; +import { getMockConfigManager } from "../../../helpers/mock-config-manager.js"; describe("channel-rule:delete command (alias)", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; const mockRuleId = "test-rule"; - let testConfigDir: string; - let originalConfigDir: string; - - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); - }); afterEach(() => { nock.cleanAll(); - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } }); describe("alias behavior", () => { it("should execute the same as apps:channel-rules:delete", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/namespaces`) + .get(`/v1/apps/${appId}/namespaces`) .reply(200, [ { id: mockRuleId, @@ -65,11 +26,11 @@ currentAppId = "${mockAppId}" ]); nock("https://control.ably.net") - .delete(`/v1/apps/${mockAppId}/namespaces/${mockRuleId}`) + .delete(`/v1/apps/${appId}/namespaces/${mockRuleId}`) .reply(200, {}); const { stdout } = await runCommand( - ["channel-rule:delete", mockRuleId, "--app", mockAppId, "--force"], + ["channel-rule:delete", mockRuleId, "--app", appId, "--force"], import.meta.url, ); @@ -77,8 +38,9 @@ currentAppId = "${mockAppId}" }); it("should require nameOrId argument", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; const { error } = await runCommand( - ["channel-rule:delete", "--app", mockAppId, "--force"], + ["channel-rule:delete", "--app", appId, "--force"], import.meta.url, ); diff --git a/test/unit/commands/channel-rule/list.test.ts b/test/unit/commands/channel-rule/list.test.ts index d51d9131..510335cd 100644 --- a/test/unit/commands/channel-rule/list.test.ts +++ b/test/unit/commands/channel-rule/list.test.ts @@ -1,58 +1,18 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, afterEach } from "vitest"; import { runCommand } from "@oclif/test"; import nock from "nock"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; +import { getMockConfigManager } from "../../../helpers/mock-config-manager.js"; describe("channel-rule:list command (alias)", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - let testConfigDir: string; - let originalConfigDir: string; - - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); - }); - afterEach(() => { nock.cleanAll(); - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } }); describe("alias behavior", () => { it("should execute the same as apps:channel-rules:list", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/namespaces`) + .get(`/v1/apps/${appId}/namespaces`) .reply(200, [ { id: "rule1", @@ -80,8 +40,9 @@ currentAppId = "${mockAppId}" }); it("should show message when no rules found", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/namespaces`) + .get(`/v1/apps/${appId}/namespaces`) .reply(200, []); const { stdout } = await runCommand( diff --git a/test/unit/commands/channel-rule/update.test.ts b/test/unit/commands/channel-rule/update.test.ts index 95861e4a..672744f8 100644 --- a/test/unit/commands/channel-rule/update.test.ts +++ b/test/unit/commands/channel-rule/update.test.ts @@ -1,59 +1,20 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, afterEach } from "vitest"; import { runCommand } from "@oclif/test"; import nock from "nock"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; +import { getMockConfigManager } from "../../../helpers/mock-config-manager.js"; describe("channel-rule:update command (alias)", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; const mockRuleId = "test-rule"; - let testConfigDir: string; - let originalConfigDir: string; - - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); - }); afterEach(() => { nock.cleanAll(); - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } }); describe("alias behavior", () => { it("should execute the same as apps:channel-rules:update", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/namespaces`) + .get(`/v1/apps/${appId}/namespaces`) .reply(200, [ { id: mockRuleId, @@ -65,7 +26,7 @@ currentAppId = "${mockAppId}" ]); nock("https://control.ably.net") - .patch(`/v1/apps/${mockAppId}/namespaces/${mockRuleId}`) + .patch(`/v1/apps/${appId}/namespaces/${mockRuleId}`) .reply(200, { id: mockRuleId, persisted: true, @@ -75,7 +36,7 @@ currentAppId = "${mockAppId}" }); const { stdout } = await runCommand( - ["channel-rule:update", mockRuleId, "--app", mockAppId, "--persisted"], + ["channel-rule:update", mockRuleId, "--app", appId, "--persisted"], import.meta.url, ); @@ -83,8 +44,9 @@ currentAppId = "${mockAppId}" }); it("should require nameOrId argument", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; const { error } = await runCommand( - ["channel-rule:update", "--app", mockAppId, "--persisted"], + ["channel-rule:update", "--app", appId, "--persisted"], import.meta.url, ); diff --git a/test/unit/commands/channels/batch-publish.test.ts b/test/unit/commands/channels/batch-publish.test.ts index 93746c4f..f8fff965 100644 --- a/test/unit/commands/channels/batch-publish.test.ts +++ b/test/unit/commands/channels/batch-publish.test.ts @@ -20,7 +20,9 @@ describe("channels:batch-publish command", () => { ], }); + // Merge with existing mocks (don't overwrite configManager) globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRestMock: { request: mockRequest, close: vi.fn(), @@ -29,7 +31,10 @@ describe("channels:batch-publish command", () => { }); afterEach(() => { - delete globalThis.__TEST_MOCKS__; + // Only delete the mock we added, not the whole object + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRestMock; + } }); describe("help", () => { diff --git a/test/unit/commands/channels/history.test.ts b/test/unit/commands/channels/history.test.ts index e6c6da85..b3a51235 100644 --- a/test/unit/commands/channels/history.test.ts +++ b/test/unit/commands/channels/history.test.ts @@ -38,7 +38,9 @@ describe("channels:history command", () => { history: mockHistory, }; + // Merge with existing mocks (don't overwrite configManager) globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRestMock: { channels: { get: vi.fn().mockReturnValue(mockChannel), @@ -49,7 +51,10 @@ describe("channels:history command", () => { }); afterEach(() => { - delete globalThis.__TEST_MOCKS__; + // Only delete the mock we added, not the whole object + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRestMock; + } vi.clearAllMocks(); }); diff --git a/test/unit/commands/channels/list.test.ts b/test/unit/commands/channels/list.test.ts index 10feb99d..2e67275b 100644 --- a/test/unit/commands/channels/list.test.ts +++ b/test/unit/commands/channels/list.test.ts @@ -47,7 +47,9 @@ describe("channels:list command", () => { beforeEach(() => { mockRequest = vi.fn().mockResolvedValue(mockChannelsResponse); + // Merge with existing mocks (don't overwrite configManager) globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRestMock: { request: mockRequest, close: vi.fn(), @@ -56,7 +58,10 @@ describe("channels:list command", () => { }); afterEach(() => { - delete globalThis.__TEST_MOCKS__; + // Only delete the mock we added, not the whole object + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRestMock; + } }); describe("help", () => { diff --git a/test/unit/commands/channels/occupancy/subscribe.test.ts b/test/unit/commands/channels/occupancy/subscribe.test.ts index 7966c247..49d1e6e6 100644 --- a/test/unit/commands/channels/occupancy/subscribe.test.ts +++ b/test/unit/commands/channels/occupancy/subscribe.test.ts @@ -1,57 +1,19 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { runCommand } from "@oclif/test"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; describe("channels:occupancy:subscribe command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - const mockApiKey = `${mockAppId}.testkey:testsecret`; - let testConfigDir: string; - let originalConfigDir: string; - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[accounts.default.apps."${mockAppId}"] -appName = "Test App" -apiKey = "${mockApiKey}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + // Clean up any previous test mocks + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + } }); afterEach(() => { - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; + // Only delete the mock we added, not the whole object + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } - - globalThis.__TEST_MOCKS__ = undefined; }); describe("command arguments and flags", () => { @@ -97,7 +59,9 @@ apiKey = "${mockApiKey}" state: "connected", }; + // Merge with existing mocks (don't overwrite configManager) globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: { channels: mockChannels, connection: mockConnection, @@ -138,7 +102,9 @@ apiKey = "${mockApiKey}" state: "connected", }; + // Merge with existing mocks (don't overwrite configManager) globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: { channels: mockChannels, connection: mockConnection, @@ -180,7 +146,9 @@ apiKey = "${mockApiKey}" state: "connected", }; + // Merge with existing mocks (don't overwrite configManager) globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: { channels: mockChannels, connection: mockConnection, @@ -225,7 +193,9 @@ apiKey = "${mockApiKey}" state: "connected", }; + // Merge with existing mocks (don't overwrite configManager) globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: { channels: mockChannels, connection: mockConnection, @@ -243,8 +213,10 @@ apiKey = "${mockApiKey}" }); it("should handle missing mock client in test mode", async () => { - // No mock set up - globalThis.__TEST_MOCKS__ = undefined; + // Clear the realtime mock but keep configManager + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + } const { error } = await runCommand( ["channels:occupancy:subscribe", "test-channel"], @@ -277,7 +249,9 @@ apiKey = "${mockApiKey}" state: "connected", }; + // Merge with existing mocks (don't overwrite configManager) globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: { channels: mockChannels, connection: mockConnection, diff --git a/test/unit/commands/channels/presence/enter.test.ts b/test/unit/commands/channels/presence/enter.test.ts index 46837cb9..2b38b4aa 100644 --- a/test/unit/commands/channels/presence/enter.test.ts +++ b/test/unit/commands/channels/presence/enter.test.ts @@ -39,7 +39,9 @@ describe("channels:presence:enter command", () => { once: vi.fn(), }; + // Merge with existing mocks (don't overwrite configManager) globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: { channels: { get: vi.fn().mockReturnValue(mockChannel), @@ -62,7 +64,10 @@ describe("channels:presence:enter command", () => { }); afterEach(() => { - delete globalThis.__TEST_MOCKS__; + // Only delete the mock we added, not the whole object + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + } }); describe("help", () => { diff --git a/test/unit/commands/channels/presence/subscribe.test.ts b/test/unit/commands/channels/presence/subscribe.test.ts index 11643c23..5ef1a3ff 100644 --- a/test/unit/commands/channels/presence/subscribe.test.ts +++ b/test/unit/commands/channels/presence/subscribe.test.ts @@ -32,7 +32,9 @@ describe("channels:presence:subscribe command", () => { once: vi.fn(), }; + // Merge with existing mocks (don't overwrite configManager) globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: { channels: { get: vi.fn().mockReturnValue(mockChannel), @@ -55,7 +57,10 @@ describe("channels:presence:subscribe command", () => { }); afterEach(() => { - delete globalThis.__TEST_MOCKS__; + // Only delete the mock we added, not the whole object + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + } }); describe("help", () => { diff --git a/test/unit/commands/channels/subscribe.test.ts b/test/unit/commands/channels/subscribe.test.ts index ed742878..f1d8ad1c 100644 --- a/test/unit/commands/channels/subscribe.test.ts +++ b/test/unit/commands/channels/subscribe.test.ts @@ -40,7 +40,9 @@ describe("channels:subscribe command", () => { get: () => mockChannelState, }); + // Merge with existing mocks (don't overwrite configManager) globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: { channels: { get: vi.fn().mockReturnValue(mockChannel), @@ -68,7 +70,10 @@ describe("channels:subscribe command", () => { | { close?: () => void } | undefined; mock?.close?.(); - delete globalThis.__TEST_MOCKS__; + // Only delete the mock we added, not the whole object + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + } vi.restoreAllMocks(); }); diff --git a/test/unit/commands/config/path.test.ts b/test/unit/commands/config/path.test.ts index 4d779fda..0f19927d 100644 --- a/test/unit/commands/config/path.test.ts +++ b/test/unit/commands/config/path.test.ts @@ -1,39 +1,13 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect } from "vitest"; import { runCommand } from "@oclif/test"; -import { resolve } from "node:path"; -import { mkdirSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; describe("config:path command", () => { - let testConfigDir: string; - let originalConfigDir: string; - - beforeEach(() => { - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - }); - - afterEach(() => { - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } - }); - describe("successful config path display", () => { it("should display the config path", async () => { const { stdout } = await runCommand(["config:path"], import.meta.url); - expect(stdout).toContain(testConfigDir); - expect(stdout).toContain("config"); + // MockConfigManager returns "/mock/config/path" + expect(stdout).toContain("/mock/config/path"); }); it("should output JSON format when --json flag is used", async () => { @@ -44,7 +18,7 @@ describe("config:path command", () => { const result = JSON.parse(stdout); expect(result).toHaveProperty("path"); - expect(result.path).toContain(testConfigDir); + expect(result.path).toBe("/mock/config/path"); }); it("should output pretty JSON format when --pretty-json flag is used", async () => { @@ -55,7 +29,7 @@ describe("config:path command", () => { const result = JSON.parse(stdout); expect(result).toHaveProperty("path"); - expect(result.path).toContain(testConfigDir); + expect(result.path).toBe("/mock/config/path"); }); }); diff --git a/test/unit/commands/config/show.test.ts b/test/unit/commands/config/show.test.ts index f202fe2a..65b7f1c4 100644 --- a/test/unit/commands/config/show.test.ts +++ b/test/unit/commands/config/show.test.ts @@ -11,8 +11,16 @@ describe("config:show command", () => { const mockApiKey = `${mockAppId}.testkey:testsecret`; let testConfigDir: string; let originalConfigDir: string; + let savedConfigManager: unknown; beforeEach(() => { + // Disable MockConfigManager so this test uses real file I/O + // Save the mock so we can restore it later + savedConfigManager = globalThis.__TEST_MOCKS__?.configManager; + if (globalThis.__TEST_MOCKS__) { + globalThis.__TEST_MOCKS__.configManager = undefined; + } + process.env.ABLY_ACCESS_TOKEN = mockAccessToken; testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); @@ -34,6 +42,11 @@ describe("config:show command", () => { if (existsSync(testConfigDir)) { rmSync(testConfigDir, { recursive: true, force: true }); } + + // Restore MockConfigManager for other tests + if (globalThis.__TEST_MOCKS__ && savedConfigManager) { + globalThis.__TEST_MOCKS__.configManager = savedConfigManager; + } }); describe("when config file exists", () => { diff --git a/test/unit/commands/integrations/create.test.ts b/test/unit/commands/integrations/create.test.ts index 8408242e..a3669585 100644 --- a/test/unit/commands/integrations/create.test.ts +++ b/test/unit/commands/integrations/create.test.ts @@ -1,79 +1,35 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, afterEach } from "vitest"; import { runCommand } from "@oclif/test"; import nock from "nock"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; +import { getMockConfigManager } from "../../../helpers/mock-config-manager.js"; describe("integrations:create command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; const mockRuleId = "rule-123456"; - let testConfigDir: string; - let originalConfigDir: string; - - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); - }); afterEach(() => { nock.cleanAll(); - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } }); describe("successful integration creation", () => { it("should create an HTTP integration successfully", async () => { - const mockIntegration = { - id: mockRuleId, - appId: mockAppId, - ruleType: "http", - requestMode: "single", - source: { - channelFilter: "chat:*", - type: "channel.message", - }, - target: { - url: "https://example.com/webhook", - format: "json", - enveloped: true, - }, - status: "enabled", - created: Date.now(), - modified: Date.now(), - }; - + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/rules`) - .reply(201, mockIntegration); + .post(`/v1/apps/${appId}/rules`) + .reply(201, { + id: mockRuleId, + appId, + ruleType: "http", + requestMode: "single", + source: { + channelFilter: "chat:*", + type: "channel.message", + }, + target: { + url: "https://example.com/webhook", + format: "json", + }, + status: "enabled", + }); const { stdout } = await runCommand( [ @@ -82,10 +38,10 @@ currentAppId = "${mockAppId}" "http", "--source-type", "channel.message", - "--target-url", - "https://example.com/webhook", "--channel-filter", "chat:*", + "--target-url", + "https://example.com/webhook", ], import.meta.url, ); @@ -96,28 +52,25 @@ currentAppId = "${mockAppId}" }); it("should create an AMQP integration successfully", async () => { - const mockIntegration = { - id: mockRuleId, - appId: mockAppId, - ruleType: "amqp", - requestMode: "single", - source: { - channelFilter: "", - type: "channel.message", - }, - target: { - enveloped: true, - format: "json", - exchangeName: "ably", - }, - status: "enabled", - created: Date.now(), - modified: Date.now(), - }; - + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/rules`) - .reply(201, mockIntegration); + .post(`/v1/apps/${appId}/rules`) + .reply(201, { + id: mockRuleId, + appId, + ruleType: "amqp", + requestMode: "single", + source: { + channelFilter: "", + type: "channel.message", + }, + target: { + enveloped: true, + format: "json", + exchangeName: "ably", + }, + status: "enabled", + }); const { stdout } = await runCommand( [ @@ -134,27 +87,26 @@ currentAppId = "${mockAppId}" expect(stdout).toContain("amqp"); }); - it("should output JSON format when --json flag is used", async () => { - const mockIntegration = { - id: mockRuleId, - appId: mockAppId, - ruleType: "http", - requestMode: "single", - source: { - channelFilter: "", - type: "channel.message", - }, - target: { - url: "https://example.com/webhook", - format: "json", - enveloped: true, - }, - status: "enabled", - }; - + it("should create a disabled integration when status is disabled", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/rules`) - .reply(201, mockIntegration); + .post(`/v1/apps/${appId}/rules`, (body: Record) => { + return body.status === "disabled"; + }) + .reply(201, { + id: mockRuleId, + appId, + ruleType: "http", + requestMode: "single", + source: { + channelFilter: "", + type: "channel.message", + }, + target: { + url: "https://example.com/webhook", + }, + status: "disabled", + }); const { stdout } = await runCommand( [ @@ -165,38 +117,37 @@ currentAppId = "${mockAppId}" "channel.message", "--target-url", "https://example.com/webhook", + "--status", + "disabled", "--json", ], import.meta.url, ); const result = JSON.parse(stdout); - expect(result).toHaveProperty("integration"); - expect(result.integration).toHaveProperty("id", mockRuleId); - expect(result.integration).toHaveProperty("ruleType", "http"); + expect(result.integration).toHaveProperty("status", "disabled"); }); - it("should create a disabled integration when status is disabled", async () => { - const mockIntegration = { - id: mockRuleId, - appId: mockAppId, - ruleType: "http", - requestMode: "single", - source: { - channelFilter: "", - type: "channel.message", - }, - target: { - url: "https://example.com/webhook", - }, - status: "disabled", - }; - + it("should create integration with batch request mode", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/rules`, (body: any) => { - return body.status === "disabled"; + .post(`/v1/apps/${appId}/rules`, (body: Record) => { + return body.requestMode === "batch"; }) - .reply(201, mockIntegration); + .reply(201, { + id: mockRuleId, + appId, + ruleType: "http", + requestMode: "batch", + source: { + channelFilter: "", + type: "channel.message", + }, + target: { + url: "https://example.com/webhook", + }, + status: "enabled", + }); const { stdout } = await runCommand( [ @@ -207,38 +158,36 @@ currentAppId = "${mockAppId}" "channel.message", "--target-url", "https://example.com/webhook", - "--status", - "disabled", + "--request-mode", + "batch", "--json", ], import.meta.url, ); const result = JSON.parse(stdout); - expect(result.integration).toHaveProperty("status", "disabled"); + expect(result.integration).toHaveProperty("requestMode", "batch"); }); - it("should create integration with batch request mode", async () => { - const mockIntegration = { - id: mockRuleId, - appId: mockAppId, - ruleType: "http", - requestMode: "batch", - source: { - channelFilter: "", - type: "channel.message", - }, - target: { - url: "https://example.com/webhook", - }, - status: "enabled", - }; - + it("should output JSON format when --json flag is used", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/rules`, (body: any) => { - return body.requestMode === "batch"; - }) - .reply(201, mockIntegration); + .post(`/v1/apps/${appId}/rules`) + .reply(201, { + id: mockRuleId, + appId, + ruleType: "http", + requestMode: "single", + source: { + channelFilter: "chat:*", + type: "channel.message", + }, + target: { + url: "https://example.com/webhook", + format: "json", + }, + status: "enabled", + }); const { stdout } = await runCommand( [ @@ -247,24 +196,34 @@ currentAppId = "${mockAppId}" "http", "--source-type", "channel.message", + "--channel-filter", + "chat:*", "--target-url", "https://example.com/webhook", - "--request-mode", - "batch", "--json", ], import.meta.url, ); const result = JSON.parse(stdout); - expect(result.integration).toHaveProperty("requestMode", "batch"); + expect(result).toHaveProperty("integration"); + expect(result.integration).toHaveProperty("id", mockRuleId); + expect(result.integration).toHaveProperty("ruleType", "http"); }); }); describe("error handling", () => { it("should require rule-type flag", async () => { const { error } = await runCommand( - ["integrations:create", "--source-type", "channel.message"], + [ + "integrations:create", + "--source-type", + "channel.message", + "--channel-filter", + "chat:*", + "--target-url", + "https://example.com/webhook", + ], import.meta.url, ); @@ -299,9 +258,10 @@ currentAppId = "${mockAppId}" }); it("should handle API errors", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/rules`) - .reply(400, { error: "Invalid integration configuration" }); + .post(`/v1/apps/${appId}/rules`) + .reply(400, { error: "Invalid configuration" }); const { error } = await runCommand( [ @@ -310,6 +270,8 @@ currentAppId = "${mockAppId}" "http", "--source-type", "channel.message", + "--channel-filter", + "chat:*", "--target-url", "https://example.com/webhook", ], @@ -317,7 +279,32 @@ currentAppId = "${mockAppId}" ); expect(error).toBeDefined(); - expect(error?.message).toMatch(/Error creating integration|400/i); + expect(error?.message).toMatch(/400/); + }); + + it("should handle 401 authentication error", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; + nock("https://control.ably.net") + .post(`/v1/apps/${appId}/rules`) + .reply(401, { error: "Unauthorized" }); + + const { error } = await runCommand( + [ + "integrations:create", + "--rule-type", + "http", + "--source-type", + "channel.message", + "--channel-filter", + "chat:*", + "--target-url", + "https://example.com/webhook", + ], + import.meta.url, + ); + + expect(error).toBeDefined(); + expect(error?.message).toMatch(/401/); }); it("should reject unknown flags", async () => { @@ -333,24 +320,23 @@ currentAppId = "${mockAppId}" describe("source type options", () => { it("should accept channel.presence source type", async () => { - const mockIntegration = { - id: mockRuleId, - appId: mockAppId, - ruleType: "http", - requestMode: "single", - source: { - channelFilter: "", - type: "channel.presence", - }, - target: { - url: "https://example.com/webhook", - }, - status: "enabled", - }; - + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/rules`) - .reply(201, mockIntegration); + .post(`/v1/apps/${appId}/rules`) + .reply(201, { + id: mockRuleId, + appId, + ruleType: "http", + requestMode: "single", + source: { + channelFilter: "", + type: "channel.presence", + }, + target: { + url: "https://example.com/webhook", + }, + status: "enabled", + }); const { stdout } = await runCommand( [ @@ -371,24 +357,23 @@ currentAppId = "${mockAppId}" }); it("should accept channel.lifecycle source type", async () => { - const mockIntegration = { - id: mockRuleId, - appId: mockAppId, - ruleType: "http", - requestMode: "single", - source: { - channelFilter: "", - type: "channel.lifecycle", - }, - target: { - url: "https://example.com/webhook", - }, - status: "enabled", - }; - + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/rules`) - .reply(201, mockIntegration); + .post(`/v1/apps/${appId}/rules`) + .reply(201, { + id: mockRuleId, + appId, + ruleType: "http", + requestMode: "single", + source: { + channelFilter: "", + type: "channel.lifecycle", + }, + target: { + url: "https://example.com/webhook", + }, + status: "enabled", + }); const { stdout } = await runCommand( [ diff --git a/test/unit/commands/integrations/delete.test.ts b/test/unit/commands/integrations/delete.test.ts index 4480b2ef..39032cec 100644 --- a/test/unit/commands/integrations/delete.test.ts +++ b/test/unit/commands/integrations/delete.test.ts @@ -1,84 +1,46 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, afterEach } from "vitest"; import { runCommand } from "@oclif/test"; import nock from "nock"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; +import { getMockConfigManager } from "../../../helpers/mock-config-manager.js"; describe("integrations:delete command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; const mockRuleId = "rule-123456"; - let testConfigDir: string; - let originalConfigDir: string; - - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); - }); afterEach(() => { nock.cleanAll(); - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } }); - const mockIntegration = { - id: mockRuleId, - appId: mockAppId, - ruleType: "http", - requestMode: "single", - source: { - channelFilter: "chat:*", - type: "channel.message", - }, - target: { - url: "https://example.com/webhook", - format: "json", - enveloped: true, - }, - status: "enabled", - created: Date.now(), - modified: Date.now(), - }; - describe("successful integration deletion", () => { it("should delete an integration with --force flag", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; + const mockIntegration = { + id: mockRuleId, + appId, + ruleType: "http", + requestMode: "single", + source: { + channelFilter: "chat:*", + type: "channel.message", + }, + target: { + url: "https://example.com/webhook", + format: "json", + enveloped: true, + }, + status: "enabled", + version: "1.0", + created: Date.now(), + modified: Date.now(), + }; + // Mock GET to fetch integration details nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .get(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(200, mockIntegration); - // Mock DELETE + // Mock DELETE endpoint nock("https://control.ably.net") - .delete(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .delete(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(204); const { stdout } = await runCommand( @@ -91,12 +53,33 @@ currentAppId = "${mockAppId}" }); it("should display integration details before deletion with --force", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; + const mockIntegration = { + id: mockRuleId, + appId, + ruleType: "http", + requestMode: "single", + source: { + channelFilter: "chat:*", + type: "channel.message", + }, + target: { + url: "https://example.com/webhook", + format: "json", + enveloped: true, + }, + status: "enabled", + version: "1.0", + created: Date.now(), + modified: Date.now(), + }; + nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .get(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(200, mockIntegration); nock("https://control.ably.net") - .delete(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .delete(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(204); const { stdout } = await runCommand( @@ -105,12 +88,12 @@ currentAppId = "${mockAppId}" ); expect(stdout).toContain("http"); - expect(stdout).toContain(mockAppId); + expect(stdout).toContain(appId); }); }); describe("error handling", () => { - it("should require integrationId argument", async () => { + it("should require ruleId argument", async () => { const { error } = await runCommand( ["integrations:delete", "--force"], import.meta.url, @@ -121,8 +104,9 @@ currentAppId = "${mockAppId}" }); it("should handle integration not found", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .get(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(404, { error: "Not found" }); const { error } = await runCommand( @@ -135,12 +119,33 @@ currentAppId = "${mockAppId}" }); it("should handle API errors during deletion", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; + const mockIntegration = { + id: mockRuleId, + appId, + ruleType: "http", + requestMode: "single", + source: { + channelFilter: "chat:*", + type: "channel.message", + }, + target: { + url: "https://example.com/webhook", + format: "json", + enveloped: true, + }, + status: "enabled", + version: "1.0", + created: Date.now(), + modified: Date.now(), + }; + nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .get(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(200, mockIntegration); nock("https://control.ably.net") - .delete(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .delete(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(500, { error: "Internal server error" }); const { error } = await runCommand( @@ -152,6 +157,21 @@ currentAppId = "${mockAppId}" expect(error?.message).toMatch(/Error deleting integration|500/i); }); + it("should handle 401 authentication error", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; + nock("https://control.ably.net") + .get(`/v1/apps/${appId}/rules/${mockRuleId}`) + .reply(401, { error: "Unauthorized" }); + + const { error } = await runCommand( + ["integrations:delete", mockRuleId, "--force"], + import.meta.url, + ); + + expect(error).toBeDefined(); + expect(error?.message).toMatch(/401/); + }); + it("should reject unknown flags", async () => { const { error } = await runCommand( ["integrations:delete", mockRuleId, "--unknown-flag"], @@ -165,12 +185,33 @@ currentAppId = "${mockAppId}" describe("flag options", () => { it("should accept -f as shorthand for --force", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; + const mockIntegration = { + id: mockRuleId, + appId, + ruleType: "http", + requestMode: "single", + source: { + channelFilter: "chat:*", + type: "channel.message", + }, + target: { + url: "https://example.com/webhook", + format: "json", + enveloped: true, + }, + status: "enabled", + version: "1.0", + created: Date.now(), + modified: Date.now(), + }; + nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .get(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(200, mockIntegration); nock("https://control.ably.net") - .delete(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .delete(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(204); const { stdout } = await runCommand( @@ -182,31 +223,52 @@ currentAppId = "${mockAppId}" }); it("should accept --app flag", async () => { + const mockConfig = getMockConfigManager(); + const appId = mockConfig.getCurrentAppId()!; + const accountId = mockConfig.getCurrentAccount()!.accountId!; + const mockIntegration = { + id: mockRuleId, + appId, + ruleType: "http", + requestMode: "single", + source: { + channelFilter: "chat:*", + type: "channel.message", + }, + target: { + url: "https://example.com/webhook", + format: "json", + enveloped: true, + }, + status: "enabled", + version: "1.0", + created: Date.now(), + modified: Date.now(), + }; + // Mock the /me endpoint (needed by listApps in resolveAppIdFromNameOrId) nock("https://control.ably.net") .get("/v1/me") .reply(200, { - account: { id: mockAccountId, name: "Test Account" }, + account: { id: accountId, name: "Test Account" }, user: { email: "test@example.com" }, }); // Mock the apps list API call for resolveAppIdFromNameOrId nock("https://control.ably.net") - .get(`/v1/accounts/${mockAccountId}/apps`) - .reply(200, [ - { id: mockAppId, name: "Test App", accountId: mockAccountId }, - ]); + .get(`/v1/accounts/${accountId}/apps`) + .reply(200, [{ id: appId, name: "Test App", accountId }]); nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .get(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(200, mockIntegration); nock("https://control.ably.net") - .delete(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .delete(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(204); const { stdout } = await runCommand( - ["integrations:delete", mockRuleId, "--app", mockAppId, "--force"], + ["integrations:delete", mockRuleId, "--app", appId, "--force"], import.meta.url, ); diff --git a/test/unit/commands/integrations/get.test.ts b/test/unit/commands/integrations/get.test.ts index 9189e8c8..dfb3fac1 100644 --- a/test/unit/commands/integrations/get.test.ts +++ b/test/unit/commands/integrations/get.test.ts @@ -1,79 +1,40 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, afterEach } from "vitest"; import { runCommand } from "@oclif/test"; import nock from "nock"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; +import { getMockConfigManager } from "../../../helpers/mock-config-manager.js"; describe("integrations:get command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; const mockRuleId = "rule-123456"; - let testConfigDir: string; - let originalConfigDir: string; - - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); - }); afterEach(() => { nock.cleanAll(); - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } }); - const mockIntegration = { - id: mockRuleId, - appId: mockAppId, - ruleType: "http", - requestMode: "single", - source: { - channelFilter: "chat:*", - type: "channel.message", - }, - target: { - url: "https://example.com/webhook", - format: "json", - enveloped: true, - }, - status: "enabled", - version: "1.0", - created: Date.now(), - modified: Date.now(), - }; - describe("successful integration retrieval", () => { - it("should display integration details", async () => { + it("should get an integration by ID", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; + const mockIntegration = { + id: mockRuleId, + appId, + ruleType: "http", + requestMode: "single", + source: { + channelFilter: "chat:*", + type: "channel.message", + }, + target: { + url: "https://example.com/webhook", + format: "json", + enveloped: true, + }, + status: "enabled", + version: "1.0", + created: Date.now(), + modified: Date.now(), + }; + nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .get(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(200, mockIntegration); const { stdout } = await runCommand( @@ -83,14 +44,35 @@ currentAppId = "${mockAppId}" expect(stdout).toContain("Integration Rule Details"); expect(stdout).toContain(mockRuleId); - expect(stdout).toContain(mockAppId); + expect(stdout).toContain(appId); expect(stdout).toContain("http"); expect(stdout).toContain("channel.message"); }); it("should output JSON format when --json flag is used", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; + const mockIntegration = { + id: mockRuleId, + appId, + ruleType: "http", + requestMode: "single", + source: { + channelFilter: "chat:*", + type: "channel.message", + }, + target: { + url: "https://example.com/webhook", + format: "json", + enveloped: true, + }, + status: "enabled", + version: "1.0", + created: Date.now(), + modified: Date.now(), + }; + nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .get(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(200, mockIntegration); const { stdout } = await runCommand( @@ -100,15 +82,36 @@ currentAppId = "${mockAppId}" const result = JSON.parse(stdout); expect(result).toHaveProperty("id", mockRuleId); - expect(result).toHaveProperty("appId", mockAppId); + expect(result).toHaveProperty("appId", appId); expect(result).toHaveProperty("ruleType", "http"); expect(result).toHaveProperty("source"); expect(result.source).toHaveProperty("type", "channel.message"); }); it("should output pretty JSON when --pretty-json flag is used", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; + const mockIntegration = { + id: mockRuleId, + appId, + ruleType: "http", + requestMode: "single", + source: { + channelFilter: "chat:*", + type: "channel.message", + }, + target: { + url: "https://example.com/webhook", + format: "json", + enveloped: true, + }, + status: "enabled", + version: "1.0", + created: Date.now(), + modified: Date.now(), + }; + nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .get(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(200, mockIntegration); const { stdout } = await runCommand( @@ -123,8 +126,29 @@ currentAppId = "${mockAppId}" }); it("should display channel filter", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; + const mockIntegration = { + id: mockRuleId, + appId, + ruleType: "http", + requestMode: "single", + source: { + channelFilter: "chat:*", + type: "channel.message", + }, + target: { + url: "https://example.com/webhook", + format: "json", + enveloped: true, + }, + status: "enabled", + version: "1.0", + created: Date.now(), + modified: Date.now(), + }; + nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .get(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(200, mockIntegration); const { stdout } = await runCommand( @@ -136,8 +160,29 @@ currentAppId = "${mockAppId}" }); it("should display target information", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; + const mockIntegration = { + id: mockRuleId, + appId, + ruleType: "http", + requestMode: "single", + source: { + channelFilter: "chat:*", + type: "channel.message", + }, + target: { + url: "https://example.com/webhook", + format: "json", + enveloped: true, + }, + status: "enabled", + version: "1.0", + created: Date.now(), + modified: Date.now(), + }; + nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .get(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(200, mockIntegration); const { stdout } = await runCommand( @@ -159,8 +204,9 @@ currentAppId = "${mockAppId}" }); it("should handle integration not found", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .get(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(404, { error: "Not found" }); const { error } = await runCommand( @@ -173,8 +219,9 @@ currentAppId = "${mockAppId}" }); it("should handle API errors", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .get(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(500, { error: "Internal server error" }); const { error } = await runCommand( @@ -186,6 +233,21 @@ currentAppId = "${mockAppId}" expect(error?.message).toMatch(/Error getting integration|500/i); }); + it("should handle 401 authentication error", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; + nock("https://control.ably.net") + .get(`/v1/apps/${appId}/rules/${mockRuleId}`) + .reply(401, { error: "Unauthorized" }); + + const { error } = await runCommand( + ["integrations:get", mockRuleId], + import.meta.url, + ); + + expect(error).toBeDefined(); + expect(error?.message).toMatch(/401/); + }); + it("should reject unknown flags", async () => { const { error } = await runCommand( ["integrations:get", mockRuleId, "--unknown-flag"], @@ -199,27 +261,48 @@ currentAppId = "${mockAppId}" describe("flag options", () => { it("should accept --app flag", async () => { + const mockConfig = getMockConfigManager(); + const appId = mockConfig.getCurrentAppId()!; + const accountId = mockConfig.getCurrentAccount()!.accountId!; + const mockIntegration = { + id: mockRuleId, + appId, + ruleType: "http", + requestMode: "single", + source: { + channelFilter: "chat:*", + type: "channel.message", + }, + target: { + url: "https://example.com/webhook", + format: "json", + enveloped: true, + }, + status: "enabled", + version: "1.0", + created: Date.now(), + modified: Date.now(), + }; + // Mock the /me endpoint (needed by listApps in resolveAppIdFromNameOrId) nock("https://control.ably.net") .get("/v1/me") .reply(200, { - account: { id: mockAccountId, name: "Test Account" }, + account: { id: accountId, name: "Test Account" }, user: { email: "test@example.com" }, }); // Mock the apps list API call for resolveAppIdFromNameOrId nock("https://control.ably.net") - .get(`/v1/accounts/${mockAccountId}/apps`) - .reply(200, [ - { id: mockAppId, name: "Test App", accountId: mockAccountId }, - ]); + .get(`/v1/accounts/${accountId}/apps`) + .reply(200, [{ id: appId, name: "Test App", accountId }]); nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .get(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(200, mockIntegration); const { stdout } = await runCommand( - ["integrations:get", mockRuleId, "--app", mockAppId], + ["integrations:get", mockRuleId, "--app", appId], import.meta.url, ); diff --git a/test/unit/commands/integrations/update.test.ts b/test/unit/commands/integrations/update.test.ts index b3ec52e6..3dda85c7 100644 --- a/test/unit/commands/integrations/update.test.ts +++ b/test/unit/commands/integrations/update.test.ts @@ -1,77 +1,37 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, afterEach } from "vitest"; import { runCommand } from "@oclif/test"; import nock from "nock"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; +import { getMockConfigManager } from "../../../helpers/mock-config-manager.js"; describe("integrations:update command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; const mockRuleId = "rule-123456"; - let testConfigDir: string; - let originalConfigDir: string; - - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); - }); afterEach(() => { nock.cleanAll(); - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } }); - const mockIntegration = { - id: mockRuleId, - appId: mockAppId, - ruleType: "http", - requestMode: "single", - source: { - channelFilter: "chat:*", - type: "channel.message", - }, - target: { - url: "https://example.com/webhook", - format: "json", - enveloped: true, - }, - status: "enabled", - version: "1.0", - created: Date.now(), - modified: Date.now(), - }; - describe("successful integration update", () => { it("should update channel filter", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; + const mockIntegration = { + id: mockRuleId, + appId, + ruleType: "http", + requestMode: "single", + source: { + channelFilter: "chat:*", + type: "channel.message", + }, + target: { + url: "https://example.com/webhook", + format: "json", + enveloped: true, + }, + status: "enabled", + version: "1.0", + created: Date.now(), + modified: Date.now(), + }; const updatedIntegration = { ...mockIntegration, source: { @@ -82,12 +42,12 @@ currentAppId = "${mockAppId}" // Mock GET to fetch existing integration nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .get(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(200, mockIntegration); // Mock PATCH to update integration nock("https://control.ably.net") - .patch(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .patch(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(200, updatedIntegration); const { stdout } = await runCommand( @@ -100,6 +60,26 @@ currentAppId = "${mockAppId}" }); it("should update target URL for HTTP integrations", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; + const mockIntegration = { + id: mockRuleId, + appId, + ruleType: "http", + requestMode: "single", + source: { + channelFilter: "chat:*", + type: "channel.message", + }, + target: { + url: "https://example.com/webhook", + format: "json", + enveloped: true, + }, + status: "enabled", + version: "1.0", + created: Date.now(), + modified: Date.now(), + }; const newUrl = "https://new-example.com/webhook"; const updatedIntegration = { ...mockIntegration, @@ -110,11 +90,11 @@ currentAppId = "${mockAppId}" }; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .get(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(200, mockIntegration); nock("https://control.ably.net") - .patch(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .patch(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(200, updatedIntegration); const { stdout } = await runCommand( @@ -126,6 +106,26 @@ currentAppId = "${mockAppId}" }); it("should output JSON format when --json flag is used", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; + const mockIntegration = { + id: mockRuleId, + appId, + ruleType: "http", + requestMode: "single", + source: { + channelFilter: "chat:*", + type: "channel.message", + }, + target: { + url: "https://example.com/webhook", + format: "json", + enveloped: true, + }, + status: "enabled", + version: "1.0", + created: Date.now(), + modified: Date.now(), + }; const updatedIntegration = { ...mockIntegration, source: { @@ -135,11 +135,11 @@ currentAppId = "${mockAppId}" }; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .get(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(200, mockIntegration); nock("https://control.ably.net") - .patch(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .patch(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(200, updatedIntegration); const { stdout } = await runCommand( @@ -159,17 +159,37 @@ currentAppId = "${mockAppId}" }); it("should update request mode", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; + const mockIntegration = { + id: mockRuleId, + appId, + ruleType: "http", + requestMode: "single", + source: { + channelFilter: "chat:*", + type: "channel.message", + }, + target: { + url: "https://example.com/webhook", + format: "json", + enveloped: true, + }, + status: "enabled", + version: "1.0", + created: Date.now(), + modified: Date.now(), + }; const updatedIntegration = { ...mockIntegration, requestMode: "batch", }; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .get(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(200, mockIntegration); nock("https://control.ably.net") - .patch(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .patch(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(200, updatedIntegration); const { stdout } = await runCommand( @@ -200,8 +220,9 @@ currentAppId = "${mockAppId}" }); it("should handle integration not found", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .get(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(404, { error: "Not found" }); const { error } = await runCommand( @@ -214,12 +235,33 @@ currentAppId = "${mockAppId}" }); it("should handle API errors during update", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; + const mockIntegration = { + id: mockRuleId, + appId, + ruleType: "http", + requestMode: "single", + source: { + channelFilter: "chat:*", + type: "channel.message", + }, + target: { + url: "https://example.com/webhook", + format: "json", + enveloped: true, + }, + status: "enabled", + version: "1.0", + created: Date.now(), + modified: Date.now(), + }; + nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .get(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(200, mockIntegration); nock("https://control.ably.net") - .patch(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .patch(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(400, { error: "Invalid update" }); const { error } = await runCommand( @@ -244,6 +286,28 @@ currentAppId = "${mockAppId}" describe("flag options", () => { it("should accept --app flag", async () => { + const mockConfig = getMockConfigManager(); + const appId = mockConfig.getCurrentAppId()!; + const accountId = mockConfig.getCurrentAccount()!.accountId!; + const mockIntegration = { + id: mockRuleId, + appId, + ruleType: "http", + requestMode: "single", + source: { + channelFilter: "chat:*", + type: "channel.message", + }, + target: { + url: "https://example.com/webhook", + format: "json", + enveloped: true, + }, + status: "enabled", + version: "1.0", + created: Date.now(), + modified: Date.now(), + }; const updatedIntegration = { ...mockIntegration, source: { @@ -256,23 +320,21 @@ currentAppId = "${mockAppId}" nock("https://control.ably.net") .get("/v1/me") .reply(200, { - account: { id: mockAccountId, name: "Test Account" }, + account: { id: accountId, name: "Test Account" }, user: { email: "test@example.com" }, }); // Mock the apps list API call for resolveAppIdFromNameOrId nock("https://control.ably.net") - .get(`/v1/accounts/${mockAccountId}/apps`) - .reply(200, [ - { id: mockAppId, name: "Test App", accountId: mockAccountId }, - ]); + .get(`/v1/accounts/${accountId}/apps`) + .reply(200, [{ id: appId, name: "Test App", accountId }]); nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .get(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(200, mockIntegration); nock("https://control.ably.net") - .patch(`/v1/apps/${mockAppId}/rules/${mockRuleId}`) + .patch(`/v1/apps/${appId}/rules/${mockRuleId}`) .reply(200, updatedIntegration); const { stdout } = await runCommand( @@ -280,7 +342,7 @@ currentAppId = "${mockAppId}" "integrations:update", mockRuleId, "--app", - mockAppId, + appId, "--channel-filter", "new:*", ], diff --git a/test/unit/commands/logs/app/subscribe.test.ts b/test/unit/commands/logs/app/subscribe.test.ts index c955a68a..c351572d 100644 --- a/test/unit/commands/logs/app/subscribe.test.ts +++ b/test/unit/commands/logs/app/subscribe.test.ts @@ -1,53 +1,16 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { runCommand } from "@oclif/test"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; describe("logs:app:subscribe command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - const mockApiKey = `${mockAppId}.testkey:testsecret`; - let testConfigDir: string; - let originalConfigDir: string; - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[accounts.defaults.apps."${mockAppId}"] -apiKey = "${mockApiKey}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + } }); afterEach(() => { - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; } }); @@ -90,10 +53,6 @@ apiKey = "${mockApiKey}" }); describe("subscription behavior", () => { - afterEach(() => { - globalThis.__TEST_MOCKS__ = undefined; - }); - it("should subscribe to log channel and show initial message", async () => { const mockChannel = { name: "[meta]log", @@ -115,6 +74,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: { channels: mockChannels, connection: mockConnection, @@ -154,6 +114,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: { channels: mockChannels, connection: mockConnection, @@ -196,6 +157,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: { channels: mockChannels, connection: mockConnection, @@ -218,12 +180,11 @@ apiKey = "${mockApiKey}" }); describe("error handling", () => { - afterEach(() => { - globalThis.__TEST_MOCKS__ = undefined; - }); - it("should handle missing mock client in test mode", async () => { - globalThis.__TEST_MOCKS__ = undefined; + // Clear the realtime mock + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + } const { error } = await runCommand( ["logs:app:subscribe"], diff --git a/test/unit/commands/logs/channel-lifecycle.test.ts b/test/unit/commands/logs/channel-lifecycle.test.ts index d4f0109c..83579087 100644 --- a/test/unit/commands/logs/channel-lifecycle.test.ts +++ b/test/unit/commands/logs/channel-lifecycle.test.ts @@ -1,57 +1,17 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { runCommand } from "@oclif/test"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; describe("logs:channel-lifecycle command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - const mockApiKey = `${mockAppId}.testkey:testsecret`; - let testConfigDir: string; - let originalConfigDir: string; - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[accounts.default.apps."${mockAppId}"] -appName = "Test App" -apiKey = "${mockApiKey}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + } }); afterEach(() => { - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } - - globalThis.__TEST_MOCKS__ = undefined; }); describe("command flags", () => { @@ -88,6 +48,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: { channels: mockChannels, connection: mockConnection, @@ -129,6 +90,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: { channels: mockChannels, connection: mockConnection, @@ -168,6 +130,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: { channels: mockChannels, connection: mockConnection, @@ -211,6 +174,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: { channels: mockChannels, connection: mockConnection, @@ -251,6 +215,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: { channels: mockChannels, connection: mockConnection, @@ -268,8 +233,10 @@ apiKey = "${mockApiKey}" }); it("should handle missing mock client in test mode", async () => { - // No mock set up - globalThis.__TEST_MOCKS__ = undefined; + // Clear the realtime mock + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + } const { error } = await runCommand( ["logs:channel-lifecycle"], @@ -305,6 +272,7 @@ apiKey = "${mockApiKey}" const mockClose = vi.fn(); globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: { channels: mockChannels, connection: mockConnection, @@ -343,6 +311,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: { channels: mockChannels, connection: mockConnection, diff --git a/test/unit/commands/logs/channel-lifecycle/subscribe.test.ts b/test/unit/commands/logs/channel-lifecycle/subscribe.test.ts index bdd24538..41cf9b73 100644 --- a/test/unit/commands/logs/channel-lifecycle/subscribe.test.ts +++ b/test/unit/commands/logs/channel-lifecycle/subscribe.test.ts @@ -1,53 +1,16 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { runCommand } from "@oclif/test"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; describe("logs:channel-lifecycle:subscribe command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - const mockApiKey = `${mockAppId}.testkey:testsecret`; - let testConfigDir: string; - let originalConfigDir: string; - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[apps."${mockAppId}"] -apiKey = "${mockApiKey}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + } }); afterEach(() => { - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; } }); @@ -83,10 +46,6 @@ apiKey = "${mockApiKey}" }); describe("subscription behavior", () => { - afterEach(() => { - globalThis.__TEST_MOCKS__ = undefined; - }); - it("should subscribe to channel lifecycle events and show initial message", async () => { const mockChannel = { name: "[meta]channel.lifecycle", @@ -108,6 +67,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: { channels: mockChannels, connection: mockConnection, @@ -148,6 +108,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: { channels: mockChannels, connection: mockConnection, @@ -183,6 +144,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: { channels: mockChannels, connection: mockConnection, @@ -204,12 +166,11 @@ apiKey = "${mockApiKey}" }); describe("error handling", () => { - afterEach(() => { - globalThis.__TEST_MOCKS__ = undefined; - }); - it("should handle missing mock client in test mode", async () => { - globalThis.__TEST_MOCKS__ = undefined; + // Clear the realtime mock + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + } const { error } = await runCommand( ["logs:channel-lifecycle:subscribe"], diff --git a/test/unit/commands/logs/connection-lifecycle/history.test.ts b/test/unit/commands/logs/connection-lifecycle/history.test.ts index 3759adc0..150a6d29 100644 --- a/test/unit/commands/logs/connection-lifecycle/history.test.ts +++ b/test/unit/commands/logs/connection-lifecycle/history.test.ts @@ -1,41 +1,13 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { runCommand } from "@oclif/test"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; describe("logs:connection-lifecycle:history command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - const mockApiKey = `${mockAppId}.testkey:testsecret`; - let testConfigDir: string; - let originalConfigDir: string; let mockHistory: ReturnType; beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[apps."${mockAppId}"] -apiKey = "${mockApiKey}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRestMock; + } // Set up mock for REST client mockHistory = vi.fn().mockResolvedValue({ @@ -57,6 +29,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRestMock: { channels: { get: vi.fn().mockReturnValue(mockChannel), @@ -68,17 +41,8 @@ apiKey = "${mockApiKey}" afterEach(() => { vi.clearAllMocks(); - delete process.env.ABLY_ACCESS_TOKEN; - globalThis.__TEST_MOCKS__ = undefined; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRestMock; } }); diff --git a/test/unit/commands/logs/push/history.test.ts b/test/unit/commands/logs/push/history.test.ts index 869e6671..a891c2f1 100644 --- a/test/unit/commands/logs/push/history.test.ts +++ b/test/unit/commands/logs/push/history.test.ts @@ -1,41 +1,13 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { runCommand } from "@oclif/test"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; describe("logs:push:history command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - const mockApiKey = `${mockAppId}.testkey:testsecret`; - let testConfigDir: string; - let originalConfigDir: string; let mockHistory: ReturnType; beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[apps."${mockAppId}"] -apiKey = "${mockApiKey}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRestMock; + } // Set up mock for REST client mockHistory = vi.fn().mockResolvedValue({ @@ -57,6 +29,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRestMock: { channels: { get: vi.fn().mockReturnValue(mockChannel), @@ -68,17 +41,8 @@ apiKey = "${mockApiKey}" afterEach(() => { vi.clearAllMocks(); - delete process.env.ABLY_ACCESS_TOKEN; - globalThis.__TEST_MOCKS__ = undefined; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRestMock; } }); diff --git a/test/unit/commands/queues/create.test.ts b/test/unit/commands/queues/create.test.ts index 9171d928..a063a5a1 100644 --- a/test/unit/commands/queues/create.test.ts +++ b/test/unit/commands/queues/create.test.ts @@ -1,136 +1,74 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, afterEach } from "vitest"; import nock from "nock"; import { runCommand } from "@oclif/test"; -import * as path from "node:path"; -import * as fs from "node:fs"; -import * as os from "node:os"; - -// Pre-define constants to avoid ESLint errors about function calls in describe blocks -const mockAccessToken = "fake_access_token"; -const mockAccountId = "test-account-id"; -const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; -const mockQueueName = "test-queue"; -const mockQueueId = "queue-550e8400-e29b-41d4-a716-446655440000"; - -// Mock data for successful responses -const mockAccountResponse = { - account: { id: mockAccountId, name: "Test Account" }, - user: { email: "test@example.com" }, -}; - -const mockAppResponse = { - id: mockAppId, - accountId: mockAccountId, - name: "Test App", - status: "active", - created: Date.now(), - modified: Date.now(), - tlsOnly: false, -}; - -const mockQueueResponse = { - id: mockQueueId, - appId: mockAppId, - name: mockQueueName, - region: "us-east-1-a", - state: "active", - maxLength: 10000, - ttl: 60, - deadletter: false, - deadletterId: "", - messages: { - ready: 0, - total: 0, - unacknowledged: 0, - }, - stats: { - publishRate: null, - deliveryRate: null, - acknowledgementRate: null, - }, - amqp: { - uri: "amqps://queue.ably.io:5671", - queueName: "test-queue", - }, - stomp: { - uri: "stomp://queue.ably.io:61614", - host: "queue.ably.io", - destination: "/queue/test-queue", - }, -}; +import { getMockConfigManager } from "../../../helpers/mock-config-manager.js"; describe("queues:create command", () => { - let testConfigDir: string; - let originalConfigDir: string; - - beforeEach(() => { - // Set environment variable for access token - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - // Create a temporary config directory for testing - testConfigDir = path.join(os.tmpdir(), `ably-cli-test-${Date.now()}`); - fs.mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - // Store original config dir and set test config dir - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - // Create a minimal config file with a default account and current app - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -currentAppId = "${mockAppId}" -accountName = "Test Account" -userEmail = "test@example.com" - -[accounts.default.apps.${mockAppId}] -appName = "Test App" -`; - fs.writeFileSync(path.join(testConfigDir, "config"), configContent); - }); + const mockQueueName = "test-queue"; + const mockQueueId = "queue-550e8400-e29b-41d4-a716-446655440000"; afterEach(() => { - // Clean up nock interceptors nock.cleanAll(); - delete process.env.ABLY_ACCESS_TOKEN; - - // Restore original config directory - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - // Clean up test config directory - if (fs.existsSync(testConfigDir)) { - fs.rmSync(testConfigDir, { recursive: true, force: true }); - } }); + function createMockQueueResponse(appId: string) { + return { + id: mockQueueId, + appId, + name: mockQueueName, + region: "us-east-1-a", + state: "active", + maxLength: 10000, + ttl: 60, + deadletter: false, + deadletterId: "", + messages: { + ready: 0, + total: 0, + unacknowledged: 0, + }, + stats: { + publishRate: null, + deliveryRate: null, + acknowledgementRate: null, + }, + amqp: { + uri: "amqps://queue.ably.io:5671", + queueName: "test-queue", + }, + stomp: { + uri: "stomp://queue.ably.io:61614", + host: "queue.ably.io", + destination: "/queue/test-queue", + }, + }; + } + describe("successful queue creation", () => { it("should create a queue successfully with default settings", async () => { - // Mock the /me endpoint to get account info + const mockConfig = getMockConfigManager(); + const appId = mockConfig.getCurrentAppId()!; + const accountId = mockConfig.getCurrentAccount()!.accountId!; + nock("https://control.ably.net") .get("/v1/me") - .reply(200, mockAccountResponse); + .reply(200, { + account: { id: accountId, name: "Test Account" }, + user: { email: "test@example.com" }, + }); - // Mock the apps listing endpoint nock("https://control.ably.net") - .get(`/v1/accounts/${mockAccountId}/apps`) - .reply(200, [mockAppResponse]); + .get(`/v1/accounts/${accountId}/apps`) + .reply(200, [{ id: appId, accountId, name: "Test App" }]); - // Mock the queue creation endpoint nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/queues`, { + .post(`/v1/apps/${appId}/queues`, { name: mockQueueName, maxLength: 10000, region: "us-east-1-a", ttl: 60, }) - .reply(201, mockQueueResponse); + .reply(201, createMockQueueResponse(appId)); const { stdout } = await runCommand( ["queues:create", "--name", mockQueueName], @@ -148,21 +86,26 @@ appName = "Test App" }); it("should create a queue with custom settings", async () => { - // Mock the /me endpoint to get account info + const mockConfig = getMockConfigManager(); + const appId = mockConfig.getCurrentAppId()!; + const accountId = mockConfig.getCurrentAccount()!.accountId!; + nock("https://control.ably.net") .get("/v1/me") - .reply(200, mockAccountResponse); + .reply(200, { + account: { id: accountId, name: "Test Account" }, + user: { email: "test@example.com" }, + }); - // Mock the queue creation endpoint with custom settings nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/queues`, { + .post(`/v1/apps/${appId}/queues`, { name: mockQueueName, maxLength: 50000, region: "eu-west-1-a", ttl: 3600, }) .reply(201, { - ...mockQueueResponse, + ...createMockQueueResponse(appId), region: "eu-west-1-a", maxLength: 50000, ttl: 3600, @@ -190,14 +133,20 @@ appName = "Test App" }); it("should output JSON format when --json flag is used", async () => { - // Mock the /me endpoint to get account info + const mockConfig = getMockConfigManager(); + const appId = mockConfig.getCurrentAppId()!; + const accountId = mockConfig.getCurrentAccount()!.accountId!; + nock("https://control.ably.net") .get("/v1/me") - .reply(200, mockAccountResponse); + .reply(200, { + account: { id: accountId, name: "Test Account" }, + user: { email: "test@example.com" }, + }); nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/queues`) - .reply(201, mockQueueResponse); + .post(`/v1/apps/${appId}/queues`) + .reply(201, createMockQueueResponse(appId)); const { stdout } = await runCommand( ["queues:create", "--name", mockQueueName, "--json"], @@ -211,28 +160,24 @@ appName = "Test App" }); it("should use custom app ID when provided", async () => { + const accountId = getMockConfigManager().getCurrentAccount()!.accountId!; const customAppId = "custom-app-id"; - // Mock the /me endpoint to get account info nock("https://control.ably.net") .get("/v1/me") - .reply(200, mockAccountResponse); + .reply(200, { + account: { id: accountId, name: "Test Account" }, + user: { email: "test@example.com" }, + }); - // Mock the apps listing endpoint to find the custom app nock("https://control.ably.net") - .get(`/v1/accounts/${mockAccountId}/apps`) - .reply(200, [ - { - ...mockAppResponse, - id: customAppId, - name: customAppId, - }, - ]); + .get(`/v1/accounts/${accountId}/apps`) + .reply(200, [{ id: customAppId, accountId, name: customAppId }]); nock("https://control.ably.net") .post(`/v1/apps/${customAppId}/queues`) .reply(201, { - ...mockQueueResponse, + ...createMockQueueResponse(customAppId), appId: customAppId, }); @@ -245,29 +190,33 @@ appName = "Test App" }); it("should use custom access token when provided", async () => { + const mockConfig = getMockConfigManager(); + const appId = mockConfig.getCurrentAppId()!; + const accountId = mockConfig.getCurrentAccount()!.accountId!; const customToken = "custom_access_token"; - // Mock the /me endpoint to get account info nock("https://control.ably.net", { reqheaders: { authorization: `Bearer ${customToken}`, }, }) .get("/v1/me") - .reply(200, mockAccountResponse); + .reply(200, { + account: { id: accountId, name: "Test Account" }, + user: { email: "test@example.com" }, + }); - // Mock the apps listing endpoint nock("https://control.ably.net") - .get(`/v1/accounts/${mockAccountId}/apps`) - .reply(200, [mockAppResponse]); + .get(`/v1/accounts/${accountId}/apps`) + .reply(200, [{ id: appId, accountId, name: "Test App" }]); nock("https://control.ably.net", { reqheaders: { authorization: `Bearer ${customToken}`, }, }) - .post(`/v1/apps/${mockAppId}/queues`) - .reply(201, mockQueueResponse); + .post(`/v1/apps/${appId}/queues`) + .reply(201, createMockQueueResponse(appId)); const { stdout } = await runCommand( [ @@ -286,14 +235,19 @@ appName = "Test App" describe("error handling", () => { it("should handle 401 authentication error", async () => { - // Mock the /me endpoint to get account info + const mockConfig = getMockConfigManager(); + const appId = mockConfig.getCurrentAppId()!; + const accountId = mockConfig.getCurrentAccount()!.accountId!; + nock("https://control.ably.net") .get("/v1/me") - .reply(200, mockAccountResponse); + .reply(200, { + account: { id: accountId, name: "Test Account" }, + user: { email: "test@example.com" }, + }); - // Mock authentication failure nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/queues`) + .post(`/v1/apps/${appId}/queues`) .reply(401, { error: "Unauthorized" }); const { error } = await runCommand( @@ -307,14 +261,19 @@ appName = "Test App" }); it("should handle 403 forbidden error", async () => { - // Mock the /me endpoint to get account info + const mockConfig = getMockConfigManager(); + const appId = mockConfig.getCurrentAppId()!; + const accountId = mockConfig.getCurrentAccount()!.accountId!; + nock("https://control.ably.net") .get("/v1/me") - .reply(200, mockAccountResponse); + .reply(200, { + account: { id: accountId, name: "Test Account" }, + user: { email: "test@example.com" }, + }); - // Mock forbidden response nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/queues`) + .post(`/v1/apps/${appId}/queues`) .reply(403, { error: "Forbidden" }); const { error } = await runCommand( @@ -328,14 +287,19 @@ appName = "Test App" }); it("should handle 404 app not found error", async () => { - // Mock the /me endpoint to get account info + const mockConfig = getMockConfigManager(); + const appId = mockConfig.getCurrentAppId()!; + const accountId = mockConfig.getCurrentAccount()!.accountId!; + nock("https://control.ably.net") .get("/v1/me") - .reply(200, mockAccountResponse); + .reply(200, { + account: { id: accountId, name: "Test Account" }, + user: { email: "test@example.com" }, + }); - // Mock not found response nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/queues`) + .post(`/v1/apps/${appId}/queues`) .reply(404, { error: "App not found" }); const { error } = await runCommand( @@ -349,14 +313,19 @@ appName = "Test App" }); it("should handle 500 server error", async () => { - // Mock the /me endpoint to get account info + const mockConfig = getMockConfigManager(); + const appId = mockConfig.getCurrentAppId()!; + const accountId = mockConfig.getCurrentAccount()!.accountId!; + nock("https://control.ably.net") .get("/v1/me") - .reply(200, mockAccountResponse); + .reply(200, { + account: { id: accountId, name: "Test Account" }, + user: { email: "test@example.com" }, + }); - // Mock server error nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/queues`) + .post(`/v1/apps/${appId}/queues`) .reply(500, { error: "Internal Server Error" }); const { error } = await runCommand( @@ -379,33 +348,7 @@ appName = "Test App" }); it("should require app to be specified when not in environment", async () => { - // Mock the /me endpoint to get account info - nock("https://control.ably.net") - .get("/v1/me") - .reply(200, mockAccountResponse); - - // Mock empty apps list to trigger "no app specified" error - nock("https://control.ably.net") - .get(`/v1/accounts/${mockAccountId}/apps`) - .reply(200, [mockAppResponse]); - - // Set up a clean config directory without current app - const emptyConfigDir = path.join( - os.tmpdir(), - `ably-cli-test-empty-${Date.now()}`, - ); - fs.mkdirSync(emptyConfigDir, { recursive: true, mode: 0o700 }); - const emptyConfigContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -`; - fs.writeFileSync(path.join(emptyConfigDir, "config"), emptyConfigContent); - process.env.ABLY_CLI_CONFIG_DIR = emptyConfigDir; + getMockConfigManager().clearAccounts(); const { error } = await runCommand( ["queues:create", "--name", mockQueueName], @@ -413,23 +356,24 @@ userEmail = "test@example.com" ); expect(error).toBeDefined(); - expect(error?.message).toMatch(/No app selected/); + expect(error?.message).toMatch(/No access token|No app|not logged in/i); expect(error?.oclif?.exit).toBeGreaterThan(0); - - // Clean up empty config directory - fs.rmSync(emptyConfigDir, { recursive: true, force: true }); - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; }); it("should handle network errors", async () => { - // Mock the /me endpoint to get account info + const mockConfig = getMockConfigManager(); + const appId = mockConfig.getCurrentAppId()!; + const accountId = mockConfig.getCurrentAccount()!.accountId!; + nock("https://control.ably.net") .get("/v1/me") - .reply(200, mockAccountResponse); + .reply(200, { + account: { id: accountId, name: "Test Account" }, + user: { email: "test@example.com" }, + }); - // Mock network error nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/queues`) + .post(`/v1/apps/${appId}/queues`) .replyWithError("Network error"); const { error } = await runCommand( @@ -443,14 +387,19 @@ userEmail = "test@example.com" }); it("should handle validation errors from API", async () => { - // Mock the /me endpoint to get account info + const mockConfig = getMockConfigManager(); + const appId = mockConfig.getCurrentAppId()!; + const accountId = mockConfig.getCurrentAccount()!.accountId!; + nock("https://control.ably.net") .get("/v1/me") - .reply(200, mockAccountResponse); + .reply(200, { + account: { id: accountId, name: "Test Account" }, + user: { email: "test@example.com" }, + }); - // Mock validation error nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/queues`) + .post(`/v1/apps/${appId}/queues`) .reply(400, { error: "Validation failed", details: "Queue name already exists", @@ -467,14 +416,19 @@ userEmail = "test@example.com" }); it("should handle 429 rate limit error", async () => { - // Mock the /me endpoint to get account info + const mockConfig = getMockConfigManager(); + const appId = mockConfig.getCurrentAppId()!; + const accountId = mockConfig.getCurrentAccount()!.accountId!; + nock("https://control.ably.net") .get("/v1/me") - .reply(200, mockAccountResponse); + .reply(200, { + account: { id: accountId, name: "Test Account" }, + user: { email: "test@example.com" }, + }); - // Mock quota exceeded error nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/queues`) + .post(`/v1/apps/${appId}/queues`) .reply(429, { error: "Rate limit exceeded", details: "Too many requests", @@ -493,20 +447,26 @@ userEmail = "test@example.com" describe("parameter validation", () => { it("should accept minimum valid parameter values", async () => { - // Mock the /me endpoint to get account info + const mockConfig = getMockConfigManager(); + const appId = mockConfig.getCurrentAppId()!; + const accountId = mockConfig.getCurrentAccount()!.accountId!; + nock("https://control.ably.net") .get("/v1/me") - .reply(200, mockAccountResponse); + .reply(200, { + account: { id: accountId, name: "Test Account" }, + user: { email: "test@example.com" }, + }); nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/queues`, { + .post(`/v1/apps/${appId}/queues`, { name: mockQueueName, maxLength: 1, region: "us-east-1-a", ttl: 1, }) .reply(201, { - ...mockQueueResponse, + ...createMockQueueResponse(appId), maxLength: 1, ttl: 1, }); @@ -530,20 +490,26 @@ userEmail = "test@example.com" }); it("should accept large parameter values and different regions", async () => { - // Mock the /me endpoint to get account info + const mockConfig = getMockConfigManager(); + const appId = mockConfig.getCurrentAppId()!; + const accountId = mockConfig.getCurrentAccount()!.accountId!; + nock("https://control.ably.net") .get("/v1/me") - .reply(200, mockAccountResponse); + .reply(200, { + account: { id: accountId, name: "Test Account" }, + user: { email: "test@example.com" }, + }); nock("https://control.ably.net") - .post(`/v1/apps/${mockAppId}/queues`, { + .post(`/v1/apps/${appId}/queues`, { name: mockQueueName, maxLength: 1000000, region: "ap-southeast-2-a", ttl: 86400, }) .reply(201, { - ...mockQueueResponse, + ...createMockQueueResponse(appId), region: "ap-southeast-2-a", maxLength: 1000000, ttl: 86400, diff --git a/test/unit/commands/queues/delete.test.ts b/test/unit/commands/queues/delete.test.ts index 57c51321..8e0418e9 100644 --- a/test/unit/commands/queues/delete.test.ts +++ b/test/unit/commands/queues/delete.test.ts @@ -1,111 +1,59 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, afterEach } from "vitest"; import nock from "nock"; import { runCommand } from "@oclif/test"; -import * as path from "node:path"; -import * as fs from "node:fs"; -import * as os from "node:os"; +import { getMockConfigManager } from "../../../helpers/mock-config-manager.js"; describe("queues:delete command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; const mockQueueName = "test-queue"; - // Queue IDs follow the format: appId:region:name - const mockQueueId = `${mockAppId}:us-east-1-a:${mockQueueName}`; - let testConfigDir: string; - let originalConfigDir: string; - - const mockAccountResponse = { - account: { id: mockAccountId, name: "Test Account" }, - user: { email: "test@example.com" }, - }; - - beforeEach(() => { - // Set environment variable for access token - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - // Create a temporary config directory for testing - testConfigDir = path.join(os.tmpdir(), `ably-cli-test-${Date.now()}`); - fs.mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - // Store original config dir and set test config dir - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - // Create a minimal config file with a default account - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" -`; - fs.writeFileSync(path.join(testConfigDir, "config"), configContent); - }); afterEach(() => { - // Clean up nock interceptors nock.cleanAll(); - delete process.env.ABLY_ACCESS_TOKEN; - - // Restore original config directory - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - // Clean up test config directory - if (fs.existsSync(testConfigDir)) { - fs.rmSync(testConfigDir, { recursive: true, force: true }); - } }); + function createMockQueue(appId: string, queueId: string) { + return { + id: queueId, + appId, + name: mockQueueName, + region: "us-east-1-a", + state: "active", + maxLength: 10000, + ttl: 60, + deadletter: false, + deadletterId: "", + messages: { + ready: 5, + total: 10, + unacknowledged: 5, + }, + stats: { + publishRate: null, + deliveryRate: null, + acknowledgementRate: null, + }, + amqp: { + uri: "amqps://queue.ably.io:5671", + queueName: mockQueueName, + }, + stomp: { + uri: "stomp://queue.ably.io:61614", + host: "queue.ably.io", + destination: `/queue/${mockQueueName}`, + }, + }; + } + describe("successful queue deletion", () => { it("should delete a queue successfully with --force flag", async () => { - // Mock the queue listing endpoint to find the queue - const mockQueue = { - id: mockQueueId, - appId: mockAppId, - name: mockQueueName, - region: "us-east-1-a", - state: "active", - maxLength: 10000, - ttl: 60, - deadletter: false, - deadletterId: "", - messages: { - ready: 5, - total: 10, - unacknowledged: 5, - }, - stats: { - publishRate: null, - deliveryRate: null, - acknowledgementRate: null, - }, - amqp: { - uri: "amqps://queue.ably.io:5671", - queueName: mockQueueName, - }, - stomp: { - uri: "stomp://queue.ably.io:61614", - host: "queue.ably.io", - destination: `/queue/${mockQueueName}`, - }, - }; + const appId = getMockConfigManager().getCurrentAppId()!; + const mockQueueId = `${appId}:us-east-1-a:${mockQueueName}`; - // Mock the apps listing endpoint nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/queues`) - .reply(200, [mockQueue]); + .get(`/v1/apps/${appId}/queues`) + .reply(200, [createMockQueue(appId, mockQueueId)]); - // Mock the queue deletion endpoint nock("https://control.ably.net") - .delete(`/v1/apps/${mockAppId}/queues/${mockQueueId}`) + .delete(`/v1/apps/${appId}/queues/${mockQueueId}`) .reply(204); const { stdout } = await runCommand( @@ -113,70 +61,31 @@ currentAppId = "${mockAppId}" import.meta.url, ); - // Command should succeed without error expect(stdout).toContain( - 'Queue "test-queue" (ID: 550e8400-e29b-41d4-a716-446655440000:us-east-1-a:test-queue) deleted successfully', + `Queue "test-queue" (ID: ${mockQueueId}) deleted successfully`, ); }); it("should delete a queue with custom app ID", async () => { + const accountId = getMockConfigManager().getCurrentAccount()!.accountId!; const customAppId = "custom-app-id"; - - const mockAppResponse = { - id: customAppId, - accountId: mockAccountId, - name: "Test App", - status: "active", - created: Date.now(), - modified: Date.now(), - tlsOnly: false, - }; + const mockQueueId = `${customAppId}:us-east-1-a:${mockQueueName}`; nock("https://control.ably.net") .get("/v1/me") - .reply(200, mockAccountResponse); + .reply(200, { + account: { id: accountId, name: "Test Account" }, + user: { email: "test@example.com" }, + }); nock("https://control.ably.net") - .get(`/v1/accounts/${mockAccountId}/apps`) - .reply(200, [mockAppResponse]); + .get(`/v1/accounts/${accountId}/apps`) + .reply(200, [{ id: customAppId, accountId, name: "Test App" }]); - // Mock the queue listing endpoint for custom app nock("https://control.ably.net") .get(`/v1/apps/${customAppId}/queues`) - .reply(200, [ - { - id: mockQueueId, - appId: customAppId, - name: mockQueueName, - region: "us-east-1-a", - state: "active", - maxLength: 10000, - ttl: 60, - deadletter: false, - deadletterId: "", - messages: { - ready: 0, - total: 0, - unacknowledged: 0, - }, - stats: { - publishRate: null, - deliveryRate: null, - acknowledgementRate: null, - }, - amqp: { - uri: "amqps://queue.ably.io:5671", - queueName: mockQueueName, - }, - stomp: { - uri: "stomp://queue.ably.io:61614", - host: "queue.ably.io", - destination: `/queue/${mockQueueName}`, - }, - }, - ]); + .reply(200, [createMockQueue(customAppId, mockQueueId)]); - // Mock the queue deletion endpoint for custom app nock("https://control.ably.net") .delete(`/v1/apps/${customAppId}/queues/${mockQueueId}`) .reply(204); @@ -186,62 +95,30 @@ currentAppId = "${mockAppId}" import.meta.url, ); - // Command should succeed without error expect(stdout).toContain( - 'Queue "test-queue" (ID: 550e8400-e29b-41d4-a716-446655440000:us-east-1-a:test-queue) deleted successfully', + `Queue "test-queue" (ID: ${mockQueueId}) deleted successfully`, ); }); it("should use custom access token when provided", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; + const mockQueueId = `${appId}:us-east-1-a:${mockQueueName}`; const customToken = "custom_access_token"; - // Mock the queue listing endpoint with custom token nock("https://control.ably.net", { reqheaders: { authorization: `Bearer ${customToken}`, }, }) - .get(`/v1/apps/${mockAppId}/queues`) - .reply(200, [ - { - id: mockQueueId, - appId: mockAppId, - name: mockQueueName, - region: "us-east-1-a", - state: "active", - maxLength: 10000, - ttl: 60, - deadletter: false, - deadletterId: "", - messages: { - ready: 0, - total: 0, - unacknowledged: 0, - }, - stats: { - publishRate: null, - deliveryRate: null, - acknowledgementRate: null, - }, - amqp: { - uri: "amqps://queue.ably.io:5671", - queueName: mockQueueName, - }, - stomp: { - uri: "stomp://queue.ably.io:61614", - host: "queue.ably.io", - destination: `/queue/${mockQueueName}`, - }, - }, - ]); + .get(`/v1/apps/${appId}/queues`) + .reply(200, [createMockQueue(appId, mockQueueId)]); - // Mock the queue deletion endpoint with custom token nock("https://control.ably.net", { reqheaders: { authorization: `Bearer ${customToken}`, }, }) - .delete(`/v1/apps/${mockAppId}/queues/${mockQueueId}`) + .delete(`/v1/apps/${appId}/queues/${mockQueueId}`) .reply(204); const { error } = await runCommand( @@ -255,16 +132,17 @@ currentAppId = "${mockAppId}" import.meta.url, ); - // Command should succeed without error expect(error).toBeUndefined(); }); }); describe("error handling", () => { it("should handle 401 authentication error", async () => { - // Mock authentication failure + const appId = getMockConfigManager().getCurrentAppId()!; + const mockQueueId = `${appId}:us-east-1-a:${mockQueueName}`; + nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/queues`) + .get(`/v1/apps/${appId}/queues`) .reply(401, { error: "Unauthorized" }); const { error } = await runCommand( @@ -278,9 +156,11 @@ currentAppId = "${mockAppId}" }); it("should handle 403 forbidden error", async () => { - // Mock forbidden response + const appId = getMockConfigManager().getCurrentAppId()!; + const mockQueueId = `${appId}:us-east-1-a:${mockQueueName}`; + nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/queues`) + .get(`/v1/apps/${appId}/queues`) .reply(403, { error: "Forbidden" }); const { error } = await runCommand( @@ -294,9 +174,11 @@ currentAppId = "${mockAppId}" }); it("should handle 404 app not found error", async () => { - // Mock app not found response + const appId = getMockConfigManager().getCurrentAppId()!; + const mockQueueId = `${appId}:us-east-1-a:${mockQueueName}`; + nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/queues`) + .get(`/v1/apps/${appId}/queues`) .reply(404, { error: "App not found" }); const { error } = await runCommand( @@ -310,9 +192,11 @@ currentAppId = "${mockAppId}" }); it("should handle 500 server error", async () => { - // Mock server error + const appId = getMockConfigManager().getCurrentAppId()!; + const mockQueueId = `${appId}:us-east-1-a:${mockQueueName}`; + nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/queues`) + .get(`/v1/apps/${appId}/queues`) .reply(500, { error: "Internal Server Error" }); const { error } = await runCommand( @@ -321,15 +205,16 @@ currentAppId = "${mockAppId}" ); expect(error).toBeDefined(); - // Queue must be found first before we can test deletion failures expect(error?.message).toMatch(/Queue.*not found|500/); expect(error?.oclif?.exit).toBeGreaterThan(0); }); it("should handle queue not found error", async () => { - // Mock empty queue list (queue not found) + const appId = getMockConfigManager().getCurrentAppId()!; + const mockQueueId = `${appId}:us-east-1-a:${mockQueueName}`; + nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/queues`) + .get(`/v1/apps/${appId}/queues`) .reply(200, []); const { error } = await runCommand( @@ -343,45 +228,15 @@ currentAppId = "${mockAppId}" }); it("should handle deletion API error", async () => { - // Mock finding the queue but deletion fails + const appId = getMockConfigManager().getCurrentAppId()!; + const mockQueueId = `${appId}:us-east-1-a:${mockQueueName}`; + nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/queues`) - .reply(200, [ - { - id: mockQueueId, - appId: mockAppId, - name: mockQueueName, - region: "us-east-1-a", - state: "active", - maxLength: 10000, - ttl: 60, - deadletter: false, - deadletterId: "", - messages: { - ready: 0, - total: 0, - unacknowledged: 0, - }, - stats: { - publishRate: null, - deliveryRate: null, - acknowledgementRate: null, - }, - amqp: { - uri: "amqps://queue.ably.io:5671", - queueName: mockQueueName, - }, - stomp: { - uri: "stomp://queue.ably.io:61614", - host: "queue.ably.io", - destination: `/queue/${mockQueueName}`, - }, - }, - ]); + .get(`/v1/apps/${appId}/queues`) + .reply(200, [createMockQueue(appId, mockQueueId)]); - // Mock deletion failure nock("https://control.ably.net") - .delete(`/v1/apps/${mockAppId}/queues/${mockQueueId}`) + .delete(`/v1/apps/${appId}/queues/${mockQueueId}`) .reply(500, { error: "Internal Server Error" }); const { error } = await runCommand( @@ -403,7 +258,8 @@ currentAppId = "${mockAppId}" }); it("should require app to be specified when not in environment", async () => { - process.env.ABLY_CLI_CONFIG_DIR = "/tmp"; + getMockConfigManager().clearAccounts(); + const mockQueueId = "some-app:us-east-1-a:test-queue"; const { error } = await runCommand( ["queues:delete", mockQueueId, "--force"], @@ -411,14 +267,16 @@ currentAppId = "${mockAppId}" ); expect(error).toBeDefined(); - expect(error?.message).toMatch(/No app|Failed to get apps/); + expect(error?.message).toMatch(/No access token|No app|not logged in/i); expect(error?.oclif?.exit).toBeGreaterThan(0); }); it("should handle network errors", async () => { - // Mock network error + const appId = getMockConfigManager().getCurrentAppId()!; + const mockQueueId = `${appId}:us-east-1-a:${mockQueueName}`; + nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/queues`) + .get(`/v1/apps/${appId}/queues`) .replyWithError("Network error"); const { error } = await runCommand( @@ -432,13 +290,15 @@ currentAppId = "${mockAppId}" }); it("should handle when specific queue ID is not found in list", async () => { - // Mock queue with different ID (not found case) + const appId = getMockConfigManager().getCurrentAppId()!; + const mockQueueId = `${appId}:us-east-1-a:${mockQueueName}`; + nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/queues`) + .get(`/v1/apps/${appId}/queues`) .reply(200, [ { id: "different-queue-id", - appId: mockAppId, + appId, name: "different-queue-name", region: "us-east-1-a", state: "active", @@ -446,11 +306,7 @@ currentAppId = "${mockAppId}" ttl: 60, deadletter: false, deadletterId: "", - messages: { - ready: 0, - total: 0, - unacknowledged: 0, - }, + messages: { ready: 0, total: 0, unacknowledged: 0 }, stats: { publishRate: null, deliveryRate: null, @@ -481,45 +337,25 @@ currentAppId = "${mockAppId}" }); it("should handle 409 conflict error when queue is in use", async () => { - // Mock conflict error when queue is in use + const appId = getMockConfigManager().getCurrentAppId()!; + const mockQueueId = `${appId}:us-east-1-a:${mockQueueName}`; + nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/queues`) + .get(`/v1/apps/${appId}/queues`) .reply(200, [ { - id: mockQueueId, - appId: mockAppId, - name: mockQueueName, - region: "us-east-1-a", - state: "active", - maxLength: 10000, - ttl: 60, - deadletter: false, - deadletterId: "", - messages: { - ready: 100, - total: 200, - unacknowledged: 100, - }, + ...createMockQueue(appId, mockQueueId), + messages: { ready: 100, total: 200, unacknowledged: 100 }, stats: { publishRate: 5, deliveryRate: 4.5, acknowledgementRate: 4, }, - amqp: { - uri: "amqps://queue.ably.io:5671", - queueName: mockQueueName, - }, - stomp: { - uri: "stomp://queue.ably.io:61614", - host: "queue.ably.io", - destination: `/queue/${mockQueueName}`, - }, }, ]); - // Mock conflict error nock("https://control.ably.net") - .delete(`/v1/apps/${mockAppId}/queues/${mockQueueId}`) + .delete(`/v1/apps/${appId}/queues/${mockQueueId}`) .reply(409, { error: "Conflict", details: "Queue is currently in use", @@ -531,7 +367,6 @@ currentAppId = "${mockAppId}" ); expect(error).toBeDefined(); - // The queue must exist first to test deletion errors expect(error?.message).toMatch(/Queue.*not found|409/); expect(error?.oclif?.exit).toBeGreaterThan(0); }); @@ -540,41 +375,12 @@ currentAppId = "${mockAppId}" describe("confirmation prompt handling", () => { it.skip("should cancel deletion when user responds no to confirmation", async () => { // SKIPPED: stdin handling in tests is problematic with runCommand - // Mock the queue listing endpoint + const appId = getMockConfigManager().getCurrentAppId()!; + const mockQueueId = `${appId}:us-east-1-a:${mockQueueName}`; + nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/queues`) - .reply(200, [ - { - id: mockQueueId, - appId: mockAppId, - name: mockQueueName, - region: "us-east-1-a", - state: "active", - maxLength: 10000, - ttl: 60, - deadletter: false, - deadletterId: "", - messages: { - ready: 5, - total: 10, - unacknowledged: 5, - }, - stats: { - publishRate: 1.5, - deliveryRate: 1.2, - acknowledgementRate: 1, - }, - amqp: { - uri: "amqps://queue.ably.io:5671", - queueName: mockQueueName, - }, - stomp: { - uri: "stomp://queue.ably.io:61614", - host: "queue.ably.io", - destination: `/queue/${mockQueueName}`, - }, - }, - ]); + .get(`/v1/apps/${appId}/queues`) + .reply(200, [createMockQueue(appId, mockQueueId)]); const { stdout } = await runCommand( ["queues:delete", mockQueueId], @@ -595,45 +401,20 @@ currentAppId = "${mockAppId}" it.skip("should proceed with deletion when user confirms", async () => { // SKIPPED: stdin handling in tests is problematic with runCommand - // Mock the queue listing endpoint + const appId = getMockConfigManager().getCurrentAppId()!; + const mockQueueId = `${appId}:us-east-1-a:${mockQueueName}`; + nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/queues`) + .get(`/v1/apps/${appId}/queues`) .reply(200, [ { - id: mockQueueId, - appId: mockAppId, - name: mockQueueName, - region: "us-east-1-a", - state: "active", - maxLength: 10000, - ttl: 60, - deadletter: false, - deadletterId: "", - messages: { - ready: 0, - total: 0, - unacknowledged: 0, - }, - stats: { - publishRate: null, - deliveryRate: null, - acknowledgementRate: null, - }, - amqp: { - uri: "amqps://queue.ably.io:5671", - queueName: mockQueueName, - }, - stomp: { - uri: "stomp://queue.ably.io:61614", - host: "queue.ably.io", - destination: `/queue/${mockQueueName}`, - }, + ...createMockQueue(appId, mockQueueId), + messages: { ready: 0, total: 0, unacknowledged: 0 }, }, ]); - // Mock the deletion endpoint nock("https://control.ably.net") - .delete(`/v1/apps/${mockAppId}/queues/${mockQueueId}`) + .delete(`/v1/apps/${appId}/queues/${mockQueueId}`) .reply(204); const { stdout } = await runCommand( diff --git a/test/unit/commands/queues/list.test.ts b/test/unit/commands/queues/list.test.ts index 1e0be908..89187294 100644 --- a/test/unit/commands/queues/list.test.ts +++ b/test/unit/commands/queues/list.test.ts @@ -1,75 +1,23 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, afterEach } from "vitest"; import nock from "nock"; import { runCommand } from "@oclif/test"; -import * as path from "node:path"; -import * as fs from "node:fs"; -import * as os from "node:os"; +import { getMockConfigManager } from "../../../helpers/mock-config-manager.js"; describe("queues:list command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - let testConfigDir: string; - let originalConfigDir: string; - - const mockAccountResponse = { - account: { id: mockAccountId, name: "Test Account" }, - user: { email: "test@example.com" }, - }; - - beforeEach(() => { - // Set environment variable for access token - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - // Create a temporary config directory for testing - testConfigDir = path.join(os.tmpdir(), `ably-cli-test-${Date.now()}`); - fs.mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - // Store original config dir and set test config dir - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - // Create a minimal config file with a default account - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" -`; - fs.writeFileSync(path.join(testConfigDir, "config"), configContent); - }); - afterEach(() => { - // Clean up nock interceptors nock.cleanAll(); - delete process.env.ABLY_ACCESS_TOKEN; - - // Restore original config directory - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - // Clean up test config directory - if (fs.existsSync(testConfigDir)) { - fs.rmSync(testConfigDir, { recursive: true, force: true }); - } }); describe("successful queue listing", () => { it("should list multiple queues successfully", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; // Mock the queue listing endpoint with multiple queues nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/queues`) + .get(`/v1/apps/${appId}/queues`) .reply(200, [ { id: "queue-1", - appId: mockAppId, + appId, name: "test-queue-1", region: "us-east-1-a", state: "active", @@ -99,7 +47,7 @@ currentAppId = "${mockAppId}" }, { id: "queue-2", - appId: mockAppId, + appId, name: "test-queue-2", region: "eu-west-1-a", state: "active", @@ -159,9 +107,10 @@ currentAppId = "${mockAppId}" }); it("should handle empty queue list", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; // Mock empty queue list nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/queues`) + .get(`/v1/apps/${appId}/queues`) .reply(200, []); const { stdout } = await runCommand(["queues:list"], import.meta.url); @@ -170,10 +119,11 @@ currentAppId = "${mockAppId}" }); it("should output JSON format when --json flag is used", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; const mockQueues = [ { id: "queue-1", - appId: mockAppId, + appId, name: "test-queue-1", region: "us-east-1-a", state: "active", @@ -204,7 +154,7 @@ currentAppId = "${mockAppId}" ]; nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/queues`) + .get(`/v1/apps/${appId}/queues`) .reply(200, mockQueues); const { stdout } = await runCommand( @@ -213,7 +163,7 @@ currentAppId = "${mockAppId}" ); const result = JSON.parse(stdout); - expect(result).toHaveProperty("appId", mockAppId); + expect(result).toHaveProperty("appId", appId); expect(result).toHaveProperty("queues"); expect(result.queues).toBeInstanceOf(Array); expect(result.queues).toHaveLength(1); @@ -224,11 +174,12 @@ currentAppId = "${mockAppId}" }); it("should use custom app ID when provided", async () => { + const accountId = getMockConfigManager().getCurrentAccount()!.accountId!; const customAppId = "custom-app-id"; const mockAppResponse = { id: customAppId, - accountId: mockAccountId, + accountId, name: "Test App", status: "active", created: Date.now(), @@ -238,18 +189,17 @@ currentAppId = "${mockAppId}" nock("https://control.ably.net") .get("/v1/me") - .reply(200, mockAccountResponse); - - // nock("https://control.ably.net") - // .get("/v1/me") - // .reply(200, mockAccountResponse); + .reply(200, { + account: { id: accountId, name: "Test Account" }, + user: { email: "test@example.com" }, + }); nock("https://control.ably.net") - .get(`/v1/accounts/${mockAccountId}/apps`) + .get(`/v1/accounts/${accountId}/apps`) .reply(200, [mockAppResponse]); nock("https://control.ably.net") - .get(`/v1/accounts/${mockAccountId}/apps`) + .get(`/v1/accounts/${accountId}/apps`) .reply(200, [mockAppResponse]); nock("https://control.ably.net") @@ -298,6 +248,7 @@ currentAppId = "${mockAppId}" }); it("should use custom access token when provided", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; const customToken = "custom_access_token"; nock("https://control.ably.net", { @@ -305,7 +256,7 @@ currentAppId = "${mockAppId}" authorization: `Bearer ${customToken}`, }, }) - .get(`/v1/apps/${mockAppId}/queues`) + .get(`/v1/apps/${appId}/queues`) .reply(200, []); const { stdout } = await runCommand( @@ -317,13 +268,14 @@ currentAppId = "${mockAppId}" }); it("should handle queues with no stats gracefully", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; // Mock queue with no stats nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/queues`) + .get(`/v1/apps/${appId}/queues`) .reply(200, [ { id: "queue-1", - appId: mockAppId, + appId, name: "test-queue-1", region: "us-east-1-a", state: "active", @@ -364,9 +316,10 @@ currentAppId = "${mockAppId}" describe("error handling", () => { it("should handle 401 authentication error", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; // Mock authentication failure nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/queues`) + .get(`/v1/apps/${appId}/queues`) .reply(401, { error: "Unauthorized" }); const { error } = await runCommand(["queues:list"], import.meta.url); @@ -377,9 +330,10 @@ currentAppId = "${mockAppId}" }); it("should handle 403 forbidden error", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; // Mock forbidden response nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/queues`) + .get(`/v1/apps/${appId}/queues`) .reply(403, { error: "Forbidden" }); const { error } = await runCommand(["queues:list"], import.meta.url); @@ -390,9 +344,10 @@ currentAppId = "${mockAppId}" }); it("should handle 404 app not found error", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; // Mock not found response nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/queues`) + .get(`/v1/apps/${appId}/queues`) .reply(404, { error: "App not found" }); const { error } = await runCommand(["queues:list"], import.meta.url); @@ -403,9 +358,10 @@ currentAppId = "${mockAppId}" }); it("should handle 500 server error", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; // Mock server error nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/queues`) + .get(`/v1/apps/${appId}/queues`) .reply(500, { error: "Internal Server Error" }); const { error } = await runCommand(["queues:list"], import.meta.url); @@ -416,19 +372,21 @@ currentAppId = "${mockAppId}" }); it("should require app to be specified when not in environment", async () => { - process.env.ABLY_CLI_CONFIG_DIR = "/tmp"; + // Clear all accounts from the mock config to simulate no config + getMockConfigManager().clearAccounts(); const { error } = await runCommand(["queues:list"], import.meta.url); expect(error).toBeDefined(); - expect(error?.message).toMatch(/No app|Failed to get apps/); + expect(error?.message).toMatch(/No access token|No app|not logged in/i); expect(error?.oclif?.exit).toBeGreaterThan(0); }); it("should handle network errors", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; // Mock network error nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/queues`) + .get(`/v1/apps/${appId}/queues`) .replyWithError("Network error"); const { error } = await runCommand(["queues:list"], import.meta.url); @@ -439,9 +397,10 @@ currentAppId = "${mockAppId}" }); it("should handle errors in JSON format when --json flag is used", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; // Mock server error for JSON output nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/queues`) + .get(`/v1/apps/${appId}/queues`) .reply(500, { error: "Internal Server Error" }); const { stdout } = await runCommand( @@ -452,13 +411,14 @@ currentAppId = "${mockAppId}" expect(stdout).toContain('"success": false'); expect(stdout).toContain('"status": "error"'); expect(stdout).toContain('"error":'); - expect(stdout).toContain(`"appId": "${mockAppId}"`); + expect(stdout).toContain(`"appId": "${appId}"`); }); it("should handle 429 rate limit error", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; // Mock rate limit error nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/queues`) + .get(`/v1/apps/${appId}/queues`) .reply(429, { error: "Rate limit exceeded", details: "Too many requests", @@ -474,12 +434,13 @@ currentAppId = "${mockAppId}" describe("large datasets and pagination", () => { it("should handle large datasets correctly", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; // Mock a large number of queues to test performance const queues: any[] = []; for (let i = 1; i <= 50; i++) { queues.push({ id: `queue-${i}`, - appId: mockAppId, + appId, name: `test-queue-${i}`, region: "us-east-1-a", state: "active", @@ -510,7 +471,7 @@ currentAppId = "${mockAppId}" } nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/queues`) + .get(`/v1/apps/${appId}/queues`) .reply(200, queues); const { stdout } = await runCommand(["queues:list"], import.meta.url); @@ -521,9 +482,10 @@ currentAppId = "${mockAppId}" }); it("should handle empty list in JSON format", async () => { + const appId = getMockConfigManager().getCurrentAppId()!; // Mock empty queue list for JSON output nock("https://control.ably.net") - .get(`/v1/apps/${mockAppId}/queues`) + .get(`/v1/apps/${appId}/queues`) .reply(200, []); const { stdout } = await runCommand( @@ -532,7 +494,7 @@ currentAppId = "${mockAppId}" ); const result = JSON.parse(stdout); - expect(result).toHaveProperty("appId", mockAppId); + expect(result).toHaveProperty("appId", appId); expect(result).toHaveProperty("queues"); expect(result.queues).toBeInstanceOf(Array); expect(result.queues).toHaveLength(0); diff --git a/test/unit/commands/rooms/messages/reactions/remove.test.ts b/test/unit/commands/rooms/messages/reactions/remove.test.ts index e7629ace..654d567d 100644 --- a/test/unit/commands/rooms/messages/reactions/remove.test.ts +++ b/test/unit/commands/rooms/messages/reactions/remove.test.ts @@ -1,54 +1,18 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { runCommand } from "@oclif/test"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; describe("rooms:messages:reactions:remove command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - const mockApiKey = `${mockAppId}.testkey:testsecret`; - let testConfigDir: string; - let originalConfigDir: string; - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[apps."${mockAppId}"] -apiKey = "${mockApiKey}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + delete globalThis.__TEST_MOCKS__.ablyChatMock; + } }); afterEach(() => { - delete process.env.ABLY_ACCESS_TOKEN; - globalThis.__TEST_MOCKS__ = undefined; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + delete globalThis.__TEST_MOCKS__.ablyChatMock; } }); @@ -145,6 +109,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablyChatMock: mockChatClient, }; diff --git a/test/unit/commands/rooms/messages/reactions/send.test.ts b/test/unit/commands/rooms/messages/reactions/send.test.ts index caf277ab..929f4695 100644 --- a/test/unit/commands/rooms/messages/reactions/send.test.ts +++ b/test/unit/commands/rooms/messages/reactions/send.test.ts @@ -1,54 +1,18 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { runCommand } from "@oclif/test"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; describe("rooms:messages:reactions:send command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - const mockApiKey = `${mockAppId}.testkey:testsecret`; - let testConfigDir: string; - let originalConfigDir: string; - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[apps."${mockAppId}"] -apiKey = "${mockApiKey}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + delete globalThis.__TEST_MOCKS__.ablyChatMock; + } }); afterEach(() => { - delete process.env.ABLY_ACCESS_TOKEN; - globalThis.__TEST_MOCKS__ = undefined; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + delete globalThis.__TEST_MOCKS__.ablyChatMock; } }); @@ -145,6 +109,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablyChatMock: mockChatClient, }; diff --git a/test/unit/commands/rooms/messages/reactions/subscribe.test.ts b/test/unit/commands/rooms/messages/reactions/subscribe.test.ts index 17b8a46f..f628614e 100644 --- a/test/unit/commands/rooms/messages/reactions/subscribe.test.ts +++ b/test/unit/commands/rooms/messages/reactions/subscribe.test.ts @@ -1,57 +1,20 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { runCommand } from "@oclif/test"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; import { RoomStatus } from "@ably/chat"; describe("rooms:messages:reactions:subscribe command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - const mockApiKey = `${mockAppId}.testkey:testsecret`; - let testConfigDir: string; - let originalConfigDir: string; - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[apps."${mockAppId}"] -apiKey = "${mockApiKey}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + delete globalThis.__TEST_MOCKS__.ablyChatMock; + } }); afterEach(() => { - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + delete globalThis.__TEST_MOCKS__.ablyChatMock; } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } - - globalThis.__TEST_MOCKS__ = undefined; }); describe("command arguments and flags", () => { @@ -122,6 +85,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyChatMock: { rooms: mockRooms, realtime: mockRealtimeClient, @@ -217,6 +181,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyChatMock: { rooms: mockRooms, realtime: mockRealtimeClient, diff --git a/test/unit/commands/rooms/presence/subscribe.test.ts b/test/unit/commands/rooms/presence/subscribe.test.ts index d713ff85..44075178 100644 --- a/test/unit/commands/rooms/presence/subscribe.test.ts +++ b/test/unit/commands/rooms/presence/subscribe.test.ts @@ -1,57 +1,20 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { runCommand } from "@oclif/test"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; import { RoomStatus } from "@ably/chat"; describe("rooms:presence:subscribe command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - const mockApiKey = `${mockAppId}.testkey:testsecret`; - let testConfigDir: string; - let originalConfigDir: string; - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[apps."${mockAppId}"] -apiKey = "${mockApiKey}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + delete globalThis.__TEST_MOCKS__.ablyChatMock; + } }); afterEach(() => { - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + delete globalThis.__TEST_MOCKS__.ablyChatMock; } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } - - globalThis.__TEST_MOCKS__ = undefined; }); describe("command arguments and flags", () => { @@ -121,6 +84,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyChatMock: { rooms: mockRooms, realtime: mockRealtimeClient, @@ -214,6 +178,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyChatMock: { rooms: mockRooms, realtime: mockRealtimeClient, diff --git a/test/unit/commands/rooms/reactions/subscribe.test.ts b/test/unit/commands/rooms/reactions/subscribe.test.ts index 7494cb74..e8940ef7 100644 --- a/test/unit/commands/rooms/reactions/subscribe.test.ts +++ b/test/unit/commands/rooms/reactions/subscribe.test.ts @@ -1,57 +1,20 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { runCommand } from "@oclif/test"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; import { RoomStatus } from "@ably/chat"; describe("rooms:reactions:subscribe command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - const mockApiKey = `${mockAppId}.testkey:testsecret`; - let testConfigDir: string; - let originalConfigDir: string; - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[apps."${mockAppId}"] -apiKey = "${mockApiKey}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + delete globalThis.__TEST_MOCKS__.ablyChatMock; + } }); afterEach(() => { - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + delete globalThis.__TEST_MOCKS__.ablyChatMock; } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } - - globalThis.__TEST_MOCKS__ = undefined; }); describe("command arguments and flags", () => { @@ -120,6 +83,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyChatMock: { rooms: mockRooms, realtime: mockRealtimeClient, @@ -212,6 +176,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyChatMock: { rooms: mockRooms, realtime: mockRealtimeClient, diff --git a/test/unit/commands/rooms/typing/subscribe.test.ts b/test/unit/commands/rooms/typing/subscribe.test.ts index b7aeb6dd..d350a817 100644 --- a/test/unit/commands/rooms/typing/subscribe.test.ts +++ b/test/unit/commands/rooms/typing/subscribe.test.ts @@ -1,57 +1,20 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { runCommand } from "@oclif/test"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; import { RoomStatus } from "@ably/chat"; describe("rooms:typing:subscribe command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - const mockApiKey = `${mockAppId}.testkey:testsecret`; - let testConfigDir: string; - let originalConfigDir: string; - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[apps."${mockAppId}"] -apiKey = "${mockApiKey}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + delete globalThis.__TEST_MOCKS__.ablyChatMock; + } }); afterEach(() => { - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + delete globalThis.__TEST_MOCKS__.ablyChatMock; } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } - - globalThis.__TEST_MOCKS__ = undefined; }); describe("command arguments and flags", () => { @@ -116,6 +79,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyChatMock: { rooms: mockRooms, realtime: mockRealtimeClient, @@ -207,6 +171,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyChatMock: { rooms: mockRooms, realtime: mockRealtimeClient, diff --git a/test/unit/commands/spaces/cursors/get-all.test.ts b/test/unit/commands/spaces/cursors/get-all.test.ts index 97ed706e..f459e0b4 100644 --- a/test/unit/commands/spaces/cursors/get-all.test.ts +++ b/test/unit/commands/spaces/cursors/get-all.test.ts @@ -1,57 +1,19 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { runCommand } from "@oclif/test"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; describe("spaces:cursors:get-all command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - const mockApiKey = `${mockAppId}.testkey:testsecret`; - let testConfigDir: string; - let originalConfigDir: string; - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[accounts.default.apps."${mockAppId}"] -appName = "Test App" -apiKey = "${mockApiKey}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + delete globalThis.__TEST_MOCKS__.ablySpacesMock; + } }); afterEach(() => { - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + delete globalThis.__TEST_MOCKS__.ablySpacesMock; } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } - - globalThis.__TEST_MOCKS__ = undefined; }); describe("command arguments and flags", () => { @@ -111,6 +73,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; @@ -186,6 +149,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; @@ -242,6 +206,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; @@ -292,6 +257,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; @@ -346,6 +312,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; diff --git a/test/unit/commands/spaces/cursors/subscribe.test.ts b/test/unit/commands/spaces/cursors/subscribe.test.ts index 9cf4d4f1..a786b526 100644 --- a/test/unit/commands/spaces/cursors/subscribe.test.ts +++ b/test/unit/commands/spaces/cursors/subscribe.test.ts @@ -1,57 +1,19 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { runCommand } from "@oclif/test"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; describe("spaces:cursors:subscribe command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - const mockApiKey = `${mockAppId}.testkey:testsecret`; - let testConfigDir: string; - let originalConfigDir: string; - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[accounts.default.apps."${mockAppId}"] -appName = "Test App" -apiKey = "${mockApiKey}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + delete globalThis.__TEST_MOCKS__.ablySpacesMock; + } }); afterEach(() => { - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + delete globalThis.__TEST_MOCKS__.ablySpacesMock; } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } - - globalThis.__TEST_MOCKS__ = undefined; }); describe("command arguments and flags", () => { @@ -136,6 +98,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; @@ -235,6 +198,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; @@ -314,6 +278,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; @@ -393,6 +358,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; @@ -479,6 +445,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; diff --git a/test/unit/commands/spaces/locations/get-all.test.ts b/test/unit/commands/spaces/locations/get-all.test.ts index f3adb952..20c0bd73 100644 --- a/test/unit/commands/spaces/locations/get-all.test.ts +++ b/test/unit/commands/spaces/locations/get-all.test.ts @@ -1,53 +1,18 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { runCommand } from "@oclif/test"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; describe("spaces:locations:get-all command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - const mockApiKey = `${mockAppId}.testkey:testsecret`; - let testConfigDir: string; - let originalConfigDir: string; - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[apps."${mockAppId}"] -apiKey = "${mockApiKey}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + delete globalThis.__TEST_MOCKS__.ablySpacesMock; + } }); afterEach(() => { - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + delete globalThis.__TEST_MOCKS__.ablySpacesMock; } }); @@ -107,6 +72,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; @@ -117,16 +83,10 @@ apiKey = "${mockApiKey}" ); expect(error?.message || "").not.toMatch(/unknown|Nonexistent flag/i); - - globalThis.__TEST_MOCKS__ = undefined; }); }); describe("location retrieval", () => { - afterEach(() => { - globalThis.__TEST_MOCKS__ = undefined; - }); - it("should get all locations from a space", async () => { const mockLocationsData = [ { @@ -170,6 +130,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; @@ -219,6 +180,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; diff --git a/test/unit/commands/spaces/locations/subscribe.test.ts b/test/unit/commands/spaces/locations/subscribe.test.ts index 103af967..6679b2e1 100644 --- a/test/unit/commands/spaces/locations/subscribe.test.ts +++ b/test/unit/commands/spaces/locations/subscribe.test.ts @@ -1,57 +1,19 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { runCommand } from "@oclif/test"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; describe("spaces:locations:subscribe command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - const mockApiKey = `${mockAppId}.testkey:testsecret`; - let testConfigDir: string; - let originalConfigDir: string; - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[accounts.default.apps."${mockAppId}"] -appName = "Test App" -apiKey = "${mockApiKey}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + delete globalThis.__TEST_MOCKS__.ablySpacesMock; + } }); afterEach(() => { - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + delete globalThis.__TEST_MOCKS__.ablySpacesMock; } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } - - globalThis.__TEST_MOCKS__ = undefined; }); describe("command arguments and flags", () => { @@ -129,6 +91,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; @@ -221,6 +184,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; @@ -293,6 +257,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; @@ -367,6 +332,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; @@ -439,6 +405,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; @@ -511,6 +478,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; diff --git a/test/unit/commands/spaces/locks/get-all.test.ts b/test/unit/commands/spaces/locks/get-all.test.ts index 3b406654..8b687e05 100644 --- a/test/unit/commands/spaces/locks/get-all.test.ts +++ b/test/unit/commands/spaces/locks/get-all.test.ts @@ -1,53 +1,18 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { runCommand } from "@oclif/test"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; describe("spaces:locks:get-all command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - const mockApiKey = `${mockAppId}.testkey:testsecret`; - let testConfigDir: string; - let originalConfigDir: string; - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[apps."${mockAppId}"] -apiKey = "${mockApiKey}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + delete globalThis.__TEST_MOCKS__.ablySpacesMock; + } }); afterEach(() => { - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + delete globalThis.__TEST_MOCKS__.ablySpacesMock; } }); @@ -107,6 +72,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; @@ -117,16 +83,10 @@ apiKey = "${mockApiKey}" ); expect(error?.message || "").not.toMatch(/unknown|Nonexistent flag/i); - - globalThis.__TEST_MOCKS__ = undefined; }); }); describe("lock retrieval", () => { - afterEach(() => { - globalThis.__TEST_MOCKS__ = undefined; - }); - it("should get all locks from a space", async () => { const mockLocksData = [ { @@ -170,6 +130,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; @@ -219,6 +180,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; diff --git a/test/unit/commands/spaces/locks/get.test.ts b/test/unit/commands/spaces/locks/get.test.ts index f3d88be6..f3c7c39f 100644 --- a/test/unit/commands/spaces/locks/get.test.ts +++ b/test/unit/commands/spaces/locks/get.test.ts @@ -1,53 +1,18 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { runCommand } from "@oclif/test"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; describe("spaces:locks:get command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - const mockApiKey = `${mockAppId}.testkey:testsecret`; - let testConfigDir: string; - let originalConfigDir: string; - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[apps."${mockAppId}"] -apiKey = "${mockApiKey}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + delete globalThis.__TEST_MOCKS__.ablySpacesMock; + } }); afterEach(() => { - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + delete globalThis.__TEST_MOCKS__.ablySpacesMock; } }); @@ -114,6 +79,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; @@ -124,16 +90,10 @@ apiKey = "${mockApiKey}" ); expect(error?.message || "").not.toMatch(/unknown|Nonexistent flag/i); - - globalThis.__TEST_MOCKS__ = undefined; }); }); describe("lock retrieval", () => { - afterEach(() => { - globalThis.__TEST_MOCKS__ = undefined; - }); - it("should get a specific lock by ID", async () => { const mockLockData = { id: "my-lock", @@ -175,6 +135,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; @@ -224,6 +185,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; diff --git a/test/unit/commands/spaces/locks/subscribe.test.ts b/test/unit/commands/spaces/locks/subscribe.test.ts index 2629ba55..a2ab46e7 100644 --- a/test/unit/commands/spaces/locks/subscribe.test.ts +++ b/test/unit/commands/spaces/locks/subscribe.test.ts @@ -1,57 +1,19 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { runCommand } from "@oclif/test"; -import { resolve } from "node:path"; -import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; describe("spaces:locks:subscribe command", () => { - const mockAccessToken = "fake_access_token"; - const mockAccountId = "test-account-id"; - const mockAppId = "550e8400-e29b-41d4-a716-446655440000"; - const mockApiKey = `${mockAppId}.testkey:testsecret`; - let testConfigDir: string; - let originalConfigDir: string; - beforeEach(() => { - process.env.ABLY_ACCESS_TOKEN = mockAccessToken; - - testConfigDir = resolve(tmpdir(), `ably-cli-test-${Date.now()}`); - mkdirSync(testConfigDir, { recursive: true, mode: 0o700 }); - - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = testConfigDir; - - const configContent = `[current] -account = "default" - -[accounts.default] -accessToken = "${mockAccessToken}" -accountId = "${mockAccountId}" -accountName = "Test Account" -userEmail = "test@example.com" -currentAppId = "${mockAppId}" - -[accounts.default.apps."${mockAppId}"] -appName = "Test App" -apiKey = "${mockApiKey}" -`; - writeFileSync(resolve(testConfigDir, "config"), configContent); + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + delete globalThis.__TEST_MOCKS__.ablySpacesMock; + } }); afterEach(() => { - delete process.env.ABLY_ACCESS_TOKEN; - - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + delete globalThis.__TEST_MOCKS__.ablySpacesMock; } - - if (existsSync(testConfigDir)) { - rmSync(testConfigDir, { recursive: true, force: true }); - } - - globalThis.__TEST_MOCKS__ = undefined; }); describe("command arguments and flags", () => { @@ -129,6 +91,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; @@ -221,6 +184,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; @@ -290,6 +254,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; @@ -372,6 +337,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; @@ -442,6 +408,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; @@ -513,6 +480,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; @@ -585,6 +553,7 @@ apiKey = "${mockApiKey}" }; globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: mockRealtimeClient, ablySpacesMock: mockSpacesClient, }; diff --git a/test/unit/commands/spaces/spaces.test.ts b/test/unit/commands/spaces/spaces.test.ts index 7c1a5fc2..79962417 100644 --- a/test/unit/commands/spaces/spaces.test.ts +++ b/test/unit/commands/spaces/spaces.test.ts @@ -70,7 +70,9 @@ describe("spaces commands", () => { }, }; + // Merge with existing mocks (don't overwrite configManager) globalThis.__TEST_MOCKS__ = { + ...globalThis.__TEST_MOCKS__, ablyRealtimeMock: { channels: { get: vi.fn().mockReturnValue({ @@ -103,7 +105,11 @@ describe("spaces commands", () => { }); afterEach(() => { - delete globalThis.__TEST_MOCKS__; + // Only delete the mocks we added, not the whole object + if (globalThis.__TEST_MOCKS__) { + delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; + delete globalThis.__TEST_MOCKS__.ablySpacesMock; + } }); describe("spaces topic", () => { diff --git a/test/unit/services/config-manager.test.ts b/test/unit/services/config-manager.test.ts index e3062e33..e3117ec8 100644 --- a/test/unit/services/config-manager.test.ts +++ b/test/unit/services/config-manager.test.ts @@ -21,7 +21,10 @@ import { import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { ConfigManager } from "../../../src/services/config-manager.js"; +import { + ConfigManager, + TomlConfigManager, +} from "../../../src/services/config-manager.js"; // Simple mock config content const DEFAULT_CONFIG = ` @@ -85,7 +88,7 @@ describe("ConfigManager", () => { // Create new ConfigManager instance for each test // It will now use the uniqueTestConfigDir via the env var - configManager = new ConfigManager(); + configManager = new TomlConfigManager(); }); // Clean up after each test @@ -136,7 +139,7 @@ describe("ConfigManager", () => { existsStub.mockReturnValue(false); // Create instance which should trigger directory creation attempt - const _manager = new ConfigManager(); + const _manager = new TomlConfigManager(); // ConfigManager constructor now uses getConfigDirPath() which relies on ABLY_CLI_CONFIG_DIR // We expect mkdirSync to be called with the uniqueTestConfigDir @@ -173,7 +176,7 @@ describe("ConfigManager", () => { vi.spyOn(fs, "readFileSync").mockReturnValue("[accounts]\n"); // Empty accounts section vi.spyOn(fs, "writeFileSync"); // Stub writeFileSync if needed - const manager = new ConfigManager(); // Create new instance with empty config + const manager = new TomlConfigManager(); // Create new instance with empty config expect(manager.getCurrentAccountAlias()).toBeUndefined(); }); @@ -199,7 +202,7 @@ accessToken = "testaccesstoken" `); // No [current] section vi.spyOn(fs, "writeFileSync"); - const manager = new ConfigManager(); + const manager = new TomlConfigManager(); expect(manager.getCurrentAccount()).toBeUndefined(); }); @@ -217,7 +220,7 @@ accessToken = "testaccesstoken" vi.spyOn(fs, "readFileSync").mockReturnValue(`[accounts]`); // No [current] section or account details vi.spyOn(fs, "writeFileSync"); - const manager = new ConfigManager(); + const manager = new TomlConfigManager(); expect(manager.getCurrentAppId()).toBeUndefined(); }); }); @@ -277,7 +280,7 @@ accessToken = "testaccesstoken" vi.spyOn(fs, "readFileSync").mockReturnValue(""); // Empty config const writeFileStub = vi.spyOn(fs, "writeFileSync"); - const manager = new ConfigManager(); + const manager = new TomlConfigManager(); manager.storeAccount("firstaccesstoken", "firstaccount"); expect(writeFileStub).toHaveBeenCalledOnce(); diff --git a/test/unit/setup.ts b/test/unit/setup.ts new file mode 100644 index 00000000..c9b2b8a9 --- /dev/null +++ b/test/unit/setup.ts @@ -0,0 +1,22 @@ +/** + * Unit test setup file. + * + * This file is loaded before each unit test file and sets up the + * MockConfigManager for tests that need config access. + */ + +import { beforeAll, beforeEach } from "vitest"; +import { + initializeMockConfigManager, + resetMockConfig, +} from "../helpers/mock-config-manager.js"; + +// Initialize the mock config manager once at the start of unit tests +beforeAll(() => { + initializeMockConfigManager(); +}); + +// Reset the mock config before each test to ensure clean state +beforeEach(() => { + resetMockConfig(); +}); diff --git a/vitest.config.ts b/vitest.config.ts index 4dbad228..a9f41453 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -33,14 +33,12 @@ export default defineConfig({ test: { name: "unit", include: ["test/unit/**/*.test.ts"], + setupFiles: ["./test/setup.ts", "./test/unit/setup.ts"], env: { ABLY_CLI_DEFAULT_DURATION: "0.25", ABLY_CLI_TEST_MODE: "true", ABLY_API_KEY: undefined, }, - // This is a temporary workaround whilst a bug / race with test config setup is fixed - // fixed as it causes races - fileParallelism: false, }, }, {