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
8 changes: 4 additions & 4 deletions tests/integration/tools/atlas/accessLists.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describeWithAtlas, withProject } from "./atlasHelpers.js";
import { afterAllWithRetry, beforeAllWithRetry, describeWithAtlas, withProject } from "./atlasHelpers.js";
import { expectDefined, getResponseElements } from "../../helpers.js";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { describe, expect, it } from "vitest";
import { ensureCurrentIpInAccessList } from "../../../../src/common/atlas/accessListUtils.js";

function generateRandomIp(): string {
Expand All @@ -17,13 +17,13 @@ describeWithAtlas("ip access lists", (integration) => {
const cidrBlocks = [generateRandomIp() + "/16", generateRandomIp() + "/24"];
const values = [...ips, ...cidrBlocks];

beforeAll(async () => {
beforeAllWithRetry(async () => {
const apiClient = integration.mcpServer().session.apiClient;
const ipInfo = await apiClient.getIpInfo();
values.push(ipInfo.currentIpv4Address);
});

afterAll(async () => {
afterAllWithRetry(async () => {
const apiClient = integration.mcpServer().session.apiClient;

const projectId = getProjectId();
Expand Down
100 changes: 89 additions & 11 deletions tests/integration/tools/atlas/atlasHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { ApiClient } from "../../../../src/common/atlas/apiClient.js";
import type { IntegrationTest } from "../../helpers.js";
import { setupIntegrationTest, defaultTestConfig, defaultDriverOptions } from "../../helpers.js";
import type { SuiteCollector } from "vitest";
import { afterAll, beforeAll, describe } from "vitest";
import { beforeAll, afterAll, describe, it } from "vitest";

export type IntegrationTestFunction = (integration: IntegrationTest) => void;

Expand Down Expand Up @@ -36,19 +36,13 @@ export function withProject(integration: IntegrationTest, fn: ProjectTestFunctio
return describe("with project", () => {
let projectId: string = "";

beforeAll(async () => {
beforeAllWithRetry(async () => {
const apiClient = integration.mcpServer().session.apiClient;

try {
const group = await createProject(apiClient);
projectId = group.id;
} catch (error) {
console.error("Failed to create project:", error);
throw error;
}
const group = await createProject(apiClient);
projectId = group.id;
});

afterAll(async () => {
afterAllWithRetry(async () => {
const apiClient = integration.mcpServer().session.apiClient;
if (projectId) {
// projectId may be empty if beforeAll failed.
Expand All @@ -70,6 +64,90 @@ export function withProject(integration: IntegrationTest, fn: ProjectTestFunctio
});
}

const MAX_ATLAS_STEP_ATTEMPTS = 10;
const SETUP_BACKOFF_MS = 10;

export function beforeAllWithRetry(fixture: () => Promise<void>): void {
beforeAll(async () => {
let lastError: Error | undefined = undefined;

for (let attempt = 0; attempt < MAX_ATLAS_STEP_ATTEMPTS; attempt++) {
try {
await fixture();
lastError = undefined;
break;
} catch (error: unknown) {
if (error instanceof Error) {
lastError = error;
} else {
lastError = new Error(String(error));
}

console.error("beforeAll(attempt:", attempt, "):", error);
await new Promise((resolve) => setTimeout(resolve, SETUP_BACKOFF_MS * attempt));
}
}

if (lastError) {
throw lastError;
}
});
}

export function afterAllWithRetry(fixture: () => Promise<void>): void {
afterAll(async () => {
let lastError: Error | undefined = undefined;

for (let attempt = 0; attempt < MAX_ATLAS_STEP_ATTEMPTS; attempt++) {
try {
await fixture();
lastError = undefined;
break;
} catch (error) {
if (error instanceof Error) {
lastError = error;
} else {
lastError = new Error(String(error));
}
console.error("afterAll(attempt:", attempt, "):", error);
await new Promise((resolve) => setTimeout(resolve, SETUP_BACKOFF_MS * attempt));
}
}

if (lastError) {
throw lastError;
}
});
}

export function itWithRetry(name: string, test: () => Promise<void>): void {
// complains about not having assertions, but assertions are inside the test function
// eslint-disable-next-line
it(name, async () => {
let lastError: Error | undefined = undefined;

for (let attempt = 0; attempt < MAX_ATLAS_STEP_ATTEMPTS; attempt++) {
try {
await test();
lastError = undefined;
break;
} catch (error) {
if (error instanceof Error) {
lastError = error;
} else {
lastError = new Error(String(error));
}
console.error(`${name} (attempt: ${attempt}):`, error);
await new Promise((resolve) => setTimeout(resolve, SETUP_BACKOFF_MS * attempt));
}
}

if (lastError) {
throw lastError;
}
});
}

export function parseTable(text: string): Record<string, string>[] {
const data = text
.split("\n")
Expand Down
8 changes: 4 additions & 4 deletions tests/integration/tools/atlas/clusters.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Session } from "../../../../src/common/session.js";
import { expectDefined, getResponseElements } from "../../helpers.js";
import { describeWithAtlas, withProject, randomId } from "./atlasHelpers.js";
import { describeWithAtlas, withProject, randomId, afterAllWithRetry, beforeAllWithRetry } from "./atlasHelpers.js";
import type { ClusterDescription20240805 } from "../../../../src/common/atlas/openapi.js";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { describe, expect, it } from "vitest";

function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
Expand Down Expand Up @@ -60,7 +60,7 @@ describeWithAtlas("clusters", (integration) => {
withProject(integration, ({ getProjectId }) => {
const clusterName = "ClusterTest-" + randomId;

afterAll(async () => {
afterAllWithRetry(async () => {
const projectId = getProjectId();
if (projectId) {
const session: Session = integration.mcpServer().session;
Expand Down Expand Up @@ -160,7 +160,7 @@ describeWithAtlas("clusters", (integration) => {
});

describe("atlas-connect-cluster", () => {
beforeAll(async () => {
beforeAllWithRetry(async () => {
const projectId = getProjectId();
await waitCluster(integration.mcpServer().session, projectId, clusterName, (cluster) => {
return (
Expand Down
15 changes: 7 additions & 8 deletions tests/integration/tools/atlas/projects.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { ObjectId } from "mongodb";
import { parseTable, describeWithAtlas } from "./atlasHelpers.js";
import { parseTable, describeWithAtlas, afterAllWithRetry, itWithRetry } from "./atlasHelpers.js";
import { expectDefined, getDataFromUntrustedContent, getResponseElements } from "../../helpers.js";
import { afterAll, describe, expect, it } from "vitest";
import { describe, expect } from "vitest";

const randomId = new ObjectId().toString();

describeWithAtlas("projects", (integration) => {
const projName = "testProj-" + randomId;

afterAll(async () => {
afterAllWithRetry(async () => {
const session = integration.mcpServer().session;

const projects = await session.apiClient.listProjects();
Expand All @@ -27,7 +27,7 @@
});

describe("atlas-create-project", () => {
it("should have correct metadata", async () => {
itWithRetry("should have correct metadata", async () => {
const { tools } = await integration.mcpClient().listTools();
const createProject = tools.find((tool) => tool.name === "atlas-create-project");
expectDefined(createProject);
Expand All @@ -36,7 +36,7 @@
expect(createProject.inputSchema.properties).toHaveProperty("projectName");
expect(createProject.inputSchema.properties).toHaveProperty("organizationId");
});
it("should create a project", async () => {
itWithRetry("should create a project", async () => {
const response = await integration.mcpClient().callTool({
name: "atlas-create-project",
arguments: { projectName: projName },
Expand All @@ -44,11 +44,11 @@

const elements = getResponseElements(response);
expect(elements).toHaveLength(1);
expect(elements[0]?.text).toContain(projName);

Check failure on line 47 in tests/integration/tools/atlas/projects.test.ts

View workflow job for this annotation

GitHub Actions / Run Atlas tests

tests/integration/tools/atlas/projects.test.ts > projects > atlas-create-project > should create a project

AssertionError: expected 'Error running atlas-create-project: C…' to contain 'testProj-68b1c4c8feb41ab8359f6547' Expected: "testProj-68b1c4c8feb41ab8359f6547" Received: "Error running atlas-create-project: Could not search for organizations in your MongoDB Atlas account, please provide an organization ID or create one first." ❯ tests/integration/tools/atlas/projects.test.ts:47:39 ❯ tests/integration/tools/atlas/atlasHelpers.ts:131:17
});
});
describe("atlas-list-projects", () => {
it("should have correct metadata", async () => {
itWithRetry("should have correct metadata", async () => {
const { tools } = await integration.mcpClient().listTools();
const listProjects = tools.find((tool) => tool.name === "atlas-list-projects");
expectDefined(listProjects);
Expand All @@ -57,11 +57,10 @@
expect(listProjects.inputSchema.properties).toHaveProperty("orgId");
});

it("returns project names", async () => {
itWithRetry("returns project names", async () => {
const response = await integration.mcpClient().callTool({ name: "atlas-list-projects", arguments: {} });
const elements = getResponseElements(response);
expect(elements).toHaveLength(2);
expect(elements[0]?.text).toMatch(/Found \d+ projects/);

Check failure on line 63 in tests/integration/tools/atlas/projects.test.ts

View workflow job for this annotation

GitHub Actions / Run Atlas tests

tests/integration/tools/atlas/projects.test.ts > projects > atlas-list-projects > returns project names

AssertionError: expected 'Unable to authenticate with MongoDB A…' to match /Found \d+ projects/ - Expected: /Found \d+ projects/ + Received: "Unable to authenticate with MongoDB Atlas, API error: [401 Unauthorized] error calling Atlas API: Unauthorized; You are not authorized for this resource. Hint: Your API credentials may be invalid, expired or lack permissions. Please check your Atlas API credentials and ensure they have the appropriate permissions. For more information on setting up API keys, visit: https://www.mongodb.com/docs/atlas/configure-api-access/" ❯ tests/integration/tools/atlas/projects.test.ts:63:39 ❯ tests/integration/tools/atlas/atlasHelpers.ts:131:17
expect(elements[1]?.text).toContain("<untrusted-user-data-");
expect(elements[1]?.text).toContain(projName);
const data = parseTable(getDataFromUntrustedContent(elements[1]?.text ?? ""));
Expand Down
Loading