Skip to content
Open
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
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
module.exports = {
testEnvironment: 'node',
testMatch: ['<rootDir>/src/**/*.test.ts'],
setupFilesAfterEnv: ['<rootDir>/src/plugins/service-mongo-atlas/__tests__/jest.setup.atlas.ts'],
transform: {
'^.+.tsx?$': ['ts-jest', {}],
},
Expand Down
16 changes: 16 additions & 0 deletions l10n/bundle.l10n.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
"Delete database \"{databaseId}\" and its contents?": "Delete database \"{databaseId}\" and its contents?",
"Delete selected document(s)": "Delete selected document(s)",
"Deleting...": "Deleting...",
"Digest credentials not found": "Digest credentials not found",
"Disable TLS/SSL (Not recommended)": "Disable TLS/SSL (Not recommended)",
"Disable TLS/SSL checks when connecting.": "Disable TLS/SSL checks when connecting.",
"Do not rely on case to distinguish between databases. For example, you cannot use two databases with names like, salesData and SalesData.": "Do not rely on case to distinguish between databases. For example, you cannot use two databases with names like, salesData and SalesData.",
Expand Down Expand Up @@ -195,19 +196,29 @@
"Failed to access Azure Databases VS Code Extension storage for migration: {error}": "Failed to access Azure Databases VS Code Extension storage for migration: {error}",
"Failed to connect to \"{cluster}\"": "Failed to connect to \"{cluster}\"",
"Failed to connect to VM \"{vmName}\"": "Failed to connect to VM \"{vmName}\"",
"Failed to create access list entries for project {0}: {1} {2}": "Failed to create access list entries for project {0}: {1} {2}",
"Failed to create Azure management clients: {0}": "Failed to create Azure management clients: {0}",
"Failed to create role assignment \"{0}\" for the {2} resource \"{1}\".": "Failed to create role assignment \"{0}\" for the {2} resource \"{1}\".",
"Failed to create role assignment(s).": "Failed to create role assignment(s).",
"Failed to delete access list entry {0} from project {1}: {2}": "Failed to delete access list entry {0} from project {1}: {2}",
"Failed to delete access list entry {0} from project {1}: {2} {3}": "Failed to delete access list entry {0} from project {1}: {2} {3}",
"Failed to delete documents. Unknown error.": "Failed to delete documents. Unknown error.",
"Failed to delete item \"{0}\".": "Failed to delete item \"{0}\".",
"Failed to delete secrets for item \"{0}\".": "Failed to delete secrets for item \"{0}\".",
"Failed to export documents. Please see the output for details.": "Failed to export documents. Please see the output for details.",
"Failed to extract cluster credentials from the selected node.": "Failed to extract cluster credentials from the selected node.",
"Failed to extract the connection string from the selected account.": "Failed to extract the connection string from the selected account.",
"Failed to find commandId on generic tree item.": "Failed to find commandId on generic tree item.",
"Failed to get access list for project {0}: {1} {2}": "Failed to get access list for project {0}: {1} {2}",
"Failed to get cluster {0} in project {1}: {2} {3}": "Failed to get cluster {0} in project {1}: {2} {3}",
"Failed to get public IP": "Failed to get public IP",
"Failed to initialize Azure management clients": "Failed to initialize Azure management clients",
"Failed to list Atlas projects: {0} {1}": "Failed to list Atlas projects: {0} {1}",
"Failed to list clusters for project {0}: {1} {2}": "Failed to list clusters for project {0}: {1} {2}",
"Failed to list database users for project {0}: {1} {2}": "Failed to list database users for project {0}: {1} {2}",
"Failed to obtain Entra ID token.": "Failed to obtain Entra ID token.",
"Failed to obtain OAuth token: {0} {1}": "Failed to obtain OAuth token: {0} {1}",
"Failed to obtain valid OAuth token": "Failed to obtain valid OAuth token",
"Failed to parse secrets for key {0}:": "Failed to parse secrets for key {0}:",
"Failed to process URI: {0}": "Failed to process URI: {0}",
"Failed to rename the connection.": "Failed to rename the connection.",
Expand Down Expand Up @@ -291,6 +302,8 @@
"New Local Connection": "New Local Connection",
"New Local Connection…": "New Local Connection…",
"No": "No",
"No Atlas credentials found for organization {0}": "No Atlas credentials found for organization {0}",
"No Atlas OAuth credentials found for organization {0}": "No Atlas OAuth credentials found for organization {0}",
"No authentication method selected.": "No authentication method selected.",
"No authentication methods available for \"{cluster}\".": "No authentication methods available for \"{cluster}\".",
"No Azure subscription found for this tree item.": "No Azure subscription found for this tree item.",
Expand All @@ -311,6 +324,7 @@
"Not connected to any MongoDB database.": "Not connected to any MongoDB database.",
"Note: This confirmation type can be configured in the extension settings.": "Note: This confirmation type can be configured in the extension settings.",
"Note: You can disable these URL handling confirmations in the extension settings.": "Note: You can disable these URL handling confirmations in the extension settings.",
"OAuth credentials not found for organization {0}": "OAuth credentials not found for organization {0}",
"Open Collection": "Open Collection",
"Open installation page": "Open installation page",
"Opening DocumentDB connection…": "Opening DocumentDB connection…",
Expand Down Expand Up @@ -341,6 +355,7 @@
"Remind Me Later": "Remind Me Later",
"Rename Connection": "Rename Connection",
"Report an issue": "Report an issue",
"Request failed with status {status}: {errorText}": "Request failed with status {status}: {errorText}",
"Resource group \"{0}\" already exists in subscription \"{1}\".": "Resource group \"{0}\" already exists in subscription \"{1}\".",
"Reusing active connection for \"{cluster}\".": "Reusing active connection for \"{cluster}\".",
"Revisit connection details and try again.": "Revisit connection details and try again.",
Expand Down Expand Up @@ -448,6 +463,7 @@
"Unrecognized node type encountered. Could not parse {constructorCall} as part of {functionCall}": "Unrecognized node type encountered. Could not parse {constructorCall} as part of {functionCall}",
"Unrecognized node type encountered. We could not parse {text}": "Unrecognized node type encountered. We could not parse {text}",
"Unrecognized token. Token text: {text}": "Unrecognized token. Token text: {text}",
"Unsupported Atlas authentication type: {0}": "Unsupported Atlas authentication type: {0}",
"Unsupported authentication mechanism. Only \"Username and Password\" (SCRAM-SHA-256) is supported.": "Unsupported authentication mechanism. Only \"Username and Password\" (SCRAM-SHA-256) is supported.",
"Unsupported authentication mechanism. Only SCRAM-SHA-256 (username/password) is supported.": "Unsupported authentication mechanism. Only SCRAM-SHA-256 (username/password) is supported.",
"Unsupported authentication method: {0}": "Unsupported authentication method: {0}",
Expand Down
34 changes: 30 additions & 4 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@
"antlr4ts": "^0.5.0-alpha.4",
"bson": "~6.10.4",
"denque": "~2.1.0",
"digest-fetch": "^3.1.1",
"es-toolkit": "~1.39.7",
"monaco-editor": "~0.51.0",
"mongodb": "~6.17.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { type AtlasApiResponse } from '../utils/AtlasAdminApiTypes';
import { AtlasAdministrationClient } from '../utils/AtlasAdministrationClient';
import { AtlasHttpClient } from '../utils/AtlasHttpClient';

function mockJson<T>(data: T): Response {
return { ok: true, status: 200, json: async () => data } as any as Response;
}

function mockFail(status: number, text: string): Response {
return { ok: false, status, text: async () => text } as any as Response;
}

describe('AtlasAdministrationClient (Jest)', () => {
const orgId = 'org';
const projectId = 'proj';

let originalGet: typeof AtlasHttpClient.get;
let originalPost: typeof AtlasHttpClient.post;
let originalDelete: typeof AtlasHttpClient.delete;

beforeEach(() => {
originalGet = AtlasHttpClient.get.bind(AtlasHttpClient);
originalPost = AtlasHttpClient.post.bind(AtlasHttpClient);
originalDelete = AtlasHttpClient.delete.bind(AtlasHttpClient);
});

afterEach(() => {
AtlasHttpClient.get = originalGet;
AtlasHttpClient.post = originalPost;
AtlasHttpClient.delete = originalDelete;
});

test('listProjects success builds query params', async () => {
const data: AtlasApiResponse<any> = {
results: [{ name: 'p', orgId: orgId, created: '', clusterCount: 0 }],
totalCount: 1,
};
let calledEndpoint = '';
AtlasHttpClient.get = (async (_org, endpoint) => {
calledEndpoint = endpoint;
return mockJson(data);
}) as any;
const resp = await AtlasAdministrationClient.listProjects(orgId, {
pageNum: 1,
itemsPerPage: 5,
includeCount: true,
});
expect(resp.totalCount).toBe(1);
expect(/pageNum=1/.test(calledEndpoint)).toBe(true);
});

test('listProjects failure throws', async () => {
AtlasHttpClient.get = (async () => mockFail(500, 'err')) as any;
await expect(AtlasAdministrationClient.listProjects(orgId)).rejects.toThrow(/Failed to list Atlas projects/);
});

test('listClusters success', async () => {
const data: AtlasApiResponse<any> = {
results: [
{
clusterType: 'REPLICASET',
providerSettings: { providerName: 'AWS', regionName: 'us', instanceSizeName: 'M10' },
stateName: 'IDLE',
},
],
totalCount: 1,
};
AtlasHttpClient.get = (async () => mockJson(data)) as any;
const resp = await AtlasAdministrationClient.listClusters(orgId, projectId);
expect(resp.results.length).toBe(1);
});

test('getCluster failure throws', async () => {
AtlasHttpClient.get = (async () => mockFail(404, 'missing')) as any;
await expect(AtlasAdministrationClient.getCluster(orgId, projectId, 'cl')).rejects.toThrow(
/Failed to get cluster/,
);
});

test('listDatabaseUsers failure throws', async () => {
AtlasHttpClient.get = (async () => mockFail(400, 'bad')) as any;
await expect(AtlasAdministrationClient.listDatabaseUsers(orgId, projectId)).rejects.toThrow(
/Failed to list database users/,
);
});

test('getAccessList failure throws', async () => {
AtlasHttpClient.get = (async () => mockFail(401, 'unauth')) as any;
await expect(AtlasAdministrationClient.getAccessList(orgId, projectId)).rejects.toThrow(
/Failed to get access list/,
);
});

test('createAccessListEntries failure throws', async () => {
AtlasHttpClient.post = (async () => mockFail(500, 'boom')) as any;
await expect(AtlasAdministrationClient.createAccessListEntries(orgId, projectId, [])).rejects.toThrow(
/Failed to create access list entries/,
);
});

test('deleteAccessListEntry failure throws', async () => {
AtlasHttpClient.delete = (async () => mockFail(403, 'deny')) as any;
await expect(AtlasAdministrationClient.deleteAccessListEntry(orgId, projectId, '1.1.1.1')).rejects.toThrow(
/Failed to delete access list entry/,
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AtlasAuthManager } from '../utils/AtlasAuthManager';
import { AtlasCredentialCache } from '../utils/AtlasCredentialCache';

type FetchFn = (url: string, init?: any) => Promise<any>;

describe('AtlasAuthManager (Jest)', () => {
const orgId = 'authOrg';
const clientId = 'client';
const clientSecret = 'secret';
let originalFetch: any;

beforeEach(() => {
originalFetch = global.fetch;
// ensure we start with a clean slate
// @ts-expect-error override for test
delete global.fetch;
});

afterEach(() => {
AtlasCredentialCache.clearAtlasCredentials(orgId);
if (originalFetch) {
global.fetch = originalFetch;
} else {
// @ts-expect-error restore
delete global.fetch;
}
});

test('getOAuthBasicAuthHeader encodes credentials', () => {
const hdr = AtlasAuthManager.getOAuthBasicAuthHeader('id', 'sec');
expect(hdr).toBe('Basic aWQ6c2Vj');
});

test('requestOAuthToken success stores nothing automatically', async () => {
const mockFetch: FetchFn = async () => ({
ok: true,
status: 200,
json: async () => ({ access_token: 'tok', expires_in: 100, token_type: 'Bearer' }),
});
global.fetch = mockFetch as any;
const resp = await AtlasAuthManager.requestOAuthToken(clientId, clientSecret);
expect(resp.access_token).toBe('tok');
});

test('requestOAuthToken failure throws with status and text', async () => {
const mockFetch: FetchFn = async () => ({ ok: false, status: 400, text: async () => 'bad request' });
global.fetch = mockFetch as any;
await expect(AtlasAuthManager.requestOAuthToken(clientId, clientSecret)).rejects.toThrow(/400/);
});

test('getAuthorizationHeader returns bearer token using cache', async () => {
AtlasCredentialCache.setAtlasOAuthCredentials(orgId, clientId, clientSecret);
AtlasCredentialCache.updateAtlasOAuthToken(orgId, 'cachedToken', 3600);
const hdr = await AtlasAuthManager.getAuthorizationHeader(orgId);
expect(hdr).toBe('Bearer cachedToken');
});

test('getAuthorizationHeader fetches new token when expired', async () => {
AtlasCredentialCache.setAtlasOAuthCredentials(orgId, clientId, clientSecret);
AtlasCredentialCache.updateAtlasOAuthToken(orgId, 'old', -1);
const mockFetch: FetchFn = async () => ({
ok: true,
status: 200,
json: async () => ({ access_token: 'newToken', expires_in: 50, token_type: 'Bearer' }),
});
global.fetch = mockFetch as any;
const hdr = await AtlasAuthManager.getAuthorizationHeader(orgId);
expect(hdr).toBe('Bearer newToken');
});

test('getAuthorizationHeader undefined when no credentials', async () => {
const hdr = await AtlasAuthManager.getAuthorizationHeader('missing');
expect(hdr).toBeUndefined();
});
});
Loading