Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions integ-tests/add-remove-gateway.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { createTestProject, runCLI } from '../src/test-utils/index.js';
import type { TestProject } from '../src/test-utils/index.js';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

async function readMcpConfig(projectPath: string) {
return JSON.parse(await readFile(join(projectPath, 'agentcore/mcp.json'), 'utf-8'));
}

describe('integration: add and remove gateway with external MCP server', () => {
let project: TestProject;
const gatewayName = 'ExaGateway';
const targetName = 'ExaSearch';

beforeAll(async () => {
project = await createTestProject({ noAgent: true });
});

afterAll(async () => {
await project.cleanup();
});

describe('gateway lifecycle', () => {
it('adds a gateway', async () => {
const result = await runCLI(['add', 'gateway', '--name', gatewayName, '--json'], project.projectPath);

expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);

const mcpSpec = await readMcpConfig(project.projectPath);
const gateway = mcpSpec.agentCoreGateways?.find((g: { name: string }) => g.name === gatewayName);
expect(gateway, `Gateway "${gatewayName}" should be in mcp.json`).toBeTruthy();
expect(gateway.authorizerType).toBe('NONE');
});

it('adds an external MCP server target to the gateway', async () => {
const result = await runCLI(
[
'add',
'gateway-target',
'--name',
targetName,
'--endpoint',
'https://mcp.exa.ai/mcp',
'--gateway',
gatewayName,
'--json',
],
project.projectPath
);

expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);

const mcpSpec = await readMcpConfig(project.projectPath);
const gateway = mcpSpec.agentCoreGateways?.find((g: { name: string }) => g.name === gatewayName);
const target = gateway?.targets?.find((t: { name: string }) => t.name === targetName);
expect(target, `Target "${targetName}" should be in gateway targets`).toBeTruthy();
});

it('removes the gateway target', async () => {
const result = await runCLI(['remove', 'gateway-target', '--name', targetName, '--json'], project.projectPath);

expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);

const mcpSpec = await readMcpConfig(project.projectPath);
const gateway = mcpSpec.agentCoreGateways?.find((g: { name: string }) => g.name === gatewayName);
const targets = gateway?.targets ?? [];
const found = targets.find((t: { name: string }) => t.name === targetName);
expect(found, `Target "${targetName}" should be removed`).toBeFalsy();
});

it('removes the gateway', async () => {
const result = await runCLI(['remove', 'gateway', '--name', gatewayName, '--json'], project.projectPath);

expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);

const mcpSpec = await readMcpConfig(project.projectPath);
const gateways = mcpSpec.agentCoreGateways ?? [];
const found = gateways.find((g: { name: string }) => g.name === gatewayName);
expect(found, `Gateway "${gatewayName}" should be removed`).toBeFalsy();
});
});
});
34 changes: 34 additions & 0 deletions src/cli/commands/add/__tests__/actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@ import { afterEach, describe, expect, it, vi } from 'vitest';

const mockCreateToolFromWizard = vi.fn().mockResolvedValue({ toolName: 'test', projectPath: '/tmp' });
const mockCreateExternalGatewayTarget = vi.fn().mockResolvedValue({ toolName: 'test', projectPath: '' });
const mockCreateCredential = vi.fn().mockResolvedValue(undefined);

vi.mock('../../../operations/mcp/create-mcp', () => ({
createToolFromWizard: (...args: unknown[]) => mockCreateToolFromWizard(...args),
createExternalGatewayTarget: (...args: unknown[]) => mockCreateExternalGatewayTarget(...args),
createGatewayFromWizard: vi.fn(),
}));

vi.mock('../../../operations/identity/create-identity', () => ({
createCredential: (...args: unknown[]) => mockCreateCredential(...args),
computeDefaultCredentialEnvVarName: vi.fn(),
resolveCredentialStrategy: vi.fn(),
}));

describe('buildGatewayTargetConfig', () => {
it('maps name, gateway, language correctly', () => {
const options: ValidatedAddGatewayTargetOptions = {
Expand Down Expand Up @@ -97,4 +104,31 @@ describe('handleAddGatewayTarget', () => {
expect(mockCreateExternalGatewayTarget).toHaveBeenCalledOnce();
expect(mockCreateToolFromWizard).not.toHaveBeenCalled();
});

it('auto-creates OAuth credential when inline fields provided', async () => {
const options: ValidatedAddGatewayTargetOptions = {
name: 'my-tool',
language: 'Other',
host: 'Lambda',
source: 'existing-endpoint',
endpoint: 'https://example.com/mcp',
gateway: 'my-gw',
oauthClientId: 'cid',
oauthClientSecret: 'csec',
oauthDiscoveryUrl: 'https://auth.example.com',
oauthScopes: 'read,write',
};

await handleAddGatewayTarget(options);

expect(mockCreateCredential).toHaveBeenCalledWith({
type: 'OAuthCredentialProvider',
name: 'my-tool-oauth',
discoveryUrl: 'https://auth.example.com',
clientId: 'cid',
clientSecret: 'csec',
scopes: ['read', 'write'],
});
expect(options.credentialName).toBe('my-tool-oauth');
});
});
102 changes: 19 additions & 83 deletions src/cli/commands/add/__tests__/add-gateway-target.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

// Gateway Target feature is disabled (coming soon) - skip all tests
describe.skip('add gateway-target command', () => {
describe('add gateway-target command', () => {
let testDir: string;
let projectDir: string;
const gatewayName = 'test-gateway';
Expand All @@ -22,6 +21,12 @@ describe.skip('add gateway-target command', () => {
throw new Error(`Failed to create project: ${result.stdout} ${result.stderr}`);
}
projectDir = join(testDir, projectName);

// Create gateway for tests
const gwResult = await runCLI(['add', 'gateway', '--name', gatewayName, '--json'], projectDir);
if (gwResult.exitCode !== 0) {
throw new Error(`Failed to create gateway: ${gwResult.stdout} ${gwResult.stderr}`);
}
});

afterAll(async () => {
Expand All @@ -37,53 +42,31 @@ describe.skip('add gateway-target command', () => {
expect(json.error.includes('--name'), `Error: ${json.error}`).toBeTruthy();
});

it('validates language', async () => {
it('requires endpoint', async () => {
const result = await runCLI(
['add', 'gateway-target', '--name', 'test', '--language', 'InvalidLang', '--json'],
['add', 'gateway-target', '--name', 'noendpoint', '--gateway', gatewayName, '--json'],
projectDir
);
expect(result.exitCode).toBe(1);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(false);
expect(
json.error.toLowerCase().includes('invalid') || json.error.toLowerCase().includes('valid options'),
`Error should mention invalid language: ${json.error}`
).toBeTruthy();
});

it('accepts Other as valid language option', async () => {
const result = await runCLI(
['add', 'gateway-target', '--name', 'container-tool', '--language', 'Other', '--json'],
projectDir
);

// Should fail with "not yet supported" error, not validation error
expect(result.exitCode).toBe(1);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(false);
expect(
json.error.toLowerCase().includes('not yet supported') || json.error.toLowerCase().includes('other'),
`Error should mention Other not supported: ${json.error}`
).toBeTruthy();
expect(json.error.includes('--endpoint'), `Error: ${json.error}`).toBeTruthy();
});
});

// Gateway disabled - skip behind-gateway tests until gateway feature is enabled
describe.skip('behind-gateway', () => {
it('creates behind-gateway tool', async () => {
const toolName = `gwtool${Date.now()}`;
describe('existing-endpoint', () => {
it('adds existing-endpoint target to gateway', async () => {
const targetName = `target${Date.now()}`;
const result = await runCLI(
[
'add',
'gateway-target',
'--name',
toolName,
'--language',
'Python',
targetName,
'--endpoint',
'https://mcp.exa.ai/mcp',
'--gateway',
gatewayName,
'--host',
'Lambda',
'--json',
],
projectDir
Expand All @@ -92,59 +75,12 @@ describe.skip('add gateway-target command', () => {
expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);
expect(json.toolName).toBe(toolName);

// Verify in mcp.json gateway targets
// Verify in mcp.json
const mcpSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/mcp.json'), 'utf-8'));
const gateway = mcpSpec.agentCoreGateways.find((g: { name: string }) => g.name === gatewayName);
const target = gateway?.targets?.find((t: { name: string }) => t.name === toolName);
expect(target, 'Tool should be in gateway targets').toBeTruthy();
});

it('requires gateway for behind-gateway', async () => {
const result = await runCLI(
['add', 'gateway-target', '--name', 'no-gw', '--language', 'Python', '--host', 'Lambda', '--json'],
projectDir
);
expect(result.exitCode).toBe(1);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(false);
expect(json.error.includes('--gateway'), `Error: ${json.error}`).toBeTruthy();
});

it('requires host for behind-gateway', async () => {
const result = await runCLI(
['add', 'gateway-target', '--name', 'no-host', '--language', 'Python', '--gateway', gatewayName, '--json'],
projectDir
);
expect(result.exitCode).toBe(1);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(false);
expect(json.error.includes('--host'), `Error: ${json.error}`).toBeTruthy();
});

it('returns clear error for Other language with behind-gateway', async () => {
const result = await runCLI(
[
'add',
'gateway-target',
'--name',
'gateway-container',
'--language',
'Other',
'--gateway',
gatewayName,
'--host',
'Lambda',
'--json',
],
projectDir
);

expect(result.exitCode).toBe(1);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(false);
expect(json.error.length > 0, 'Should have error message').toBeTruthy();
const target = gateway?.targets?.find((t: { name: string }) => t.name === targetName);
expect(target, 'Target should be in gateway targets').toBeTruthy();
});
});
});
3 changes: 1 addition & 2 deletions src/cli/commands/add/__tests__/add-gateway.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

// Gateway disabled - skip until gateway feature is enabled
describe.skip('add gateway command', () => {
describe('add gateway command', () => {
let testDir: string;
let projectDir: string;

Expand Down
Loading
Loading