Skip to content
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
31 changes: 22 additions & 9 deletions src/httpClient.js
Original file line number Diff line number Diff line change
@@ -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;
}
Expand Down
4 changes: 4 additions & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>;
}

export interface AtlasClientOptions {
Expand Down
2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions test/alert.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,15 +90,15 @@ 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();
});
});

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();
});
Expand Down
48 changes: 45 additions & 3 deletions test/atlasSearch.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand All @@ -137,15 +161,15 @@ 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();
});
});

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();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot shouldn't there be a check that headers are being passed correctly?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're absolutely right! I've added tests to verify that headers are being passed correctly when using AtlasSearch methods:

  1. Integration test: Verifies that custom headers configured on the client are included in actual HTTP requests made by AtlasSearch methods
  2. Unit test: Verifies that custom headers are properly passed to the underlying request mechanism

The tests ensure that when a client is configured with custom headers (like "Accept": "application/vnd.atlas.2025-03-12+json"), those headers are correctly propagated through AtlasSearch method calls.

Commit: 1105829

});
Expand All @@ -166,7 +190,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();
});
Expand All @@ -177,6 +201,7 @@ describe("AtlasSearch Class", () => {
const requestParams = {
"digestAuth": "dummyPublicKey:dummyPrivateKey",
"dataType": "json",
"headers": {},
"method": "DELETE"
};
await atlasSearch.delete("clusterName", "indexId", {"queryStringParam1": "value1", "httpOptions": {"options1": "value1"}});
Expand Down Expand Up @@ -212,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();
});
});

});
6 changes: 3 additions & 3 deletions test/atlasUser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,23 +117,23 @@ 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();
});
});

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();
});
});

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();
});
Expand Down
173 changes: 173 additions & 0 deletions test/headers.test.js
Original file line number Diff line number Diff line change
@@ -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();
});
});
Loading