Skip to content

chore: add atlas tests #83

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
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
23 changes: 23 additions & 0 deletions .github/workflows/atlas_tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
name: Atlas Tests
on:
schedule:
- cron: "0 7 * * 1-5" # Every week day at 7am UTC
jobs:
run-tests-daily:
runs-on: ubuntu-latest
steps:
- uses: GitHubSecurityLab/actions-permissions/monitor@v1
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: package.json
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run tests
env:
MDB_MCP_API_CLIENT_ID: ${{ secrets.TEST_ATLAS_CLIENT_ID }}
MDB_MCP_API_CLIENT_SECRET: ${{ secrets.TEST_ATLAS_CLIENT_SECRET }}
MDB_MCP_API_BASE_URL: ${{ vars.TEST_ATLAS_BASE_URL }}
run: npm test
20 changes: 14 additions & 6 deletions .github/workflows/code_health.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,20 @@ jobs:
- name: Run style check
run: npm run check

check-generate:
runs-on: ubuntu-latest
steps:
- uses: GitHubSecurityLab/actions-permissions/monitor@v1
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: package.json
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run style check
run: npm run generate

run-tests:
strategy:
matrix:
Expand All @@ -29,12 +43,6 @@ jobs:
steps:
- uses: GitHubSecurityLab/actions-permissions/monitor@v1
if: matrix.os != 'windows-latest'
- name: Install keyring deps on Ubuntu
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt update -y
sudo apt install -y gnome-keyring libdbus-1-dev

- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
Expand Down
16 changes: 15 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@
"ts-jest": "^29.3.1",
"tsx": "^4.19.3",
"typescript": "^5.8.2",
"typescript-eslint": "^8.29.1"
"typescript-eslint": "^8.29.1",
"yaml": "^2.7.1"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.8.0",
Expand Down
67 changes: 50 additions & 17 deletions scripts/apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@ import fs from "fs/promises";
import { OpenAPIV3_1 } from "openapi-types";
import argv from "yargs-parser";

function findParamFromRef(ref: string, openapi: OpenAPIV3_1.Document): OpenAPIV3_1.ParameterObject {
function findObjectFromRef<T>(obj: T | OpenAPIV3_1.ReferenceObject, openapi: OpenAPIV3_1.Document): T {
const ref = (obj as OpenAPIV3_1.ReferenceObject).$ref;
if (ref === undefined) {
return obj as T;
}
const paramParts = ref.split("/");
paramParts.shift(); // Remove the first part which is always '#'
let param: any = openapi; // eslint-disable-line @typescript-eslint/no-explicit-any
let foundObj: any = openapi; // eslint-disable-line @typescript-eslint/no-explicit-any
while (true) {
const part = paramParts.shift();
if (!part) {
break;
}
param = param[part];
foundObj = foundObj[part];
}
return param;
return foundObj as T;
}

async function main() {
Expand All @@ -32,6 +36,7 @@ async function main() {
operationId: string;
requiredParams: boolean;
tag: string;
hasResponseBody: boolean;
}[] = [];

const openapi = JSON.parse(specFile) as OpenAPIV3_1.Document;
Expand All @@ -44,13 +49,27 @@ async function main() {
}

let requiredParams = !!operation.requestBody;
let hasResponseBody = false;
for(const code in operation.responses) {
try {
const httpCode = parseInt(code, 10);
if (httpCode >= 200 && httpCode < 300) {
const response = operation.responses[code];
const responseObject = findObjectFromRef(response, openapi);
if (responseObject.content) {
for (const contentType in responseObject.content) {
const content = responseObject.content[contentType];
hasResponseBody = !!content.schema;
}
}
}
} catch {
continue;
}
}

for (const param of operation.parameters || []) {
const ref = (param as OpenAPIV3_1.ReferenceObject).$ref as string | undefined;
let paramObject: OpenAPIV3_1.ParameterObject = param as OpenAPIV3_1.ParameterObject;
if (ref) {
paramObject = findParamFromRef(ref, openapi);
}
const paramObject = findObjectFromRef(param, openapi);
if (paramObject.in === "path") {
requiredParams = true;
}
Expand All @@ -61,27 +80,41 @@ async function main() {
method: method.toUpperCase(),
operationId: operation.operationId || "",
requiredParams,
hasResponseBody,
tag: operation.tags[0],
});
}
}

const operationOutput = operations
.map((operation) => {
const { operationId, method, path, requiredParams } = operation;
const { operationId, method, path, requiredParams, hasResponseBody } = operation;
return `async ${operationId}(options${requiredParams ? "" : "?"}: FetchOptions<operations["${operationId}"]>) {
const { data } = await this.client.${method}("${path}", options);
return data;
}
${hasResponseBody ? `const { data } = ` : ``}await this.client.${method}("${path}", options);
${hasResponseBody ? `return data;
` : ``}}
`;
})
.join("\n");

const templateFile = (await fs.readFile(file, "utf8")) as string;
const output = templateFile.replace(
/\/\/ DO NOT EDIT\. This is auto-generated code\.\n.*\/\/ DO NOT EDIT\. This is auto-generated code\./g,
operationOutput
);
const templateLines = templateFile.split("\n");
let outputLines: string[] = [];
let addLines = true;
for(const line of templateLines) {
if (line.includes("DO NOT EDIT. This is auto-generated code.")) {
addLines = !addLines;
outputLines.push(line);
if (!addLines) {
outputLines.push(operationOutput);
}
continue;
}
if (addLines) {
outputLines.push(line);
}
}
const output = outputLines.join("\n");

await fs.writeFile(file, output, "utf8");
}
Expand Down
3 changes: 3 additions & 0 deletions scripts/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@ function filterOpenapi(openapi: OpenAPIV3_1.Document): OpenAPIV3_1.Document {
"listClusters",
"getCluster",
"createCluster",
"deleteCluster",
"listClustersForAllProjects",
"createDatabaseUser",
"deleteDatabaseUser",
"listDatabaseUsers",
"listProjectIpAccessLists",
"createProjectIpAccessList",
"deleteProjectIpAccessList",
];

const filteredPaths = {};
Expand Down
2 changes: 1 addition & 1 deletion scripts/generate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
set -Eeou pipefail

curl -Lo ./scripts/spec.json https://github.com/mongodb/openapi/raw/refs/heads/main/openapi/v2/openapi-2025-03-12.json
tsx ./scripts/filter.ts > ./scripts/filteredSpec.json < ./scripts/spec.json
tsx --debug-port 5858 ./scripts/filter.ts > ./scripts/filteredSpec.json < ./scripts/spec.json
redocly bundle --ext json --remove-unused-components ./scripts/filteredSpec.json --output ./scripts/bundledSpec.json
openapi-typescript ./scripts/bundledSpec.json --root-types-no-schema-prefix --root-types --output ./src/common/atlas/openapi.d.ts
tsx ./scripts/apply.ts --spec ./scripts/bundledSpec.json --file ./src/common/atlas/apiClient.ts
Expand Down
13 changes: 13 additions & 0 deletions src/common/atlas/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ export class ApiClient {
return data;
}

async deleteProjectIpAccessList(options: FetchOptions<operations["deleteProjectIpAccessList"]>) {
await this.client.DELETE("/api/atlas/v2/groups/{groupId}/accessList/{entryValue}", options);
}

async listClusters(options: FetchOptions<operations["listClusters"]>) {
const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}/clusters", options);
return data;
Expand All @@ -157,6 +161,10 @@ export class ApiClient {
return data;
}

async deleteCluster(options: FetchOptions<operations["deleteCluster"]>) {
await this.client.DELETE("/api/atlas/v2/groups/{groupId}/clusters/{clusterName}", options);
}

async getCluster(options: FetchOptions<operations["getCluster"]>) {
const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}/clusters/{clusterName}", options);
return data;
Expand All @@ -171,5 +179,10 @@ export class ApiClient {
const { data } = await this.client.POST("/api/atlas/v2/groups/{groupId}/databaseUsers", options);
return data;
}

async deleteDatabaseUser(options: FetchOptions<operations["deleteDatabaseUser"]>) {
await this.client.DELETE("/api/atlas/v2/groups/{groupId}/databaseUsers/{databaseName}/{username}", options);
}

// DO NOT EDIT. This is auto-generated code.
}
Loading
Loading