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 13 commits into
base: main
Choose a base branch
from
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
71 changes: 54 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,45 @@ 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
5 changes: 5 additions & 0 deletions scripts/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,19 @@ function filterOpenapi(openapi: OpenAPIV3_1.Document): OpenAPIV3_1.Document {
"listOrganizations",
"getProject",
"createProject",
"deleteProject",
"listClusters",
"getCluster",
"createCluster",
"deleteCluster",
"listClustersForAllProjects",
"createDatabaseUser",
"deleteDatabaseUser",
"listDatabaseUsers",
"listProjectIpAccessLists",
"createProjectIpAccessList",
"deleteProjectIpAccessList",
"listOrganizationProjects",
];

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
46 changes: 34 additions & 12 deletions src/common/atlas/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ export class ApiClient {
}

// DO NOT EDIT. This is auto-generated code.
async listOrganizations(options?: FetchOptions<operations["listOrganizations"]>) {
const { data } = await this.client.GET("/api/atlas/v2/orgs", options);
async listClustersForAllProjects(options?: FetchOptions<operations["listClustersForAllProjects"]>) {
const { data } = await this.client.GET("/api/atlas/v2/clusters", options);
return data;
}

Expand All @@ -132,16 +132,29 @@ export class ApiClient {
return data;
}

async listClustersForAllProjects(options?: FetchOptions<operations["listClustersForAllProjects"]>) {
const { data } = await this.client.GET("/api/atlas/v2/clusters", options);
return data;
async deleteProject(options: FetchOptions<operations["deleteProject"]>) {
await this.client.DELETE("/api/atlas/v2/groups/{groupId}", options);
}

async getProject(options: FetchOptions<operations["getProject"]>) {
const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}", options);
return data;
}

async listProjectIpAccessLists(options: FetchOptions<operations["listProjectIpAccessLists"]>) {
const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}/accessList", options);
return data;
}

async createProjectIpAccessList(options: FetchOptions<operations["createProjectIpAccessList"]>) {
const { data } = await this.client.POST("/api/atlas/v2/groups/{groupId}/accessList", options);
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 @@ -152,13 +165,12 @@ export class ApiClient {
return data;
}

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

async createProjectIpAccessList(options: FetchOptions<operations["createProjectIpAccessList"]>) {
const { data } = await this.client.POST("/api/atlas/v2/groups/{groupId}/accessList", 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 @@ -172,9 +184,19 @@ export class ApiClient {
return data;
}

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

async listOrganizations(options?: FetchOptions<operations["listOrganizations"]>) {
const { data } = await this.client.GET("/api/atlas/v2/orgs", options);
return data;
}

async listOrganizationProjects(options: FetchOptions<operations["listOrganizationProjects"]>) {
const { data } = await this.client.GET("/api/atlas/v2/orgs/{orgId}/groups", options);
return data;
}

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