From 1bc60e4535e28f982d2460a7382e51d57ad96c47 Mon Sep 17 00:00:00 2001 From: Andy Ford Date: Sat, 13 Dec 2025 23:27:23 +0000 Subject: [PATCH] test: remove redundant integration tests A lot of our integraiton tests are just unit tests with different mocking methods. This change removes tests that are covered by unit tests, moves some tests into relevant unit/e2e. --- test/{integration => e2e}/control-api.test.ts | 2 +- test/integration/auth/auth-flow.test.ts | 371 ----------- .../auth/enhanced-login-flow.test.ts | 194 ------ .../channels-publish-ordering.test.ts | 268 -------- test/integration/commands/channels.test.ts | 366 ---------- .../integration/commands/minimal-test.test.ts | 12 - .../commands/rooms-messages-ordering.test.ts | 301 --------- test/integration/commands/rooms.test.ts | 429 ------------ test/integration/commands/spaces.test.ts | 630 ------------------ test/integration/core/autocomplete.test.ts | 56 -- test/integration/core/help.test.ts | 49 -- test/integration/test-utils.ts | 101 --- .../integration/topic-command-display.test.ts | 249 ------- .../base-command}/agent-header.test.ts | 2 +- test/unit/commands/channels/publish.test.ts | 226 +++++++ test/unit/commands/rooms/messages.test.ts | 105 +++ 16 files changed, 333 insertions(+), 3028 deletions(-) rename test/{integration => e2e}/control-api.test.ts (99%) delete mode 100644 test/integration/auth/auth-flow.test.ts delete mode 100644 test/integration/auth/enhanced-login-flow.test.ts delete mode 100644 test/integration/commands/channels-publish-ordering.test.ts delete mode 100644 test/integration/commands/channels.test.ts delete mode 100644 test/integration/commands/minimal-test.test.ts delete mode 100644 test/integration/commands/rooms-messages-ordering.test.ts delete mode 100644 test/integration/commands/rooms.test.ts delete mode 100644 test/integration/commands/spaces.test.ts delete mode 100644 test/integration/core/autocomplete.test.ts delete mode 100644 test/integration/core/help.test.ts delete mode 100644 test/integration/test-utils.ts delete mode 100644 test/integration/topic-command-display.test.ts rename test/{integration/core => unit/base-command}/agent-header.test.ts (96%) diff --git a/test/integration/control-api.test.ts b/test/e2e/control-api.test.ts similarity index 99% rename from test/integration/control-api.test.ts rename to test/e2e/control-api.test.ts index f45cb587..b6aa387e 100644 --- a/test/integration/control-api.test.ts +++ b/test/e2e/control-api.test.ts @@ -3,7 +3,7 @@ import { describe, it, expect, beforeAll, afterAll } from "vitest"; import { ControlApi } from "../../src/services/control-api.js"; describe.skipIf(!process.env.E2E_ABLY_ACCESS_TOKEN)( - "Control API Integration Tests", + "Control API E2E Tests", () => { let controlApi: ControlApi; let testAppId: string; diff --git a/test/integration/auth/auth-flow.test.ts b/test/integration/auth/auth-flow.test.ts deleted file mode 100644 index 25fa23a9..00000000 --- a/test/integration/auth/auth-flow.test.ts +++ /dev/null @@ -1,371 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { ConfigManager } from "../../../src/services/config-manager.js"; -import _AccountsLogin from "../../../src/commands/accounts/login.js"; -import _AccountsLogout from "../../../src/commands/accounts/logout.js"; - -describe("Authentication Flow Integration", function () { - let originalEnv: NodeJS.ProcessEnv; - let tempConfigDir: string; - - beforeEach(function () { - originalEnv = { ...process.env }; - - // Create a unique temporary directory for each test - tempConfigDir = fs.mkdtempSync( - path.join(os.tmpdir(), "ably-cli-integration-test-"), - ); - - // Set test environment - process.env = { ...originalEnv }; - process.env.ABLY_CLI_TEST_MODE = "true"; - process.env.ABLY_CLI_CONFIG_DIR = tempConfigDir; - - // Don't mock fs operations for integration tests - use real file system - // but in isolated temp directory - }); - - afterEach(function () { - process.env = originalEnv; - - // Clean up temporary directory - if (tempConfigDir && fs.existsSync(tempConfigDir)) { - fs.rmSync(tempConfigDir, { recursive: true, force: true }); - } - }); - - describe("login → logout flow", function () { - it("should complete full authentication cycle", function () { - // Create ConfigManager with temporary directory - const configManager = new ConfigManager(); - - // Test initial state - no accounts - const initialAccounts = configManager.listAccounts(); - expect(initialAccounts).toHaveLength(0); - - // Simulate login by storing account directly - configManager.storeAccount("test-access-token", "test-account", { - accountId: "acc_123", - accountName: "Test Account", - tokenId: "token_123", - userEmail: "test@example.com", - }); - - // Verify account was stored - const accountsAfterLogin = configManager.listAccounts(); - expect(accountsAfterLogin).toHaveLength(1); - expect(accountsAfterLogin[0].alias).toBe("test-account"); - - // Verify current account is set - expect(configManager.getCurrentAccountAlias()).toBe("test-account"); - - // Simulate logout by removing account - const logoutSuccess = configManager.removeAccount("test-account"); - expect(logoutSuccess).toBe(true); - - // Verify account was removed - const accountsAfterLogout = configManager.listAccounts(); - expect(accountsAfterLogout).toHaveLength(0); - expect(configManager.getCurrentAccountAlias()).toBeUndefined(); - }); - - it("should handle multiple accounts", function () { - const configManager = new ConfigManager(); - - // Store multiple accounts - configManager.storeAccount("token1", "account1", { - accountId: "acc_1", - accountName: "Account 1", - }); - - configManager.storeAccount("token2", "account2", { - accountId: "acc_2", - accountName: "Account 2", - }); - - // Verify both accounts exist - const accounts = configManager.listAccounts(); - expect(accounts).toHaveLength(2); - - // Verify both accounts are present by alias - const aliases = accounts.map((a) => a.alias); - expect(aliases).toContain("account1"); - expect(aliases).toContain("account2"); - - // Current should be set (could be either one based on implementation) - const currentBeforeSwitch = configManager.getCurrentAccountAlias(); - expect(["account1", "account2"]).toContain(currentBeforeSwitch); - - // Switch to account1 specifically - const switchSuccess = configManager.switchAccount("account1"); - expect(switchSuccess).toBe(true); - expect(configManager.getCurrentAccountAlias()).toBe("account1"); - - // Remove account1 - const removeSuccess = configManager.removeAccount("account1"); - expect(removeSuccess).toBe(true); - - // Should still have one account left - const remainingAccounts = configManager.listAccounts(); - expect(remainingAccounts).toHaveLength(1); - - // The remaining account should be account2 - const remainingAccount = remainingAccounts[0]; - expect(remainingAccount.alias).toBe("account2"); - expect(remainingAccount.account.accountId).toBe("acc_2"); - - // After removing the current account, current should be cleared - // (this is the expected behavior based on ConfigManager implementation) - const currentAfterRemoval = configManager.getCurrentAccountAlias(); - expect(currentAfterRemoval).toBeUndefined(); - }); - }); - - describe("config persistence", function () { - it("should persist configuration across ConfigManager instances", function () { - // First ConfigManager instance - const configManager1 = new ConfigManager(); - - configManager1.storeAccount("persistent-token", "persistent-account", { - accountId: "persistent_acc", - accountName: "Persistent Account", - }); - - configManager1.storeAppKey("test-app", "test-app.key:secret", { - appName: "Test App", - keyName: "Test Key", - }); - - // Set the current app so it persists - configManager1.setCurrentApp("test-app"); - - // Create second ConfigManager instance (should read from same config file) - const configManager2 = new ConfigManager(); - - // Verify data persisted - const accounts = configManager2.listAccounts(); - expect(accounts).toHaveLength(1); - expect(accounts[0].alias).toBe("persistent-account"); - - expect(configManager2.getCurrentAccountAlias()).toBe( - "persistent-account", - ); - expect(configManager2.getCurrentAppId()).toBe("test-app"); - expect(configManager2.getApiKey("test-app")).toBe("test-app.key:secret"); - expect(configManager2.getAppName("test-app")).toBe("Test App"); - }); - - it("should handle config file corruption gracefully", function () { - const configManager = new ConfigManager(); - - // Store valid config first - configManager.storeAccount("token", "account", { - accountId: "acc_123", - accountName: "Test Account", - }); - - // Verify config file exists - const configPath = path.join(tempConfigDir, "config"); - expect(fs.existsSync(configPath)).toBe(true); - - // Corrupt the config file - fs.writeFileSync(configPath, "invalid toml content [[["); - - // Create new ConfigManager - should throw an error for corrupted config - // This is the actual behavior - it doesn't silently handle corruption - expect(() => { - new ConfigManager(); - }).toThrow(/Failed to load Ably config/); - }); - }); - - describe("environment variable precedence", function () { - it("should prioritize environment variables over config", function () { - const configManager = new ConfigManager(); - - // Store config values - configManager.storeAccount("config-token", "config-account", { - accountId: "config_acc", - accountName: "Config Account", - }); - - configManager.storeAppKey("config-app", "config-app.key:config-secret", { - appName: "Config App", - }); - - // Set environment variables - process.env.ABLY_API_KEY = "env-app.env-key:env-secret"; - process.env.ABLY_ACCESS_TOKEN = "env-access-token"; - - // Environment variables should take precedence - // Note: This tests the expected behavior when BaseCommand uses these - expect(process.env.ABLY_API_KEY).toBe("env-app.env-key:env-secret"); - expect(process.env.ABLY_ACCESS_TOKEN).toBe("env-access-token"); - - // Config values should still be accessible - expect(configManager.getCurrentAccountAlias()).toBe("config-account"); - expect(configManager.getApiKey("config-app")).toBe( - "config-app.key:config-secret", - ); - }); - }); - - describe("initialization hooks", function () { - it("should create config directory if it doesn't exist", function () { - // Remove the temp directory to simulate first run - fs.rmSync(tempConfigDir, { recursive: true, force: true }); - expect(fs.existsSync(tempConfigDir)).toBe(false); - - // Creating ConfigManager should recreate directory - const configManager = new ConfigManager(); - - expect(fs.existsSync(tempConfigDir)).toBe(true); - - // Should be able to store config - configManager.storeAccount("test-token", "test-account", { - accountId: "test_acc", - accountName: "Test Account", - }); - - const accounts = configManager.listAccounts(); - expect(accounts).toHaveLength(1); - }); - - it("should handle permissions issues gracefully", function () { - // This is a conceptual test - actual permissions testing - // would be complex in a cross-platform way - const configManager = new ConfigManager(); - - // Should be able to create and use config - expect(() => { - configManager.storeAccount("test", "test", { accountId: "test" }); - }).not.toThrow(); - }); - }); - - describe("error scenarios", function () { - it("should handle account switching to non-existent account", function () { - const configManager = new ConfigManager(); - - configManager.storeAccount("token", "existing", { - accountId: "existing_acc", - accountName: "Existing Account", - }); - - // Try to switch to non-existent account - const switchResult = configManager.switchAccount("non-existent"); - expect(switchResult).toBe(false); - - // Current account should remain unchanged - expect(configManager.getCurrentAccountAlias()).toBe("existing"); - }); - - it("should handle removing non-existent account", function () { - const configManager = new ConfigManager(); - - // Try to remove account that doesn't exist - const removeResult = configManager.removeAccount("non-existent"); - expect(removeResult).toBe(false); - }); - - it("should handle duplicate account aliases", function () { - const configManager = new ConfigManager(); - - configManager.storeAccount("token1", "duplicate", { - accountId: "acc_1", - accountName: "Account 1", - }); - - // Store another account with same alias (should overwrite) - configManager.storeAccount("token2", "duplicate", { - accountId: "acc_2", - accountName: "Account 2", - }); - - const accounts = configManager.listAccounts(); - expect(accounts).toHaveLength(1); - expect(accounts[0].account.accountId).toBe("acc_2"); - }); - }); - - describe("app and key management", function () { - it("should manage app keys within accounts", function () { - const configManager = new ConfigManager(); - - // Store account - configManager.storeAccount("token", "account", { - accountId: "acc_123", - accountName: "Test Account", - }); - - // Store app keys - configManager.storeAppKey("app1", "app1.key1:secret1", { - appName: "App 1", - keyName: "Key 1", - }); - - configManager.storeAppKey("app2", "app2.key2:secret2", { - appName: "App 2", - keyName: "Key 2", - }); - - // Verify keys are stored - expect(configManager.getApiKey("app1")).toBe("app1.key1:secret1"); - expect(configManager.getApiKey("app2")).toBe("app2.key2:secret2"); - expect(configManager.getAppName("app1")).toBe("App 1"); - expect(configManager.getAppName("app2")).toBe("App 2"); - - // Set current app - configManager.setCurrentApp("app1"); - expect(configManager.getCurrentAppId()).toBe("app1"); - }); - - it("should isolate app keys between accounts", function () { - const configManager = new ConfigManager(); - - // Create first account and add app - configManager.storeAccount("token1", "account1", { - accountId: "acc_1", - accountName: "Account 1", - }); - - configManager.storeAppKey("shared-app", "shared-app.key1:secret1", { - appName: "Shared App Account 1", - }); - - // Create second account and add app with same ID - configManager.storeAccount("token2", "account2", { - accountId: "acc_2", - accountName: "Account 2", - }); - - configManager.storeAppKey( - "shared-app", - "shared-app.key2:secret2", - { - appName: "Shared App Account 2", - }, - "account2", - ); - - // Switch between accounts and verify isolation - configManager.switchAccount("account1"); - expect(configManager.getApiKey("shared-app")).toBe( - "shared-app.key1:secret1", - ); - expect(configManager.getAppName("shared-app")).toBe( - "Shared App Account 1", - ); - - configManager.switchAccount("account2"); - expect(configManager.getApiKey("shared-app")).toBe( - "shared-app.key2:secret2", - ); - expect(configManager.getAppName("shared-app")).toBe( - "Shared App Account 2", - ); - }); - }); -}); diff --git a/test/integration/auth/enhanced-login-flow.test.ts b/test/integration/auth/enhanced-login-flow.test.ts deleted file mode 100644 index 7fbe1142..00000000 --- a/test/integration/auth/enhanced-login-flow.test.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { describe, it, expect } from "vitest"; - -describe("Enhanced Login Flow Integration", function () { - describe("app selection logic integration", function () { - it("should handle single app auto-selection scenario", function () { - const apps = [{ id: "app-only", name: "Only App", accountId: "acc-123" }]; - - // Test single app logic - expect(apps.length).toBe(1); - - // Auto-selection should happen - const selectedApp = apps[0]; - const isAutoSelected = apps.length === 1; - - expect(selectedApp.id).toBe("app-only"); - expect(selectedApp.name).toBe("Only App"); - expect(isAutoSelected).toBe(true); - }); - - it("should handle multiple apps requiring selection", function () { - const apps = [ - { id: "app-prod", name: "Production", accountId: "acc-123" }, - { id: "app-dev", name: "Development", accountId: "acc-123" }, - { id: "app-test", name: "Testing", accountId: "acc-123" }, - ]; - - // Test multiple apps logic - expect(apps.length).toBeGreaterThan(1); - - // Should require user selection - const requiresSelection = apps.length > 1; - expect(requiresSelection).toBe(true); - - // Simulate user selection (second app) - const userSelectedApp = apps[1]; - expect(userSelectedApp.id).toBe("app-dev"); - expect(userSelectedApp.name).toBe("Development"); - }); - - it("should handle no apps scenario", function () { - const apps: any[] = []; - - // Test no apps scenario - expect(apps.length).toBe(0); - - // Should offer app creation - const shouldOfferCreation = apps.length === 0; - expect(shouldOfferCreation).toBe(true); - }); - }); - - describe("key selection logic integration", function () { - it("should handle single key auto-selection", function () { - const keys = [ - { - id: "key-only", - name: "Only Key", - key: "app.key:secret", - appId: "app-123", - }, - ]; - - // Test single key logic - expect(keys.length).toBe(1); - - // Auto-selection should happen - const selectedKey = keys[0]; - const isAutoSelected = keys.length === 1; - - expect(selectedKey.id).toBe("key-only"); - expect(selectedKey.name).toBe("Only Key"); - expect(isAutoSelected).toBe(true); - }); - - it("should handle multiple keys requiring selection", function () { - const keys = [ - { - id: "key-root", - name: "Root Key", - key: "app.root:secret", - appId: "app-123", - }, - { - id: "key-sub", - name: "Subscribe Key", - key: "app.sub:secret", - appId: "app-123", - }, - { - id: "key-pub", - name: "Publish Key", - key: "app.pub:secret", - appId: "app-123", - }, - ]; - - // Test multiple keys logic - expect(keys.length).toBeGreaterThan(1); - - // Should require user selection - const requiresSelection = keys.length > 1; - expect(requiresSelection).toBe(true); - - // Simulate user selection (root key) - const userSelectedKey = keys[0]; - expect(userSelectedKey.id).toBe("key-root"); - expect(userSelectedKey.name).toBe("Root Key"); - }); - - it("should handle no keys gracefully", function () { - const keys: any[] = []; - - // Test no keys scenario - expect(keys.length).toBe(0); - - // Should continue without error (rare for new apps) - const shouldContinue = true; - expect(shouldContinue).toBe(true); - }); - }); - - describe("response structure validation", function () { - it("should validate complete login response structure", function () { - const loginResponse = { - account: { - alias: "production", - id: "acc-123", - name: "Test Company", - user: { email: "user@test.com" }, - }, - app: { - id: "app-456", - name: "Production App", - autoSelected: true, - }, - key: { - id: "key-789", - name: "Root Key", - autoSelected: false, - }, - success: true, - }; - - // Verify complete structure - expect(loginResponse).toHaveProperty("account"); - expect(loginResponse).toHaveProperty("app"); - expect(loginResponse).toHaveProperty("key"); - expect(loginResponse.success).toBe(true); - - // Verify app info - expect(loginResponse.app.autoSelected).toBe(true); - expect(loginResponse.app.id).toBe("app-456"); - - // Verify key info - expect(loginResponse.key.autoSelected).toBe(false); - expect(loginResponse.key.id).toBe("key-789"); - }); - - it("should validate minimal login response structure", function () { - const loginResponse = { - account: { - alias: "default", - id: "acc-123", - name: "Test Company", - user: { email: "user@test.com" }, - }, - success: true, - }; - - // Verify minimal structure when no app/key selected - expect(loginResponse).toHaveProperty("account"); - expect(loginResponse).not.toHaveProperty("app"); - expect(loginResponse).not.toHaveProperty("key"); - expect(loginResponse.success).toBe(true); - }); - }); - - describe("alias validation integration", function () { - it("should validate alias format correctly", function () { - const validAliases = ["production", "dev-env", "staging_2", "test123"]; - const invalidAliases = ["123invalid", "invalid@domain", "has spaces"]; - - const aliasPattern = /^[a-z][\d_a-z-]*$/i; - - validAliases.forEach((alias) => { - expect(aliasPattern.test(alias)).toBe(true); - }); - - invalidAliases.forEach((alias) => { - expect(aliasPattern.test(alias)).toBe(false); - }); - }); - }); -}); diff --git a/test/integration/commands/channels-publish-ordering.test.ts b/test/integration/commands/channels-publish-ordering.test.ts deleted file mode 100644 index 224ff086..00000000 --- a/test/integration/commands/channels-publish-ordering.test.ts +++ /dev/null @@ -1,268 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { runCommand } from "@oclif/test"; - -describe("Channels publish ordering integration tests", function () { - let originalEnv: NodeJS.ProcessEnv; - let publishedMessages: Array<{ data: string; timestamp: number }>; - let realtimeConnectionUsed: boolean; - - beforeEach(function () { - // Store original env vars - originalEnv = { ...process.env }; - publishedMessages = []; - realtimeConnectionUsed = false; - - // Create a function that tracks published messages with timestamps - const realtimePublishFunction = async (message: any) => { - realtimeConnectionUsed = true; - publishedMessages.push({ - data: message.data, - timestamp: Date.now(), - }); - // Simulate some network latency - await new Promise((resolve) => setTimeout(resolve, Math.random() * 10)); - return; - }; - - const restPublishFunction = async (message: any) => { - publishedMessages.push({ - data: message.data, - timestamp: Date.now(), - }); - // Simulate some network latency - await new Promise((resolve) => setTimeout(resolve, Math.random() * 10)); - return; - }; - - // Create mock Ably clients - const mockRealtimeClient = { - channels: { - get: () => ({ - publish: realtimePublishFunction, - on: () => {}, - }), - }, - connection: { - once: (event: string, callback: () => void) => { - realtimeConnectionUsed = true; - if (event === "connected") { - setTimeout(callback, 0); - } - }, - on: () => {}, - state: "connected", - }, - close: () => {}, - }; - - const mockRestClient = { - channels: { - get: () => ({ - publish: restPublishFunction, - on: () => {}, - }), - }, - }; - - // Make the mocks globally available - globalThis.__TEST_MOCKS__ = { - ablyRealtimeMock: mockRealtimeClient, - ablyRestMock: mockRestClient, - }; - - process.env.ABLY_TEST_MODE = "true"; - process.env.ABLY_SUPPRESS_PROCESS_EXIT = "true"; - process.env.ABLY_KEY = "test_key"; - }); - - afterEach(function () { - process.env = originalEnv; - delete globalThis.__TEST_MOCKS__; - }); - - describe("Multiple message publishing", function () { - it("should use realtime transport by default when publishing multiple messages", async function () { - const { stdout } = await runCommand( - [ - "channels:publish", - "test-channel", - '"Message {{.Count}}"', - "--count", - "3", - ], - import.meta.url, - ); - expect(stdout).toContain("3/3 messages published successfully"); - // Should have used realtime connection - expect(realtimeConnectionUsed).toBe(true); - }); - - it("should respect explicit rest transport flag", async function () { - const { stdout } = await runCommand( - [ - "channels:publish", - "test-channel", - '"Message {{.Count}}"', - "--count", - "3", - "--transport", - "rest", - ], - import.meta.url, - ); - expect(stdout).toContain("3/3 messages published successfully"); - // Should not have used realtime connection - expect(realtimeConnectionUsed).toBe(false); - }); - - it("should use rest transport for single message by default", async function () { - const { stdout } = await runCommand( - ["channels:publish", "test-channel", '"Single message"'], - import.meta.url, - ); - expect(stdout).toContain("Message published successfully"); - // Should not have used realtime connection - expect(realtimeConnectionUsed).toBe(false); - }); - }); - - describe("Message delay and ordering", function () { - it("should have 40ms default delay between messages", async function () { - const startTime = Date.now(); - const { stdout } = await runCommand( - [ - "channels:publish", - "test-channel", - '"Message {{.Count}}"', - "--count", - "3", - ], - import.meta.url, - ); - expect(stdout).toContain("Publishing 3 messages with 40ms delay"); - expect(stdout).toContain("3/3 messages published successfully"); - - // Check that messages were published with appropriate delays - expect(publishedMessages).toHaveLength(3); - - // Check message order - expect(publishedMessages[0].data).toBe("Message 1"); - expect(publishedMessages[1].data).toBe("Message 2"); - expect(publishedMessages[2].data).toBe("Message 3"); - - // Check timing - should take at least 80ms (2 delays of 40ms) - const totalTime = Date.now() - startTime; - expect(totalTime).toBeGreaterThanOrEqual(80); - }); - - it("should respect custom delay value", async function () { - const startTime = Date.now(); - const { stdout } = await runCommand( - [ - "channels:publish", - "test-channel", - '"Message {{.Count}}"', - "--count", - "3", - "--delay", - "100", - ], - import.meta.url, - ); - expect(stdout).toContain("Publishing 3 messages with 100ms delay"); - expect(stdout).toContain("3/3 messages published successfully"); - - // Check timing - should take at least 200ms (2 delays of 100ms) - const totalTime = Date.now() - startTime; - expect(totalTime).toBeGreaterThanOrEqual(200); - }); - - it("should allow zero delay when explicitly set", async function () { - const { stdout } = await runCommand( - [ - "channels:publish", - "test-channel", - '"Message {{.Count}}"', - "--count", - "3", - "--delay", - "0", - ], - import.meta.url, - ); - expect(stdout).toContain("Publishing 3 messages with 0ms delay"); - expect(stdout).toContain("3/3 messages published successfully"); - }); - - it("should publish messages in sequential order with delay", async function () { - await runCommand( - [ - "channels:publish", - "test-channel", - '"Message {{.Count}}"', - "--count", - "5", - ], - import.meta.url, - ); - expect(publishedMessages).toHaveLength(5); - - // Verify messages are in correct order - for (let i = 0; i < 5; i++) { - expect(publishedMessages[i].data).toBe(`Message ${i + 1}`); - } - - // Verify timestamps are sequential (each should be at least 40ms apart) - for (let i = 1; i < publishedMessages.length; i++) { - const timeDiff = - publishedMessages[i].timestamp - publishedMessages[i - 1].timestamp; - expect(timeDiff).toBeGreaterThanOrEqual(35); // Allow some margin for timer precision - } - }); - }); - - describe("Error handling with multiple messages", function () { - it("should continue publishing remaining messages on error", async function () { - // Override the publish function to make the 3rd message fail - let callCount = 0; - const failingPublishFunction = async (message: any) => { - callCount++; - if (callCount === 3) { - throw new Error("Network error"); - } - publishedMessages.push({ - data: message.data, - timestamp: Date.now(), - }); - return; - }; - - // Update both mocks to use the failing function - if (globalThis.__TEST_MOCKS__) { - globalThis.__TEST_MOCKS__.ablyRealtimeMock.channels.get = () => ({ - publish: failingPublishFunction, - on: () => {}, - }); - globalThis.__TEST_MOCKS__.ablyRestMock.channels.get = () => ({ - publish: failingPublishFunction, - on: () => {}, - }); - } - - const { stdout } = await runCommand( - [ - "channels:publish", - "test-channel", - '"Message {{.Count}}"', - "--count", - "5", - ], - import.meta.url, - ); - expect(stdout).toContain( - "4/5 messages published successfully (1 errors)", - ); - expect(publishedMessages).toHaveLength(4); - }); - }); -}); diff --git a/test/integration/commands/channels.test.ts b/test/integration/commands/channels.test.ts deleted file mode 100644 index c3f52512..00000000 --- a/test/integration/commands/channels.test.ts +++ /dev/null @@ -1,366 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { runCommand } from "@oclif/test"; - -// Create a more comprehensive mock for Ably client -const mockPresenceMembers = [ - { clientId: "user1", data: { name: "User 1" } }, - { clientId: "user2", data: { name: "User 2" } }, -]; - -const mockMessages = [ - { - name: "event1", - data: { text: "Test message 1" }, - timestamp: Date.now() - 10000, - clientId: "user1", - }, - { - name: "event2", - data: { text: "Test message 2" }, - timestamp: Date.now() - 5000, - clientId: "user2", - }, -]; - -const mockOccupancyMetrics = { - metrics: { - connections: 5, - publishers: 2, - subscribers: 3, - presenceConnections: 2, - presenceMembers: 2, - }, -}; - -// More comprehensive mock Ably client -const mockClient = { - request: () => { - // Return channel list response - return { - statusCode: 200, - items: [ - { - channelId: "test-channel-1", - status: { occupancy: mockOccupancyMetrics }, - }, - { channelId: "test-channel-2" }, - ], - }; - }, - channels: { - get: () => ({ - name: "test-channel-1", - publish: async () => true, - history: () => { - // Return channel history response - return { - items: mockMessages, - }; - }, - on: () => {}, - presence: { - get: () => mockPresenceMembers, - enter: async () => true, - leave: async () => true, - subscribe: (callback: (message: any) => void) => { - // Simulate presence update - setTimeout(() => { - callback({ - action: "enter", - clientId: "user3", - data: { name: "User 3" }, - }); - }, 100); - }, - }, - subscribe: (eventName: string, callback: (message: any) => void) => { - // Simulate message received - setTimeout(() => { - callback({ name: "message", data: { text: "New message" } }); - }, 100); - }, - }), - }, - connection: { - once: (event: string, callback: () => void) => { - if (event === "connected") { - setTimeout(callback, 0); - } - }, - on: () => {}, - }, - stats: async () => { - return { - items: [ - { - inbound: {}, - outbound: {}, - persisted: {}, - connections: {}, - channels: 5, - apiRequests: 120, - tokenRequests: 10, - }, - ], - }; - }, - close: () => { - // Mock close method - }, - auth: { - clientId: "foo", - }, -}; - -// Pre-define variables used in tests to avoid linter errors -const publishFlowUniqueChannel = `test-channel-${Date.now()}`; -const publishFlowUniqueMessage = `Test message ${Date.now()}`; -const presenceFlowUniqueChannel = `test-presence-${Date.now()}`; -const presenceFlowUniqueClientId = `client-${Date.now()}`; - -let originalEnv: NodeJS.ProcessEnv; - -describe("Channels integration tests", function () { - beforeEach(function () { - // Store original env vars - originalEnv = { ...process.env }; - - // Make the mock globally available - globalThis.__TEST_MOCKS__ = { - ablyRealtimeMock: mockClient, - ablyRestMock: mockClient, - }; - - // Set environment variables for this test file - process.env.ABLY_CLI_TEST_MODE = "true"; - process.env.ABLY_API_KEY = "test.key:secret"; // Using a consistent mock key for integration tests - }); - - afterEach(function () { - // Clean up global mock - delete globalThis.__TEST_MOCKS__; - // Restore original environment variables - process.env = originalEnv; - }); - - // Core channel operations - describe("Core channel operations", function () { - it("lists active channels", async function () { - const { stdout } = await runCommand( - ["channels", "list"], - import.meta.url, - ); - - expect(stdout).toContain("test-channel-1"); - expect(stdout).toContain("test-channel-2"); - }); - - it("outputs channels list in JSON format", async function () { - const { stdout } = await runCommand( - ["channels", "list", "--json"], - import.meta.url, - ); - - const output = JSON.parse(stdout); - expect(output).toHaveProperty("channels"); - expect(Array.isArray(output.channels)).toBe(true); - expect(output.channels.length).toBeGreaterThanOrEqual(2); - }); - - it("publishes message to a channel", async function () { - const { stdout } = await runCommand( - ["channels", "publish", "test-channel-1", '{"text":"Hello World"}'], - import.meta.url, - ); - - expect(stdout).toContain("Message published successfully"); - expect(stdout).toContain("test-channel-1"); - }); - - it("retrieves channel history", async function () { - const { stdout } = await runCommand( - ["channels", "history", "test-channel-1"], - import.meta.url, - ); - - expect(stdout).toContain("Test message 1"); - expect(stdout).toContain("Test message 2"); - }); - }); - - // Presence operations - describe("Presence operations", function () { - it("enters channel presence", async function () { - const { stdout } = await runCommand( - [ - "channels", - "presence", - "enter", - "test-channel-1", - "--client-id", - "test-client", - ], - import.meta.url, - ); - - expect(stdout).toContain("Entered channel"); - expect(stdout).toContain("test-channel-1"); - }); - }); - - // Occupancy operations - describe("Occupancy operations", function () { - it("gets channel occupancy metrics", async function () { - const { stdout } = await runCommand( - ["channels", "occupancy", "get", "test-channel-1"], - import.meta.url, - ); - - expect(stdout).toContain("test-channel-1"); - expect(stdout).toContain("Connections: 5"); - expect(stdout).toContain("Publishers: 2"); - expect(stdout).toContain("Subscribers: 3"); - expect(stdout).toContain("Presence Members: 2"); - }); - }); - - // Batch operations - describe("Batch operations", function () { - it("batch publishes to multiple channels", async function () { - const { stdout } = await runCommand( - [ - "channels", - "batch-publish", - "--channels", - "test-channel-1,test-channel-2", - '{"text":"Batch Message"}', - ], - import.meta.url, - ); - - expect(stdout).toContain("Batch publish successful"); - expect(stdout).toContain("test-channel-1"); - expect(stdout).toContain("test-channel-2"); - }); - }); - - // Test flow: Publish -> history - describe("Publish to history flow", function () { - // We need to split these into separate tests rather than using .do() which causes linter errors - it("publishes message then retrieves it in history", async function () { - const { stdout } = await runCommand( - [ - "channels", - "publish", - publishFlowUniqueChannel, - `{"text":"${publishFlowUniqueMessage}"}`, - ], - import.meta.url, - ); - - expect(stdout).toContain("Message published successfully"); - }); - - it("retrieves published message from history", async function () { - const { stdout } = await runCommand( - ["channels", "history", publishFlowUniqueChannel], - import.meta.url, - ); - - // In the real world the message would be in history, but in our mock - // we're just checking that history command was executed correctly - expect(stdout).toContain("Found 2 messages"); - }); - }); - - // Test flow: Presence enter -> presence list - describe("Presence enter to list flow", function () { - // We need to split these into separate tests rather than using .do() which causes linter errors - it("enters presence on unique channel", async function () { - const { stdout } = await runCommand( - [ - "channels", - "presence", - "enter", - presenceFlowUniqueChannel, - "--client-id", - presenceFlowUniqueClientId, - ], - import.meta.url, - ); - - expect(stdout).toContain("Entered channel " + presenceFlowUniqueChannel); - }); - }); - - // Connection monitoring operations - describe("Connection monitoring operations", function () { - it("retrieves connection stats with default parameters", async function () { - const { stdout } = await runCommand( - ["connections", "stats"], - import.meta.url, - ); - - expect(stdout).toContain("Connections:"); - expect(stdout).toContain("Channels:"); - expect(stdout).toContain("Messages:"); - }); - - it("retrieves connection stats in JSON format", async function () { - const { stdout } = await runCommand( - ["connections", "stats", "--json"], - import.meta.url, - ); - - const output = JSON.parse(stdout); - expect(output).toHaveProperty("inbound"); - expect(output).toHaveProperty("outbound"); - expect(output).toHaveProperty("connections"); - }); - - it("retrieves connection stats with custom time range", async function () { - const { stdout } = await runCommand( - [ - "connections", - "stats", - "--start", - Date.now(), - "--end", - Date.now() + 10000, - ], - import.meta.url, - ); - - expect(stdout).toContain("Stats for"); - }); - - it("retrieves connection stats with different time units", async function () { - const { stdout } = await runCommand( - ["connections", "stats", "--unit", "hour", "--limit", "5"], - import.meta.url, - ); - - expect(stdout).toContain("Connections:"); - expect(stdout).toContain("Channels:"); - }); - }); - - // Error recovery scenarios - describe("Error recovery scenarios", function () { - it("handles channel operations with invalid channel names gracefully", async function () { - const { error } = await runCommand( - ["channels", "history", ""], - import.meta.url, - ); - expect(error.oclif.exit).toBe(2); - }); - - it("handles connection stats with invalid parameters gracefully", async function () { - const { error } = await runCommand( - ["connections", "stats", "--start", "invalid-timestamp"], - import.meta.url, - ); - expect(error.oclif.exit).toBe(2); - }); - }); -}); diff --git a/test/integration/commands/minimal-test.test.ts b/test/integration/commands/minimal-test.test.ts deleted file mode 100644 index 6363e3ca..00000000 --- a/test/integration/commands/minimal-test.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { runCommand } from "@oclif/test"; -import { describe, it, expect } from "vitest"; - -// This is the absolute minimum test to see if oclif tests work at all -describe("Minimal oclif test", function () { - // Just try to execute the help command which should be fast and reliable - it("runs help command", async function () { - // Try to run the simplest possible command - const { stdout } = await runCommand(["help"]); - expect(stdout).toContain("$ ably [COMMAND]"); - }); -}); diff --git a/test/integration/commands/rooms-messages-ordering.test.ts b/test/integration/commands/rooms-messages-ordering.test.ts deleted file mode 100644 index 2f61a4a5..00000000 --- a/test/integration/commands/rooms-messages-ordering.test.ts +++ /dev/null @@ -1,301 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { runCommand } from "@oclif/test"; -import { registerMock } from "../test-utils.js"; -import { RoomStatus } from "@ably/chat"; - -describe("Rooms messages send ordering integration tests", function () { - let originalEnv: NodeJS.ProcessEnv; - let sentMessages: Array<{ text: string; timestamp: number }>; - - beforeEach(function () { - // Store original env vars - originalEnv = { ...process.env }; - - // Set environment variables for this test file - process.env.ABLY_CLI_TEST_MODE = "true"; - process.env.ABLY_API_KEY = "test.key:secret"; - - // Reset sent messages for each test - sentMessages = []; - - // Create a function that tracks sent messages with timestamps - const sendFunction = async (message: any) => { - sentMessages.push({ - text: message.text, - timestamp: Date.now(), - }); - // Simulate some network latency - await new Promise((resolve) => setTimeout(resolve, Math.random() * 10)); - return; - }; - - // Create mock room - const createMockRoom = (room: string) => ({ - id: room, - attach: async () => {}, - detach: async () => {}, - - onStatusChange: (callback: (change: any) => void) => { - setTimeout(() => { - callback({ - current: RoomStatus.Attached, - }); - }, 100); - }, - - messages: { - send: sendFunction, - }, - }); - - const mockRealtimeClient = { - connection: { - once: (event: string, callback: () => void) => { - if (event === "connected") { - setTimeout(callback, 0); - } - }, - on: (callback: (stateChange: any) => void) => { - setTimeout(() => { - callback({ current: "connected", reason: null }); - }, 10); - }, - state: "connected", - id: "test-connection-id", - }, - close: () => {}, - }; - - const mockChatClient = { - rooms: { - get: (room: string) => createMockRoom(room), - release: async (_: string) => {}, - }, - connection: { - onStatusChange: (_: (change: any) => void) => {}, - }, - realtime: mockRealtimeClient, - }; - - // Register the chat and realtime mocks using the test-utils system - registerMock("ablyChatMock", mockChatClient); - registerMock("ablyRealtimeMock", mockRealtimeClient); - }); - - afterEach(function () { - // Restore original environment variables - process.env = originalEnv; - }); - - describe("Message delay and ordering", function () { - it("should have 40ms default delay between messages", async function () { - const startTime = Date.now(); - const { stdout } = await runCommand( - [ - "rooms", - "messages", - "send", - "test-room", - '"Message {{.Count}}"', - "--count", - "3", - ], - import.meta.url, - ); - - expect(stdout).toContain("Sending 3 messages with 40ms delay"); - expect(stdout).toContain("3/3 messages sent successfully"); - - // Check that messages were sent with appropriate delays - expect(sentMessages).toHaveLength(3); - - // Check message order - expect(sentMessages[0].text).toBe("Message 1"); - expect(sentMessages[1].text).toBe("Message 2"); - expect(sentMessages[2].text).toBe("Message 3"); - - // Check timing - should take at least 80ms (2 delays of 40ms) - const totalTime = Date.now() - startTime; - expect(totalTime).toBeGreaterThanOrEqual(80); - }); - - it("should respect custom delay value", async function () { - const startTime = Date.now(); - const { stdout } = await runCommand( - [ - "rooms", - "messages", - "send", - "test-room", - '"Message {{.Count}}"', - "--count", - "3", - "--delay", - "100", - ], - import.meta.url, - ); - - expect(stdout).toContain("Sending 3 messages with 100ms delay"); - expect(stdout).toContain("3/3 messages sent successfully"); - - // Check timing - should take at least 200ms (2 delays of 100ms) - const totalTime = Date.now() - startTime; - expect(totalTime).toBeGreaterThanOrEqual(200); - }); - - it("should enforce minimum 40ms delay even if lower value specified", async function () { - const startTime = Date.now(); - const { stdout } = await runCommand( - [ - "rooms", - "messages", - "send", - "test-room", - '"Message {{.Count}}"', - "--count", - "3", - "--delay", - "10", - ], - import.meta.url, - ); - - // Should use 40ms instead of 10ms - expect(stdout).toContain("Sending 3 messages with 40ms delay"); - expect(stdout).toContain("3/3 messages sent successfully"); - - // Check timing - should take at least 80ms (2 delays of 40ms) - const totalTime = Date.now() - startTime; - expect(totalTime).toBeGreaterThanOrEqual(80); - }); - - it("should send messages in sequential order with delay", async function () { - await runCommand( - [ - "rooms", - "messages", - "send", - "test-room", - '"Message {{.Count}}"', - "--count", - "5", - ], - import.meta.url, - ); - - expect(sentMessages).toHaveLength(5); - - // Verify messages are in correct order - for (let i = 0; i < 5; i++) { - expect(sentMessages[i].text).toBe(`Message ${i + 1}`); - } - - // Verify timestamps are sequential (each should be at least 40ms apart) - for (let i = 1; i < sentMessages.length; i++) { - const timeDiff = - sentMessages[i].timestamp - sentMessages[i - 1].timestamp; - expect(timeDiff).toBeGreaterThanOrEqual(35); // Allow some margin for timer precision - } - }); - }); - - describe("Single message sending", function () { - it("should send single message without delay", async function () { - const { stdout } = await runCommand( - ["rooms", "messages", "send", "test-room", '"Single message"'], - import.meta.url, - ); - - expect(stdout).toContain("Message sent successfully"); - expect(sentMessages).toHaveLength(1); - expect(sentMessages[0].text).toBe("Single message"); - }); - }); - - describe("Error handling with multiple messages", function () { - it("should continue sending remaining messages on error", async function () { - // Override the send function to make the 3rd message fail - let callCount = 0; - const failingSendFunction = async (message: any) => { - callCount++; - if (callCount === 3) { - throw new Error("Network error"); - } - sentMessages.push({ - text: message.text, - timestamp: Date.now(), - }); - return; - }; - - // Create mock room with failing send function - const createMockRoomWithError = (room: string) => ({ - id: room, - attach: async () => {}, - detach: async () => {}, - - onStatusChange: (callback: (change: any) => void) => { - setTimeout(() => { - callback({ - current: RoomStatus.Attached, - }); - }, 100); - }, - - messages: { - send: failingSendFunction, - }, - }); - - const mockRealtimeClient = { - connection: { - once: (event: string, callback: () => void) => { - if (event === "connected") { - setTimeout(callback, 0); - } - }, - on: (callback: (stateChange: any) => void) => { - setTimeout(() => { - callback({ current: "connected", reason: null }); - }, 10); - }, - state: "connected", - id: "test-connection-id", - }, - close: () => {}, - }; - - const mockChatClientWithError = { - rooms: { - get: (room: string) => createMockRoomWithError(room), - release: async (_: string) => {}, - }, - connection: { - onStatusChange: (_: (change: any) => void) => {}, - }, - realtime: mockRealtimeClient, - }; - - // Re-register mocks with error-inducing client - registerMock("ablyChatMock", mockChatClientWithError); - registerMock("ablyRealtimeMock", mockRealtimeClient); - - const { stdout } = await runCommand( - [ - "rooms", - "messages", - "send", - "test-room", - '"Message {{.Count}}"', - "--count", - "5", - ], - import.meta.url, - ); - - expect(stdout).toContain("4/5 messages sent successfully (1 errors)"); - expect(sentMessages).toHaveLength(4); - }); - }); -}); diff --git a/test/integration/commands/rooms.test.ts b/test/integration/commands/rooms.test.ts deleted file mode 100644 index 936326d5..00000000 --- a/test/integration/commands/rooms.test.ts +++ /dev/null @@ -1,429 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { runCommand } from "@oclif/test"; -import { registerMock } from "../test-utils.js"; -import { RoomStatus } from "@ably/chat"; - -// Mock room data -const mockMessages = [ - { - text: "Hello room!", - clientId: "test-client", - timestamp: new Date(Date.now() - 10000), - metadata: { isImportant: true }, - }, - { - text: "How is everyone?", - clientId: "other-client", - timestamp: new Date(Date.now() - 5000), - metadata: { thread: "general" }, - }, -]; - -const mockPresenceMembers = [ - { clientId: "user1", data: { name: "Alice", status: "online" } }, - { clientId: "user2", data: { name: "Bob", status: "busy" } }, -]; - -const mockReactions = [ - { emoji: "👍", count: 3, clientIds: ["user1", "user2", "user3"] }, - { emoji: "❤️", count: 1, clientIds: ["user1"] }, -]; - -const mockOccupancy = { - connections: 5, - publishers: 2, - subscribers: 3, - presenceConnections: 2, - presenceMembers: 2, -}; - -// Create comprehensive mock for Chat client and room -const createMockRoom = (room: string) => ({ - id: room, - attach: async () => {}, - detach: async () => {}, - - onStatusChange: (callback: (change: any) => void) => { - setTimeout(() => { - callback({ - current: RoomStatus.Attached, - }); - }, 100); - }, - - // Messages functionality - messages: { - send: async (message: any) => { - mockMessages.push({ - text: message.text, - clientId: "test-client", - timestamp: new Date(), - metadata: message.metadata || {}, - }); - return; - }, - subscribe: (callback: (message: any) => void) => { - // Simulate receiving messages - setTimeout(() => { - callback({ - text: "New message", - clientId: "live-client", - timestamp: new Date(), - }); - }, 100); - return Promise.resolve(); - }, - unsubscribe: async () => {}, - history: async (options?: any) => { - const limit = options?.limit || 50; - const direction = options?.direction || "backwards"; - - let messages = [...mockMessages]; - if (direction === "backwards") { - messages.reverse(); - } - - return { - items: messages.slice(0, limit), - hasNext: () => false, - isLast: () => true, - }; - }, - }, - - // Presence functionality - presence: { - enter: async (data?: any) => { - mockPresenceMembers.push({ - clientId: "test-client", - data: data || { status: "online" }, - }); - return; - }, - leave: async () => {}, - get: async () => [...mockPresenceMembers], - subscribe: (callback: (member: any) => void) => { - setTimeout(() => { - callback({ - member: { - action: "enter", - clientId: "new-member", - data: { name: "Charlie", status: "active" }, - }, - }); - }, 100); - return Promise.resolve(); - }, - unsubscribe: async () => {}, - }, - - // Reactions functionality - reactions: { - send: async (emoji: string, _?: any) => { - const existingReaction = mockReactions.find((r) => r.emoji === emoji); - if (existingReaction) { - existingReaction.count++; - existingReaction.clientIds.push("test-client"); - } else { - mockReactions.push({ - emoji, - count: 1, - clientIds: ["test-client"], - }); - } - return; - }, - subscribe: (callback: (reaction: any) => void) => { - setTimeout(() => { - callback({ - emoji: "🎉", - clientId: "celebration-client", - timestamp: new Date(), - }); - }, 100); - return Promise.resolve(); - }, - unsubscribe: async () => {}, - }, - - // Typing functionality - typing: { - keystroke: async () => {}, - stop: async () => {}, - subscribe: (callback: (event: any) => void) => { - setTimeout(() => { - callback({ - clientId: "typing-client", - event: "start", - timestamp: new Date(), - }); - }, 100); - return Promise.resolve(); - }, - unsubscribe: async () => {}, - }, - - // Occupancy functionality - occupancy: { - get: async () => ({ ...mockOccupancy }), - subscribe: (callback: (occupancy: any) => void) => { - setTimeout(() => { - callback({ - ...mockOccupancy, - connections: mockOccupancy.connections + 1, - }); - }, 100); - return Promise.resolve(); - }, - unsubscribe: async () => {}, - }, -}); - -const mockRealtimeClient = { - connection: { - once: (event: string, callback: () => void) => { - if (event === "connected") { - setTimeout(callback, 0); - } - }, - on: (callback: (stateChange: any) => void) => { - // Simulate connection state changes - setTimeout(() => { - callback({ current: "connected", reason: null }); - }, 10); - }, - state: "connected", - id: "test-connection-id", - auth: { - clientId: "foo", - }, - }, - close: () => { - // Mock close method - }, -}; - -const mockChatClient = { - rooms: { - get: (room: string) => createMockRoom(room), - release: async (_: string) => {}, - }, - connection: { - onStatusChange: (_: (change: any) => void) => {}, - }, - realtime: mockRealtimeClient, -}; - -let originalEnv: NodeJS.ProcessEnv; - -describe("Rooms integration tests", function () { - beforeEach(function () { - // Store original env vars - originalEnv = { ...process.env }; - - // Set environment variables for this test file - process.env.ABLY_CLI_TEST_MODE = "true"; - process.env.ABLY_API_KEY = "test.key:secret"; - - // Register the chat and realtime mocks using the test-utils system - registerMock("ablyChatMock", mockChatClient); - registerMock("ablyRealtimeMock", mockRealtimeClient); - }); - - afterEach(function () { - // Restore original environment variables - process.env = originalEnv; - }); - - describe("Chat room lifecycle", function () { - const testRoom = "integration-test-room"; - - it("sends a message to a room", async function () { - const { stdout } = await runCommand( - [ - "rooms", - "messages", - "send", - testRoom, - '"Hello from integration test!"', - ], - import.meta.url, - ); - expect(stdout).toContain("Message sent successfully"); - }); - - it("sends multiple messages with metadata", async function () { - const { stdout } = await runCommand( - [ - "rooms", - "messages", - "send", - testRoom, - '"Message with metadata"', - "--metadata", - '{"priority":"high"}', - "--count", - "3", - ], - import.meta.url, - ); - expect(stdout).toContain("messages sent successfully"); - }); - - it("retrieves message history", async function () { - const { stdout } = await runCommand( - ["rooms", "messages", "history", testRoom, "--limit", "10"], - import.meta.url, - ); - expect(stdout).toContain("Hello room!"); - expect(stdout).toContain("How is everyone?"); - }); - - it("enters room presence with data", async function () { - const { stdout } = await runCommand( - [ - "rooms", - "presence", - "enter", - testRoom, - "--data", - '{"name":"Integration Tester","role":"tester"}', - ], - import.meta.url, - ); - - // Since presence enter runs indefinitely, we check initial setup - expect(stdout).toContain("Entered room"); - }); - - it("gets room occupancy metrics", async function () { - const { stdout } = await runCommand( - ["rooms", "occupancy", "get", testRoom], - import.meta.url, - ); - expect(stdout).toContain("Connections:"); - expect(stdout).toContain("Presence Members:"); - }); - - it("sends a reaction to a room", async function () { - const { stdout } = await runCommand( - ["rooms", "reactions", "send", testRoom, "🚀"], - import.meta.url, - ); - expect(stdout).toContain( - "Sent reaction 🚀 in room integration-test-room", - ); - }); - - it("starts typing indicator", async function () { - const { stdout } = await runCommand( - ["rooms", "typing", "keystroke", testRoom], - import.meta.url, - ); - - expect(stdout).toContain("Started typing in room"); - }); - }); - - describe("JSON output format", function () { - const testRoom = "json-test-room"; - - it("outputs message send result in JSON format", async function () { - const { stdout } = await runCommand( - [ - "rooms", - "messages", - "send", - testRoom, - '"JSON test message"', - "--json", - ], - import.meta.url, - ); - expect(stdout).toContain('"success": true'); - expect(stdout).toContain('"room": "'); - }); - - it("outputs message history in JSON format", async function () { - const { stdout } = await runCommand( - ["rooms", "messages", "history", testRoom, "--json"], - import.meta.url, - ); - expect(stdout).toContain('"messages": ['); - }); - - it("outputs occupancy metrics in JSON format", async function () { - const { stdout } = await runCommand( - ["rooms", "occupancy", "get", testRoom, "--json"], - import.meta.url, - ); - expect(stdout).toContain('"connections":'); - expect(stdout).toContain('"publishers":'); - expect(stdout).toContain('"subscribers":'); - }); - }); - - describe("Error handling", function () { - it("handles invalid metadata JSON", async function () { - const { error } = await runCommand( - [ - "rooms", - "messages", - "send", - "test-room", - '"test message"', - "--metadata", - "{]", - ], - import.meta.url, - ); - expect(error).toBeDefined(); - expect(error?.message).toContain("Invalid metadata JSON"); - }); - - it("handles missing message text", async function () { - const { error } = await runCommand( - ["rooms", "messages", "send", "test-room"], - import.meta.url, - ); - expect(error).toBeDefined(); - expect(error?.message).toContain("Missing 1 required arg"); - expect(error?.message).toContain("text The message text to send"); - }); - }); - - describe("Real-time message flow simulation", function () { - const testRoom = "realtime-test-room"; - - it("simulates sending and then subscribing to messages", async function () { - // This test simulates a real flow where we send a message and then subscribe - const { stdout } = await runCommand( - [ - "rooms", - "messages", - "send", - testRoom, - '"Test message for subscription"', - ], - import.meta.url, - ); - expect(stdout).toContain("Message sent successfully"); - }); - - it("simulates presence lifecycle", async function () { - // Test presence enter followed by checking presence - const { stdout } = await runCommand( - [ - "rooms", - "presence", - "enter", - testRoom, - "--data", - '{"status":"testing"}', - ], - import.meta.url, - ); - expect(stdout).toContain("Entered room realtime-test-room"); - }); - }); -}); diff --git a/test/integration/commands/spaces.test.ts b/test/integration/commands/spaces.test.ts deleted file mode 100644 index a387e529..00000000 --- a/test/integration/commands/spaces.test.ts +++ /dev/null @@ -1,630 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { runCommand } from "@oclif/test"; -import { registerMock } from "../test-utils.js"; - -// Mock spaces data -const mockMembers = [ - { - clientId: "alice", - connectionId: "conn_1", - profileData: { name: "Alice", role: "designer" }, - isConnected: true, - lastEvent: { name: "enter" }, - }, - { - clientId: "bob", - connectionId: "conn_2", - profileData: { name: "Bob", role: "developer" }, - isConnected: true, - lastEvent: { name: "enter" }, - }, -]; - -const mockLocations = [ - { - clientId: "alice", - location: { x: 100, y: 200, page: "dashboard" }, - timestamp: Date.now() - 5000, - }, - { - clientId: "bob", - location: { x: 300, y: 150, page: "editor" }, - timestamp: Date.now() - 3000, - }, -]; - -const mockCursors = [ - { - clientId: "alice", - position: { x: 150, y: 250 }, - data: { color: "red", size: "medium" }, - timestamp: Date.now() - 2000, - }, - { - clientId: "bob", - position: { x: 400, y: 300 }, - data: { color: "blue", size: "large" }, - timestamp: Date.now() - 1000, - }, -]; - -const mockLocks = [ - { - id: "document-1", - member: { clientId: "alice" }, - timestamp: Date.now() - 10000, - attributes: { priority: "high" }, - }, - { - id: "section-2", - member: { clientId: "bob" }, - timestamp: Date.now() - 5000, - attributes: { timeout: 30000 }, - }, -]; - -// Create comprehensive mock for Spaces client and space -const createMockSpace = (spaceId: string) => ({ - name: spaceId, - - // Members functionality - members: { - enter: async (profileData?: any) => { - mockMembers.push({ - clientId: "test-client", - connectionId: "test-conn", - profileData: profileData || { status: "active" }, - isConnected: true, - lastEvent: { name: "enter" }, - }); - return; - }, - leave: async () => {}, - getAll: async () => [...mockMembers], - subscribe: (eventType: string, callback: (member: any) => void) => { - setTimeout(() => { - callback({ - clientId: "new-member", - connectionId: "new-conn", - profileData: { name: "Charlie", role: "tester" }, - isConnected: true, - lastEvent: { name: "enter" }, - }); - }, 100); - return Promise.resolve(); - }, - unsubscribe: async () => {}, - }, - - // Locations functionality - locations: { - set: async (location: any) => { - const existingIndex = mockLocations.findIndex( - (l) => l.clientId === "test-client", - ); - const locationData = { - clientId: "test-client", - location, - timestamp: Date.now(), - }; - - if (existingIndex === -1) { - mockLocations.push(locationData); - } else { - mockLocations[existingIndex] = locationData; - } - return; - }, - getAll: async () => [...mockLocations], - subscribe: ( - eventOrCallback: string | ((cursor: any) => void), - callback?: (cursor: any) => void, - ) => { - if (typeof eventOrCallback !== "string") { - callback = eventOrCallback; - } - - setTimeout(() => { - callback!({ - member: { - clientId: "moving-client", - connectionId: "connection-foo", - location: { x: 500, y: 600, page: "settings" }, - timestamp: Date.now(), - }, - }); - }, 100); - return Promise.resolve(); - }, - unsubscribe: async () => {}, - }, - - // Cursors functionality - cursors: { - set: async (position: any, data?: any) => { - const existingIndex = mockCursors.findIndex( - (c) => c.clientId === "test-client", - ); - const cursorData = { - clientId: "test-client", - position, - data: data || {}, - timestamp: Date.now(), - }; - - if (existingIndex === -1) { - mockCursors.push(cursorData); - } else { - mockCursors[existingIndex] = cursorData; - } - return; - }, - getAll: async () => [...mockCursors], - subscribe: ( - eventOrCallback: string | ((cursor: any) => void), - callback?: (cursor: any) => void, - ) => { - if (typeof eventOrCallback !== "string") { - callback = eventOrCallback; - } - - setTimeout(() => { - callback!({ - clientId: "cursor-client", - position: { x: 200, y: 100 }, - data: { color: "green" }, - timestamp: Date.now(), - }); - }, 100); - return Promise.resolve(); - }, - unsubscribe: async () => {}, - }, - - // Locks functionality - locks: { - acquire: async (lockId: string, attributes?: any) => { - const lockData = { - id: lockId, - member: { clientId: "test-client" }, - timestamp: Date.now(), - attributes: attributes || {}, - }; - mockLocks.push(lockData); - return lockData; - }, - release: async (lockId: string) => { - const index = mockLocks.findIndex((l) => l.id === lockId); - if (index !== -1) { - mockLocks.splice(index, 1); - } - return; - }, - get: async (lockId: string) => { - return mockLocks.find((l) => l.id === lockId) || null; - }, - getAll: async () => [...mockLocks], - subscribe: (callback: (lock: any) => void) => { - setTimeout(() => { - callback({ - id: "new-lock", - member: { clientId: "lock-client" }, - timestamp: Date.now(), - event: "acquire", - }); - }, 100); - return Promise.resolve(); - }, - unsubscribe: async () => {}, - }, - - // Space lifecycle - enter: async (_?: any) => { - return mockMembers[0]; // Return first member as entered member - }, - leave: async () => {}, -}); - -const mockSpacesClient = { - get: (spaceId: string) => createMockSpace(spaceId), - release: async (_: string) => {}, -}; - -const mockRealtimeClient = { - connection: { - once: (event: string, callback: () => void) => { - if (event === "connected") { - setTimeout(callback, 0); - } - }, - on: (callback: (stateChange: any) => void) => { - setTimeout(() => { - callback({ current: "connected", reason: null }); - }, 10); - }, - state: "connected", - id: "test-connection-id", - auth: { - clientId: "foo", - }, - }, - close: () => { - // Mock close method - }, - auth: { - clientId: "foo", - }, -}; - -let originalEnv: NodeJS.ProcessEnv; - -describe("Spaces integration tests", function () { - beforeEach(function () { - // Store original env vars - originalEnv = { ...process.env }; - - // Set environment variables for this test file - process.env.ABLY_CLI_TEST_MODE = "true"; - process.env.ABLY_API_KEY = "test.key:secret"; - - // Register the spaces and realtime mocks using the test-utils system - registerMock("ablySpacesMock", mockSpacesClient); - registerMock("ablyRealtimeMock", mockRealtimeClient); - }); - - afterEach(function () { - // Restore original environment variables - process.env = originalEnv; - }); - - describe("Spaces state synchronization", function () { - const testSpaceId = "integration-test-space"; - - it("enters a space with profile data", async function () { - const { stdout } = await runCommand( - [ - "spaces", - "members", - "enter", - testSpaceId, - "--profile", - '{"name":"Integration Tester","department":"QA"}', - ], - import.meta.url, - ); - expect(stdout).toContain("Successfully entered space"); - }); - - it("sets location in a space", async function () { - const { stdout } = await runCommand( - [ - "spaces", - "locations", - "set", - testSpaceId, - "--location", - '{"x":200,"y":300,"page":"test-page"}', - ], - import.meta.url, - ); - expect(stdout).toContain("Successfully set location"); - }); - - // See: FF-154 - // eslint-disable-next-line vitest/no-disabled-tests - it.skip("gets all locations in a space", async function () { - const { stdout } = await runCommand( - ["spaces", "locations", "get-all", testSpaceId], - import.meta.url, - ); - - expect(stdout).toContain("alice"); - expect(stdout).toContain("dashboard"); - expect(stdout).toContain("bob"); - expect(stdout).toContain("editor"); - }); - - it("sets cursor position in a space", async function () { - const { stdout } = await runCommand( - [ - "spaces", - "cursors", - "set", - testSpaceId, - "--x", - "400", - "--y", - "500", - "--data", - '{"color":"purple"}', - ], - import.meta.url, - ); - expect(stdout).toContain("Set cursor in space"); - }); - - // See: FF-154 - // eslint-disable-next-line vitest/no-disabled-tests - it.skip("gets all cursors in a space", async function () { - const { stdout } = await runCommand( - ["spaces", "cursors", "get-all", testSpaceId], - import.meta.url, - ); - expect(stdout).toContain("alice"); - expect(stdout).toContain("bob"); - expect(stdout).toContain("x:"); - expect(stdout).toContain("y:"); - }); - - it("acquires a lock in a space", async function () { - const { stdout } = await runCommand( - [ - "spaces", - "locks", - "acquire", - testSpaceId, - "test-lock", - "--data", - '{"priority":"high","timeout":60000}', - ], - import.meta.url, - ); - expect(stdout).toContain("Successfully acquired lock"); - expect(stdout).toContain("test-lock"); - }); - - it("gets a specific lock in a space", async function () { - const { stdout } = await runCommand( - ["spaces", "locks", "get", testSpaceId, "document-1"], - import.meta.url, - ); - expect(stdout).toContain("document-1"); - expect(stdout).toContain("alice"); - }); - - it("gets all locks in a space", async function () { - const { stdout } = await runCommand( - ["spaces", "locks", "get-all", testSpaceId], - import.meta.url, - ); - expect(stdout).toContain("document-1"); - expect(stdout).toContain("section-2"); - expect(stdout).toContain("alice"); - expect(stdout).toContain("bob"); - }); - }); - - describe("JSON output format", function () { - const testSpaceId = "json-test-space"; - - it("outputs member enter result in JSON format", async function () { - const { stdout } = await runCommand( - [ - "spaces", - "members", - "enter", - testSpaceId, - "--profile", - '{"name":"JSON Tester"}', - "--json", - ], - import.meta.url, - ); - expect(stdout).toContain('"spaceName": "'); - expect(stdout).toContain('"success": true'); - }); - - it("outputs locations in JSON format", async function () { - const { stdout } = await runCommand( - ["spaces", "locations", "get-all", testSpaceId, "--json"], - import.meta.url, - ); - expect(stdout).toContain('"locations": ['); - }); - - it("outputs cursors in JSON format", async function () { - const { stdout } = await runCommand( - ["spaces", "cursors", "get-all", testSpaceId, "--json"], - import.meta.url, - ); - expect(stdout).toContain('"cursors": ['); - }); - - it("outputs locks in JSON format", async function () { - const { stdout } = await runCommand( - ["spaces", "locks", "get-all", testSpaceId, "--json"], - import.meta.url, - ); - expect(stdout).toContain('"locks": ['); - }); - }); - - describe("Error handling", function () { - it("handles invalid space ID gracefully", async function () { - const { error } = await runCommand( - ["spaces", "members", "enter", ""], - import.meta.url, - ); - expect(error?.message).toContain("Missing 1 required arg"); - expect(error?.message).toContain("space"); - }); - - it("handles invalid profile JSON", async function () { - const { error } = await runCommand( - [ - "spaces", - "members", - "enter", - "test-space", - "--profile", - "invalid-json", - ], - import.meta.url, - ); - expect(error?.message).toContain("Invalid profile JSON"); - }); - - it("handles invalid location JSON", async function () { - const { error } = await runCommand( - [ - "spaces", - "locations", - "set", - "test-space", - "--location", - "invalid-json", - ], - import.meta.url, - ); - expect(error?.message).toContain("Invalid location JSON"); - }); - - it("handles invalid cursor position JSON", async function () { - const { error } = await runCommand( - ["spaces", "cursors", "set", "test-space", "--data", "invalid-json"], - import.meta.url, - ); - - expect(error).toBeDefined(); - expect(error?.message).toContain("Invalid JSON in --data flag"); - }); - }); - - describe("Collaboration scenarios", function () { - const testSpaceId = "collaboration-test-space"; - - it("simulates multiple members entering a space", async function () { - const { stdout } = await runCommand( - [ - "spaces", - "members", - "enter", - testSpaceId, - "--profile", - '{"name":"Collaborator 1","role":"editor"}', - ], - import.meta.url, - ); - expect(stdout).toContain("Successfully entered space"); - }); - - it("simulates location updates during collaboration", async function () { - const { stdout } = await runCommand( - [ - "spaces", - "locations", - "set", - testSpaceId, - "--location", - '{"x":100,"y":200,"page":"document","section":"intro"}', - ], - import.meta.url, - ); - expect(stdout).toContain("Successfully set location"); - }); - - it("simulates cursor movement during collaboration", async function () { - const { stdout } = await runCommand( - [ - "spaces", - "cursors", - "set", - testSpaceId, - "--x", - "250", - "--y", - "350", - "--data", - '{"action":"editing","element":"paragraph-1"}', - ], - import.meta.url, - ); - expect(stdout).toContain("Set cursor in space"); - }); - - it("simulates lock acquisition for collaborative editing", async function () { - const { stdout } = await runCommand( - [ - "spaces", - "locks", - "acquire", - testSpaceId, - "paragraph-1", - "--data", - '{"operation":"edit","timeout":30000}', - ], - import.meta.url, - ); - expect(stdout).toContain("Successfully acquired lock"); - }); - }); - - describe("Real-time state synchronization", function () { - const testSpaceId = "realtime-sync-space"; - - it("tests member presence updates", async function () { - const { stdout } = await runCommand( - [ - "spaces", - "members", - "enter", - testSpaceId, - "--profile", - '{"status":"active","currentTask":"reviewing"}', - ], - import.meta.url, - ); - expect(stdout).toContain("Successfully entered space"); - }); - - it("tests location state synchronization", async function () { - const { stdout } = await runCommand( - [ - "spaces", - "locations", - "set", - testSpaceId, - "--location", - '{"x":500,"y":600,"page":"review","viewport":{"zoom":1.5}}', - ], - import.meta.url, - ); - expect(stdout).toContain("Successfully set location"); - }); - - it("tests cursor state synchronization", async function () { - const { stdout } = await runCommand( - [ - "spaces", - "cursors", - "set", - testSpaceId, - "--x", - "300", - "--y", - "400", - "--data", - '{"isSelecting":true,"selectionStart":{"x":300,"y":400}}', - ], - import.meta.url, - ); - expect(stdout).toContain("Set cursor in space"); - }); - - it("tests lock state synchronization", async function () { - const { stdout } = await runCommand( - [ - "spaces", - "locks", - "acquire", - testSpaceId, - "shared-document", - "--data", - '{"lockType":"exclusive","reason":"formatting"}', - ], - import.meta.url, - ); - expect(stdout).toContain("Successfully acquired lock"); - }); - }); -}); diff --git a/test/integration/core/autocomplete.test.ts b/test/integration/core/autocomplete.test.ts deleted file mode 100644 index c0b32d5b..00000000 --- a/test/integration/core/autocomplete.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { runCommand } from "@oclif/test"; - -describe("autocomplete command", function () { - it("should have autocomplete command available and show instructions", async function () { - const { stdout } = await runCommand(["autocomplete"], import.meta.url); - - expect(stdout).toContain("Setup Instructions"); - expect(stdout).toContain("autocomplete"); - // Should detect the current shell and show relevant instructions - expect(stdout).toMatch(/zsh|bash|powershell/i); - }); - - it("should show bash-specific instructions", async function () { - const { stdout } = await runCommand( - ["autocomplete", "bash"], - import.meta.url, - ); - - expect(stdout).toContain("Setup Instructions"); - expect(stdout).toContain("bash"); - expect(stdout).toContain(".bashrc"); - }); - - it("should show zsh-specific instructions", async function () { - const { stdout } = await runCommand( - ["autocomplete", "zsh"], - import.meta.url, - ); - - expect(stdout).toContain("Setup Instructions"); - expect(stdout).toContain("zsh"); - expect(stdout).toContain(".zshrc"); - }); - - it("should show powershell-specific instructions", async function () { - const { stdout } = await runCommand( - ["autocomplete", "powershell"], - import.meta.url, - ); - - expect(stdout).toContain("Setup Instructions"); - expect(stdout).toContain("powershell"); - }); - - it("should support refresh-cache flag", async function () { - const { stdout, stderr } = await runCommand( - ["autocomplete", "--refresh-cache"], - import.meta.url, - ); - - // The refresh-cache flag causes the cache to be rebuilt - // The stderr output includes "done" when cache building completes - expect(stderr || stdout).toContain("done"); - }); -}); diff --git a/test/integration/core/help.test.ts b/test/integration/core/help.test.ts deleted file mode 100644 index bbc49326..00000000 --- a/test/integration/core/help.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { runCommand } from "@oclif/test"; -import fs from "fs-extra"; -import os from "node:os"; -import path from "node:path"; - -// Helper function to get a temporary config directory -const getTestConfigDir = () => - path.join(os.tmpdir(), `ably-cli-test-${Date.now()}`); - -describe("Help commands integration", function () { - let configDir: string; - let originalConfigDir: string; - - beforeEach(function () { - // Create a temporary directory for config for each test - configDir = getTestConfigDir(); - fs.ensureDirSync(configDir); - - // Store and set config directory - originalConfigDir = process.env.ABLY_CLI_CONFIG_DIR || ""; - process.env.ABLY_CLI_CONFIG_DIR = configDir; - }); - - afterEach(function () { - // Restore original config directory - if (originalConfigDir) { - process.env.ABLY_CLI_CONFIG_DIR = originalConfigDir; - } else { - delete process.env.ABLY_CLI_CONFIG_DIR; - } - - // Clean up the temporary config directory - fs.removeSync(configDir); - }); - - describe("root help command", function () { - it("should show all high-level topics", async function () { - const { stdout, stderr } = await runCommand(["--help"], import.meta.url); - - // Allow warnings in stderr (e.g., version mismatch warnings), otherwise should be empty - expect(!stderr || stderr.includes("Warning:")).toBe(true); - expect(stdout).toContain("USAGE"); - // Check for some core topics - expect(stdout).toContain("ably.com CLI for Pub/Sub"); - expect(stdout).toContain("COMMANDS"); - }); - }); -}); diff --git a/test/integration/test-utils.ts b/test/integration/test-utils.ts deleted file mode 100644 index f53ddc77..00000000 --- a/test/integration/test-utils.ts +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Test utilities for integration tests - * - * This file provides utilities to help with integration testing of the CLI - */ - -import isTestMode from "../../src/utils/test-mode.js"; - -/** - * Gets the mock Ably Rest client from the global test mocks - */ -export function getMockAblyRest(): unknown { - if (!isTestMode() || !globalThis.__TEST_MOCKS__) { - throw new Error("Not running in test mode or test mocks not set up"); - } - return globalThis.__TEST_MOCKS__.ablyRestMock; -} - -/** - * Registers a mock for use in tests - * @param key The key to register the mock under - * @param mock The mock object - */ -export function registerMock(key: string, mock: T): void { - if (!globalThis.__TEST_MOCKS__) { - // Provide a more structured default mock - globalThis.__TEST_MOCKS__ = { - ablyRestMock: { - // Default successful empty list response for Control API usually - request: () => - Promise.resolve({ - statusCode: 200, - items: [], - headers: new Map(), - status: 200, - }), - channels: { - // Fix: make channels.get compatible with expected signature - get: () => ({ - name: "mock-channel", - publish: async () => ({}), // Mock publish - subscribe: () => {}, // Mock subscribe - presence: { - // Mock presence - get: async () => [], - subscribe: () => {}, - enter: async () => ({}), - leave: async () => ({}), - }, - history: async () => ({ items: [] }), // Mock history - attach: async () => ({}), - detach: async () => ({}), - }), - }, - auth: { - // Add required auth object - clientId: "mock-clientId", - requestToken: async () => ({ token: "mock-token" }), - createTokenRequest: async () => ({ keyName: "mock-keyName" }), - }, - options: { - // Add required options object - key: "mock-app.key:secret", - clientId: "mock-clientId", - }, - close: () => {}, - connection: { - // Fix: make connection compatible with base expectations - once: (event: string, cb: () => void) => { - if (event === "connected") setTimeout(cb, 0); - }, - }, - }, - }; - } - // Fix: Add null check before assignment - if (globalThis.__TEST_MOCKS__) { - globalThis.__TEST_MOCKS__[key] = mock; - } -} - -/** - * Gets a registered mock - * @param key The key of the mock to get - */ -export function getMock(key: string): T { - // Check for test mode first - if (!isTestMode()) { - throw new Error("Attempted to get mock outside of test mode"); - } - // Fix: Add proper null checking for globalThis.__TEST_MOCKS__ - if (!globalThis.__TEST_MOCKS__ || !(key in globalThis.__TEST_MOCKS__)) { - throw new Error(`Mock not found for key: ${key}`); - } - // Safe to access now after null check - const mock = globalThis.__TEST_MOCKS__[key]; - if (!mock) { - throw new Error(`Mock value is null or undefined for key: ${key}`); - } - return mock as T; -} diff --git a/test/integration/topic-command-display.test.ts b/test/integration/topic-command-display.test.ts deleted file mode 100644 index b6fbf827..00000000 --- a/test/integration/topic-command-display.test.ts +++ /dev/null @@ -1,249 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { runCommand } from "@oclif/test"; - -async function testTopicFormatting(topic: string) { - const { stdout } = await runCommand([topic], import.meta.url); - - // Should have header - expect(stdout).toMatch(/^Ably .+ commands:$/m); - - // Should have empty line after header - const lines = stdout.split("\n"); - const headerIndex = lines.findIndex(function (line) { - return line.includes("commands:"); - }); - expect(lines[headerIndex + 1]).toBe(""); - - // Should have commands indented with consistent spacing - const commandLines = lines.filter(function (line) { - return line.match(/^\s+ably/); - }); - commandLines.forEach(function (line) { - expect(line).toMatch(/^\s{2}ably/); // Two spaces indent - expect(line).toContain(" - "); // Separator between command and description - }); - - // Should have help text at the end - expect(stdout).toContain(`Run \`ably ${topic} COMMAND --help\``); -} - -describe("topic command display", function () { - describe("accounts topic", function () { - it("should display accounts commands correctly", async function () { - const { stdout } = await runCommand(["accounts"], import.meta.url); - - expect(stdout).toContain("Ably accounts management commands:"); - expect(stdout).toContain("ably accounts login"); - expect(stdout).toContain("ably accounts list"); - expect(stdout).toContain("ably accounts current"); - expect(stdout).toContain("ably accounts logout"); - expect(stdout).toContain("ably accounts switch"); - expect(stdout).toContain("ably accounts stats"); - expect(stdout).toContain("Run `ably accounts COMMAND --help`"); - expect(stdout).not.toContain("Example:"); // Examples only with --help - }); - }); - - describe("apps topic", function () { - it("should display apps commands correctly", async function () { - const { stdout } = await runCommand(["apps"], import.meta.url); - - expect(stdout).toContain("Ably apps management commands:"); - expect(stdout).toContain("ably apps create"); - expect(stdout).toContain("ably apps list"); - expect(stdout).toContain("ably apps update"); - expect(stdout).toContain("ably apps delete"); - expect(stdout).toContain("ably apps channel-rules"); - expect(stdout).toContain("ably apps stats"); - expect(stdout).toContain("ably apps logs"); - expect(stdout).toContain("ably apps switch"); - expect(stdout).toContain("Run `ably apps COMMAND --help`"); - }); - }); - - describe("auth topic", function () { - it("should display auth commands correctly", async function () { - const { stdout } = await runCommand(["auth"], import.meta.url); - - expect(stdout).toContain("Ably authentication commands:"); - expect(stdout).toContain("ably auth keys"); - expect(stdout).toContain("ably auth issue-jwt-token"); - expect(stdout).toContain("ably auth issue-ably-token"); - expect(stdout).toContain("ably auth revoke-token"); - expect(stdout).toContain("Run `ably auth COMMAND --help`"); - }); - }); - - describe("bench topic", function () { - it("should display bench commands correctly", async function () { - const { stdout } = await runCommand(["bench"], import.meta.url); - - expect(stdout).toContain("Ably benchmark testing commands:"); - expect(stdout).toContain("ably bench publisher"); - expect(stdout).toContain("ably bench subscriber"); - expect(stdout).toContain("Run `ably bench COMMAND --help`"); - }); - }); - - describe("channels topic", function () { - it("should display channels commands correctly", async function () { - const { stdout } = await runCommand(["channels"], import.meta.url); - - expect(stdout).toContain("Ably Pub/Sub channel commands:"); - expect(stdout).toContain("ably channels list"); - expect(stdout).toContain("ably channels publish"); - expect(stdout).toContain("ably channels batch-publish"); - expect(stdout).toContain("ably channels subscribe"); - expect(stdout).toContain("ably channels history"); - expect(stdout).toContain("ably channels occupancy"); - expect(stdout).toContain("ably channels presence"); - expect(stdout).toContain("Run `ably channels COMMAND --help`"); - }); - }); - - describe("connections topic", function () { - it("should display connections commands correctly", async function () { - const { stdout } = await runCommand(["connections"], import.meta.url); - - expect(stdout).toContain("Ably Pub/Sub connection commands:"); - expect(stdout).toContain("ably connections stats"); - expect(stdout).toContain("ably connections test"); - expect(stdout).toContain("Run `ably connections COMMAND --help`"); - }); - }); - - describe("integrations topic", function () { - it("should display integrations commands correctly", async function () { - const { stdout } = await runCommand(["integrations"], import.meta.url); - - expect(stdout).toContain("Ably integrations management commands:"); - expect(stdout).toContain("ably integrations list"); - expect(stdout).toContain("ably integrations get"); - expect(stdout).toContain("ably integrations create"); - expect(stdout).toContain("ably integrations update"); - expect(stdout).toContain("ably integrations delete"); - expect(stdout).toContain("Run `ably integrations COMMAND --help`"); - }); - }); - - describe("logs topic", function () { - it("should display logs commands correctly", async function () { - const { stdout } = await runCommand(["logs"], import.meta.url); - - expect(stdout).toContain("Ably logging commands:"); - expect(stdout).toContain("ably logs app"); - expect(stdout).toContain("ably logs channel-lifecycle"); - expect(stdout).toContain("ably logs connection-lifecycle"); - expect(stdout).toContain("ably logs push"); - expect(stdout).toContain("Run `ably logs COMMAND --help`"); - }); - }); - - describe("queues topic", function () { - it("should display queues commands correctly", async function () { - const { stdout } = await runCommand(["queues"], import.meta.url); - - expect(stdout).toContain("Ably queues management commands:"); - expect(stdout).toContain("ably queues list"); - expect(stdout).toContain("ably queues create"); - expect(stdout).toContain("ably queues delete"); - expect(stdout).toContain("Run `ably queues COMMAND --help`"); - }); - }); - - describe("rooms topic", function () { - it("should display rooms commands correctly", async function () { - const { stdout } = await runCommand(["rooms"], import.meta.url); - - expect(stdout).toContain("Ably Chat rooms commands:"); - expect(stdout).toContain("ably rooms list"); - expect(stdout).toContain("ably rooms messages"); - expect(stdout).toContain("ably rooms occupancy"); - expect(stdout).toContain("ably rooms presence"); - expect(stdout).toContain("ably rooms reactions"); - expect(stdout).toContain("ably rooms typing"); - expect(stdout).toContain("Run `ably rooms COMMAND --help`"); - }); - }); - - describe("spaces topic", function () { - it("should display spaces commands correctly", async function () { - const { stdout } = await runCommand(["spaces"], import.meta.url); - - expect(stdout).toContain("Ably Spaces commands:"); - expect(stdout).toContain("ably spaces list"); - expect(stdout).toContain("ably spaces cursors"); - expect(stdout).toContain("ably spaces locations"); - expect(stdout).toContain("ably spaces locks"); - expect(stdout).toContain("ably spaces members"); - expect(stdout).toContain("Run `ably spaces COMMAND --help`"); - }); - }); - - describe("formatting consistency", function () { - it("should have consistent formatting for accounts", async function () { - await expect(testTopicFormatting("accounts")).resolves.toBeUndefined(); - }); - - it("should have consistent formatting for apps", async function () { - await expect(testTopicFormatting("apps")).resolves.toBeUndefined(); - }); - - it("should have consistent formatting for auth", async function () { - await expect(testTopicFormatting("auth")).resolves.toBeUndefined(); - }); - - it("should have consistent formatting for bench", async function () { - await expect(testTopicFormatting("bench")).resolves.toBeUndefined(); - }); - - it("should have consistent formatting for channels", async function () { - await expect(testTopicFormatting("channels")).resolves.toBeUndefined(); - }); - - it("should have consistent formatting for connections", async function () { - await expect(testTopicFormatting("connections")).resolves.toBeUndefined(); - }); - - it("should have consistent formatting for integrations", async function () { - await expect( - testTopicFormatting("integrations"), - ).resolves.toBeUndefined(); - }); - - it("should have consistent formatting for logs", async function () { - await expect(testTopicFormatting("logs")).resolves.toBeUndefined(); - }); - - it("should have consistent formatting for queues", async function () { - await expect(testTopicFormatting("queues")).resolves.toBeUndefined(); - }); - - it("should have consistent formatting for rooms", async function () { - await expect(testTopicFormatting("rooms")).resolves.toBeUndefined(); - }); - - it("should have consistent formatting for spaces", async function () { - await expect(testTopicFormatting("spaces")).resolves.toBeUndefined(); - }); - }); - - describe("hidden commands", function () { - it("should not display hidden commands", async function () { - const { stdout } = await runCommand(["accounts"], import.meta.url); - - // The accounts command should not show any hidden sub-commands - // This test ensures that if any sub-commands are marked as hidden, - // they won't appear in the output - const lines = stdout.split("\n"); - const commandLines = lines.filter(function (line) { - return line.match(/^\s+ably/); - }); - - // All displayed commands should be valid, non-hidden commands - commandLines.forEach(function (line) { - expect(line).toMatch(/^\s{2}ably accounts \w+/); - }); - }); - }); -}); diff --git a/test/integration/core/agent-header.test.ts b/test/unit/base-command/agent-header.test.ts similarity index 96% rename from test/integration/core/agent-header.test.ts rename to test/unit/base-command/agent-header.test.ts index a2922530..b621e49c 100644 --- a/test/integration/core/agent-header.test.ts +++ b/test/unit/base-command/agent-header.test.ts @@ -15,7 +15,7 @@ class TestCommand extends AblyBaseCommand { } } -describe("Agent Header Integration Tests", function () { +describe("Agent Header Unit Tests", function () { beforeEach(function () {}); describe("Ably SDK Agent Header", function () { diff --git a/test/unit/commands/channels/publish.test.ts b/test/unit/commands/channels/publish.test.ts index b02a760d..7e414386 100644 --- a/test/unit/commands/channels/publish.test.ts +++ b/test/unit/commands/channels/publish.test.ts @@ -335,4 +335,230 @@ describe("ChannelsPublish", function () { await expect(command.run()).rejects.toThrow("Invalid JSON"); }); + + describe("transport selection", function () { + it("should use realtime transport by default when publishing multiple messages", async function () { + command.setParseResult({ + flags: { + transport: undefined, // No explicit transport + name: undefined, + encoding: undefined, + count: 3, + delay: 40, + }, + args: { + channel: "test-channel", + message: '{"data":"Message {{.Count}}"}', + }, + argv: [], + raw: [], + }); + + await command.run(); + + // With count > 1 and no explicit transport, should use realtime + expect(mockRealtimePublish).toHaveBeenCalledTimes(3); + expect(mockRestPublish).not.toHaveBeenCalled(); + }); + + it("should respect explicit rest transport flag for multiple messages", async function () { + command.setParseResult({ + flags: { + transport: "rest", + name: undefined, + encoding: undefined, + count: 3, + delay: 0, + }, + args: { + channel: "test-channel", + message: '{"data":"Message {{.Count}}"}', + }, + argv: [], + raw: [], + }); + + await command.run(); + + expect(mockRestPublish).toHaveBeenCalledTimes(3); + expect(mockRealtimePublish).not.toHaveBeenCalled(); + }); + + it("should use rest transport for single message by default", async function () { + command.setParseResult({ + flags: { + transport: undefined, // No explicit transport + name: undefined, + encoding: undefined, + count: 1, + delay: 0, + }, + args: { channel: "test-channel", message: '{"data":"Single message"}' }, + argv: [], + raw: [], + }); + + await command.run(); + + expect(mockRestPublish).toHaveBeenCalledOnce(); + expect(mockRealtimePublish).not.toHaveBeenCalled(); + }); + }); + + describe("message delay and ordering", function () { + it("should publish messages with default 40ms delay", async function () { + const timestamps: number[] = []; + mockRealtimePublish.mockImplementation(async () => { + timestamps.push(Date.now()); + }); + + command.setParseResult({ + flags: { + transport: "realtime", + name: undefined, + encoding: undefined, + count: 3, + delay: 40, + }, + args: { + channel: "test-channel", + message: '{"data":"Message {{.Count}}"}', + }, + argv: [], + raw: [], + }); + + const startTime = Date.now(); + await command.run(); + const totalTime = Date.now() - startTime; + + expect(mockRealtimePublish).toHaveBeenCalledTimes(3); + // Should take at least 80ms (2 delays of 40ms between 3 messages) + expect(totalTime).toBeGreaterThanOrEqual(80); + }); + + it("should respect custom delay value", async function () { + command.setParseResult({ + flags: { + transport: "realtime", + name: undefined, + encoding: undefined, + count: 3, + delay: 100, + }, + args: { + channel: "test-channel", + message: '{"data":"Message {{.Count}}"}', + }, + argv: [], + raw: [], + }); + + const startTime = Date.now(); + await command.run(); + const totalTime = Date.now() - startTime; + + expect(mockRealtimePublish).toHaveBeenCalledTimes(3); + // Should take at least 200ms (2 delays of 100ms between 3 messages) + expect(totalTime).toBeGreaterThanOrEqual(200); + }); + + it("should allow zero delay when explicitly set", async function () { + command.setParseResult({ + flags: { + transport: "realtime", + name: undefined, + encoding: undefined, + count: 3, + delay: 0, + }, + args: { + channel: "test-channel", + message: '{"data":"Message {{.Count}}"}', + }, + argv: [], + raw: [], + }); + + const startTime = Date.now(); + await command.run(); + const totalTime = Date.now() - startTime; + + expect(mockRealtimePublish).toHaveBeenCalledTimes(3); + // With zero delay, should complete quickly (under 50ms accounting for overhead) + expect(totalTime).toBeLessThan(50); + }); + + it("should publish messages in sequential order", async function () { + const publishedData: string[] = []; + mockRealtimePublish.mockImplementation(async (message: any) => { + publishedData.push(message.data); + }); + + command.setParseResult({ + flags: { + transport: "realtime", + name: undefined, + encoding: undefined, + count: 5, + delay: 0, + }, + args: { + channel: "test-channel", + message: '{"data":"Message {{.Count}}"}', + }, + argv: [], + raw: [], + }); + + await command.run(); + + expect(publishedData).toEqual([ + "Message 1", + "Message 2", + "Message 3", + "Message 4", + "Message 5", + ]); + }); + }); + + describe("error handling with multiple messages", function () { + it("should continue publishing remaining messages on error", async function () { + let callCount = 0; + const publishedData: string[] = []; + + mockRealtimePublish.mockImplementation(async (message: any) => { + callCount++; + if (callCount === 3) { + throw new Error("Network error"); + } + publishedData.push(message.data); + }); + + command.setParseResult({ + flags: { + transport: "realtime", + name: undefined, + encoding: undefined, + count: 5, + delay: 0, + }, + args: { + channel: "test-channel", + message: '{"data":"Message {{.Count}}"}', + }, + argv: [], + raw: [], + }); + + await command.run(); + + // Should have attempted all 5, but only 4 succeeded + expect(mockRealtimePublish).toHaveBeenCalledTimes(5); + expect(publishedData).toHaveLength(4); + expect(command.logOutput.join("\n")).toContain("4/5"); + expect(command.logOutput.join("\n")).toContain("1 errors"); + }); + }); }); diff --git a/test/unit/commands/rooms/messages.test.ts b/test/unit/commands/rooms/messages.test.ts index ebd6dca1..a425539d 100644 --- a/test/unit/commands/rooms/messages.test.ts +++ b/test/unit/commands/rooms/messages.test.ts @@ -235,6 +235,111 @@ describe("rooms messages commands", function () { await expect(command.run()).rejects.toThrow("Invalid metadata JSON"); }); + + describe("message delay and ordering", function () { + it("should send messages with default 40ms delay", async function () { + command.setParseResult({ + flags: { count: 3, delay: 40 }, + args: { room: "test-room", text: "Message {{.Count}}" }, + argv: [], + raw: [], + }); + + const startTime = Date.now(); + await command.run(); + const totalTime = Date.now() - startTime; + + expect(sendStub).toHaveBeenCalledTimes(3); + // Should take at least 80ms (2 delays of 40ms between 3 messages) + expect(totalTime).toBeGreaterThanOrEqual(80); + }); + + it("should respect custom delay value", async function () { + command.setParseResult({ + flags: { count: 3, delay: 100 }, + args: { room: "test-room", text: "Message {{.Count}}" }, + argv: [], + raw: [], + }); + + const startTime = Date.now(); + await command.run(); + const totalTime = Date.now() - startTime; + + expect(sendStub).toHaveBeenCalledTimes(3); + // Should take at least 200ms (2 delays of 100ms between 3 messages) + expect(totalTime).toBeGreaterThanOrEqual(200); + }); + + it("should enforce minimum 40ms delay even if lower value specified", async function () { + command.setParseResult({ + flags: { count: 3, delay: 10 }, // Below minimum + args: { room: "test-room", text: "Message {{.Count}}" }, + argv: [], + raw: [], + }); + + const startTime = Date.now(); + await command.run(); + const totalTime = Date.now() - startTime; + + expect(sendStub).toHaveBeenCalledTimes(3); + // Should take at least 80ms (minimum 40ms delay enforced) + expect(totalTime).toBeGreaterThanOrEqual(80); + }); + + it("should send messages in sequential order", async function () { + const sentTexts: string[] = []; + sendStub.mockImplementation(async (message: any) => { + sentTexts.push(message.text); + }); + + command.setParseResult({ + flags: { count: 5, delay: 10 }, + args: { room: "test-room", text: "Message {{.Count}}" }, + argv: [], + raw: [], + }); + + await command.run(); + + expect(sentTexts).toEqual([ + "Message 1", + "Message 2", + "Message 3", + "Message 4", + "Message 5", + ]); + }); + }); + + describe("error handling with multiple messages", function () { + it("should continue sending remaining messages on error", async function () { + let callCount = 0; + const sentTexts: string[] = []; + + sendStub.mockImplementation(async (message: any) => { + callCount++; + if (callCount === 3) { + throw new Error("Network error"); + } + sentTexts.push(message.text); + }); + + command.setParseResult({ + flags: { count: 5, delay: 10 }, + args: { room: "test-room", text: "Message {{.Count}}" }, + argv: [], + raw: [], + }); + + await command.run(); + + // Should have attempted all 5, but only 4 succeeded + expect(sendStub).toHaveBeenCalledTimes(5); + expect(sentTexts).toHaveLength(4); + }); + }); }); describe("rooms messages subscribe", function () {