diff --git a/.github/workflows/actions/test-and-build/action.yaml b/.github/workflows/actions/test-and-build/action.yaml index ad06d2a54..6d51d8cc9 100644 --- a/.github/workflows/actions/test-and-build/action.yaml +++ b/.github/workflows/actions/test-and-build/action.yaml @@ -69,7 +69,15 @@ runs: if: ${{ runner.os != 'Windows' }} shell: bash + - name: Set BROWSER_AUTH_COMMAND + run: | + BROWSER_AUTH_COMMAND=$(echo "$(which node) $(pwd)/src/test/fixture/curl.js") + echo "BROWSER_AUTH_COMMAND=$BROWSER_AUTH_COMMAND" >> $GITHUB_ENV + shell: bash + - name: Run Tests + env: + BROWSER_AUTH_COMMAND: ${{ env.BROWSER_AUTH_COMMAND }} run: | npm run test shell: bash diff --git a/package-lock.json b/package-lock.json index c95435af8..f01657c60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,6 +54,7 @@ }, "devDependencies": { "@babel/preset-typescript": "^7.22.5", + "@mongodb-js/oidc-mock-provider": "^0.6.9", "@mongodb-js/oidc-plugin": "^0.3.0", "@mongodb-js/prettier-config-compass": "^1.0.0", "@mongodb-js/sbom-tools": "^0.5.4", @@ -107,6 +108,7 @@ "mocha-multi": "^1.1.7", "mongodb-client-encryption": "^6.0.0", "mongodb-runner": "^5.4.5", + "node-fetch": "^2.7.0", "node-loader": "^0.6.0", "npm-run-all": "^4.1.5", "ora": "^5.4.1", @@ -4885,6 +4887,59 @@ "tar": "^6.1.15" } }, + "node_modules/@mongodb-js/oidc-mock-provider": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-mock-provider/-/oidc-mock-provider-0.6.9.tgz", + "integrity": "sha512-4D9y7w7k0f7z6OkFJ8Ux5UhMG7Tg287CC1KmpW43BMzMx5gPXhostYK+OtpZNBlOoB9yrlHLusLKtpqQywMaog==", + "dev": true, + "dependencies": { + "yargs": "17.7.2" + }, + "bin": { + "oidc-mock-provider": "bin/oidc-mock-provider.js" + } + }, + "node_modules/@mongodb-js/oidc-mock-provider/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@mongodb-js/oidc-mock-provider/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@mongodb-js/oidc-mock-provider/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/@mongodb-js/oidc-plugin": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-plugin/-/oidc-plugin-0.3.0.tgz", @@ -18252,9 +18307,9 @@ "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==" }, "node_modules/node-fetch": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", - "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -28774,6 +28829,49 @@ "tar": "^6.1.15" } }, + "@mongodb-js/oidc-mock-provider": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-mock-provider/-/oidc-mock-provider-0.6.9.tgz", + "integrity": "sha512-4D9y7w7k0f7z6OkFJ8Ux5UhMG7Tg287CC1KmpW43BMzMx5gPXhostYK+OtpZNBlOoB9yrlHLusLKtpqQywMaog==", + "dev": true, + "requires": { + "yargs": "17.7.2" + }, + "dependencies": { + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + } + } + }, "@mongodb-js/oidc-plugin": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-plugin/-/oidc-plugin-0.3.0.tgz", @@ -39202,9 +39300,9 @@ "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==" }, "node-fetch": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", - "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "requires": { "whatwg-url": "^5.0.0" }, diff --git a/package.json b/package.json index 9a65eaf7e..f03fc8a5c 100644 --- a/package.json +++ b/package.json @@ -1029,6 +1029,11 @@ "type": "boolean", "default": false, "description": "The default behavior is to generate a single ObjectId and insert it on all cursors. Set to true to generate a unique ObjectId per cursor instead." + }, + "mdb.browserCommandForOIDCAuth": { + "type": "string", + "default": "", + "description": "Specify a shell command that is run to start the browser for authenticating with the OIDC identity provider for the server connection. Leave this empty for default browser." } } } @@ -1079,6 +1084,7 @@ }, "devDependencies": { "@babel/preset-typescript": "^7.22.5", + "@mongodb-js/oidc-mock-provider": "^0.6.9", "@mongodb-js/oidc-plugin": "^0.3.0", "@mongodb-js/prettier-config-compass": "^1.0.0", "@mongodb-js/sbom-tools": "^0.5.4", @@ -1132,6 +1138,7 @@ "mocha-multi": "^1.1.7", "mongodb-client-encryption": "^6.0.0", "mongodb-runner": "^5.4.5", + "node-fetch": "^2.7.0", "node-loader": "^0.6.0", "npm-run-all": "^4.1.5", "ora": "^5.4.1", diff --git a/src/connectionController.ts b/src/connectionController.ts index 5e72ff334..7ed6283d4 100644 --- a/src/connectionController.ts +++ b/src/connectionController.ts @@ -336,22 +336,27 @@ export default class ConnectionController { browserCommandForOIDCAuth: undefined, // We overwrite this below. }, }); + const browserAuthCommand = vscode.workspace + .getConfiguration('mdb') + .get('browserCommandForOIDCAuth'); dataService = await connectionAttempt.connect({ ...connectionOptions, oidc: { ...cloneDeep(connectionOptions.oidc), - openBrowser: async ({ signal, url }) => { - try { - await openLink(url); - } catch (err) { - if (signal.aborted) return; - // If opening the link fails we default to regular link opening. - await vscode.commands.executeCommand( - 'vscode.open', - vscode.Uri.parse(url) - ); - } - }, + openBrowser: browserAuthCommand + ? { command: browserAuthCommand } + : async ({ signal, url }) => { + try { + await openLink(url); + } catch (err) { + if (signal.aborted) return; + // If opening the link fails we default to regular link opening. + await vscode.commands.executeCommand( + 'vscode.open', + vscode.Uri.parse(url) + ); + } + }, }, }); @@ -425,6 +430,7 @@ export default class ConnectionController { ); if (removeConfirmationResponse !== 'Confirm') { + await this.disconnect(); throw new Error('Reauthentication declined by user'); } } diff --git a/src/test/fixture/curl.js b/src/test/fixture/curl.js new file mode 100644 index 000000000..d7108c327 --- /dev/null +++ b/src/test/fixture/curl.js @@ -0,0 +1,13 @@ +#!/usr/bin/env node +/* eslint-disable */ +'use strict'; +const fetch = require('node-fetch'); + +// fetch() an URL and ignore the response body +(async function () { + (await fetch(process.argv[2])).body?.resume(); +})().catch((err) => { + process.nextTick(() => { + throw err; + }); +}); diff --git a/src/test/suite/oidc.test.ts b/src/test/suite/oidc.test.ts new file mode 100644 index 000000000..fc6a1450c --- /dev/null +++ b/src/test/suite/oidc.test.ts @@ -0,0 +1,436 @@ +import os from 'os'; +import path from 'path'; +import chai, { expect } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import fs from 'fs/promises'; +import sinon from 'sinon'; +import type { SinonStub } from 'sinon'; +import * as vscode from 'vscode'; +import { createHash } from 'crypto'; +import { before, after, afterEach, beforeEach } from 'mocha'; +import EventEmitter, { once } from 'events'; +import { ExtensionContextStub } from './stubs'; +import { StorageController } from '../../storage'; +import { TelemetryService } from '../../telemetry'; +import ConnectionController from '../../connectionController'; +import { StatusView } from '../../views'; + +import { MongoCluster } from 'mongodb-runner'; +import type { MongoClusterOptions } from 'mongodb-runner'; +import { OIDCMockProvider } from '@mongodb-js/oidc-mock-provider'; +import type { OIDCMockProviderConfig } from '@mongodb-js/oidc-mock-provider'; +import { ConnectionString } from 'mongodb-connection-string-url'; + +import launchMongoShell from '../../commands/launchMongoShell'; +import { getFullRange } from './suggestTestHelpers'; + +chai.use(chaiAsPromised); + +function hash(input: string): string { + return createHash('sha256').update(input).digest('hex').slice(0, 12); +} + +// Need to be provided via CI env because we can't get a hold for node.js exec +// path in our tests - they run inside a vscode process +const browserShellCommand = process.env.BROWSER_AUTH_COMMAND; + +const UNIQUE_TASK_ID = + process.env.GITHUB_RUN_ID && process.env.GITHUB_RUN_NUMBER + ? `${process.env.GITHUB_RUN_ID}-${process.env.GITHUB_RUN_NUMBER}` + : ''; +const defaultClusterOptions: MongoClusterOptions = { + topology: 'standalone', + tmpDir: path.join(os.tmpdir(), `vscode-tests-${hash(UNIQUE_TASK_ID)}-data`), + logDir: process.env.MONGODB_RUNNER_LOGDIR, + version: process.env.MONGODB_VERSION, +}; + +const DEFAULT_TOKEN_PAYLOAD = { + expires_in: 3600, + payload: { + // Define the user information stored inside the access tokens + groups: ['testgroup'], + sub: 'testuser', + aud: 'resource-server-audience-value', + }, +}; + +suite('OIDC Tests', function () { + this.timeout(50000); + + const extensionContextStub = new ExtensionContextStub(); + const testStorageController = new StorageController(extensionContextStub); + const testTelemetryService = new TelemetryService( + testStorageController, + extensionContextStub + ); + const testConnectionController = new ConnectionController({ + statusView: new StatusView(extensionContextStub), + storageController: testStorageController, + telemetryService: testTelemetryService, + }); + let showInformationMessageStub: SinonStub; + const sandbox = sinon.createSandbox(); + + // OIDC related variables + let getTokenPayload: typeof oidcMockProviderConfig.getTokenPayload = () => + DEFAULT_TOKEN_PAYLOAD; + let overrideRequestHandler: typeof oidcMockProviderConfig.overrideRequestHandler; + let oidcMockProviderConfig: OIDCMockProviderConfig; + let oidcMockProvider: OIDCMockProvider; + let oidcMockProviderEndpointAccesses: Record; + + let tmpdir: string; + let cluster: MongoCluster; + let connectionString: string; + + let createTerminalStub: SinonStub; + let sendTextStub: SinonStub; + + before(async function () { + if (process.platform !== 'linux') { + // OIDC is only supported on Linux in the 7.0+ enterprise server. + return this.skip(); + } + + oidcMockProviderEndpointAccesses = {}; + oidcMockProviderConfig = { + getTokenPayload(metadata: Parameters[0]) { + return getTokenPayload(metadata); + }, + overrideRequestHandler(url, req, res) { + const { pathname } = new URL(url); + oidcMockProviderEndpointAccesses[pathname] ??= 0; + oidcMockProviderEndpointAccesses[pathname]++; + return overrideRequestHandler?.(url, req, res); + }, + }; + oidcMockProvider = await OIDCMockProvider.create(oidcMockProviderConfig); + + tmpdir = path.join(os.tmpdir(), `vscode-oidc-${Date.now().toString(32)}`); + await fs.mkdir(path.join(tmpdir, 'db'), { recursive: true }); + const serverOidcConfig = { + issuer: oidcMockProvider.issuer, + clientId: 'testServer', + requestScopes: ['mongodbGroups'], + authorizationClaim: 'groups', + audience: 'resource-server-audience-value', + authNamePrefix: 'dev', + }; + + cluster = await MongoCluster.start({ + ...defaultClusterOptions, + version: '7.0.x', + downloadOptions: { enterprise: true }, + args: [ + '--setParameter', + 'authenticationMechanisms=SCRAM-SHA-256,MONGODB-OIDC', + // enableTestCommands allows using http:// issuers such as http://localhost + '--setParameter', + 'enableTestCommands=true', + '--setParameter', + `oidcIdentityProviders=${JSON.stringify([serverOidcConfig])}`, + ], + }); + + const cs = new ConnectionString(cluster.connectionString); + cs.searchParams.set('authMechanism', 'MONGODB-OIDC'); + + connectionString = cs.toString(); + }); + + after(async function () { + if (process.platform !== 'linux') { + return; + } + + await oidcMockProvider?.close(); + await cluster?.close(); + }); + + beforeEach(async function () { + sandbox.stub(testTelemetryService, 'trackNewConnection'); + showInformationMessageStub = sandbox.stub( + vscode.window, + 'showInformationMessage' + ); + + // This is required to follow through the redirect while establishing + // connection + await vscode.workspace + .getConfiguration('mdb') + .update('browserCommandForOIDCAuth', browserShellCommand); + + createTerminalStub = sandbox.stub(vscode.window, 'createTerminal'); + sendTextStub = sandbox.stub(); + createTerminalStub.returns({ + sendText: sendTextStub, + show: () => {}, + }); + }); + + afterEach(async function () { + // Reset our mock extension's state. + extensionContextStub._workspaceState = {}; + extensionContextStub._globalState = {}; + + await testConnectionController.disconnect(); + testConnectionController.clearAllConnections(); + + sandbox.restore(); + }); + + test('can successfully connect with a connection string', async function () { + const succesfullyConnected = + await testConnectionController.addNewConnectionStringAndConnect( + connectionString + ); + expect(succesfullyConnected).to.be.true; + + await launchMongoShell(testConnectionController); + expect(createTerminalStub).to.be.called; + + const terminalOptions: vscode.TerminalOptions = + createTerminalStub.firstCall.args[0]; + const terminalConnectionString = terminalOptions.env?.MDB_CONNECTION_STRING; + + if (!terminalConnectionString) { + expect.fail('Terminal connection string not found'); + } + const terminalCsWithoutAppName = new ConnectionString( + terminalConnectionString + ); + terminalCsWithoutAppName.searchParams.delete('appname'); + + expect(terminalCsWithoutAppName.toString()).to.equal(connectionString); + + const shellCommandText = sendTextStub.firstCall.args[0]; + expect(shellCommandText).to.equal('mongosh $MDB_CONNECTION_STRING;'); + + // Required for shell to share the OIDC state + expect(terminalOptions.env?.MONGOSH_OIDC_PARENT_HANDLE).to.not.be.undefined; + }); + + test('it persists tokens for further attempt if the settings is set to true', async function () { + await vscode.workspace + .getConfiguration('mdb') + .update('persistOIDCTokens', true); + let tokenFetchCalls = 0; + getTokenPayload = () => { + tokenFetchCalls++; + return DEFAULT_TOKEN_PAYLOAD; + }; + + expect( + await testConnectionController.addNewConnectionStringAndConnect( + connectionString + ) + ).to.be.true; + + const connectionId = testConnectionController.getActiveConnectionId(); + if (!connectionId) { + expect.fail('Connection id not found for active connection'); + } + + await testConnectionController.disconnect(); + + expect( + await testConnectionController.connectWithConnectionId(connectionId) + ).to.be.true; + expect(tokenFetchCalls).to.equal(1); + }); + + test('it will not persist tokens for further attempt if the settings is set to false', async function () { + await vscode.workspace + .getConfiguration('mdb') + .update('persistOIDCTokens', false); + let tokenFetchCalls = 0; + getTokenPayload = () => { + tokenFetchCalls++; + return DEFAULT_TOKEN_PAYLOAD; + }; + + expect( + await testConnectionController.addNewConnectionStringAndConnect( + connectionString + ) + ).to.be.true; + + const connectionId = testConnectionController.getActiveConnectionId(); + if (!connectionId) { + expect.fail('Connection id not found for active connection'); + } + + await testConnectionController.disconnect(); + + expect( + await testConnectionController.connectWithConnectionId(connectionId) + ).to.be.true; + expect(tokenFetchCalls).to.equal(2); + }); + + test('can cancel a connection attempt and then successfully connect', async function () { + const emitter = new EventEmitter(); + const secondConnectionEstablished = once( + emitter, + 'secondConnectionEstablished' + ); + overrideRequestHandler = async (url) => { + if (new URL(url).pathname === '/authorize') { + emitter.emit('authorizeEndpointCalled'); + // This does effectively mean that our 'fake browser' + // will never get a response from the authorization endpoint + // during the first connection attempt, and that therefore + // the local HTTP server will never have its redirect endpoint + // accessed. + await secondConnectionEstablished; + } + }; + + testConnectionController + .addNewConnectionStringAndConnect(connectionString) + .catch(() => { + // ignored + }); + + await once(emitter, 'authorizeEndpointCalled'); + overrideRequestHandler = () => {}; + const connected = + await testConnectionController.addNewConnectionStringAndConnect( + connectionString + ); + emitter.emit('secondConnectionEstablished'); + expect(connected).to.be.true; + }); + + test('can successfully re-authenticate', async function () { + showInformationMessageStub.resolves('Confirm'); + const originalReAuthHandler = + testConnectionController._reauthenticationHandler.bind( + testConnectionController + ); + let reAuthCalled = false; + let resolveReAuthPromise: (value?: unknown) => void; + const reAuthPromise = new Promise((resolve) => { + resolveReAuthPromise = resolve; + }); + sandbox + .stub(testConnectionController, '_reauthenticationHandler') + .callsFake(async () => { + reAuthCalled = true; + resolveReAuthPromise(); + await originalReAuthHandler(); + }); + let tokenFetchCalls = 0; + let afterReauth = false; + getTokenPayload = () => { + tokenFetchCalls++; + return { + ...DEFAULT_TOKEN_PAYLOAD, + ...(afterReauth ? {} : { expires_in: 1 }), + }; + }; + + expect( + await testConnectionController.addNewConnectionStringAndConnect( + connectionString + ) + ).to.be.true; + afterReauth = true; + + // Trigger a command on data service for reauthentication + while (reAuthCalled === false) { + await testConnectionController.getActiveDataService()?.count('x.y', {}); + } + + // Wait for reauthentication promise to resolve + await reAuthPromise; + + expect(tokenFetchCalls).to.equal(2); + expect(testConnectionController.isCurrentlyConnected()).to.be.true; + }); + + test('can decline re-authentication if wanted', async function () { + showInformationMessageStub.resolves('Declined'); + const originalReAuthHandler = + testConnectionController._reauthenticationHandler.bind( + testConnectionController + ); + let reAuthCalled = false; + let resolveReAuthPromise: (value?: unknown) => void; + const reAuthPromise = new Promise((resolve) => { + resolveReAuthPromise = resolve; + }); + sandbox + .stub(testConnectionController, '_reauthenticationHandler') + .callsFake(async () => { + reAuthCalled = true; + resolveReAuthPromise(); + await originalReAuthHandler(); + }); + let tokenFetchCalls = 0; + let afterReauth = false; + getTokenPayload = () => { + tokenFetchCalls++; + return { + ...DEFAULT_TOKEN_PAYLOAD, + ...(afterReauth ? {} : { expires_in: 1 }), + }; + }; + + expect( + await testConnectionController.addNewConnectionStringAndConnect( + connectionString + ) + ).to.be.true; + afterReauth = true; + + // Trigger a command on data service for reauthentication + while (reAuthCalled === false) { + await testConnectionController + .getActiveDataService() + ?.count('x.y', {}) + .catch((error) => { + expect(error.message).to.equal('Reauthentication declined by user'); + }); + } + + await reAuthPromise; + + // Because we declined the auth in showInformationMessage above + expect(tokenFetchCalls).to.equal(1); + expect(testConnectionController.isCurrentlyConnected()).to.be.false; + }); + + test('shares the oidc state also with the playgrounds', async function () { + let tokenFetchCalls = 0; + getTokenPayload = () => { + tokenFetchCalls++; + return DEFAULT_TOKEN_PAYLOAD; + }; + + expect( + await testConnectionController.addNewConnectionStringAndConnect( + connectionString + ) + ).to.be.true; + + await vscode.commands.executeCommand('mdb.createPlayground'); + + const editor = vscode.window.activeTextEditor; + if (!editor) { + throw new Error('Window active text editor is undefined'); + } + + const testDocumentUri = editor.document.uri; + const edit = new vscode.WorkspaceEdit(); + edit.replace( + testDocumentUri, + getFullRange(editor.document), + "use('random'); db.randomColl.find({}).count();" + ); + await vscode.workspace.applyEdit(edit); + await vscode.commands.executeCommand('mdb.runPlayground'); + expect(tokenFetchCalls).to.equal(1); + }); +});