Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
14 changes: 14 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,7 @@
"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 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 +323,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 @@ -448,6 +461,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