diff --git a/.github/workflows/sdk_cli_test.yaml b/.github/workflows/sdk_cli_test.yaml index 66259f0f61..3c570d2559 100644 --- a/.github/workflows/sdk_cli_test.yaml +++ b/.github/workflows/sdk_cli_test.yaml @@ -8,7 +8,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest] - python-version: [3.11.6, 3.10.12, 3.12.3, 3.9.1] + python-version: [3.11.6, 3.10.12, 3.12.3, 3.9.19] runs-on: ${{ matrix.os }} steps: @@ -28,7 +28,7 @@ jobs: run: cd core && pip install -r requirements.txt - name: Install dependencies - run: npm install + run: npm install -g pnpm && pnpm install - name: Run tests - run: npm test \ No newline at end of file + run: pnpm test \ No newline at end of file diff --git a/core/composio/composio_cli.py b/core/composio/composio_cli.py index 48176fd618..9db261509f 100755 --- a/core/composio/composio_cli.py +++ b/core/composio/composio_cli.py @@ -310,14 +310,36 @@ def add_integration(args): console.print(f"\n[green]> Adding integration: {integration_name.capitalize()}...[/green]\n") try: - # @TODO: add logic to wait and ask for API_KEY - connection = client.initiate_connection("test-" + integration_name.lower() + "-connector") - webbrowser.open(connection.redirectUrl) - print(f"Please authenticate {integration_name} in the browser and come back here. URL: {connection.redirectUrl}") - spinner = Spinner(DOTS, f"[yellow]⚠[/yellow] Waiting for {integration_name} authentication...") - spinner.start() - connected_account = connection.wait_until_active() - spinner.stop() + app = client.sdk.get_app(args.integration_name) + auth_schemes = app.get('auth_schemes') + auth_schemes_arr = [auth_scheme.get('auth_mode') for auth_scheme in auth_schemes] + if len(auth_schemes_arr) > 1 and auth_schemes_arr[0] == 'API_KEY': + connection = client.initiate_connection("test-" + integration_name.lower() + "-connector") + fields = auth_schemes[0].get('fields') + fields_input = {} + for field in fields: + if field.get('expected_from_customer', True): + if field.get('required', False): + console.print(f"[green]> Enter {field.get('displayName', field.get('name'))}: [/green]", end="") + value = input() or field.get('default') + if not value: # If a required field is not provided and no default is available + console.print(f"[red]Error: {field.get('displayName', field.get('name'))} is required[/red]") + sys.exit(1) + else: + console.print(f"[green]> Enter {field.get('displayName', field.get('name'))} (Optional): [/green]", end="") + value = input() or field.get('default') + fields_input[field.get('name')] = value + + connection.save_user_access_data(fields_input) + else: + # @TODO: add logic to wait and ask for API_KEY + connection = client.initiate_connection("test-" + integration_name.lower() + "-connector") + webbrowser.open(connection.redirectUrl) + print(f"Please authenticate {integration_name} in the browser and come back here. URL: {connection.redirectUrl}") + spinner = Spinner(DOTS, f"[yellow]⚠[/yellow] Waiting for {integration_name} authentication...") + spinner.start() + connected_account = connection.wait_until_active() + spinner.stop() save_user_connection(connected_account.id, integration_name) print("") console.print(f"[green]✔[/green] {integration_name} added successfully!") @@ -342,7 +364,7 @@ def list_connections(args): appName = args.appName console.print(f"\n[green]> Listing connections for: {appName}...[/green]\n") try: - connections = client.get_list_of_connections(appName) + connections = client.get_list_of_connections([appName]) if connections: for connection in connections: console.print(f"[yellow]- {connection['integrationId']} ({connection['status']})[/yellow]") diff --git a/core/composio/sdk/core.py b/core/composio/sdk/core.py index 97a1de8d1f..e6b993e23b 100644 --- a/core/composio/sdk/core.py +++ b/core/composio/sdk/core.py @@ -161,7 +161,8 @@ def get_list_of_connections(self, app_name: list[Union[App, str]] = None) -> lis for i, item in enumerate(app_name): if isinstance(item, App): app_name[i] = item.value - + + resp = [] if app_name is not None: resp = [item for item in resp if item.appUniqueId in app_name] diff --git a/core/composio/sdk/sdk.py b/core/composio/sdk/sdk.py index 29f68e7773..e71c39bdbd 100644 --- a/core/composio/sdk/sdk.py +++ b/core/composio/sdk/sdk.py @@ -28,6 +28,14 @@ def __init__(self, sdk_instance: "Composio", **data): super().__init__(**data) self.sdk_instance = sdk_instance + def save_user_access_data(self, field_inputs: dict): + connected_account_id = self.sdk_instance.get_connected_account(self.connectedAccountId) + resp = self.sdk_instance.http_client.post(f"{self.sdk_instance.base_url}/v1/connectedAccounts", json={ + "integrationId": connected_account_id.integrationId, + "data": field_inputs, + }) + return resp.json() + def wait_until_active( self, timeout=60 ) -> "ConnectedAccount": # Timeout adjusted to seconds @@ -248,6 +256,10 @@ def set_global_trigger(self, callback_url: str): def get_list_of_apps(self): resp = self.http_client.get(f"{self.base_url}/v1/apps") return resp.json() + + def get_app(self, app_name: str): + resp = self.http_client.get(f"{self.base_url}/v1/apps/{app_name}") + return resp.json() def get_list_of_actions( self, apps: list[App] = None, actions: list[Action] = None diff --git a/package.json b/package.json index 98480e4dc8..f70841a699 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,14 @@ "description": "1. Core - To access base APIs 2. Autogen - Use Composio tools with Autogen 3. CrewAI - Use Composio tools with CrewAI 4. Langchain - Use Composio tools with Langchain", "main": "index.js", "scripts": { - "test": "zx test/test.mjs" + "test": "playwright test" }, "author": "", "license": "ISC", "dependencies": { + "@playwright/test": "^1.43.0", "chalk": "^5.3.0", + "playwright": "^1.43.0", "zx": "^8.0.1" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000000..34151516c7 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,33 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + // Look for test files in the "tests" directory, relative to this configuration file. + testDir: 'tests', + + // Run all tests in parallel. + fullyParallel: true, + + // Fail the build on CI if you accidentally left test.only in the source code. + forbidOnly: !!process.env.CI, + + // Retry on CI only. + retries: process.env.CI ? 2 : 0, + + // Opt out of parallel tests on CI. + workers: process.env.CI ? 1 : undefined, + projects: [ + { + name: 'user session management', + testMatch: /global\.setup\.ts/, + teardown: 'logout user session', + }, + { + name: 'logout user session', + testMatch: /global\.teardown\.ts/, + }, + { + name: 'cli', + dependencies: ['user session management'], + }, + ] +}); \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 478f8f7f9b..d297a3045a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,15 +5,29 @@ settings: excludeLinksFromLockfile: false dependencies: + '@playwright/test': + specifier: ^1.43.0 + version: 1.43.0 chalk: specifier: ^5.3.0 version: 5.3.0 + playwright: + specifier: ^1.43.0 + version: 1.43.0 zx: specifier: ^8.0.1 version: 8.0.1 packages: + /@playwright/test@1.43.0: + resolution: {integrity: sha512-Ebw0+MCqoYflop7wVKj711ccbNlrwTBCtjY5rlbiY9kHL2bCYxq+qltK6uPsVBGGAOb033H2VO0YobcQVxoW7Q==} + engines: {node: '>=16'} + hasBin: true + dependencies: + playwright: 1.43.0 + dev: false + /@types/fs-extra@11.0.4: resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} requiresBuild: true @@ -44,6 +58,30 @@ packages: engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} dev: false + /fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /playwright-core@1.43.0: + resolution: {integrity: sha512-iWFjyBUH97+pUFiyTqSLd8cDMMOS0r2ZYz2qEsPjH8/bX++sbIJT35MSwKnp1r/OQBAqC5XO99xFbJ9XClhf4w==} + engines: {node: '>=16'} + hasBin: true + dev: false + + /playwright@1.43.0: + resolution: {integrity: sha512-SiOKHbVjTSf6wHuGCbqrEyzlm6qvXcv7mENP+OZon1I07brfZLGdfWV0l/efAzVx7TF3Z45ov1gPEkku9q25YQ==} + engines: {node: '>=16'} + hasBin: true + dependencies: + playwright-core: 1.43.0 + optionalDependencies: + fsevents: 2.3.2 + dev: false + /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} requiresBuild: true diff --git a/test/test.mjs b/test/test.mjs deleted file mode 100755 index 86e43cd5fa..0000000000 --- a/test/test.mjs +++ /dev/null @@ -1,59 +0,0 @@ -import chalk from "chalk" - -async function saveUserData() { - const userDataPath = `${process.env.HOME}/.composio`; - - // Check if directory exists, delete it if it does - try { - console.log(`rm -rf ${userDataPath}`) - await $`rm -rf ${userDataPath}`; - console.log(`Existing directory '${userDataPath}' deleted successfully.`); - } catch (error) { - console.error(`Error deleting directory '${userDataPath}':`, error); - } - - // Create directory and write file - await $`mkdir -p ${userDataPath}`; - await fs.writeFileSync( - `${userDataPath}/user_data.json`, - JSON.stringify({ "api_key": "3kmtwhffkxvwebhnm7qwzj" }, null, 2) - ); - - // Read file - const data = await fs.readFile(`${userDataPath}/user_data.json`, 'utf8'); - console.log('user_data.json created successfully.', data); - - console.log(chalk.green("Saving user data")); -} - -await saveUserData(); - - -async function runPythonCli(){ - - - console.log("Running ", chalk.green(`whoami`)); - let data = await $`python3 core/start_cli.py whoami`; - console.log(data.stdout); - - console.log("Running ", chalk.green(`show-apps`)); - data = await $`python3 core/start_cli.py show-apps`; - console.log(data.stdout); - - - console.log("Running ", chalk.green(`show-connections github`)); - data = await $`python3 core/start_cli.py show-connections github`; - console.log(data.stdout); - - console.log("Running ", chalk.green(`list-triggers github`)); - data = await $`python3 core/start_cli.py list-triggers github`; - console.log(data.stdout); - - console.log("Running ", chalk.green(`list-triggers logout`)); - data = await $`python3 core/start_cli.py logout`; - console.log(data.stdout); - -} - - -await runPythonCli() \ No newline at end of file diff --git a/tests/global.setup.ts b/tests/global.setup.ts new file mode 100644 index 0000000000..f69852f674 --- /dev/null +++ b/tests/global.setup.ts @@ -0,0 +1,25 @@ +import { test as setup, expect } from '@playwright/test'; +import { execSync } from 'child_process'; +import fs from "fs"; + +const userDataPath = `${process.env.HOME}/.composio`; + +setup('user session management', async ({ }) => { + // Check if directory exists, delete it if it does + if (fs.existsSync(userDataPath)) { + execSync(`rm -rf ${userDataPath}`); + console.log(`Existing directory '${userDataPath}' deleted successfully.`); + } + + // Create directory and write file + execSync(`mkdir -p ${userDataPath}`); + fs.writeFileSync( + `${userDataPath}/user_data.json`, + JSON.stringify({ "api_key": "3kmtwhffkxvwebhnm7qwzj" }, null, 2) + ); + + // Read file and verify content + const data = fs.readFileSync(`${userDataPath}/user_data.json`, 'utf8'); + expect(data).toContain('3kmtwhffkxvwebhnm7qwzj'); + console.log('user_data.json created and verified successfully.'); +}); \ No newline at end of file diff --git a/tests/global.teardown.ts b/tests/global.teardown.ts new file mode 100644 index 0000000000..3d0f1e80fe --- /dev/null +++ b/tests/global.teardown.ts @@ -0,0 +1,8 @@ +import { test as teardown, expect } from '@playwright/test'; +import { execSync } from 'child_process'; + +teardown('logout user session', async ({ }) => { + const output = execSync(`python3 core/start_cli.py logout`).toString(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + expect(output).not.toBeNull(); +}); \ No newline at end of file diff --git a/tests/test.spec.ts b/tests/test.spec.ts new file mode 100755 index 0000000000..8440600876 --- /dev/null +++ b/tests/test.spec.ts @@ -0,0 +1,21 @@ +import { test, expect } from '@playwright/test'; +import fs from 'fs'; +import { execSync } from 'child_process'; + +test.describe('Python CLI Operations', () => { + const commands = [ + { command: 'whoami', description: 'Running whoami' }, + { command: 'show-apps', description: 'Running show-apps' }, + { command: 'show-connections github', description: 'Running show-connections github' }, + { command: 'list-triggers github', description: 'Running list-triggers github' }, + ]; + + commands.forEach(({ command, description }) => { + test(description, async () => { + const output = execSync(`python3 core/start_cli.py ${command}`).toString(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + console.log(description + ':', output); + expect(output).not.toBeNull(); + }); + }); +}); \ No newline at end of file