Skip to content

Commit a85d6d1

Browse files
Merge remote-tracking branch 'origin/main' into beta-releases
2 parents 385fafd + d016374 commit a85d6d1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+2008
-398
lines changed

THIRD-PARTY-NOTICES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
The following third-party software is used by and included in **Mongodb Compass**.
2-
This document was automatically generated on Sun Aug 20 2023.
2+
This document was automatically generated on Wed Aug 23 2023.
33

44
## List of dependencies
55

configs/mocha-config-compass/compass-plugin.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module.exports = {
77
require: [
88
...reactConfig.require,
99
path.resolve(__dirname, 'register', 'electron-renderer-register.js'),
10+
path.resolve(__dirname, 'register', 'compass-preferences-register.js'),
1011
],
1112
// electron-mocha config options (ignored when run with just mocha)
1213
// https://github.com/jprichardson/electron-mocha
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
let setupPreferencesPromise;
2+
3+
module.exports = {
4+
mochaHooks: {
5+
async beforeAll() {
6+
// Only do this setup if we are not in electron environment, electron case
7+
// is handled in main-process.js
8+
if (typeof process.type !== 'undefined') {
9+
return;
10+
}
11+
// For compass-plugin test preset we want to make sure that the
12+
// environment is matching compass environment, including the implicit
13+
// preferences used by a lot of packages in the monorepo
14+
process.env.COMPASS_TEST_USE_PREFERENCES_SANDBOX =
15+
process.env.COMPASS_TEST_USE_PREFERENCES_SANDBOX ?? 'true';
16+
// Make sure we only do this once so that --watch mode doesn't try to set
17+
// up preferences again on re-run
18+
setupPreferencesPromise ??=
19+
require('compass-preferences-model').setupPreferences();
20+
// NB: Not adding this as a dep in package.json to avoid circular dependency
21+
await setupPreferencesPromise;
22+
},
23+
},
24+
};

package-lock.json

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/atlas-service/.mocharc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
module.exports = require('@mongodb-js/mocha-config-compass/react');
1+
module.exports = require('@mongodb-js/mocha-config-compass/compass-plugin');

packages/atlas-service/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"@mongodb-js/compass-utils": "^0.3.3",
7373
"@mongodb-js/devtools-connect": "^2.4.0",
7474
"@mongodb-js/oidc-plugin": "^0.3.0",
75+
"compass-preferences-model": "^2.11.1",
7576
"electron": "^23.3.12",
7677
"keytar": "^7.9.0",
7778
"node-fetch": "^2.6.7",

packages/atlas-service/src/components/ai-signin-modal.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,9 @@ const AISignInModal: React.FunctionComponent<SignInModalProps> = ({
109109
</Button>
110110
<Button
111111
variant="primaryOutline"
112-
onClick={onSignInModalClose}
112+
onClick={() => {
113+
onSignInModalClose?.();
114+
}}
113115
className={cx(buttonStyles, maybeLaterButtonStyles)}
114116
>
115117
Maybe later

packages/atlas-service/src/main.spec.ts

Lines changed: 133 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import Sinon from 'sinon';
22
import { expect } from 'chai';
33
import { AtlasService, throwIfNotOk } from './main';
44
import { promisify } from 'util';
5-
import type { EventEmitter } from 'events';
5+
import { EventEmitter } from 'events';
66
import { once } from 'events';
7+
import preferencesAccess from 'compass-preferences-model';
78

89
const wait = promisify(setTimeout);
910

@@ -13,20 +14,6 @@ function getListenerCount(emitter: EventEmitter) {
1314
}, 0);
1415
}
1516

16-
const atlasAIServiceTests: {
17-
functionName: 'getQueryFromUserInput' | 'getAggregationFromUserInput';
18-
aiEndpoint: string;
19-
}[] = [
20-
{
21-
functionName: 'getQueryFromUserInput',
22-
aiEndpoint: 'mql-query',
23-
},
24-
{
25-
functionName: 'getAggregationFromUserInput',
26-
aiEndpoint: 'mql-aggregation',
27-
},
28-
];
29-
3017
describe('AtlasServiceMain', function () {
3118
const sandbox = Sinon.createSandbox();
3219

@@ -52,6 +39,7 @@ describe('AtlasServiceMain', function () {
5239

5340
const fetch = AtlasService['fetch'];
5441
const ipcMain = AtlasService['ipcMain'];
42+
const createPlugin = AtlasService['createMongoDBOIDCPlugin'];
5543
const apiBaseUrl = process.env.COMPASS_ATLAS_SERVICE_BASE_URL;
5644
const issuer = process.env.COMPASS_OIDC_ISSUER;
5745
const clientId = process.env.COMPASS_CLIENT_ID;
@@ -74,6 +62,7 @@ describe('AtlasServiceMain', function () {
7462
AtlasService['token'] = null;
7563
AtlasService['initPromise'] = null;
7664
AtlasService['oidcPluginSyncedFromLoggerState'] = 'initial';
65+
AtlasService['createMongoDBOIDCPlugin'] = createPlugin;
7766

7867
sandbox.resetHistory();
7968
});
@@ -213,15 +202,46 @@ describe('AtlasServiceMain', function () {
213202
});
214203
});
215204

216-
for (const { functionName, aiEndpoint } of atlasAIServiceTests) {
205+
const atlasAIServiceTests = [
206+
{
207+
functionName: 'getQueryFromUserInput',
208+
aiEndpoint: 'mql-query',
209+
responses: {
210+
success: {
211+
content: { query: { filter: "{ test: 'pineapple' }" } },
212+
},
213+
invalid: [
214+
{},
215+
{ countent: {} },
216+
{ content: { qooery: {} } },
217+
{ content: { query: { filter: { foo: 1 } } } },
218+
],
219+
},
220+
},
221+
{
222+
functionName: 'getAggregationFromUserInput',
223+
aiEndpoint: 'mql-aggregation',
224+
responses: {
225+
success: {
226+
content: { aggregation: { pipeline: "[{ test: 'pineapple' }]" } },
227+
},
228+
invalid: [
229+
{},
230+
{ content: { aggregation: {} } },
231+
{ content: { aggrogation: {} } },
232+
{ content: { aggregation: { pipeline: true } } },
233+
],
234+
},
235+
},
236+
] as const;
237+
238+
for (const { functionName, aiEndpoint, responses } of atlasAIServiceTests) {
217239
describe(functionName, function () {
218240
it('makes a post request with the user input to the endpoint in the environment', async function () {
219241
AtlasService['fetch'] = sandbox.stub().resolves({
220242
ok: true,
221243
json() {
222-
return Promise.resolve({
223-
content: { query: { filter: "{ test: 'pineapple' }" } },
224-
});
244+
return Promise.resolve(responses.success);
225245
},
226246
}) as any;
227247

@@ -243,10 +263,28 @@ describe('AtlasServiceMain', function () {
243263
expect(args[1].body).to.eq(
244264
'{"userInput":"test","collectionName":"jam","databaseName":"peanut","schema":{"_id":{"types":[{"bsonType":"ObjectId"}]}},"sampleDocuments":[{"_id":1234}]}'
245265
);
246-
expect(res).to.have.nested.property(
247-
'content.query.filter',
248-
"{ test: 'pineapple' }"
249-
);
266+
expect(res).to.deep.eq(responses.success);
267+
});
268+
269+
it('should fail when response is not matching expected schema', async function () {
270+
for (const res of responses.invalid) {
271+
AtlasService['fetch'] = sandbox.stub().resolves({
272+
ok: true,
273+
json() {
274+
return Promise.resolve(res);
275+
},
276+
}) as any;
277+
try {
278+
await AtlasService[functionName]({
279+
userInput: 'test',
280+
collectionName: 'test',
281+
databaseName: 'peanut',
282+
});
283+
expect.fail(`Expected ${functionName} to throw`);
284+
} catch (err) {
285+
expect((err as Error).message).to.match(/Unexpected.+?response/);
286+
}
287+
}
250288
});
251289

252290
it('uses the abort signal in the fetch request', async function () {
@@ -286,7 +324,7 @@ describe('AtlasServiceMain', function () {
286324
AtlasService['fetch'] = sandbox.stub().resolves({
287325
ok: true,
288326
json() {
289-
return Promise.resolve({});
327+
return Promise.resolve(responses.success);
290328
},
291329
}) as any;
292330

@@ -353,13 +391,13 @@ describe('AtlasServiceMain', function () {
353391
AtlasService['fetch'] = sandbox.stub().resolves({
354392
ok: true,
355393
json() {
356-
return Promise.resolve({ test: 1 });
394+
return Promise.resolve(responses.success);
357395
},
358396
}) as any;
359397
AtlasService['oidcPluginLogger'].emit(
360398
'mongodb-oidc-plugin:refresh-started'
361399
);
362-
const [query] = await Promise.all([
400+
const [res] = await Promise.all([
363401
AtlasService[functionName]({
364402
userInput: 'test',
365403
collectionName: 'test',
@@ -375,7 +413,7 @@ describe('AtlasServiceMain', function () {
375413
);
376414
})(),
377415
]);
378-
expect(query).to.deep.eq({ test: 1 });
416+
expect(res).to.deep.eq(responses.success);
379417
});
380418
});
381419
}
@@ -542,7 +580,8 @@ describe('AtlasServiceMain', function () {
542580
.stub()
543581
.rejects(new Error('Could not retrieve valid access token'));
544582
const createPlugin = () => mockOidcPlugin;
545-
const initPromise = AtlasService.init(createPlugin);
583+
AtlasService['createMongoDBOIDCPlugin'] = createPlugin;
584+
const initPromise = AtlasService.init();
546585
expect(AtlasService).to.have.property(
547586
'oidcPluginSyncedFromLoggerState',
548587
'restoring'
@@ -558,7 +597,8 @@ describe('AtlasServiceMain', function () {
558597
mockOidcPlugin.mongoClientOptions.authMechanismProperties.REQUEST_TOKEN_CALLBACK =
559598
sandbox.stub().resolves({ accessToken: 'token' });
560599
const createPlugin = () => mockOidcPlugin;
561-
const initPromise = AtlasService.init(createPlugin);
600+
AtlasService['createMongoDBOIDCPlugin'] = createPlugin;
601+
const initPromise = AtlasService.init();
562602
expect(AtlasService).to.have.property(
563603
'oidcPluginSyncedFromLoggerState',
564604
'restoring'
@@ -570,4 +610,68 @@ describe('AtlasServiceMain', function () {
570610
);
571611
});
572612
});
613+
614+
describe('with networkTraffic turned off', function () {
615+
let networkTraffic: boolean;
616+
617+
before(async function () {
618+
networkTraffic = preferencesAccess.getPreferences().networkTraffic;
619+
await preferencesAccess.savePreferences({ networkTraffic: false });
620+
});
621+
622+
after(async function () {
623+
await preferencesAccess.savePreferences({ networkTraffic });
624+
});
625+
626+
for (const methodName of [
627+
'requestOAuthToken',
628+
'signIn',
629+
'getUserInfo',
630+
'introspect',
631+
'revoke',
632+
'getAggregationFromUserInput',
633+
'getQueryFromUserInput',
634+
]) {
635+
it(`${methodName} should throw`, async function () {
636+
try {
637+
await (AtlasService as any)[methodName]({});
638+
expect.fail(`Expected ${methodName} to throw`);
639+
} catch (err) {
640+
expect(err).to.have.property(
641+
'message',
642+
'Network traffic is not allowed'
643+
);
644+
}
645+
});
646+
}
647+
});
648+
649+
describe('signOut', function () {
650+
it('should reset service state, revoke tokens, and destroy plugin', async function () {
651+
const logger = new EventEmitter();
652+
const plugin = {
653+
destroy: sandbox.stub().resolves(),
654+
};
655+
AtlasService['openExternal'] = sandbox.stub().resolves();
656+
AtlasService['token'] = { accessToken: '1234' };
657+
AtlasService['createMongoDBOIDCPlugin'] = (() => {
658+
return plugin;
659+
}) as any;
660+
AtlasService['fetch'] = sandbox.stub().resolves({ ok: true }) as any;
661+
AtlasService['oidcPluginLogger'] = logger;
662+
663+
await AtlasService.init();
664+
expect(getListenerCount(logger)).to.eq(25);
665+
666+
await AtlasService.signOut();
667+
expect(getListenerCount(logger)).to.eq(0);
668+
expect(logger).to.not.eq(AtlasService['oidcPluginLogger']);
669+
expect(plugin.destroy).to.have.been.calledOnce;
670+
expect(AtlasService['fetch']).to.have.been.calledOnceWith(
671+
'http://example.com/v1/revoke?client_id=1234abcd'
672+
);
673+
expect(AtlasService['token']).to.eq(null);
674+
expect(AtlasService['openExternal']).to.have.been.calledOnce;
675+
});
676+
});
573677
});

0 commit comments

Comments
 (0)