From 3d9591d8ba0b889bf7080e474b0b3e359a59b383 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Sep 2025 11:30:26 +0000 Subject: [PATCH 1/3] Initial plan From eca8fbdc417264ab9e303fe83ada3c3b7297716f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Sep 2025 11:43:50 +0000 Subject: [PATCH 2/3] Add headers support for MongoDB Atlas API v2 compatibility Co-authored-by: montumodi <12596663+montumodi@users.noreply.github.com> --- src/httpClient.js | 31 +++++-- src/index.d.ts | 4 + src/index.js | 2 +- test/alert.test.js | 4 +- test/atlasSearch.test.js | 7 +- test/atlasUser.test.js | 6 +- test/headers.test.js | 173 +++++++++++++++++++++++++++++++++++++++ test/httpClient.test.js | 31 +++++++ 8 files changed, 240 insertions(+), 18 deletions(-) create mode 100644 test/headers.test.js create mode 100644 test/httpClient.test.js diff --git a/src/httpClient.js b/src/httpClient.js index 0cfdceb..40c1174 100644 --- a/src/httpClient.js +++ b/src/httpClient.js @@ -1,25 +1,38 @@ class HttpClient { - constructor(client, publicKey, privateKey) { + constructor(client, publicKey, privateKey, headers = {}) { this.client_ = client; this.digestAuth_ = `${publicKey}:${privateKey}`; + this.defaultHeaders_ = headers; } - async fetch(url, options) { - const response = await this.client_.request(url, { + async fetch(url, options = {}) { + const mergedOptions = { "digestAuth": this.digestAuth_, "dataType": "json", - ...options - }); + ...options, + "headers": { + ...this.defaultHeaders_, + ...(options.headers || {}) + } + }; + + const response = await this.client_.request(url, mergedOptions); return response.data; } - async fetchStream(url, options) { - const response = await this.client_.request(url, { + async fetchStream(url, options = {}) { + const mergedOptions = { "digestAuth": this.digestAuth_, "streaming": true, - ...options - }); + ...options, + "headers": { + ...this.defaultHeaders_, + ...(options.headers || {}) + } + }; + + const response = await this.client_.request(url, mergedOptions); return response.res; } diff --git a/src/index.d.ts b/src/index.d.ts index 3fc63ec..11f090d 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -92,6 +92,10 @@ export interface AtlasClientConfig { * Target Project ID in Atlas account */ projectId?: String; + /** + * Custom headers to be sent with each request + */ + headers?: Record; } export interface AtlasClientOptions { diff --git a/src/index.js b/src/index.js index dfed608..3dc657b 100644 --- a/src/index.js +++ b/src/index.js @@ -28,7 +28,7 @@ function getFunctions(instance) { function getMongodbAtlasApiClient(options) { - const client = new HttpClient(urllibClient, options.publicKey, options.privateKey); + const client = new HttpClient(urllibClient, options.publicKey, options.privateKey, options.headers); const user = new User(client, options.baseUrl, options.projectId); const cluster = new Cluster(client, options.baseUrl, options.projectId); const cloudBackup = new CloudBackup(client, options.baseUrl, options.projectId); diff --git a/test/alert.test.js b/test/alert.test.js index 03c635e..7509771 100644 --- a/test/alert.test.js +++ b/test/alert.test.js @@ -90,7 +90,7 @@ describe("Alert Class", () => { describe("When GetAll method is called with querystring parameters and httpOptions", () => { it("Should send appropriate parameters to underlying request", async () => { - const requestParams = {"digestAuth": "dummyPublicKey:dummyPrivateKey", "dataType": "json"}; + const requestParams = {"digestAuth": "dummyPublicKey:dummyPrivateKey", "dataType": "json", "headers": {}}; await alert.getAll({"queryStringParam1": "value1", "httpOptions": {"options1": "value1"}}); expect(mockRequest.request.calledWith("dummyBaseUrl/groups/dummyProjectId/alerts?queryStringParam1=value1", {...requestParams, "options1": "value1"})).to.be.true(); }); @@ -98,7 +98,7 @@ describe("Alert Class", () => { describe("When Get method is called with querystring parameters and httpOptions", () => { it("Should send appropriate parameters to underlying request", async () => { - const requestParams = {"digestAuth": "dummyPublicKey:dummyPrivateKey", "dataType": "json"}; + const requestParams = {"digestAuth": "dummyPublicKey:dummyPrivateKey", "dataType": "json", "headers": {}}; await alert.get("alertId", {"queryStringParam1": "value1", "httpOptions": {"options1": "value1"}}); expect(mockRequest.request.calledWith("dummyBaseUrl/groups/dummyProjectId/alerts/alertId?queryStringParam1=value1", {...requestParams, "options1": "value1"})).to.be.true(); }); diff --git a/test/atlasSearch.test.js b/test/atlasSearch.test.js index a99122c..44fc25a 100644 --- a/test/atlasSearch.test.js +++ b/test/atlasSearch.test.js @@ -137,7 +137,7 @@ describe("AtlasSearch Class", () => { describe("When getAllAnalyzers method is called with querystring parameters and httpOptions", () => { it("Should send appropriate parameters to underlying request", async () => { - const requestParams = {"digestAuth": "dummyPublicKey:dummyPrivateKey", "dataType": "json"}; + const requestParams = {"digestAuth": "dummyPublicKey:dummyPrivateKey", "dataType": "json", "headers": {}}; await atlasSearch.getAllAnalyzers("clusterName", {"queryStringParam1": "value1", "httpOptions": {"options1": "value1"}}); expect(mockRequest.request.calledWith("dummyBaseUrl/groups/dummyProjectId/clusters/clusterName/fts/analyzers?queryStringParam1=value1", {...requestParams, "options1": "value1"})).to.be.true(); }); @@ -145,7 +145,7 @@ describe("AtlasSearch Class", () => { describe("When Get method is called with querystring parameters and httpOptions", () => { it("Should send appropriate parameters to underlying request", async () => { - const requestParams = {"digestAuth": "dummyPublicKey:dummyPrivateKey", "dataType": "json"}; + const requestParams = {"digestAuth": "dummyPublicKey:dummyPrivateKey", "dataType": "json", "headers": {}}; await atlasSearch.get("clusterName", "indexId", {"queryStringParam1": "value1", "httpOptions": {"options1": "value1"}}); expect(mockRequest.request.calledWith("dummyBaseUrl/groups/dummyProjectId/clusters/clusterName/fts/indexes/indexId?queryStringParam1=value1", {...requestParams, "options1": "value1"})).to.be.true(); }); @@ -166,7 +166,7 @@ describe("AtlasSearch Class", () => { describe("When getAll method is called with querystring parameters and httpOptions", () => { it("Should send appropriate parameters to underlying request", async () => { - const requestParams = {"digestAuth": "dummyPublicKey:dummyPrivateKey", "dataType": "json"}; + const requestParams = {"digestAuth": "dummyPublicKey:dummyPrivateKey", "dataType": "json", "headers": {}}; await atlasSearch.getAll("clusterName", "databaseName", "collectionName", {"queryStringParam1": "value1", "httpOptions": {"options1": "value1"}}); expect(mockRequest.request.calledWith("dummyBaseUrl/groups/dummyProjectId/clusters/clusterName/fts/indexes/databaseName/collectionName?queryStringParam1=value1", {...requestParams, "options1": "value1"})).to.be.true(); }); @@ -177,6 +177,7 @@ describe("AtlasSearch Class", () => { const requestParams = { "digestAuth": "dummyPublicKey:dummyPrivateKey", "dataType": "json", + "headers": {}, "method": "DELETE" }; await atlasSearch.delete("clusterName", "indexId", {"queryStringParam1": "value1", "httpOptions": {"options1": "value1"}}); diff --git a/test/atlasUser.test.js b/test/atlasUser.test.js index 55be604..99c097d 100644 --- a/test/atlasUser.test.js +++ b/test/atlasUser.test.js @@ -117,7 +117,7 @@ describe("AtlasUser Class", () => { describe("When getByName method is called with querystring parameters and httpOptions", () => { it("Should send appropriate parameters to underlying request", async () => { - const requestParams = {"digestAuth": "dummyPublicKey:dummyPrivateKey", "dataType": "json"}; + const requestParams = {"digestAuth": "dummyPublicKey:dummyPrivateKey", "dataType": "json", "headers": {}}; await atlasUser.getByName("username", {"queryStringParam1": "value1", "httpOptions": {"options1": "value1"}}); expect(mockRequest.request.calledWith("dummyBaseUrl/users/byName/username?queryStringParam1=value1", {...requestParams, "options1": "value1"})).to.be.true(); }); @@ -125,7 +125,7 @@ describe("AtlasUser Class", () => { describe("When getById method is called with querystring parameters and httpOptions", () => { it("Should send appropriate parameters to underlying request", async () => { - const requestParams = {"digestAuth": "dummyPublicKey:dummyPrivateKey", "dataType": "json"}; + const requestParams = {"digestAuth": "dummyPublicKey:dummyPrivateKey", "dataType": "json", "headers": {}}; await atlasUser.getById("userId", {"queryStringParam1": "value1", "httpOptions": {"options1": "value1"}}); expect(mockRequest.request.calledWith("dummyBaseUrl/users/userId?queryStringParam1=value1", {...requestParams, "options1": "value1"})).to.be.true(); }); @@ -133,7 +133,7 @@ describe("AtlasUser Class", () => { describe("When getAll method is called with querystring parameters and httpOptions", () => { it("Should send appropriate parameters to underlying request", async () => { - const requestParams = {"digestAuth": "dummyPublicKey:dummyPrivateKey", "dataType": "json"}; + const requestParams = {"digestAuth": "dummyPublicKey:dummyPrivateKey", "dataType": "json", "headers": {}}; await atlasUser.getAll({"queryStringParam1": "value1", "httpOptions": {"options1": "value1"}}); expect(mockRequest.request.calledWith("dummyBaseUrl/groups/dummyProjectId/users?queryStringParam1=value1", {...requestParams, "options1": "value1"})).to.be.true(); }); diff --git a/test/headers.test.js b/test/headers.test.js new file mode 100644 index 0000000..5066aea --- /dev/null +++ b/test/headers.test.js @@ -0,0 +1,173 @@ +const {describe, it, afterEach, before, beforeEach} = exports.lab = require("@hapi/lab").script(); +const {expect} = require('@hapi/code'); +const getClient = require('../src/index.js'); +const HttpClient = require('../src/httpClient.js'); +const {stub} = require("sinon"); +const {MockAgent, setGlobalDispatcher} = require('urllib'); + +const baseUrl = "http://localhost:7001"; +const projectId = "dummyProjectId"; + +describe("Mongo Atlas Api Client - Headers Support", () => { + + let mockAgent; + let mockPool; + before(() => { + mockAgent = new MockAgent(); + setGlobalDispatcher(mockAgent); + }); + + beforeEach(() => { + mockPool = mockAgent.get(baseUrl); + }); + + afterEach(() => { + mockAgent.assertNoPendingInterceptors(); + }); + + describe("When client is created with custom headers", () => { + + it("should include custom headers in requests", async () => { + const customHeaders = {"Accept": "application/vnd.atlas.2025-03-12+json"}; + const client = getClient({ + "publicKey": "dummuyPublicKey", + "privateKey": "dummyPrivateKey", + "baseUrl": baseUrl, + "projectId": projectId, + "headers": customHeaders + }); + + mockPool.intercept({ + "path": `/groups/${projectId}/databaseUsers/admin/myUsername?key1=value1`, + "method": "get", + "headers": { + "Accept": "application/vnd.atlas.2025-03-12+json" + } + }) + .reply(200, {"user": "data"}); + + const result = await client.user.get("myUsername", {"key1": "value1"}); + expect(result).to.equal({"user": "data"}); + }); + + it("should work with empty headers object", async () => { + const client = getClient({ + "publicKey": "dummuyPublicKey", + "privateKey": "dummyPrivateKey", + "baseUrl": baseUrl, + "projectId": projectId, + "headers": {} + }); + + mockPool.intercept({ + "path": `/groups/${projectId}/databaseUsers/admin/myUsername`, + "method": "get" + }) + .reply(200, {"user": "data"}); + + const result = await client.user.get("myUsername"); + expect(result).to.equal({"user": "data"}); + }); + + it("should work without headers property", async () => { + const client = getClient({ + "publicKey": "dummuyPublicKey", + "privateKey": "dummyPrivateKey", + "baseUrl": baseUrl, + "projectId": projectId + }); + + mockPool.intercept({ + "path": `/groups/${projectId}/databaseUsers/admin/myUsername`, + "method": "get" + }) + .reply(200, {"user": "data"}); + + const result = await client.user.get("myUsername"); + expect(result).to.equal({"user": "data"}); + }); + + it("should merge custom headers with method-specific headers", async () => { + const customHeaders = {"Accept": "application/vnd.atlas.2025-03-12+json"}; + const client = getClient({ + "publicKey": "dummuyPublicKey", + "privateKey": "dummyPrivateKey", + "baseUrl": baseUrl, + "projectId": projectId, + "headers": customHeaders + }); + + mockPool.intercept({ + "path": `/groups/${projectId}/databaseUsers?key1=value1`, + "method": "POST", + "data": {"username": "testUser"}, + "headers": { + "Accept": "application/vnd.atlas.2025-03-12+json", + "Content-Type": "application/json" + } + }) + .reply(200, {"user": "created"}); + + const result = await client.user.create({"username": "testUser"}, {"key1": "value1"}); + expect(result).to.equal({"user": "created"}); + }); + + it("should merge custom headers with httpOptions headers", async () => { + const customHeaders = {"Accept": "application/vnd.atlas.2025-03-12+json"}; + const client = getClient({ + "publicKey": "dummuyPublicKey", + "privateKey": "dummyPrivateKey", + "baseUrl": baseUrl, + "projectId": projectId, + "headers": customHeaders + }); + + mockPool.intercept({ + "path": `/groups/${projectId}/databaseUsers/admin/myUsername?key1=value1`, + "method": "get", + "headers": { + "Accept": "application/vnd.atlas.2025-03-12+json", + "Custom-Header": "custom-value" + } + }) + .reply(200, {"user": "data"}); + + const result = await client.user.get("myUsername", { + "key1": "value1", + "httpOptions": { + "headers": { + "Custom-Header": "custom-value" + } + } + }); + expect(result).to.equal({"user": "data"}); + }); + }); +}); + +describe("HttpClient Direct Tests", () => { + const mockRequest = { + "request": stub().returns(new Promise(resolve => resolve({"data": "test data"}))) + }; + + it("should handle headers merging properly", async () => { + const httpClient = new HttpClient(mockRequest, "publicKey", "privateKey", {"Default-Header": "default-value"}); + + await httpClient.fetch("test-url", { + "headers": { + "Custom-Header": "custom-value" + } + }); + + const expectedParams = { + "digestAuth": "publicKey:privateKey", + "dataType": "json", + "headers": { + "Default-Header": "default-value", + "Custom-Header": "custom-value" + } + }; + + expect(mockRequest.request.calledWith("test-url", expectedParams)).to.be.true(); + }); +}); \ No newline at end of file diff --git a/test/httpClient.test.js b/test/httpClient.test.js new file mode 100644 index 0000000..4a0c2a3 --- /dev/null +++ b/test/httpClient.test.js @@ -0,0 +1,31 @@ +const {describe, it} = exports.lab = require("@hapi/lab").script(); +const {expect} = require('@hapi/code'); +const HttpClient = require('../src/httpClient.js'); +const {stub} = require("sinon"); + +describe("HttpClient Coverage Tests", () => { + const mockRequest = { + "request": stub().returns(new Promise(resolve => resolve({"data": "test data"}))) + }; + + it("should cover fetchStream with header merging", async () => { + const httpClient = new HttpClient(mockRequest, "publicKey", "privateKey", {"Default-Header": "default-value"}); + + await httpClient.fetchStream("test-url", { + "headers": { + "Custom-Header": "custom-value" + } + }); + + const expectedParams = { + "digestAuth": "publicKey:privateKey", + "streaming": true, + "headers": { + "Default-Header": "default-value", + "Custom-Header": "custom-value" + } + }; + + expect(mockRequest.request.calledWith("test-url", expectedParams)).to.be.true(); + }); +}); \ No newline at end of file From 1105829771abcaf0a2426179ee8c19c411eb903b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Sep 2025 11:54:39 +0000 Subject: [PATCH 3/3] Add header verification tests for AtlasSearch methods Co-authored-by: montumodi <12596663+montumodi@users.noreply.github.com> --- test/atlasSearch.test.js | 41 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/test/atlasSearch.test.js b/test/atlasSearch.test.js index 44fc25a..6bebf33 100644 --- a/test/atlasSearch.test.js +++ b/test/atlasSearch.test.js @@ -124,6 +124,30 @@ describe("Mongo Atlas Api Client - atlasSearch", () => { expect(result).to.be.true(); }); }); + + describe("When client is configured with custom headers", () => { + it("should include custom headers in atlasSearch requests", async () => { + const customHeaders = {"Accept": "application/vnd.atlas.2025-03-12+json"}; + const clientWithHeaders = getClient({ + "publicKey": "dummuyPublicKey", + "privateKey": "dummyPrivateKey", + "baseUrl": baseUrl, + "projectId": projectId, + "headers": customHeaders + }); + + mockPool.intercept({ + "path": `/groups/${projectId}/clusters/mycluster/fts/indexes/indexId?key1=value1`, + "method": "GET", + "headers": { + "Accept": "application/vnd.atlas.2025-03-12+json" + } + }).reply(200, {"atlasSearch": "index"}); + + const result = await clientWithHeaders.atlasSearch.get("mycluster", "indexId", {"key1": "value1"}); + expect(result).to.equal({"atlasSearch": "index"}); + }); + }); }); describe("AtlasSearch Class", () => { @@ -213,4 +237,21 @@ describe("AtlasSearch Class", () => { }); }); + describe("When AtlasSearch is used with custom headers", () => { + it("Should pass custom headers to underlying request", async () => { + const customHeaders = {"Accept": "application/vnd.atlas.2025-03-12+json"}; + const mockHttpClientWithHeaders = new HttpClient(mockRequest, "dummyPublicKey", "dummyPrivateKey", customHeaders); + const atlasSearchWithHeaders = new AtlasSearch(mockHttpClientWithHeaders, "dummyBaseUrl", "dummyProjectId"); + + const requestParams = { + "digestAuth": "dummyPublicKey:dummyPrivateKey", + "dataType": "json", + "headers": {"Accept": "application/vnd.atlas.2025-03-12+json"} + }; + + await atlasSearchWithHeaders.get("clusterName", "indexId", {"queryStringParam1": "value1"}); + expect(mockRequest.request.calledWith("dummyBaseUrl/groups/dummyProjectId/clusters/clusterName/fts/indexes/indexId?queryStringParam1=value1", requestParams)).to.be.true(); + }); + }); + });