Skip to content

Commit 73d22ee

Browse files
authored
VSCODE-110: Enable linux tests with graphics workaround for credential store (#81)
1 parent ba707e1 commit 73d22ee

File tree

7 files changed

+158
-68
lines changed

7 files changed

+158
-68
lines changed

azure-pipelines.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,8 @@ trigger:
99
# https://docs.microsoft.com/en-us/azure/devops/pipelines/agents/hosted?view=azure-devops
1010
strategy:
1111
matrix:
12-
# # Linux testing is currently disabled because of issues with
13-
# # headless linux & keytar. Tracked: VSCODE-110
14-
# linux:
15-
# imageName: 'ubuntu-latest'
12+
linux:
13+
imageName: 'ubuntu-latest'
1614
mac:
1715
imageName: 'macos-latest'
1816
windows:

src/connectionController.ts

Lines changed: 18 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,31 @@ import { v4 as uuidv4 } from 'uuid';
22
import * as vscode from 'vscode';
33
import Connection = require('mongodb-connection-model/lib/model');
44
import DataService = require('mongodb-data-service');
5-
import * as keytarType from 'keytar';
5+
66
import { ConnectionModelType } from './connectionModelType';
77
import { DataServiceType } from './dataServiceType';
88
import { createLogger } from './logging';
99
import { StatusView } from './views';
1010
import { EventEmitter } from 'events';
1111
import { StorageController, StorageVariables } from './storage';
1212
import { SavedConnection, StorageScope } from './storage/storageController';
13-
import { getNodeModule } from './utils/getNodeModule';
1413
import TelemetryController from './telemetry/telemetryController';
14+
import { ext } from './extensionConstants';
1515

1616
const { name, version } = require('../package.json');
1717
const log = createLogger('connection controller');
1818
const MAX_CONNECTION_NAME_LENGTH = 512;
1919

20-
type KeyTar = typeof keytarType;
21-
2220
export enum DataServiceEventTypes {
2321
CONNECTIONS_DID_CHANGE = 'CONNECTIONS_DID_CHANGE',
2422
ACTIVE_CONNECTION_CHANGED = 'ACTIVE_CONNECTION_CHANGED',
25-
ACTIVE_CONNECTION_CHANGING = 'ACTIVE_CONNECTION_CHANGING'
23+
ACTIVE_CONNECTION_CHANGING = 'ACTIVE_CONNECTION_CHANGING',
2624
}
2725

2826
export enum ConnectionTypes {
2927
CONNECTION_FORM = 'CONNECTION_FORM',
3028
CONNECTION_STRING = 'CONNECTION_STRING',
31-
CONNECTION_ID = 'CONNECTION_ID'
29+
CONNECTION_ID = 'CONNECTION_ID',
3230
}
3331

3432
export type SavedConnectionInformation = {
@@ -48,8 +46,6 @@ export default class ConnectionController {
4846
} = {};
4947

5048
private readonly _serviceName = 'mdb.vscode.savedConnections';
51-
private _keytar: KeyTar | undefined;
52-
5349
_activeDataService: null | DataServiceType = null;
5450
_activeConnectionModel: null | ConnectionModelType = null;
5551
private _currentConnectionId: null | string = null;
@@ -73,33 +69,20 @@ export default class ConnectionController {
7369
this._statusView = _statusView;
7470
this._storageController = storageController;
7571
this._telemetryController = telemetryController;
76-
77-
try {
78-
// We load keytar in two different ways. This is because when the
79-
// extension is webpacked it requires the vscode external keytar dependency
80-
// differently then our testing development environment.
81-
this._keytar = require('keytar');
82-
83-
if (!this._keytar) {
84-
this._keytar = getNodeModule<typeof keytarType>('keytar');
85-
}
86-
} catch (err) {
87-
// Couldn't load keytar, proceed without storing & loading connections.
88-
}
8972
}
9073

9174
_loadSavedConnection = async (
9275
connectionId: string,
9376
savedConnection: SavedConnection
9477
): Promise<void> => {
95-
if (!this._keytar) {
78+
if (!ext.keytarModule) {
9679
return;
9780
}
9881

9982
let loadedSavedConnection: LoadedConnection;
10083

10184
try {
102-
const unparsedConnectionInformation = await this._keytar.getPassword(
85+
const unparsedConnectionInformation = await ext.keytarModule.getPassword(
10386
this._serviceName,
10487
connectionId
10588
);
@@ -138,7 +121,7 @@ export default class ConnectionController {
138121
};
139122

140123
loadSavedConnections = async (): Promise<void> => {
141-
if (!this._keytar) {
124+
if (!ext.keytarModule) {
142125
return;
143126
}
144127

@@ -297,10 +280,10 @@ export default class ConnectionController {
297280

298281
this._connections[connectionId] = newLoadedConnection;
299282

300-
if (this._keytar) {
283+
if (ext.keytarModule) {
301284
const connectionInfoAsString = JSON.stringify(connectionInformation);
302285

303-
await this._keytar.setPassword(
286+
await ext.keytarModule.setPassword(
304287
this._serviceName,
305288
connectionId,
306289
connectionInfoAsString
@@ -496,8 +479,8 @@ export default class ConnectionController {
496479
): Promise<void> => {
497480
delete this._connections[connectionId];
498481

499-
if (this._keytar) {
500-
await this._keytar.deletePassword(this._serviceName, connectionId);
482+
if (ext.keytarModule) {
483+
await ext.keytarModule.deletePassword(this._serviceName, connectionId);
501484
// We only remove the connection from the saved connections if we
502485
// have deleted the connection information with keytar.
503486
this._storageController.removeConnection(connectionId);
@@ -595,13 +578,13 @@ export default class ConnectionController {
595578
const connectionNameToRemove:
596579
| string
597580
| undefined = await vscode.window.showQuickPick(
598-
connectionIds.map(
599-
(id, index) => `${index + 1}: ${this._connections[id].name}`
600-
),
601-
{
602-
placeHolder: 'Choose a connection to remove...'
603-
}
604-
);
581+
connectionIds.map(
582+
(id, index) => `${index + 1}: ${this._connections[id].name}`
583+
),
584+
{
585+
placeHolder: 'Choose a connection to remove...'
586+
}
587+
);
605588

606589
if (!connectionNameToRemove) {
607590
return Promise.resolve(false);

src/extension.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import * as vscode from 'vscode';
44

55
import { ext } from './extensionConstants';
6+
import { createKeytar } from './utils/keytar';
67
import { createLogger } from './logging';
78
const log = createLogger('extension.ts');
89

@@ -26,6 +27,13 @@ export function activate(context: vscode.ExtensionContext): void {
2627
log.info('activate extension called');
2728

2829
ext.context = context;
30+
31+
try {
32+
ext.keytarModule = createKeytar();
33+
} catch (err) {
34+
// Couldn't load keytar, proceed without storing & loading connections.
35+
}
36+
2937
mdbExtension = new MDBExtensionController(context);
3038
mdbExtension.activate();
3139

src/extensionConstants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { ExtensionContext } from 'vscode';
2+
import { KeytarInterface } from './utils/keytar';
23

34
// eslint-disable-next-line @typescript-eslint/no-namespace
45
export namespace ext {
56
export let context: ExtensionContext;
7+
export let keytarModule: KeytarInterface | undefined;
68
}
79

810
export function getImagesPath(): string {

src/test/suite/index.ts

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@ import path = require('path');
55
import * as keytarType from 'keytar';
66

77
import MDBExtensionController from '../../mdbExtensionController';
8+
import { ext } from '../../extensionConstants';
9+
import KeytarStub from './keytarStub';
810
import { TestExtensionContext } from './stubs';
911
import { mdbTestExtension } from './stubbableMdbExtension';
1012

13+
type KeyTar = typeof keytarType;
14+
1115
export function run(): Promise<void> {
1216
const reporterOptions = {
1317
spec: '-',
@@ -37,46 +41,25 @@ export function run(): Promise<void> {
3741
);
3842
mdbTestExtension.testExtensionController.activate();
3943

44+
// We avoid using the user's credential store when running tests
45+
// in order to ensure we're not polluting the credential store
46+
// and because its tough to get the credential store running on
47+
// headless linux.
48+
ext.keytarModule = new KeytarStub();
49+
4050
// Disable metrics.
4151
vscode.workspace.getConfiguration('mdb').update('sendTelemetry', false);
4252

4353
// Disable the dialogue for prompting the user where to store the connection.
4454
vscode.workspace
4555
.getConfiguration('mdb.connectionSaving')
4656
.update('hideOptionToChooseWhereToSaveNewConnections', true)
47-
.then(async () => {
48-
// We require keytar in runtime because it is a vscode provided
49-
// native node module.
50-
const keytar: typeof keytarType = require('keytar');
51-
const existingCredentials = await keytar.findCredentials(
52-
'mdb.vscode.savedConnections'
53-
);
54-
57+
.then(() => {
5558
// Add files to the test suite.
5659
files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f)));
5760
try {
5861
// Run the mocha test.
59-
mocha.run(async (failures) => {
60-
// After tests are run we clear any passwords added
61-
// to local secure storage.
62-
const postRunCredentials = await keytar.findCredentials(
63-
'mdb.vscode.savedConnections'
64-
);
65-
postRunCredentials.forEach((credential) => {
66-
if (
67-
!existingCredentials.find(
68-
(existingCredential) =>
69-
existingCredential.account === credential.account
70-
)
71-
) {
72-
// If the credential is newly added, we remove it.
73-
keytar.deletePassword(
74-
'mdb.vscode.savedConnections',
75-
credential.account
76-
);
77-
}
78-
});
79-
62+
mocha.run((failures) => {
8063
if (failures > 0) {
8164
e(new Error(`${failures} tests failed.`));
8265
} else {

src/test/suite/keytarStub.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { KeytarInterface } from '../../utils/keytar';
2+
3+
const retrievalDelay = 1; // ms simulated delay on keytar methods.
4+
5+
export default class KeytarStub implements KeytarInterface {
6+
private _services: Map<string, Map<string, string>> = new Map<string, Map<string, string>>();
7+
8+
public async findCredentials(service: string): Promise<Map<string, string> | undefined> {
9+
await this.delay();
10+
const savedServices = this._services.get(service);
11+
if (savedServices) {
12+
return savedServices;
13+
}
14+
15+
return undefined;
16+
}
17+
18+
public async getPassword(service: string, account: string): Promise<string | null> {
19+
await this.delay();
20+
const savedService = this._services.get(service);
21+
if (savedService) {
22+
const savedAccount = savedService.get(account);
23+
24+
if (savedAccount !== undefined) {
25+
return savedAccount;
26+
}
27+
}
28+
29+
return null;
30+
}
31+
32+
public async setPassword(service: string, account: string, password: string): Promise<void> {
33+
await this.delay();
34+
let savedService = this._services.get(service);
35+
if (!savedService) {
36+
savedService = new Map<string, string>();
37+
this._services.set(service, savedService);
38+
}
39+
40+
savedService.set(account, password);
41+
}
42+
43+
public async deletePassword(service: string, account: string): Promise<boolean> {
44+
await this.delay();
45+
const savedService = this._services.get(service);
46+
if (savedService) {
47+
if (savedService.has(account)) {
48+
savedService.delete(account);
49+
return true;
50+
}
51+
}
52+
53+
return false;
54+
}
55+
56+
private async delay(): Promise<void> {
57+
return new Promise<void>(resolve => {
58+
setTimeout(() => {
59+
resolve();
60+
}, retrievalDelay);
61+
});
62+
}
63+
}

src/utils/keytar.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import * as keytarType from 'keytar';
2+
3+
import { getNodeModule } from './getNodeModule';
4+
5+
export interface KeytarInterface {
6+
/**
7+
* Get the stored password for the service and account.
8+
*
9+
* @param service The string service name.
10+
* @param account The string account name.
11+
*
12+
* @returns A promise for the password string.
13+
*/
14+
getPassword(service: string, account: string): Promise<string | null>;
15+
16+
/**
17+
* Add the password for the service and account to the keychain.
18+
*
19+
* @param service The string service name.
20+
* @param account The string account name.
21+
* @param password The string password.
22+
*
23+
* @returns A promise for the set password completion.
24+
*/
25+
setPassword(
26+
service: string,
27+
account: string,
28+
password: string
29+
): Promise<void>;
30+
31+
/**
32+
* Delete the stored password for the service and account.
33+
*
34+
* @param service The string service name.
35+
* @param account The string account name.
36+
*
37+
* @returns A promise for the deletion status. True on success.
38+
*/
39+
deletePassword(service: string, account: string): Promise<boolean>;
40+
}
41+
42+
export const createKeytar = (): KeytarInterface | undefined => {
43+
// We load keytar in two different ways. This is because when the
44+
// extension is webpacked it requires the vscode external keytar dependency
45+
// differently then our development environment.
46+
let keytarModule: KeytarInterface | undefined = require('keytar');
47+
48+
if (!keytarModule) {
49+
keytarModule = getNodeModule<typeof keytarType>('keytar');
50+
}
51+
52+
return keytarModule;
53+
};

0 commit comments

Comments
 (0)