diff --git a/src/configurations/dev.config.json b/src/configurations/dev.config.json index b5a403db2..e8fecc89e 100644 --- a/src/configurations/dev.config.json +++ b/src/configurations/dev.config.json @@ -1,7 +1,7 @@ { "service": { "downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}", - "version": "2.0.0-release.30", + "version": "2.0.0-release.39", "downloadFileNames": { "Windows_7_86": "win-x86-netcoreapp2.2.zip", "Windows_7_64": "win-x64-netcoreapp2.2.zip", diff --git a/src/constants/constants.ts b/src/constants/constants.ts index 7a7948869..837e10b5a 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -130,4 +130,6 @@ export const sqlToolsServiceConfigKey = 'service'; export const v1SqlToolsServiceConfigKey = 'v1Service'; export const scriptSelectText = 'SELECT TOP (1000) * FROM '; export const tenantDisplayName = 'Microsoft'; -export const firewallErrorMessage = 'To enable access, use the Windows Azure Management Portal or run sp_set_firewall_rule on the master database to create a firewall rule for this IP address or address range.'; \ No newline at end of file +export const firewallErrorMessage = 'To enable access, use the Windows Azure Management Portal or run sp_set_firewall_rule on the master database to create a firewall rule for this IP address or address range.'; +export const windowsResourceClientPath = 'SqlToolsResourceProviderService.exe'; +export const unixResourceClientPath = 'SqlToolsResourceProviderService'; \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index b5226b7f6..50b753f4c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -25,6 +25,9 @@ export function activate(context: vscode.ExtensionContext): Promise { if (applyLocalization) { LocalizedConstants.loadLocalizedConstants(vscode.env.language); } + + // Exposed for testing purposes + vscode.commands.registerCommand('mssql.getControllerForTests', () => controller); return controller.activate(); } @@ -39,6 +42,10 @@ export function deactivate(): void { /** * Exposed for testing purposes */ -export function getController(): MainController { +export async function getController(): Promise { + if (!controller) { + let savedController: MainController = await vscode.commands.executeCommand('mssql.getControllerForTests'); + return savedController; + } return controller; } diff --git a/src/firewall/firewallService.ts b/src/firewall/firewallService.ts index 340f905bc..abc381145 100644 --- a/src/firewall/firewallService.ts +++ b/src/firewall/firewallService.ts @@ -36,6 +36,13 @@ export class FirewallService { return this._account; } + /** + * Public for testing purposes only + */ + public set token(value: any) { + this._token = value; + } + private convertToAzureAccount(azureSession: AzureSession): Account { let tenant = { displayName: Constants.tenantDisplayName, diff --git a/src/languageservice/httpClient.ts b/src/languageservice/httpClient.ts index 05e803ec2..a916ca7b8 100644 --- a/src/languageservice/httpClient.ts +++ b/src/languageservice/httpClient.ts @@ -146,7 +146,7 @@ export default class HttpClient implements IHttpClient { /* * Interface to store the values needed to calculate download percentage */ -interface IDownloadProgress { +export interface IDownloadProgress { packageSize: number; downloadedBytes: number; downloadPercentage: number; diff --git a/src/languageservice/serviceclient.ts b/src/languageservice/serviceclient.ts index 70d090a18..f63330a75 100644 --- a/src/languageservice/serviceclient.ts +++ b/src/languageservice/serviceclient.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. * ------------------------------------------------------------------------------------------ */ 'use strict'; - import { ExtensionContext, workspace, window, OutputChannel, languages } from 'vscode'; import { LanguageClient, LanguageClientOptions, ServerOptions, @@ -190,11 +189,11 @@ export default class SqlToolsServiceClient { _channel.show(); } let installedServerPath = await this._server.downloadServerFiles(platformInfo.runtimeId); - this.initializeLanguageClient(installedServerPath, context); + this.initializeLanguageClient(installedServerPath, context, platformInfo.isWindows()); await this._client.onReady(); resolve(new ServerInitializationResult(true, true, installedServerPath)); } else { - this.initializeLanguageClient(serverPath, context); + this.initializeLanguageClient(serverPath, context, platformInfo.isWindows()); await this._client.onReady(); resolve(new ServerInitializationResult(false, true, serverPath)); } @@ -256,7 +255,7 @@ export default class SqlToolsServiceClient { }); } - private initializeLanguageClient(serverPath: string, context: ExtensionContext): void { + private initializeLanguageClient(serverPath: string, context: ExtensionContext, isWindows: boolean): void { if (serverPath === undefined) { Utils.logDebug(Constants.invalidServiceFilePath); throw new Error(Constants.invalidServiceFilePath); @@ -265,7 +264,8 @@ export default class SqlToolsServiceClient { self.initializeLanguageConfiguration(); let serverOptions: ServerOptions = this.createServerOptions(serverPath); this.client = this.createLanguageClient(serverOptions); - let resourcePath = path.join(path.dirname(serverPath), 'SqlToolsResourceProviderService.exe'); + let executablePath = isWindows ? Constants.windowsResourceClientPath : Constants.unixResourceClientPath; + let resourcePath = path.join(path.dirname(serverPath), executablePath); this._resourceClient = this.createResourceClient(resourcePath); if (context !== undefined) { diff --git a/test/firewallService.test.ts b/test/firewallService.test.ts new file mode 100644 index 000000000..440b00c78 --- /dev/null +++ b/test/firewallService.test.ts @@ -0,0 +1,83 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +import * as vscode from 'vscode'; +import * as TypeMoq from 'typemoq'; +import SqlToolsServiceClient from '../src/languageservice/serviceclient'; +import { FirewallService } from '../src/firewall/firewallService'; +import { HandleFirewallRuleRequest, HandleFirewallRuleResponse, + CreateFirewallRuleRequest, CreateFirewallRuleResponse, HandleFirewallRuleParams } from '../src/models/contracts/firewall/firewallRequest'; +import VscodeWrapper from '../src/controllers/vscodeWrapper'; +import { assert } from 'chai'; +import { AzureSession } from '../src/models/interfaces'; + + +suite('Firewall Service Tests', () => { + let firewallService: FirewallService; + let client: TypeMoq.IMock; + let vscodeWrapper: TypeMoq.IMock; + + setup(() => { + client = TypeMoq.Mock.ofType(SqlToolsServiceClient, TypeMoq.MockBehavior.Loose); + let mockHandleFirewallResponse: HandleFirewallRuleResponse = { + result: true, + ipAddress: '128.0.0.0' + }; + let mockCreateFirewallRuleResponse: CreateFirewallRuleResponse = { + result: true, + errorMessage: '' + }; + client.setup(c => c.sendResourceRequest(HandleFirewallRuleRequest.type, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockHandleFirewallResponse)); + client.setup(c => c.sendResourceRequest(CreateFirewallRuleRequest.type, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockCreateFirewallRuleResponse)); + vscodeWrapper = TypeMoq.Mock.ofType(VscodeWrapper, TypeMoq.MockBehavior.Loose); + let mockSession: AzureSession = { + environment: undefined, + userId: 'test', + tenantId: 'test', + credentials: undefined + }; + let mockExtension: vscode.Extension = { + id: '', + extensionKind: undefined, + extensionPath: '', + isActive: true, + packageJSON: undefined, + activate: undefined, + exports: { + sessions: [mockSession] + } + }; + vscodeWrapper.setup(v => v.azureAccountExtension).returns(() => mockExtension); + firewallService = new FirewallService(client.object, vscodeWrapper.object); + }); + + test('isSignedIn Test', () => { + let isSignedIn = firewallService.isSignedIn; + assert.isNotTrue(isSignedIn, 'Firewall Service should not be signed in initially'); + firewallService.isSignedIn = true; + assert.isTrue(firewallService.isSignedIn, 'Firewall Service should be signed in once set'); + }); + + test('Handle Firewall Rule test', async () => { + let result = await firewallService.handleFirewallRule(12345, 'firewall error!'); + assert.isNotNull(result, 'Handle Firewall Rule request is sent successfully'); + }); + + test('Create Firewall Rule Test', async () => { + let server = 'test_server'; + let startIpAddress = '1.2.3.1'; + let endIpAddress = '1.2.3.255'; + firewallService.isSignedIn = true; + let mockToken = { + expiresOn: new Date(), + resource: undefined, + tokenType: 'test', + accessToken: 'test_token' + } + firewallService.token = mockToken; + let result = await firewallService.createFirewallRule(server, startIpAddress, endIpAddress); + assert.isNotNull(result, 'Create Firewall Rule request is sent successfully'); + }); +}); + diff --git a/test/initialization.test.ts b/test/initialization.test.ts index 9d9e28f6c..b7d101e43 100644 --- a/test/initialization.test.ts +++ b/test/initialization.test.ts @@ -32,13 +32,14 @@ suite('Initialization Tests', () => { Telemetry.disable(); }); - test('Connection manager is initialized properly', () => { + test('Connection manager is initialized properly', (done) => { // Wait for the extension to activate - ensureExtensionIsActive().then(() => { + ensureExtensionIsActive().then(async () => { // Verify that the connection manager was initialized properly - let controller: MainController = Extension.getController(); + let controller: MainController = await Extension.getController(); let connectionManager: ConnectionManager = controller.connectionManager; assert.notStrictEqual(undefined, connectionManager.client); + done(); }); }); }); diff --git a/test/languageService.test.ts b/test/languageService.test.ts new file mode 100644 index 000000000..cf5df382a --- /dev/null +++ b/test/languageService.test.ts @@ -0,0 +1,70 @@ +import DecompressProvider from "../src/languageservice/decompressProvider"; +import { IPackage, IStatusView } from "../src/languageservice/interfaces"; +import { ILogger } from "../src/models/interfaces"; +import { assert } from "chai"; +import HttpClient, { IDownloadProgress } from "../src/languageservice/httpClient"; +import { HttpModule } from "../out/src/views/htmlcontent/src/js/lib/@angular/http"; + +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +suite('Language Service Tests', () => { + + suite('Decompress Provider Tests', () => { + let decompressProvider = new DecompressProvider(); + + test('Decompress package test', async () => { + let testPackage: IPackage = { + url: 'test_url', + tmpFile: undefined + }; + let testLogger: ILogger = { + logDebug: undefined, + increaseIndent: undefined, + decreaseIndent: undefined, + append: undefined, + appendLine: undefined + + }; + try { + await decompressProvider.decompress(testPackage, testLogger); + } catch (err) { + assert.isNotNull(err, 'Should throw an error'); + } + }); + }); + + suite('HttpClient Tests', () => { + let httpClient = new HttpClient(); + + test('handleDataReceivedEvent test', () => { + let mockProgress: IDownloadProgress = { + packageSize: 10, + downloadedBytes: 0, + downloadPercentage: 0, + dots: 0 + }; + let testLogger: ILogger = { + logDebug: () => undefined, + increaseIndent: () => undefined, + decreaseIndent: () => undefined, + append: () => undefined, + appendLine: () => undefined + + }; + let mockStatusView: IStatusView = { + installingService: () => undefined, + serviceInstalled: () => undefined, + serviceInstallationFailed: () => undefined, + updateServiceDownloadingProgress: (downloadPercentage: number) => undefined + } + httpClient.handleDataReceivedEvent(mockProgress, [1,2,3,4,5], testLogger, mockStatusView); + assert.strictEqual(mockProgress.downloadPercentage, 50); + assert.strictEqual(mockProgress.downloadedBytes, 5); + assert.strictEqual(mockProgress.dots, 10); + }); + }); +}); + diff --git a/test/mainController.test.ts b/test/mainController.test.ts index a1bedef97..0a5731b09 100644 --- a/test/mainController.test.ts +++ b/test/mainController.test.ts @@ -17,7 +17,7 @@ import VscodeWrapper from '../src/controllers/vscodeWrapper'; import { TestExtensionContext } from './stubs'; import assert = require('assert'); -suite('MainController Tests', async () => { +suite('MainController Tests', () => { let document: vscode.TextDocument; let newDocument: vscode.TextDocument; let mainController: MainController; @@ -28,52 +28,7 @@ suite('MainController Tests', async () => { let docUriCallback: string; let newDocUriCallback: string; - function ensureExtensionIsActive(): Promise { - return new Promise((resolve, reject) => { - waitForExtensionToBeActive(resolve); - }); - } - - function waitForExtensionToBeActive(resolve): void { - if (typeof(vscode.extensions.getExtension('ms-mssql.mssql')) === 'undefined' || - !vscode.extensions.getExtension('ms-mssql.mssql').isActive) { - setTimeout(waitForExtensionToBeActive.bind(this, resolve), 50); - } else { - resolve(); - } - } - - async function setupMaincontroller(): Promise { - return ensureExtensionIsActive().then(() => { - // Using the mainController that was instantiated with the extension - mainController = Extension.getController(); - - // Setting up a mocked connectionManager - connectionManager = TypeMoq.Mock.ofType(ConnectionManager); - mainController.connectionManager = connectionManager.object; - - untitledSqlDocumentService = TypeMoq.Mock.ofType(UntitledSqlDocumentService); - mainController.untitledSqlDocumentService = untitledSqlDocumentService.object; - - // Watching these functions and input paramters - connectionManager.setup(x => x.onDidOpenTextDocument(TypeMoq.It.isAny())).callback((doc) => { - docUriCallback = doc.uri.toString(); - }); - - connectionManager.setup(x => x.onDidCloseTextDocument(TypeMoq.It.isAny())).callback((doc) => { - docUriCallback = doc.uri.toString(); - }); - - connectionManager.setup(x => x.transferFileConnection(TypeMoq.It.isAny(), TypeMoq.It.isAny())).callback((doc, newDoc) => { - docUriCallback = doc; - newDocUriCallback = newDoc; - }); - - return mainController; - }); - } - - setup(() => { + setup(async () => { // Setup a standard document and a new document docUri = 'docURI.sql'; newDocUri = 'newDocURI.sql'; @@ -99,9 +54,32 @@ suite('MainController Tests', async () => { // Resetting call back variables docUriCallback = ''; newDocUriCallback = ''; - }); - mainController = await setupMaincontroller(); + + // Using the mainController that was instantiated with the extension + mainController = await Extension.getController(); + + // Setting up a mocked connectionManager + connectionManager = TypeMoq.Mock.ofType(ConnectionManager); + mainController.connectionManager = connectionManager.object; + + untitledSqlDocumentService = TypeMoq.Mock.ofType(UntitledSqlDocumentService); + mainController.untitledSqlDocumentService = untitledSqlDocumentService.object; + + // Watching these functions and input paramters + connectionManager.setup(x => x.onDidOpenTextDocument(TypeMoq.It.isAny())).callback((doc) => { + docUriCallback = doc.uri.toString(); + }); + + connectionManager.setup(x => x.onDidCloseTextDocument(TypeMoq.It.isAny())).callback((doc) => { + docUriCallback = doc.uri.toString(); + }); + + connectionManager.setup(x => x.transferFileConnection(TypeMoq.It.isAny(), TypeMoq.It.isAny())).callback((doc, newDoc) => { + docUriCallback = doc; + newDocUriCallback = newDoc; + }); + }); // Standard closed document event test test('onDidCloseTextDocument should propogate onDidCloseTextDocument to connectionManager' , () => { @@ -115,24 +93,6 @@ suite('MainController Tests', async () => { } }); - // Renamed file event test - test('onDidCloseTextDocument should call renamedDoc function when rename occurs' , done => { - // A renamed doc constitutes an openDoc event directly followed by a closeDoc event - mainController.onDidOpenTextDocument(newDocument); - mainController.onDidCloseTextDocument(document); - - // Verify renameDoc function was called - try { - connectionManager.verify(x => x.transferFileConnection(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once()); - assert.equal(docUriCallback, document.uri.toString()); - assert.equal(newDocUriCallback, newDocument.uri.toString()); - done(); - } catch (err) { - done(new Error(err)); - } - }); - - // Saved Untitled file event test test('onDidCloseTextDocument should call untitledDoc function when an untitled file is saved' , done => { // Scheme of older doc must be untitled @@ -154,6 +114,23 @@ suite('MainController Tests', async () => { } }); + // Renamed file event test + test('onDidCloseTextDocument should call renamedDoc function when rename occurs' , done => { + // A renamed doc constitutes an openDoc event directly followed by a closeDoc event + mainController.onDidOpenTextDocument(newDocument); + mainController.onDidCloseTextDocument(document); + + // Verify renameDoc function was called + try { + connectionManager.verify(x => x.transferFileConnection(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once()); + assert.equal(docUriCallback, document.uri.toString()); + assert.equal(newDocUriCallback, newDocument.uri.toString()); + done(); + } catch (err) { + done(new Error(err)); + } + }); + // Closed document event called to test rename and untitled save file event timeouts test('onDidCloseTextDocument should propogate to the connectionManager even if a special event occured before it' , done => {