From 972b6c9e4db89b3008222a22c3bdba4564064082 Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Wed, 24 Sep 2025 15:49:59 -0700 Subject: [PATCH 01/30] converted reactWebviewPanelController tests --- test/unit/reactWebviewPanelController.test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/unit/reactWebviewPanelController.test.ts b/test/unit/reactWebviewPanelController.test.ts index 279190b9cf..f46d7ee6fb 100644 --- a/test/unit/reactWebviewPanelController.test.ts +++ b/test/unit/reactWebviewPanelController.test.ts @@ -8,11 +8,10 @@ import * as locConstants from "../../src/constants/locConstants"; import * as sinon from "sinon"; import * as utils from "../../src/utils/utils"; import * as vscode from "vscode"; -import * as TypeMoq from "typemoq"; import { MssqlWebviewPanelOptions } from "../../src/sharedInterfaces/webview"; import { ReactWebviewPanelController } from "../../src/controllers/reactWebviewPanelController"; -import { stubTelemetry } from "./utils"; +import { stubTelemetry, stubVscodeWrapper } from "./utils"; import VscodeWrapper from "../../src/controllers/vscodeWrapper"; suite("ReactWebviewPanelController", () => { @@ -66,7 +65,7 @@ suite("ReactWebviewPanelController", () => { } as unknown as vscode.ExtensionContext; sandbox.stub(utils, "getNonce").returns("test-nonce"); - vscodeWrapper.reset(); + vscodeWrapper = stubVscodeWrapper(sandbox); }); teardown(() => { @@ -361,7 +360,7 @@ interface TestReducers { decrement: { amount: number }; } -const vscodeWrapper = TypeMoq.Mock.ofType(VscodeWrapper, TypeMoq.MockBehavior.Loose); +let vscodeWrapper: sinon.SinonStubbedInstance; class TestReactWebviewPanelController extends ReactWebviewPanelController< TestState, @@ -369,6 +368,6 @@ class TestReactWebviewPanelController extends ReactWebviewPanelControll TResult > { constructor(context: vscode.ExtensionContext, options: MssqlWebviewPanelOptions) { - super(context, vscodeWrapper.object, "testSource", "testSource", { count: 0 }, options); + super(context, vscodeWrapper!, "testSource", "testSource", { count: 0 }, options); } } From f026f6823f8dd643208491fc77e07a014541feba Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Fri, 26 Sep 2025 14:25:49 -0700 Subject: [PATCH 02/30] converted userSurvey tests --- test/unit/userSurvey.test.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/unit/userSurvey.test.ts b/test/unit/userSurvey.test.ts index f2a73ad6d8..6427da16db 100644 --- a/test/unit/userSurvey.test.ts +++ b/test/unit/userSurvey.test.ts @@ -7,13 +7,11 @@ import * as assert from "assert"; import * as locConstants from "../../src/constants/locConstants"; import * as sinon from "sinon"; import * as vscode from "vscode"; -import * as TypeMoq from "typemoq"; import { TelemetryActions, TelemetryViews } from "../../src/sharedInterfaces/telemetry"; import { UserSurvey } from "../../src/nps/userSurvey"; -import { stubTelemetry } from "./utils"; -import VscodeWrapper from "../../src/controllers/vscodeWrapper"; +import { stubTelemetry, stubVscodeWrapper } from "./utils"; suite("UserSurvey Tests", () => { let sandbox: sinon.SinonSandbox; @@ -28,7 +26,7 @@ suite("UserSurvey Tests", () => { update: sandbox.stub(), }; - const vscodeWrapper = TypeMoq.Mock.ofType(VscodeWrapper, TypeMoq.MockBehavior.Loose); + const vscodeWrapper = stubVscodeWrapper(sandbox); context = { globalState: globalState, @@ -36,7 +34,7 @@ suite("UserSurvey Tests", () => { }; showInformationMessageStub = sandbox.stub(vscode.window, "showInformationMessage"); - UserSurvey.createInstance(context, vscodeWrapper.object); + UserSurvey.createInstance(context, vscodeWrapper); }); teardown(() => { From bd2099771833e4114c7f657878e2f745fadf0c6c Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Fri, 26 Sep 2025 14:32:23 -0700 Subject: [PATCH 03/30] test: replace TypeMoq with Sinon in azureResourceService tests --- test/unit/azureResourceService.test.ts | 373 ++++++++++++------------- 1 file changed, 182 insertions(+), 191 deletions(-) diff --git a/test/unit/azureResourceService.test.ts b/test/unit/azureResourceService.test.ts index 74a6b00e7b..0c42d2039b 100644 --- a/test/unit/azureResourceService.test.ts +++ b/test/unit/azureResourceService.test.ts @@ -1,191 +1,182 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from "assert"; -import * as TypeMoq from "typemoq"; -import { AzureAuthType, IAccount } from "../../src/models/contracts/azure"; -import { - SubscriptionClient, - Subscription, - Subscriptions, - Location, -} from "@azure/arm-subscriptions"; -import { PagedAsyncIterableIterator } from "@azure/core-paging"; -import { ResourceGroup, ResourceGroups, ResourceManagementClient } from "@azure/arm-resources"; -import { AzureResourceController } from "../../src/azure/azureResourceController"; -import { AzureAccountService } from "../../src/services/azureAccountService"; -import { TokenCredentialWrapper } from "../../src/azure/credentialWrapper"; -import allSettings from "../../src/azure/providerSettings"; -import { IAzureAccountSession } from "vscode-mssql"; - -export interface ITestContext { - azureAccountService: TypeMoq.IMock; - accounts: IAccount[]; - session: IAzureAccountSession; - subscriptionClient: TypeMoq.IMock; - subscriptions: Subscription[]; - locations: Location[]; - groups: ResourceGroup[]; -} - -export function createContext(): ITestContext { - const accounts = [ - { - key: undefined!, - displayInfo: undefined!, - properties: { - tenants: [ - { - id: "", - displayName: "", - }, - ], - azureAuthType: AzureAuthType.AuthCodeGrant, - isMsAccount: false, - owningTenant: { - id: "", - displayName: "", - }, - providerSettings: allSettings, - }, - isStale: false, - isSignedIn: true, - }, - ]; - const subscriptions: Subscription[] = [{ subscriptionId: "id1" }, { subscriptionId: "id2" }]; - const locations: Location[] = [{ id: "id1" }, { id: "id2" }]; - const groups: ResourceGroup[] = [ - { id: "id1", location: "l1" }, - { id: "id2", location: "l2" }, - ]; - const session0: IAzureAccountSession = { - account: accounts[0], - subscription: subscriptions[0], - tenantId: "tenantId", - token: { - key: "", - token: "", - tokenType: "", - }, - }; - const session1: IAzureAccountSession = { - account: accounts[0], - subscription: subscriptions[1], - tenantId: "tenantId", - token: { - key: "", - token: "", - tokenType: "", - }, - }; - const azureAccountService = TypeMoq.Mock.ofType(AzureAccountService, undefined, undefined); - azureAccountService.setup((x) => x.getAccounts()).returns(() => Promise.resolve(accounts)); - azureAccountService.setup((x) => x.addAccount()).returns(() => Promise.resolve(accounts[0])); - azureAccountService - .setup((x) => x.getAccountSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => - Promise.resolve({ - key: "", - token: "", - tokenType: "", - }), - ); - azureAccountService - .setup((x) => x.getAccountSessions(TypeMoq.It.isAny())) - .returns(() => Promise.resolve([session0, session1])); - - return { - groups: groups, - locations: locations, - subscriptions: subscriptions, - subscriptionClient: TypeMoq.Mock.ofType( - SubscriptionClient, - undefined, - new TokenCredentialWrapper(session0.token), - ), - session: session0, - accounts: accounts, - azureAccountService: azureAccountService, - }; -} - -suite("Azure SQL client", function (): void { - test("Should return locations successfully", async function (): Promise { - const testContext = createContext(); - const azureSqlClient = new AzureResourceController( - () => testContext.subscriptionClient.object, - ); - - let index = 0; - let maxLength = testContext.locations.length; - const pages: PagedAsyncIterableIterator = { - next: () => { - if (index < maxLength) { - return Promise.resolve({ - done: false, - value: testContext.locations[index++], - }); - } else { - return Promise.resolve({ done: true, value: undefined }); - } - }, - byPage: () => undefined!, - [Symbol.asyncIterator]: undefined!, - }; - const subscriptions: Subscriptions = { - listLocations: () => pages, - list: () => undefined!, - get: () => undefined!, - }; - testContext.subscriptionClient.setup((x) => x.subscriptions).returns(() => subscriptions); - - const result = await azureSqlClient.getLocations(testContext.session); - assert.deepStrictEqual(result.length, testContext.locations.length); - }); - - test("Should return resource groups successfully", async function (): Promise { - const testContext = createContext(); - const azureSqlClient = new AzureResourceController(undefined, () => groupClient.object); - - let index = 0; - let maxLength = testContext.groups.length; - const pages: PagedAsyncIterableIterator = { - next: () => { - if (index < maxLength) { - return Promise.resolve({ - done: false, - value: testContext.groups[index++], - }); - } else { - return Promise.resolve({ done: true, value: undefined }); - } - }, - byPage: () => undefined!, - [Symbol.asyncIterator]: undefined!, - }; - const resourceGroups: ResourceGroups = { - list: () => pages, - get: () => undefined!, - beginDelete: undefined!, - beginDeleteAndWait: undefined!, - beginExportTemplate: undefined!, - beginExportTemplateAndWait: undefined!, - checkExistence: undefined!, - createOrUpdate: undefined!, - update: undefined!, - }; - const groupClient = TypeMoq.Mock.ofType( - ResourceManagementClient, - undefined, - new TokenCredentialWrapper(testContext.session.token), - testContext.subscriptions[0].subscriptionId, - ); - groupClient.setup((x) => x.resourceGroups).returns(() => resourceGroups); - - const result = await azureSqlClient.getResourceGroups(testContext.session); - assert.deepStrictEqual(result.length, testContext.groups.length); - assert.deepStrictEqual(result[0].location, testContext.groups[0].location); - }); -}); +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from "assert"; +import * as sinon from "sinon"; +import { AzureAuthType, IAccount } from "../../src/models/contracts/azure"; +import { + SubscriptionClient, + Subscription, + Subscriptions, + Location, +} from "@azure/arm-subscriptions"; +import { PagedAsyncIterableIterator } from "@azure/core-paging"; +import { ResourceGroup, ResourceGroups, ResourceManagementClient } from "@azure/arm-resources"; +import { AzureResourceController } from "../../src/azure/azureResourceController"; +import { AzureAccountService } from "../../src/services/azureAccountService"; +import allSettings from "../../src/azure/providerSettings"; +import { IAzureAccountSession } from "vscode-mssql"; + +export interface ITestContext { + azureAccountService: sinon.SinonStubbedInstance; + accounts: IAccount[]; + session: IAzureAccountSession; + subscriptionClient: sinon.SinonStubbedInstance; + subscriptions: Subscription[]; + locations: Location[]; + groups: ResourceGroup[]; +} + +export function createContext(): ITestContext { + const accounts = [ + { + key: undefined!, + displayInfo: undefined!, + properties: { + tenants: [ + { + id: "", + displayName: "", + }, + ], + azureAuthType: AzureAuthType.AuthCodeGrant, + isMsAccount: false, + owningTenant: { + id: "", + displayName: "", + }, + providerSettings: allSettings, + }, + isStale: false, + isSignedIn: true, + }, + ]; + const subscriptions: Subscription[] = [{ subscriptionId: "id1" }, { subscriptionId: "id2" }]; + const locations: Location[] = [{ id: "id1" }, { id: "id2" }]; + const groups: ResourceGroup[] = [ + { id: "id1", location: "l1" }, + { id: "id2", location: "l2" }, + ]; + const session0: IAzureAccountSession = { + account: accounts[0], + subscription: subscriptions[0], + tenantId: "tenantId", + token: { + key: "", + token: "", + tokenType: "", + }, + }; + const session1: IAzureAccountSession = { + account: accounts[0], + subscription: subscriptions[1], + tenantId: "tenantId", + token: { + key: "", + token: "", + tokenType: "", + }, + }; + + const azureAccountService = sinon.createStubInstance(AzureAccountService); + azureAccountService.getAccounts.resolves(accounts); + azureAccountService.addAccount.resolves(accounts[0]); + azureAccountService.getAccountSecurityToken.resolves({ + key: "", + token: "", + tokenType: "", + }); + azureAccountService.getAccountSessions.resolves([session0, session1]); + + const subscriptionClient = sinon.createStubInstance(SubscriptionClient); + + return { + groups: groups, + locations: locations, + subscriptions: subscriptions, + subscriptionClient, + session: session0, + accounts: accounts, + azureAccountService, + }; +} + +suite("Azure SQL client", function (): void { + test("Should return locations successfully", async function (): Promise { + const testContext = createContext(); + const azureSqlClient = new AzureResourceController(() => testContext.subscriptionClient); + + let index = 0; + let maxLength = testContext.locations.length; + const pages: PagedAsyncIterableIterator = { + next: () => { + if (index < maxLength) { + return Promise.resolve({ + done: false, + value: testContext.locations[index++], + }); + } else { + return Promise.resolve({ done: true, value: undefined }); + } + }, + byPage: () => undefined!, + [Symbol.asyncIterator]: undefined!, + }; + const subscriptions: Subscriptions = { + listLocations: () => pages, + list: () => undefined!, + get: () => undefined!, + }; + + Object.defineProperty(testContext.subscriptionClient, "subscriptions", { + get: () => subscriptions, + }); + + const result = await azureSqlClient.getLocations(testContext.session); + assert.deepStrictEqual(result.length, testContext.locations.length); + }); + + test("Should return resource groups successfully", async function (): Promise { + const testContext = createContext(); + const groupClient = sinon.createStubInstance(ResourceManagementClient); + const azureSqlClient = new AzureResourceController(undefined, () => groupClient); + + let index = 0; + let maxLength = testContext.groups.length; + const pages: PagedAsyncIterableIterator = { + next: () => { + if (index < maxLength) { + return Promise.resolve({ + done: false, + value: testContext.groups[index++], + }); + } else { + return Promise.resolve({ done: true, value: undefined }); + } + }, + byPage: () => undefined!, + [Symbol.asyncIterator]: undefined!, + }; + const resourceGroups: ResourceGroups = { + list: () => pages, + get: () => undefined!, + beginDelete: undefined!, + beginDeleteAndWait: undefined!, + beginExportTemplate: undefined!, + beginExportTemplateAndWait: undefined!, + checkExistence: undefined!, + createOrUpdate: undefined!, + update: undefined!, + }; + + Object.defineProperty(groupClient, "resourceGroups", { + get: () => resourceGroups, + }); + + const result = await azureSqlClient.getResourceGroups(testContext.session); + assert.deepStrictEqual(result.length, testContext.groups.length); + assert.deepStrictEqual(result[0].location, testContext.groups[0].location); + }); +}); From 7697f8b7477480f54fbc83056460cc149f4a561c Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Mon, 29 Sep 2025 14:01:43 -0700 Subject: [PATCH 04/30] converted azureResourceService.test.ts --- test/unit/azureResourceService.test.ts | 226 ++++++++++--------------- 1 file changed, 93 insertions(+), 133 deletions(-) diff --git a/test/unit/azureResourceService.test.ts b/test/unit/azureResourceService.test.ts index 0c42d2039b..6406974cb8 100644 --- a/test/unit/azureResourceService.test.ts +++ b/test/unit/azureResourceService.test.ts @@ -3,9 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from "assert"; import * as sinon from "sinon"; -import { AzureAuthType, IAccount } from "../../src/models/contracts/azure"; +import sinonChai from "sinon-chai"; +import { expect } from "chai"; +import * as chai from "chai"; import { SubscriptionClient, Subscription, @@ -15,168 +16,127 @@ import { import { PagedAsyncIterableIterator } from "@azure/core-paging"; import { ResourceGroup, ResourceGroups, ResourceManagementClient } from "@azure/arm-resources"; import { AzureResourceController } from "../../src/azure/azureResourceController"; -import { AzureAccountService } from "../../src/services/azureAccountService"; import allSettings from "../../src/azure/providerSettings"; +import { AzureAuthType, IAccount } from "../../src/models/contracts/azure"; import { IAzureAccountSession } from "vscode-mssql"; -export interface ITestContext { - azureAccountService: sinon.SinonStubbedInstance; - accounts: IAccount[]; - session: IAzureAccountSession; - subscriptionClient: sinon.SinonStubbedInstance; - subscriptions: Subscription[]; - locations: Location[]; - groups: ResourceGroup[]; -} +chai.use(sinonChai); -export function createContext(): ITestContext { - const accounts = [ - { - key: undefined!, - displayInfo: undefined!, - properties: { - tenants: [ - { - id: "", - displayName: "", - }, - ], - azureAuthType: AzureAuthType.AuthCodeGrant, - isMsAccount: false, - owningTenant: { +const mockAccounts: IAccount[] = [ + { + key: undefined!, + displayInfo: undefined!, + properties: { + tenants: [ + { id: "", displayName: "", }, - providerSettings: allSettings, + ], + azureAuthType: AzureAuthType.AuthCodeGrant, + isMsAccount: false, + owningTenant: { + id: "", + displayName: "", }, - isStale: false, - isSignedIn: true, - }, - ]; - const subscriptions: Subscription[] = [{ subscriptionId: "id1" }, { subscriptionId: "id2" }]; - const locations: Location[] = [{ id: "id1" }, { id: "id2" }]; - const groups: ResourceGroup[] = [ - { id: "id1", location: "l1" }, - { id: "id2", location: "l2" }, - ]; - const session0: IAzureAccountSession = { - account: accounts[0], - subscription: subscriptions[0], - tenantId: "tenantId", - token: { - key: "", - token: "", - tokenType: "", - }, - }; - const session1: IAzureAccountSession = { - account: accounts[0], - subscription: subscriptions[1], - tenantId: "tenantId", - token: { - key: "", - token: "", - tokenType: "", + providerSettings: allSettings, }, - }; + isStale: false, + isSignedIn: true, + }, +]; + +const mockSubscriptions: Subscription[] = [{ subscriptionId: "id1" }, { subscriptionId: "id2" }]; +const mockLocations: Location[] = [{ id: "id1" }, { id: "id2" }]; +const mockGroups: ResourceGroup[] = [ + { id: "id1", location: "l1" }, + { id: "id2", location: "l2" }, +]; - const azureAccountService = sinon.createStubInstance(AzureAccountService); - azureAccountService.getAccounts.resolves(accounts); - azureAccountService.addAccount.resolves(accounts[0]); - azureAccountService.getAccountSecurityToken.resolves({ +const primarySession: IAzureAccountSession = { + account: mockAccounts[0], + subscription: mockSubscriptions[0], + tenantId: "tenantId", + token: { key: "", token: "", tokenType: "", - }); - azureAccountService.getAccountSessions.resolves([session0, session1]); + }, +}; - const subscriptionClient = sinon.createStubInstance(SubscriptionClient); +function createPagedIterator(items: T[]): PagedAsyncIterableIterator { + let index = 0; + const maxLength = items.length; return { - groups: groups, - locations: locations, - subscriptions: subscriptions, - subscriptionClient, - session: session0, - accounts: accounts, - azureAccountService, + next: async () => { + if (index < maxLength) { + return { + done: false, + value: items[index++], + }; + } + + return { done: true, value: undefined }; + }, + byPage: () => undefined!, + [Symbol.asyncIterator]: undefined!, }; } suite("Azure SQL client", function (): void { + let sandbox: sinon.SinonSandbox; + + setup(() => { + sandbox = sinon.createSandbox(); + }); + + teardown(() => { + sandbox.restore(); + }); + test("Should return locations successfully", async function (): Promise { - const testContext = createContext(); - const azureSqlClient = new AzureResourceController(() => testContext.subscriptionClient); - - let index = 0; - let maxLength = testContext.locations.length; - const pages: PagedAsyncIterableIterator = { - next: () => { - if (index < maxLength) { - return Promise.resolve({ - done: false, - value: testContext.locations[index++], - }); - } else { - return Promise.resolve({ done: true, value: undefined }); - } - }, - byPage: () => undefined!, - [Symbol.asyncIterator]: undefined!, - }; + const pages = createPagedIterator(mockLocations); const subscriptions: Subscriptions = { - listLocations: () => pages, - list: () => undefined!, - get: () => undefined!, + listLocations: sandbox.stub().returns(pages), + list: sandbox.stub().returns(undefined), + get: sandbox.stub().returns(undefined), }; + const subscriptionClient = { + subscriptions, + } as unknown as SubscriptionClient; + const azureSqlClient = new AzureResourceController(() => subscriptionClient); - Object.defineProperty(testContext.subscriptionClient, "subscriptions", { - get: () => subscriptions, - }); + const result = await azureSqlClient.getLocations(primarySession); - const result = await azureSqlClient.getLocations(testContext.session); - assert.deepStrictEqual(result.length, testContext.locations.length); + expect(subscriptions.listLocations).to.have.been.calledOnceWithExactly( + primarySession.subscription?.subscriptionId, + ); + expect(result).to.have.lengthOf(mockLocations.length); }); test("Should return resource groups successfully", async function (): Promise { - const testContext = createContext(); - const groupClient = sinon.createStubInstance(ResourceManagementClient); - const azureSqlClient = new AzureResourceController(undefined, () => groupClient); - - let index = 0; - let maxLength = testContext.groups.length; - const pages: PagedAsyncIterableIterator = { - next: () => { - if (index < maxLength) { - return Promise.resolve({ - done: false, - value: testContext.groups[index++], - }); - } else { - return Promise.resolve({ done: true, value: undefined }); - } - }, - byPage: () => undefined!, - [Symbol.asyncIterator]: undefined!, - }; + const pages = createPagedIterator(mockGroups); const resourceGroups: ResourceGroups = { - list: () => pages, - get: () => undefined!, - beginDelete: undefined!, - beginDeleteAndWait: undefined!, - beginExportTemplate: undefined!, - beginExportTemplateAndWait: undefined!, - checkExistence: undefined!, - createOrUpdate: undefined!, - update: undefined!, + list: sandbox.stub().returns(pages), + get: sandbox.stub().returns(undefined), + beginDelete: sandbox.stub(), + beginDeleteAndWait: sandbox.stub(), + beginExportTemplate: sandbox.stub(), + beginExportTemplateAndWait: sandbox.stub(), + checkExistence: sandbox.stub(), + createOrUpdate: sandbox.stub(), + update: sandbox.stub(), }; + const resourceClient = { + resourceGroups, + } as unknown as ResourceManagementClient; + const azureSqlClient = new AzureResourceController(undefined, () => resourceClient); - Object.defineProperty(groupClient, "resourceGroups", { - get: () => resourceGroups, - }); + const result = await azureSqlClient.getResourceGroups(primarySession); - const result = await azureSqlClient.getResourceGroups(testContext.session); - assert.deepStrictEqual(result.length, testContext.groups.length); - assert.deepStrictEqual(result[0].location, testContext.groups[0].location); + expect(resourceGroups.list).to.have.been.calledOnceWithExactly(); + expect(result).to.have.lengthOf(mockGroups.length); + expect(result[0].location).to.equal(mockGroups[0].location); }); }); From 7620780e1a7688db757fe2797bc5bda8e74e5664 Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Mon, 29 Sep 2025 15:29:54 -0700 Subject: [PATCH 05/30] converted queryNotificationHandler.test.ts --- test/unit/queryNotificationHandler.test.ts | 332 +++++++++------------ 1 file changed, 140 insertions(+), 192 deletions(-) diff --git a/test/unit/queryNotificationHandler.test.ts b/test/unit/queryNotificationHandler.test.ts index 2cd0367603..7f24aaf104 100644 --- a/test/unit/queryNotificationHandler.test.ts +++ b/test/unit/queryNotificationHandler.test.ts @@ -1,192 +1,140 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as TypeMoq from "typemoq"; -import * as assert from "assert"; -import QueryRunner from "../../src/controllers/queryRunner"; -import { QueryNotificationHandler } from "../../src/controllers/queryNotificationHandler"; -import { NotificationHandler } from "vscode-languageclient"; - -// TESTS ////////////////////////////////////////////////////////////////////////////////////////// -suite("QueryNotificationHandler tests", () => { - let notificationHandler: QueryNotificationHandler; - let eventData: any; - let runnerMock: TypeMoq.IMock; - - let batchStartHandlerCalled: boolean; - let messageHandlerCalled: boolean; - let resultSetCompleteHandlerCalled: boolean; - let batchCompleteHandlerCalled: boolean; - let queryCompleteHandlerCalled: boolean; - - let batchStartHandler: NotificationHandler; - let messageHandler: NotificationHandler; - let resultSetCompleteHandler: NotificationHandler; - let batchCompleteHandler: NotificationHandler; - let queryCompleteHandler: NotificationHandler; - - setup(() => { - notificationHandler = new QueryNotificationHandler(); - eventData = { ownerUri: "testUri" }; - - // Setup mock - Use the same QueryRunner for the whole test - this tests if it can be reused - runnerMock = TypeMoq.Mock.ofType(QueryRunner, TypeMoq.MockBehavior.Loose); - runnerMock.callBase = true; - runnerMock - .setup((x) => x.handleBatchStart(TypeMoq.It.isAny())) - .callback((event) => { - batchStartHandlerCalled = true; - }); - runnerMock - .setup((x) => x.handleMessage(TypeMoq.It.isAny())) - .callback((event) => { - messageHandlerCalled = true; - }); - runnerMock - .setup((x) => x.handleResultSetComplete(TypeMoq.It.isAny())) - .callback((event) => { - resultSetCompleteHandlerCalled = true; - }); - runnerMock - .setup((x) => x.handleBatchComplete(TypeMoq.It.isAny())) - .callback((event) => { - batchCompleteHandlerCalled = true; - }); - runnerMock - .setup((x) => x.handleQueryComplete(TypeMoq.It.isAny())) - .callback((event) => { - queryCompleteHandlerCalled = true; - runnerMock.object.setHasCompleted(); - }); - - // Get handlers - batchStartHandler = notificationHandler.handleBatchStartNotification(); - messageHandler = notificationHandler.handleMessageNotification(); - resultSetCompleteHandler = notificationHandler.handleResultSetCompleteNotification(); - batchCompleteHandler = notificationHandler.handleBatchCompleteNotification(); - queryCompleteHandler = notificationHandler.handleQueryCompleteNotification(); - }); - - // Setup booleans to track if handlers were called - function resetBools(): void { - batchStartHandlerCalled = false; - messageHandlerCalled = false; - resultSetCompleteHandlerCalled = false; - batchCompleteHandlerCalled = false; - queryCompleteHandlerCalled = false; - runnerMock.object.resetHasCompleted(); - } - - test("QueryNotificationHandler handles registerRunner at the beginning of the event flow", (done) => { - resetBools(); - - // If registerRunner is called, the query runner map should be populated - notificationHandler.registerRunner(runnerMock.object, eventData.ownerUri); - assert.equal(notificationHandler._queryRunners.size, 1); - - // If the notifications are fired, the callbacks should be immediately fired too - batchStartHandler(eventData); - assert.equal(batchStartHandlerCalled, true); - messageHandler(eventData); - assert.equal(messageHandlerCalled, true); - resultSetCompleteHandler(eventData); - assert.equal(resultSetCompleteHandlerCalled, true); - batchCompleteHandler(eventData); - assert.equal(batchCompleteHandlerCalled, true); - queryCompleteHandler(eventData); - assert.equal(queryCompleteHandlerCalled, true); - - // And cleanup should happen after queryCompleteHandlerCalled - assert.equal( - notificationHandler._queryRunners.size, - 0, - "Query runner map not cleared after call to handleQueryCompleteNotification()", - ); - - done(); - }); - - test("QueryNotificationHandler ignores notifications when no runner is registered", (done) => { - resetBools(); - - // If notifications are fired before registerRunner, they should be ignored (not queued) - batchStartHandler(eventData); - messageHandler(eventData); - - // The callbacks should not be fired since no runner is registered - assert.equal(batchStartHandlerCalled, false); - assert.equal(messageHandlerCalled, false); - - // If register runner is then called, the query runner map should be populated - notificationHandler.registerRunner(runnerMock.object, eventData.ownerUri); - assert.equal(notificationHandler._queryRunners.size, 1); - - // Previous notifications were ignored, so handlers still not called - assert.equal(batchStartHandlerCalled, false); - assert.equal(messageHandlerCalled, false); - - // If new notifications are fired, the callbacks should be immediately fired - resultSetCompleteHandler(eventData); - assert.equal(resultSetCompleteHandlerCalled, true); - batchCompleteHandler(eventData); - assert.equal(batchCompleteHandlerCalled, true); - queryCompleteHandler(eventData); - assert.equal(queryCompleteHandlerCalled, true); - - // And cleanup should happen after queryCompleteHandlerCalled - assert.equal( - notificationHandler._queryRunners.size, - 0, - "Query runner map not cleared after call to handleQueryCompleteNotification()", - ); - - done(); - }); - - test("QueryNotificationHandler properly unregisters runner after query completion", (done) => { - resetBools(); - - // Register runner first - notificationHandler.registerRunner(runnerMock.object, eventData.ownerUri); - assert.equal(notificationHandler._queryRunners.size, 1); - - // Fire all notifications - they should all be handled - batchStartHandler(eventData); - assert.equal(batchStartHandlerCalled, true); - messageHandler(eventData); - assert.equal(messageHandlerCalled, true); - resultSetCompleteHandler(eventData); - assert.equal(resultSetCompleteHandlerCalled, true); - batchCompleteHandler(eventData); - assert.equal(batchCompleteHandlerCalled, true); - queryCompleteHandler(eventData); - assert.equal(queryCompleteHandlerCalled, true); - - // After query complete, runner should be unregistered automatically - assert.equal(notificationHandler._queryRunners.size, 0); - - done(); - }); - - test("QueryNotificationHandler handles manual unregister", (done) => { - resetBools(); - - // Register runner - notificationHandler.registerRunner(runnerMock.object, eventData.ownerUri); - assert.equal(notificationHandler._queryRunners.size, 1); - - // Manually unregister - notificationHandler.unregisterRunner(eventData.ownerUri); - assert.equal(notificationHandler._queryRunners.size, 0); - - // Notifications should be ignored after unregister - batchStartHandler(eventData); - assert.equal(batchStartHandlerCalled, false); - messageHandler(eventData); - assert.equal(messageHandlerCalled, false); - - done(); - }); -}); +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as sinon from "sinon"; +import sinonChai from "sinon-chai"; +import { expect } from "chai"; +import * as chai from "chai"; +import QueryRunner from "../../src/controllers/queryRunner"; +import { QueryNotificationHandler } from "../../src/controllers/queryNotificationHandler"; +import { NotificationHandler } from "vscode-languageclient"; + +chai.use(sinonChai); + +suite("QueryNotificationHandler tests", () => { + let sandbox: sinon.SinonSandbox; + let notificationHandler: QueryNotificationHandler; + let eventData: { ownerUri: string }; + let runnerMock: sinon.SinonStubbedInstance; + let runner: QueryRunner; + + let batchStartHandler: NotificationHandler; + let messageHandler: NotificationHandler; + let resultSetCompleteHandler: NotificationHandler; + let batchCompleteHandler: NotificationHandler; + let queryCompleteHandler: NotificationHandler; + + setup(() => { + sandbox = sinon.createSandbox(); + notificationHandler = new QueryNotificationHandler(); + eventData = { ownerUri: "testUri" }; + + runnerMock = sandbox.createStubInstance(QueryRunner); + runnerMock.handleQueryComplete.callsFake(() => { + runnerMock.setHasCompleted(); + }); + + runner = runnerMock as unknown as QueryRunner; + + batchStartHandler = notificationHandler.handleBatchStartNotification(); + messageHandler = notificationHandler.handleMessageNotification(); + resultSetCompleteHandler = notificationHandler.handleResultSetCompleteNotification(); + batchCompleteHandler = notificationHandler.handleBatchCompleteNotification(); + queryCompleteHandler = notificationHandler.handleQueryCompleteNotification(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test("QueryNotificationHandler handles registerRunner at the beginning of the event flow", () => { + notificationHandler.registerRunner(runner, eventData.ownerUri); + expect(notificationHandler._queryRunners.size).to.equal(1); + + batchStartHandler(eventData); + expect(runnerMock.handleBatchStart).to.have.been.calledOnceWithExactly(eventData); + + messageHandler(eventData); + expect(runnerMock.handleMessage).to.have.been.calledOnceWithExactly(eventData); + + resultSetCompleteHandler(eventData); + expect(runnerMock.handleResultSetComplete).to.have.been.calledOnceWithExactly(eventData); + + batchCompleteHandler(eventData); + expect(runnerMock.handleBatchComplete).to.have.been.calledOnceWithExactly(eventData); + + queryCompleteHandler(eventData); + expect(runnerMock.handleQueryComplete).to.have.been.calledOnceWithExactly(eventData); + expect(runnerMock.setHasCompleted).to.have.been.calledOnce; + + expect(notificationHandler._queryRunners.size).to.equal(0); + }); + + test("QueryNotificationHandler ignores notifications when no runner is registered", () => { + // If notifications are fired before registerRunner, they should be ignored (not queued) + batchStartHandler(eventData); + messageHandler(eventData); + + expect(runnerMock.handleBatchStart).to.not.have.been.called; + expect(runnerMock.handleMessage).to.not.have.been.called; + + // If register runner is then called, the query runner map should be populated + notificationHandler.registerRunner(runner, eventData.ownerUri); + expect(notificationHandler._queryRunners.size).to.equal(1); + // Previous notifications were ignored, so handlers still not called + expect(runnerMock.handleBatchStart).to.not.have.been.called; + expect(runnerMock.handleMessage).to.not.have.been.called; + + // If new notifications are fired, the callbacks should be immediately fired + resultSetCompleteHandler(eventData); + expect(runnerMock.handleResultSetComplete).to.have.been.calledOnceWithExactly(eventData); + + batchCompleteHandler(eventData); + expect(runnerMock.handleBatchComplete).to.have.been.calledOnceWithExactly(eventData); + + queryCompleteHandler(eventData); + expect(runnerMock.handleQueryComplete).to.have.been.calledOnceWithExactly(eventData); + expect(runnerMock.setHasCompleted).to.have.been.calledOnce; + + expect(notificationHandler._queryRunners.size).to.equal(0); + }); + + test("QueryNotificationHandler properly unregisters runner after query completion", () => { + notificationHandler.registerRunner(runner, eventData.ownerUri); + expect(notificationHandler._queryRunners.size).to.equal(1); + + batchStartHandler(eventData); + expect(runnerMock.handleBatchStart).to.have.been.calledOnceWithExactly(eventData); + + messageHandler(eventData); + expect(runnerMock.handleMessage).to.have.been.calledOnceWithExactly(eventData); + + resultSetCompleteHandler(eventData); + expect(runnerMock.handleResultSetComplete).to.have.been.calledOnceWithExactly(eventData); + + batchCompleteHandler(eventData); + expect(runnerMock.handleBatchComplete).to.have.been.calledOnceWithExactly(eventData); + + queryCompleteHandler(eventData); + expect(runnerMock.handleQueryComplete).to.have.been.calledOnceWithExactly(eventData); + expect(runnerMock.setHasCompleted).to.have.been.calledOnce; + + expect(notificationHandler._queryRunners.size).to.equal(0); + }); + + test("QueryNotificationHandler handles manual unregister", () => { + notificationHandler.registerRunner(runner, eventData.ownerUri); + expect(notificationHandler._queryRunners.size).to.equal(1); + + notificationHandler.unregisterRunner(eventData.ownerUri); + expect(notificationHandler._queryRunners.size).to.equal(0); + + batchStartHandler(eventData); + expect(runnerMock.handleBatchStart).to.not.have.been.called; + + messageHandler(eventData); + expect(runnerMock.handleMessage).to.not.have.been.called; + }); +}); From bf57343ef939d753bc443e24c9a5eb2992abdebb Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Mon, 29 Sep 2025 15:42:10 -0700 Subject: [PATCH 06/30] converted confirm.test.ts --- test/unit/confirm.test.ts | 97 +++++++++++++++++++++------------------ 1 file changed, 53 insertions(+), 44 deletions(-) diff --git a/test/unit/confirm.test.ts b/test/unit/confirm.test.ts index b757bdd0a2..7063143924 100644 --- a/test/unit/confirm.test.ts +++ b/test/unit/confirm.test.ts @@ -1,44 +1,53 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as TypeMoq from "typemoq"; -import VscodeWrapper from "../../src/controllers/vscodeWrapper"; -import * as LocalizedConstants from "../../src/constants/locConstants"; -import ConfirmPrompt from "../../src/prompts/confirm"; - -// @cssuh 10/22 - commented this test because it was throwing some random undefined errors -suite.skip("Test Confirm Prompt", () => { - test("Test Confirm prompt with simple question", () => { - let question = { - name: "test", - }; - let vscodeWrapper = TypeMoq.Mock.ofType(VscodeWrapper, TypeMoq.MockBehavior.Loose); - vscodeWrapper - .setup((v) => v.showQuickPickStrings(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(LocalizedConstants.msgYes)); - let confirm = new ConfirmPrompt(question, vscodeWrapper.object); - confirm.render(); - vscodeWrapper.verify( - (v) => v.showQuickPickStrings(TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.once(), - ); - }); - - test("Test Checkbox prompt with error", () => { - let question = { - name: "test", - }; - let vscodeWrapper = TypeMoq.Mock.ofType(VscodeWrapper, TypeMoq.MockBehavior.Loose); - vscodeWrapper - .setup((v) => v.showQuickPickStrings(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)); - let confirm = new ConfirmPrompt(question, vscodeWrapper.object); - confirm.render(); - vscodeWrapper.verify( - (v) => v.showQuickPickStrings(TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.once(), - ); - }); -}); +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as sinon from "sinon"; +import sinonChai from "sinon-chai"; +import { expect } from "chai"; +import * as chai from "chai"; +import VscodeWrapper from "../../src/controllers/vscodeWrapper"; +import * as LocalizedConstants from "../../src/constants/locConstants"; +import ConfirmPrompt from "../../src/prompts/confirm"; + +chai.use(sinonChai); + +suite("Test Confirm Prompt", () => { + let sandbox: sinon.SinonSandbox; + + setup(() => { + sandbox = sinon.createSandbox(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test("Test Confirm prompt with simple question", async () => { + const question = { + name: "test", + }; + const vscodeWrapper = sandbox.createStubInstance(VscodeWrapper); + vscodeWrapper.showQuickPickStrings.resolves(LocalizedConstants.msgYes); + + const confirm = new ConfirmPrompt(question, vscodeWrapper); + await confirm.render(); + + expect(vscodeWrapper.showQuickPickStrings).to.have.been.calledOnce; + }); + + test("Test Checkbox prompt with error", async () => { + const question = { + name: "test", + }; + const vscodeWrapper = sandbox.createStubInstance(VscodeWrapper); + vscodeWrapper.showQuickPickStrings.resolves(undefined); + + const confirm = new ConfirmPrompt(question, vscodeWrapper); + + await confirm.render().catch(() => undefined); + + expect(vscodeWrapper.showQuickPickStrings).to.have.been.calledOnce; + }); +}); From dbfa8f65dc74022f157f30e8ec5481c338373637 Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Mon, 29 Sep 2025 16:19:47 -0700 Subject: [PATCH 07/30] Converted prompts.test.ts --- test/unit/prompts.test.ts | 188 ++++++++++++++++++++------------------ 1 file changed, 99 insertions(+), 89 deletions(-) diff --git a/test/unit/prompts.test.ts b/test/unit/prompts.test.ts index 51fe08a2d5..4772344c6b 100644 --- a/test/unit/prompts.test.ts +++ b/test/unit/prompts.test.ts @@ -1,89 +1,99 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as TypeMoq from "typemoq"; -import PromptFactory from "../../src/prompts/factory"; -import { assert } from "chai"; -import InputPrompt from "../../src/prompts/input"; -import PasswordPrompt from "../../src/prompts/password"; -import ListPrompt from "../../src/prompts/list"; -import ConfirmPrompt from "../../src/prompts/confirm"; -import CheckboxPrompt from "../../src/prompts/checkbox"; -import ExpandPrompt from "../../src/prompts/expand"; -import VscodeWrapper from "../../src/controllers/vscodeWrapper"; - -suite("Prompts test", () => { - let vscodeWrapper: TypeMoq.IMock; - - setup(() => { - vscodeWrapper = TypeMoq.Mock.ofType(VscodeWrapper, TypeMoq.MockBehavior.Loose); - }); - - test("Test string prompt", () => { - let question: any = { - type: "string", - }; - let prompt = PromptFactory.createPrompt(question, vscodeWrapper.object); - assert.equal(prompt instanceof InputPrompt, true); - }); - - test("Test input prompt", () => { - let question: any = { - type: "input", - default: Error("test"), - placeHolder: "test_placeHolder", - }; - let prompt = PromptFactory.createPrompt(question, vscodeWrapper.object); - assert.equal(prompt instanceof InputPrompt, true); - assert.equal(question.type, InputPrompt.promptType); - }); - - test("Test password prompt", () => { - let question: any = { - type: "password", - }; - let prompt = PromptFactory.createPrompt(question, vscodeWrapper.object); - assert.equal(prompt instanceof PasswordPrompt, true); - }); - - test("Test list prompt", () => { - let question: any = { - type: "list", - }; - let prompt = PromptFactory.createPrompt(question, vscodeWrapper.object); - assert.equal(prompt instanceof ListPrompt, true); - }); - - test("Test confirm prompt", () => { - let question: any = { - type: "confirm", - }; - let prompt = PromptFactory.createPrompt(question, vscodeWrapper.object); - assert.equal(prompt instanceof ConfirmPrompt, true); - }); - - test("Test checkbox prompt", () => { - let question: any = { - type: "checkbox", - }; - let prompt = PromptFactory.createPrompt(question, vscodeWrapper.object); - assert.equal(prompt instanceof CheckboxPrompt, true); - }); - - test("Test expand prompt", () => { - let question: any = { - type: "expand", - }; - let prompt = PromptFactory.createPrompt(question, vscodeWrapper.object); - assert.equal(prompt instanceof ExpandPrompt, true); - }); - - test("Test bogus prompt", () => { - let question: any = { - type: "fail", - }; - assert.Throw(() => PromptFactory.createPrompt(question, vscodeWrapper.object)); - }); -}); +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as sinon from "sinon"; +import sinonChai from "sinon-chai"; +import { expect } from "chai"; +import * as chai from "chai"; +import PromptFactory from "../../src/prompts/factory"; +import InputPrompt from "../../src/prompts/input"; +import PasswordPrompt from "../../src/prompts/password"; +import ListPrompt from "../../src/prompts/list"; +import ConfirmPrompt from "../../src/prompts/confirm"; +import CheckboxPrompt from "../../src/prompts/checkbox"; +import ExpandPrompt from "../../src/prompts/expand"; +import VscodeWrapper from "../../src/controllers/vscodeWrapper"; + +chai.use(sinonChai); + +suite("Prompts test", () => { + let sandbox: sinon.SinonSandbox; + let vscodeWrapper: sinon.SinonStubbedInstance; + + setup(() => { + sandbox = sinon.createSandbox(); + vscodeWrapper = sandbox.createStubInstance(VscodeWrapper); + }); + + teardown(() => { + sandbox.restore(); + }); + + test("Test string prompt", () => { + const question: any = { + type: "string", + }; + const prompt = PromptFactory.createPrompt(question, vscodeWrapper); + expect(prompt).to.be.instanceOf(InputPrompt); + }); + + test("Test input prompt", () => { + const question: any = { + type: "input", + default: Error("test"), + placeHolder: "test_placeHolder", + }; + const prompt = PromptFactory.createPrompt(question, vscodeWrapper); + expect(prompt).to.be.instanceOf(InputPrompt); + expect(question.type).to.equal(InputPrompt.promptType); + }); + + test("Test password prompt", () => { + const question: any = { + type: "password", + }; + const prompt = PromptFactory.createPrompt(question, vscodeWrapper); + expect(prompt).to.be.instanceOf(PasswordPrompt); + }); + + test("Test list prompt", () => { + const question: any = { + type: "list", + }; + const prompt = PromptFactory.createPrompt(question, vscodeWrapper); + expect(prompt).to.be.instanceOf(ListPrompt); + }); + + test("Test confirm prompt", () => { + const question: any = { + type: "confirm", + }; + const prompt = PromptFactory.createPrompt(question, vscodeWrapper); + expect(prompt).to.be.instanceOf(ConfirmPrompt); + }); + + test("Test checkbox prompt", () => { + const question: any = { + type: "checkbox", + }; + const prompt = PromptFactory.createPrompt(question, vscodeWrapper); + expect(prompt).to.be.instanceOf(CheckboxPrompt); + }); + + test("Test expand prompt", () => { + const question: any = { + type: "expand", + }; + const prompt = PromptFactory.createPrompt(question, vscodeWrapper); + expect(prompt).to.be.instanceOf(ExpandPrompt); + }); + + test("Test bogus prompt", () => { + const question: any = { + type: "fail", + }; + expect(() => PromptFactory.createPrompt(question, vscodeWrapper)).to.throw(); + }); +}); From db0f6b154207338ebb75e25fe7f2ff8a12001fd9 Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Mon, 29 Sep 2025 17:18:03 -0700 Subject: [PATCH 08/30] converting mssqlProtocolHandler.test.ts --- test/unit/mocks.ts | 174 ++++---- test/unit/mssqlProtocolHandler.test.ts | 568 ++++++++++++------------- test/unit/utils.ts | 14 + 3 files changed, 384 insertions(+), 372 deletions(-) diff --git a/test/unit/mocks.ts b/test/unit/mocks.ts index b29554d20a..3b26104516 100644 --- a/test/unit/mocks.ts +++ b/test/unit/mocks.ts @@ -9,94 +9,98 @@ import { ServiceOption } from "vscode-mssql"; import { CapabilitiesResult, GetCapabilitiesRequest } from "../../src/models/contracts/connection"; import { AuthenticationType } from "../../src/sharedInterfaces/connectionDialog"; -export function mockGetCapabilitiesRequest(serviceClientMock: TypeMoq.IMock) { +export function mockGetCapabilitiesRequest( + serviceClientMock: TypeMoq.IMock, +): void { serviceClientMock .setup((s) => s.sendRequest(TypeMoq.It.isValue(GetCapabilitiesRequest.type), TypeMoq.It.isAny()), ) - .returns(() => - Promise.resolve({ - capabilities: { - connectionProvider: { - groupDisplayNames: { - group1: "Group 1", - group2: "Group 2", - }, - options: [ - { - name: "server", - displayName: "Server", - isRequired: true, - valueType: "string", - }, - { - name: "user", - displayName: "User", - isRequired: false, - valueType: "string", - }, - { - name: "password", - displayName: "Password", - isRequired: false, - valueType: "password", - }, - { - name: "trustServerCertificate", - displayName: "Trust Server Certificate", - isRequired: false, - valueType: "boolean", - }, - { - name: "authenticationType", - displayName: "Authentication Type", - isRequired: false, - valueType: "category", - categoryValues: [ - AuthenticationType.SqlLogin, - AuthenticationType.Integrated, - AuthenticationType.AzureMFA, - ], - }, - { - name: "savePassword", - displayName: "Save Password", - isRequired: false, - valueType: "boolean", - }, - { - name: "accountId", - displayName: "Account Id", - isRequired: false, - valueType: "string", - }, - { - name: "tenantId", - displayName: "Tenant Id", - isRequired: false, - valueType: "string", - }, - { - name: "database", - displayName: "Database", - isRequired: false, - valueType: "string", - }, - { - name: "encrypt", - displayName: "Encrypt", - isRequired: false, - valueType: "boolean", - }, - { - name: "connectTimeout", - displayName: "Connect timeout", - isRequired: false, - valueType: "number", - }, - ] as ServiceOption[], - }, + .returns(() => Promise.resolve(buildCapabilitiesResult())); +} + +export function buildCapabilitiesResult(): CapabilitiesResult { + return { + capabilities: { + connectionProvider: { + groupDisplayNames: { + group1: "Group 1", + group2: "Group 2", }, - } as unknown as CapabilitiesResult), - ); + options: [ + { + name: "server", + displayName: "Server", + isRequired: true, + valueType: "string", + }, + { + name: "user", + displayName: "User", + isRequired: false, + valueType: "string", + }, + { + name: "password", + displayName: "Password", + isRequired: false, + valueType: "password", + }, + { + name: "trustServerCertificate", + displayName: "Trust Server Certificate", + isRequired: false, + valueType: "boolean", + }, + { + name: "authenticationType", + displayName: "Authentication Type", + isRequired: false, + valueType: "category", + categoryValues: [ + AuthenticationType.SqlLogin, + AuthenticationType.Integrated, + AuthenticationType.AzureMFA, + ], + }, + { + name: "savePassword", + displayName: "Save Password", + isRequired: false, + valueType: "boolean", + }, + { + name: "accountId", + displayName: "Account Id", + isRequired: false, + valueType: "string", + }, + { + name: "tenantId", + displayName: "Tenant Id", + isRequired: false, + valueType: "string", + }, + { + name: "database", + displayName: "Database", + isRequired: false, + valueType: "string", + }, + { + name: "encrypt", + displayName: "Encrypt", + isRequired: false, + valueType: "boolean", + }, + { + name: "connectTimeout", + displayName: "Connect timeout", + isRequired: false, + valueType: "number", + }, + ] as ServiceOption[], + }, + }, + } as unknown as CapabilitiesResult; } diff --git a/test/unit/mssqlProtocolHandler.test.ts b/test/unit/mssqlProtocolHandler.test.ts index f37f2163d1..066421ebdd 100644 --- a/test/unit/mssqlProtocolHandler.test.ts +++ b/test/unit/mssqlProtocolHandler.test.ts @@ -1,287 +1,281 @@ -/*--------------------------------------------------------------------------------------------- - * 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 * as sinon from "sinon"; -import sinonChai from "sinon-chai"; -import { expect } from "chai"; -import * as chai from "chai"; -import { MssqlProtocolHandler } from "../../src/mssqlProtocolHandler"; -import SqlToolsServiceClient from "../../src/languageservice/serviceclient"; -import { Uri } from "vscode"; -import { mockGetCapabilitiesRequest } from "./mocks"; -import { Logger } from "../../src/models/logger"; -import VscodeWrapper from "../../src/controllers/vscodeWrapper"; -import MainController from "../../src/controllers/mainController"; -import { generateUUID } from "../e2e/baseFixtures"; -import ConnectionManager from "../../src/controllers/connectionManager"; -import { MatchScore } from "../../src/models/utils"; -import { IConnectionProfile } from "../../src/models/interfaces"; - -chai.use(sinonChai); - -suite("MssqlProtocolHandler Tests", () => { - let sandbox: sinon.SinonSandbox; - let mssqlProtocolHandler: MssqlProtocolHandler; - let sqlToolsServiceClientMock: TypeMoq.IMock; - let mockVscodeWrapper: sinon.SinonStubbedInstance; - let mockLogger: sinon.SinonStubbedInstance; - let mockMainController: sinon.SinonStubbedInstance; - let openConnectionDialogStub: sinon.SinonStub; - let connectProfileStub: sinon.SinonStub; - - setup(() => { - sandbox = sinon.createSandbox(); - mockVscodeWrapper = sandbox.createStubInstance(VscodeWrapper); - mockLogger = sandbox.createStubInstance(Logger); - mockMainController = sandbox.createStubInstance(MainController); - - const outputChannel = sinon.stub({ - append: () => sinon.stub(), - appendLine: () => sinon.stub(), - }) as unknown as vscode.OutputChannel; - - sinon.stub(mockVscodeWrapper, "outputChannel").get(() => { - return outputChannel; - }); - - sandbox.stub(Logger, "create").returns(mockLogger); - - sqlToolsServiceClientMock = TypeMoq.Mock.ofType( - SqlToolsServiceClient, - TypeMoq.MockBehavior.Loose, - ); - - mockGetCapabilitiesRequest(sqlToolsServiceClientMock); - - mssqlProtocolHandler = new MssqlProtocolHandler( - mockVscodeWrapper, - mockMainController, - sqlToolsServiceClientMock.object, - ); - - openConnectionDialogStub = sandbox.stub( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - mssqlProtocolHandler as any, - "openConnectionDialog", - ); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - connectProfileStub = sandbox.stub(mssqlProtocolHandler as any, "connectProfile").resolves(); - }); - - teardown(() => { - sandbox.restore(); - }); - - test("No command", async () => { - await mssqlProtocolHandler.handleUri(Uri.parse("vscode://ms-mssql.mssql/")); - - expect(openConnectionDialogStub).to.have.been.calledOnceWith(undefined); - }); - - suite("Connect command", () => { - test("Should open connection dialog when no query is provided", async () => { - await mssqlProtocolHandler.handleUri(Uri.parse("vscode://ms-mssql.mssql/connect")); - - expect(openConnectionDialogStub).to.have.been.calledOnceWith(undefined); - expect(connectProfileStub).to.not.have.been.called; - }); - - test("Should find matching profile when connection string is provided", async () => { - const connString = `Server=myServerAddress;Database=myDataBase;User Id=myUsername;Password=${generateUUID()};`; - const mockConnectionManager = sandbox.createStubInstance(ConnectionManager); - - sandbox.stub(mockMainController, "connectionManager").get(() => { - return mockConnectionManager; - }); - - mockConnectionManager.findMatchingProfile.resolves({ - profile: { connectionString: connString } as IConnectionProfile, - score: MatchScore.AllAvailableProps, - }); - - await mssqlProtocolHandler.handleUri( - Uri.parse( - `vscode://ms-mssql.mssql/connect?connectionString=${encodeURIComponent(connString)}`, - ), - ); - - expect(connectProfileStub).to.have.been.calledOnceWith({ - connectionString: connString, - }); - - expect(openConnectionDialogStub).to.not.have.been.called; - }); - - test("Should find matching profile when parameters are provided", async () => { - const params: Record = { - server: "myServer", - database: "dbName", - user: "testUser", - }; - - const mockConnectionManager = sandbox.createStubInstance(ConnectionManager); - - sandbox.stub(mockMainController, "connectionManager").get(() => { - return mockConnectionManager; - }); - - mockConnectionManager.findMatchingProfile.resolves({ - profile: { - server: "myServer", - database: "dbName", - user: "testUser", - } as IConnectionProfile, - score: MatchScore.ServerDatabaseAndAuth, - }); - - await mssqlProtocolHandler.handleUri( - Uri.parse( - `vscode://ms-mssql.mssql/connect?${new URLSearchParams(params).toString()}`, - ), - ); - - expect(connectProfileStub).to.have.been.calledOnceWith(params); - expect(openConnectionDialogStub).to.not.have.been.called; - }); - - test("Should open connection dialog with populated parameters when no matching profile is found", async () => { - const params: Record = { - server: "myServer", - database: "dbName", - user: "testUser", - authenticationType: "SqlLogin", - connectTimeout: "15", - trustServerCertificate: "true", - }; - - const mockConnectionManager = sandbox.createStubInstance(ConnectionManager); - - sandbox.stub(mockMainController, "connectionManager").get(() => { - return mockConnectionManager; - }); - - const findMatchingProfileStub = mockConnectionManager.findMatchingProfile.resolves({ - profile: undefined, - score: MatchScore.NotMatch, - }); - - await mssqlProtocolHandler.handleUri( - Uri.parse( - `vscode://ms-mssql.mssql/connect?${new URLSearchParams(params).toString()}`, - ), - ); - - expect(openConnectionDialogStub).to.have.been.calledOnceWith({ - ...params, - // savePassword is auto-added, and non-string values are converted - savePassword: true, - connectTimeout: 15, - trustServerCertificate: true, - }); - expect(connectProfileStub).to.not.have.been.called; - - // Reset stubs for server-only test case - - findMatchingProfileStub.reset(); - openConnectionDialogStub.resetHistory(); - connectProfileStub.resetHistory(); - - findMatchingProfileStub.resolves({ - profile: { server: "myServer", database: "otherDatabase" } as IConnectionProfile, - score: MatchScore.Server, - }); - - await mssqlProtocolHandler.handleUri( - Uri.parse( - `vscode://ms-mssql.mssql/connect?${new URLSearchParams(params).toString()}`, - ), - ); - - expect(openConnectionDialogStub).to.have.been.calledOnceWith({ - ...params, - // savePassword is auto-added, and non-string values are converted - savePassword: true, - connectTimeout: 15, - trustServerCertificate: true, - }); - expect(connectProfileStub).to.not.have.been.called; - }); - }); - - suite("OpenConnectionDialog command", () => { - test("Should open blank connection dialog when no parameters are provided", async () => { - await mssqlProtocolHandler.handleUri( - Uri.parse("vscode://ms-mssql.mssql/openConnectionDialog"), - ); - - expect(openConnectionDialogStub).to.have.been.calledOnceWith(undefined); - expect(connectProfileStub).to.not.have.been.called; - }); - - test("Should open populated connection dialog when parameters are provided", async () => { - const params: Record = { - server: "myServer", - database: "dbName", - user: "testUser", - authenticationType: "SqlLogin", - connectTimeout: "15", - trustServerCertificate: "true", - }; - - await mssqlProtocolHandler.handleUri( - Uri.parse( - `vscode://ms-mssql.mssql/openConnectionDialog?${new URLSearchParams(params).toString()}`, - ), - ); - - expect(openConnectionDialogStub).to.have.been.calledOnceWith({ - ...params, - // savePassword is auto-added, and non-string values are converted - savePassword: true, - connectTimeout: 15, - trustServerCertificate: true, - }); - expect(connectProfileStub).to.not.have.been.called; - }); - }); - - suite("readProfileFromArgs", () => { - test("Should ignore invalid values for booleans and numbers", async () => { - const connInfo = await mssqlProtocolHandler["readProfileFromArgs"]( - "server=myServer&database=dbName&trustServerCertificate=yes&connectTimeout=twenty", - ); - - expect(connInfo).to.be.an("object"); - expect(connInfo.server).to.equal("myServer"); - expect(connInfo.database).to.equal("dbName"); - expect( - connInfo.trustServerCertificate, - "trustServerCertificate should be false from an invalid value", - ).to.be.false; - expect( - connInfo.connectTimeout, - "connectTimeout should be undefined from an invalid value", - ).to.be.undefined; - }); - - test("Should handle invalid parameter by ignoring it", async () => { - const connInfo = await mssqlProtocolHandler["readProfileFromArgs"]( - "server=myServer&database=dbName&madeUpParam=great", - ); - - expect(connInfo).to.be.an("object"); - expect(connInfo.server).to.equal("myServer"); - expect(connInfo.database).to.equal("dbName"); - expect( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (connInfo as any).madeUpParam, - "madeUpParam should be undefined from an invalid value", - ).to.be.undefined; - }); - }); -}); +/*--------------------------------------------------------------------------------------------- + * 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 sinon from "sinon"; +import sinonChai from "sinon-chai"; +import { expect } from "chai"; +import * as chai from "chai"; +import { MssqlProtocolHandler } from "../../src/mssqlProtocolHandler"; +import SqlToolsServiceClient from "../../src/languageservice/serviceclient"; +import { Uri } from "vscode"; +import { Logger } from "../../src/models/logger"; +import VscodeWrapper from "../../src/controllers/vscodeWrapper"; +import MainController from "../../src/controllers/mainController"; +import { generateUUID } from "../e2e/baseFixtures"; +import ConnectionManager from "../../src/controllers/connectionManager"; +import { MatchScore } from "../../src/models/utils"; +import { IConnectionProfile } from "../../src/models/interfaces"; +import { stubGetCapabilitiesRequest } from "./utils"; + +chai.use(sinonChai); + +suite("MssqlProtocolHandler Tests", () => { + let sandbox: sinon.SinonSandbox; + let mssqlProtocolHandler: MssqlProtocolHandler; + let sqlToolsServiceClientMock: sinon.SinonStubbedInstance; + let mockVscodeWrapper: sinon.SinonStubbedInstance; + let mockLogger: sinon.SinonStubbedInstance; + let mockMainController: sinon.SinonStubbedInstance; + let openConnectionDialogStub: sinon.SinonStub; + let connectProfileStub: sinon.SinonStub; + + setup(() => { + sandbox = sinon.createSandbox(); + mockVscodeWrapper = sandbox.createStubInstance(VscodeWrapper); + mockLogger = sandbox.createStubInstance(Logger); + mockMainController = sandbox.createStubInstance(MainController); + + const outputChannel = sinon.stub({ + append: () => sinon.stub(), + appendLine: () => sinon.stub(), + }) as unknown as vscode.OutputChannel; + + sinon.stub(mockVscodeWrapper, "outputChannel").get(() => { + return outputChannel; + }); + + sandbox.stub(Logger, "create").returns(mockLogger); + + sqlToolsServiceClientMock = stubGetCapabilitiesRequest(sandbox); + + mssqlProtocolHandler = new MssqlProtocolHandler( + mockVscodeWrapper, + mockMainController, + sqlToolsServiceClientMock as unknown as SqlToolsServiceClient, + ); + + openConnectionDialogStub = sandbox.stub( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mssqlProtocolHandler as any, + "openConnectionDialog", + ); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + connectProfileStub = sandbox.stub(mssqlProtocolHandler as any, "connectProfile").resolves(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test("No command", async () => { + await mssqlProtocolHandler.handleUri(Uri.parse("vscode://ms-mssql.mssql/")); + + expect(openConnectionDialogStub).to.have.been.calledOnceWith(undefined); + }); + + suite("Connect command", () => { + test("Should open connection dialog when no query is provided", async () => { + await mssqlProtocolHandler.handleUri(Uri.parse("vscode://ms-mssql.mssql/connect")); + + expect(openConnectionDialogStub).to.have.been.calledOnceWith(undefined); + expect(connectProfileStub).to.not.have.been.called; + }); + + test("Should find matching profile when connection string is provided", async () => { + const connString = `Server=myServerAddress;Database=myDataBase;User Id=myUsername;Password=${generateUUID()};`; + const mockConnectionManager = sandbox.createStubInstance(ConnectionManager); + + sandbox.stub(mockMainController, "connectionManager").get(() => { + return mockConnectionManager; + }); + + mockConnectionManager.findMatchingProfile.resolves({ + profile: { connectionString: connString } as IConnectionProfile, + score: MatchScore.AllAvailableProps, + }); + + await mssqlProtocolHandler.handleUri( + Uri.parse( + `vscode://ms-mssql.mssql/connect?connectionString=${encodeURIComponent(connString)}`, + ), + ); + + expect(connectProfileStub).to.have.been.calledOnceWith({ + connectionString: connString, + }); + + expect(openConnectionDialogStub).to.not.have.been.called; + }); + + test("Should find matching profile when parameters are provided", async () => { + const params: Record = { + server: "myServer", + database: "dbName", + user: "testUser", + }; + + const mockConnectionManager = sandbox.createStubInstance(ConnectionManager); + + sandbox.stub(mockMainController, "connectionManager").get(() => { + return mockConnectionManager; + }); + + mockConnectionManager.findMatchingProfile.resolves({ + profile: { + server: "myServer", + database: "dbName", + user: "testUser", + } as IConnectionProfile, + score: MatchScore.ServerDatabaseAndAuth, + }); + + await mssqlProtocolHandler.handleUri( + Uri.parse( + `vscode://ms-mssql.mssql/connect?${new URLSearchParams(params).toString()}`, + ), + ); + + expect(connectProfileStub).to.have.been.calledOnceWith(params); + expect(openConnectionDialogStub).to.not.have.been.called; + }); + + test("Should open connection dialog with populated parameters when no matching profile is found", async () => { + const params: Record = { + server: "myServer", + database: "dbName", + user: "testUser", + authenticationType: "SqlLogin", + connectTimeout: "15", + trustServerCertificate: "true", + }; + + const mockConnectionManager = sandbox.createStubInstance(ConnectionManager); + + sandbox.stub(mockMainController, "connectionManager").get(() => { + return mockConnectionManager; + }); + + const findMatchingProfileStub = mockConnectionManager.findMatchingProfile.resolves({ + profile: undefined, + score: MatchScore.NotMatch, + }); + + await mssqlProtocolHandler.handleUri( + Uri.parse( + `vscode://ms-mssql.mssql/connect?${new URLSearchParams(params).toString()}`, + ), + ); + + expect(openConnectionDialogStub).to.have.been.calledOnceWith({ + ...params, + // savePassword is auto-added, and non-string values are converted + savePassword: true, + connectTimeout: 15, + trustServerCertificate: true, + }); + expect(connectProfileStub).to.not.have.been.called; + + // Reset stubs for server-only test case + + findMatchingProfileStub.reset(); + openConnectionDialogStub.resetHistory(); + connectProfileStub.resetHistory(); + + findMatchingProfileStub.resolves({ + profile: { server: "myServer", database: "otherDatabase" } as IConnectionProfile, + score: MatchScore.Server, + }); + + await mssqlProtocolHandler.handleUri( + Uri.parse( + `vscode://ms-mssql.mssql/connect?${new URLSearchParams(params).toString()}`, + ), + ); + + expect(openConnectionDialogStub).to.have.been.calledOnceWith({ + ...params, + // savePassword is auto-added, and non-string values are converted + savePassword: true, + connectTimeout: 15, + trustServerCertificate: true, + }); + expect(connectProfileStub).to.not.have.been.called; + }); + }); + + suite("OpenConnectionDialog command", () => { + test("Should open blank connection dialog when no parameters are provided", async () => { + await mssqlProtocolHandler.handleUri( + Uri.parse("vscode://ms-mssql.mssql/openConnectionDialog"), + ); + + expect(openConnectionDialogStub).to.have.been.calledOnceWith(undefined); + expect(connectProfileStub).to.not.have.been.called; + }); + + test("Should open populated connection dialog when parameters are provided", async () => { + const params: Record = { + server: "myServer", + database: "dbName", + user: "testUser", + authenticationType: "SqlLogin", + connectTimeout: "15", + trustServerCertificate: "true", + }; + + await mssqlProtocolHandler.handleUri( + Uri.parse( + `vscode://ms-mssql.mssql/openConnectionDialog?${new URLSearchParams(params).toString()}`, + ), + ); + + expect(openConnectionDialogStub).to.have.been.calledOnceWith({ + ...params, + // savePassword is auto-added, and non-string values are converted + savePassword: true, + connectTimeout: 15, + trustServerCertificate: true, + }); + expect(connectProfileStub).to.not.have.been.called; + }); + }); + + suite("readProfileFromArgs", () => { + test("Should ignore invalid values for booleans and numbers", async () => { + const connInfo = await mssqlProtocolHandler["readProfileFromArgs"]( + "server=myServer&database=dbName&trustServerCertificate=yes&connectTimeout=twenty", + ); + + expect(connInfo).to.be.an("object"); + expect(connInfo.server).to.equal("myServer"); + expect(connInfo.database).to.equal("dbName"); + expect( + connInfo.trustServerCertificate, + "trustServerCertificate should be false from an invalid value", + ).to.be.false; + expect( + connInfo.connectTimeout, + "connectTimeout should be undefined from an invalid value", + ).to.be.undefined; + }); + + test("Should handle invalid parameter by ignoring it", async () => { + const connInfo = await mssqlProtocolHandler["readProfileFromArgs"]( + "server=myServer&database=dbName&madeUpParam=great", + ); + + expect(connInfo).to.be.an("object"); + expect(connInfo.server).to.equal("myServer"); + expect(connInfo.database).to.equal("dbName"); + expect( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (connInfo as any).madeUpParam, + "madeUpParam should be undefined from an invalid value", + ).to.be.undefined; + }); + }); +}); diff --git a/test/unit/utils.ts b/test/unit/utils.ts index 469b869794..38a7dbd54d 100644 --- a/test/unit/utils.ts +++ b/test/unit/utils.ts @@ -10,6 +10,9 @@ import * as vscode from "vscode"; import { IExtension } from "vscode-mssql"; import VscodeWrapper from "../../src/controllers/vscodeWrapper"; import * as path from "path"; +import SqlToolsServerClient from "../../src/languageservice/serviceclient"; +import { GetCapabilitiesRequest } from "../../src/models/contracts/connection"; +import { buildCapabilitiesResult } from "./mocks"; // Launches and activates the extension export async function activateExtension(): Promise { @@ -47,6 +50,17 @@ export function stubVscodeWrapper( return vscodeWrapper; } +export function stubGetCapabilitiesRequest( + sandbox?: sinon.SinonSandbox, +): sinon.SinonStubbedInstance { + const stubber = sandbox || sinon; + const serviceClientMock = stubber.createStubInstance(SqlToolsServerClient); + serviceClientMock.sendRequest + .withArgs(GetCapabilitiesRequest.type, sinon.match.any) + .resolves(buildCapabilitiesResult()); + return serviceClientMock; +} + export function initializeIconUtils(): void { const { IconUtils } = require("../../src/utils/iconUtils"); IconUtils.initialize(vscode.Uri.file(path.join(__dirname, "..", ".."))); From a7a1de182f5215b4ee4499290a017010d1e22e1f Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Mon, 29 Sep 2025 19:56:50 -0700 Subject: [PATCH 09/30] converted server.test.ts --- test/unit/server.test.ts | 306 +++++++++++++++++---------------------- 1 file changed, 134 insertions(+), 172 deletions(-) diff --git a/test/unit/server.test.ts b/test/unit/server.test.ts index 5ee9209d2c..2f2dc34ad1 100644 --- a/test/unit/server.test.ts +++ b/test/unit/server.test.ts @@ -1,172 +1,134 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as TypeMoq from "typemoq"; -import * as assert from "assert"; -import ServiceDownloadProvider from "../../src/languageservice/serviceDownloadProvider"; -import ServerProvider from "../../src/languageservice/server"; -import { ServerStatusView } from "../../src/languageservice/serverStatus"; -import ConfigUtils from "../../src/configurations/configUtils"; -import { Runtime } from "../../src/models/platform"; -import { IConfigUtils, IStatusView } from "../../src/languageservice/interfaces"; - -interface IFixture { - executableFileName: string; - executablesFromConfig: string[]; - runtime: Runtime; - installDir: string; -} - -suite("Server tests", () => { - let testDownloadProvider: TypeMoq.IMock; - let testStatusView: TypeMoq.IMock; - let testConfig: TypeMoq.IMock; - - setup(() => { - testDownloadProvider = TypeMoq.Mock.ofType( - ServiceDownloadProvider, - TypeMoq.MockBehavior.Strict, - ); - testStatusView = TypeMoq.Mock.ofType(ServerStatusView, TypeMoq.MockBehavior.Strict); - testConfig = TypeMoq.Mock.ofType(ConfigUtils, TypeMoq.MockBehavior.Strict); - }); - - function setupMocks(fixture: IFixture): void { - testConfig - .setup((x) => x.getSqlToolsExecutableFiles()) - .returns(() => fixture.executablesFromConfig); - testDownloadProvider - .setup((x) => x.getOrMakeInstallDirectory(fixture.runtime)) - .returns(() => Promise.resolve(fixture.installDir)); - testDownloadProvider - .setup((x) => x.installSQLToolsService(fixture.runtime)) - .callback(() => { - fixture.executablesFromConfig = [ - fixture.executableFileName.replace(fixture.installDir, ""), - ]; - }) - .returns(() => { - return Promise.resolve(true); - }); - } - - test("findServerPath should return error given a folder with no installed service", () => { - let fixture: IFixture = { - executableFileName: "", - runtime: Runtime.Windows_64, - installDir: __dirname, - executablesFromConfig: ["exeFile1", "exeFile2"], - }; - - setupMocks(fixture); - let server = new ServerProvider( - testDownloadProvider.object, - testConfig.object, - testStatusView.object, - ); - - return server.findServerPath(fixture.installDir).then((result) => { - assert.equal(result, undefined); - }); - }); - - test("findServerPath should return the file path given a file that exists", () => { - let fixture: IFixture = { - executableFileName: __filename, - runtime: Runtime.Windows_64, - installDir: __dirname, - executablesFromConfig: undefined, - }; - setupMocks(fixture); - let server = new ServerProvider( - testDownloadProvider.object, - testConfig.object, - testStatusView.object, - ); - - return server.findServerPath(fixture.executableFileName).then((result) => { - assert.equal(result, fixture.executableFileName); - }); - }); - - test("findServerPath should not return the given file path if does not exist", () => { - let fixture: IFixture = { - executableFileName: __filename, - runtime: Runtime.Windows_64, - installDir: __dirname, - executablesFromConfig: ["exeFile1", "exeFile2"], - }; - setupMocks(fixture); - let server = new ServerProvider( - testDownloadProvider.object, - testConfig.object, - testStatusView.object, - ); - - return server.findServerPath(fixture.installDir).then((result) => { - assert.equal(result, undefined); - }); - }); - - test("findServerPath should return a valid file path given a folder with installed service", () => { - let fixture: IFixture = { - executableFileName: __filename, - runtime: Runtime.Windows_64, - installDir: __dirname, - executablesFromConfig: ["exeFile1", __filename], - }; - setupMocks(fixture); - let server = new ServerProvider( - testDownloadProvider.object, - testConfig.object, - testStatusView.object, - ); - - return server.findServerPath(fixture.executableFileName).then((result) => { - assert.equal(result, fixture.executableFileName); - }); - }); - - test("getOrDownloadServer should download the service if not exist and return the valid service file path", () => { - let fixture: IFixture = { - executableFileName: __filename, - runtime: Runtime.Windows_64, - installDir: __dirname, - executablesFromConfig: ["exeFile1"], - }; - - setupMocks(fixture); - let server = new ServerProvider( - testDownloadProvider.object, - testConfig.object, - testStatusView.object, - ); - - return server.getOrDownloadServer(fixture.runtime).then((result) => { - assert.equal(result, fixture.executableFileName); - }); - }); - - test("getOrDownloadServer should not download the service if already exist", () => { - let fixture: IFixture = { - executableFileName: __filename, - runtime: Runtime.Windows_64, - installDir: __dirname, - executablesFromConfig: [__filename.replace(__dirname, "")], - }; - - setupMocks(fixture); - - let server = new ServerProvider( - testDownloadProvider.object, - testConfig.object, - testStatusView.object, - ); - - return server.getOrDownloadServer(fixture.runtime).then((result) => { - assert.equal(result, fixture.executableFileName); - }); - }); -}); +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as sinon from "sinon"; +import { expect } from "chai"; +import * as chai from "chai"; +import sinonChai from "sinon-chai"; +import ServiceDownloadProvider from "../../src/languageservice/serviceDownloadProvider"; +import ServerProvider from "../../src/languageservice/server"; +import { ServerStatusView } from "../../src/languageservice/serverStatus"; +import ConfigUtils from "../../src/configurations/configUtils"; +import { Runtime } from "../../src/models/platform"; +import { IConfigUtils, IStatusView } from "../../src/languageservice/interfaces"; + +chai.use(sinonChai); + +suite("Server tests", () => { + let sandbox: sinon.SinonSandbox; + let downloadProvider: sinon.SinonStubbedInstance; + let statusView: sinon.SinonStubbedInstance; + let configUtils: sinon.SinonStubbedInstance; + + setup(() => { + sandbox = sinon.createSandbox(); + downloadProvider = sandbox.createStubInstance(ServiceDownloadProvider); + statusView = sandbox.createStubInstance(ServerStatusView); + configUtils = sandbox.createStubInstance(ConfigUtils); + }); + + teardown(() => { + sandbox.restore(); + }); + + function createServer(fixture: IFixture): ServerProvider { + configUtils.getSqlToolsExecutableFiles.callsFake(() => fixture.executablesFromConfig); + downloadProvider.getOrMakeInstallDirectory.callsFake(async () => fixture.installDir); + downloadProvider.installSQLToolsService.callsFake(async () => { + if (fixture.executablesFromConfig) { + fixture.executablesFromConfig = [ + fixture.executableFileName.replace(fixture.installDir, ""), + ]; + } + return true; + }); + + return new ServerProvider(downloadProvider, configUtils, statusView); + } + + test("findServerPath should return error given a folder with no installed service", async () => { + const fixture: IFixture = { + executableFileName: "", + runtime: Runtime.Windows_64, + installDir: __dirname, + executablesFromConfig: ["exeFile1", "exeFile2"], + }; + + const server = createServer(fixture); + const result = await server.findServerPath(fixture.installDir); + expect(result).to.be.undefined; + }); + + test("findServerPath should return the file path given a file that exists", async () => { + const fixture: IFixture = { + executableFileName: __filename, + runtime: Runtime.Windows_64, + installDir: __dirname, + executablesFromConfig: undefined, + }; + const server = createServer(fixture); + const result = await server.findServerPath(fixture.executableFileName); + expect(result).to.equal(fixture.executableFileName); + }); + + test("findServerPath should not return the given file path if does not exist", async () => { + const fixture: IFixture = { + executableFileName: __filename, + runtime: Runtime.Windows_64, + installDir: __dirname, + executablesFromConfig: ["exeFile1", "exeFile2"], + }; + const server = createServer(fixture); + const result = await server.findServerPath(fixture.installDir); + expect(result).to.be.undefined; + }); + + test("findServerPath should return a valid file path given a folder with installed service", async () => { + const fixture: IFixture = { + executableFileName: __filename, + runtime: Runtime.Windows_64, + installDir: __dirname, + executablesFromConfig: ["exeFile1", __filename], + }; + const server = createServer(fixture); + const result = await server.findServerPath(fixture.executableFileName); + expect(result).to.equal(fixture.executableFileName); + }); + + test("getOrDownloadServer should download the service if not exist and return the valid service file path", async () => { + const fixture: IFixture = { + executableFileName: __filename, + runtime: Runtime.Windows_64, + installDir: __dirname, + executablesFromConfig: ["exeFile1"], + }; + const server = createServer(fixture); + const result = await server.getOrDownloadServer(fixture.runtime); + expect(result).to.equal(fixture.executableFileName); + expect(downloadProvider.installSQLToolsService).to.have.been.calledOnceWithExactly( + fixture.runtime, + ); + }); + + test("getOrDownloadServer should not download the service if already exist", async () => { + const fixture: IFixture = { + executableFileName: __filename, + runtime: Runtime.Windows_64, + installDir: __dirname, + executablesFromConfig: [__filename.replace(__dirname, "")], + }; + const server = createServer(fixture); + const result = await server.getOrDownloadServer(fixture.runtime); + expect(result).to.equal(fixture.executableFileName); + expect(downloadProvider.installSQLToolsService).to.not.have.been.called; + }); +}); + +interface IFixture { + executableFileName: string; + executablesFromConfig: string[] | undefined; + runtime: Runtime; + installDir: string; +} From 6a13f032573e621fb4b71ecbb1b879e00433a9cd Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Mon, 29 Sep 2025 20:28:14 -0700 Subject: [PATCH 10/30] converted scriptingService.test.ts --- test/unit/scriptingService.test.ts | 495 ++++++++++++----------------- 1 file changed, 208 insertions(+), 287 deletions(-) diff --git a/test/unit/scriptingService.test.ts b/test/unit/scriptingService.test.ts index 4ef727bab4..b1e7e5a6d3 100644 --- a/test/unit/scriptingService.test.ts +++ b/test/unit/scriptingService.test.ts @@ -1,287 +1,208 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { assert } from "chai"; -import * as TypeMoq from "typemoq"; -import { IScriptingObject, IServerInfo, MetadataType, ObjectMetadata } from "vscode-mssql"; -import ConnectionManager from "../../src/controllers/connectionManager"; -import SqlToolsServiceClient from "../../src/languageservice/serviceclient"; -import { - IScriptingResult, - ScriptingRequest, - ScriptOperation, -} from "../../src/models/contracts/scripting/scriptingRequest"; -import { TreeNodeInfo } from "../../src/objectExplorer/nodes/treeNodeInfo"; -import { ScriptingService } from "../../src/scripting/scriptingService"; -import { TestExtensionContext } from "./stubs"; -import { initializeIconUtils } from "./utils"; - -suite("Scripting Service Tests", () => { - let scriptingService: ScriptingService; - let connectionManager: TypeMoq.IMock; - let client: TypeMoq.IMock; - - setup(() => { - initializeIconUtils(); - connectionManager = TypeMoq.Mock.ofType( - ConnectionManager, - TypeMoq.MockBehavior.Loose, - TestExtensionContext.object, - ); - connectionManager.setup((c) => c.client).returns(() => client.object); - client = TypeMoq.Mock.ofType(SqlToolsServiceClient, TypeMoq.MockBehavior.Loose); - const mockScriptResult: IScriptingResult = { - operationId: undefined, - script: "test_script", - }; - client - .setup((c) => c.sendRequest(ScriptingRequest.type, TypeMoq.It.isAny())) - .returns(() => Promise.resolve(mockScriptResult)); - connectionManager.object.client = client.object; - connectionManager - .setup((c) => c.getServerInfo(TypeMoq.It.isAny())) - .returns(() => { - const serverInfo: IServerInfo = { - engineEditionId: 2, - serverMajorVersion: 1, - isCloud: true, - serverMinorVersion: 0, - serverReleaseVersion: 0, - serverVersion: "", - serverLevel: "", - serverEdition: "", - azureVersion: 0, - osVersion: "", - }; - return serverInfo; - }); - }); - - test("Test Get Object From Node function", () => { - const testNodeMetadata: ObjectMetadata = { - metadataType: MetadataType.Table, - metadataTypeName: "Table", - urn: undefined, - schema: "dbo", - name: "test_table", - }; - const testNode = new TreeNodeInfo( - "test_table (System Versioned)", - undefined, - undefined, - undefined, - undefined, - "Table", - undefined, - undefined, - undefined, - undefined, - undefined, - testNodeMetadata, - ); - scriptingService = new ScriptingService(connectionManager.object); - const expectedScriptingObject: IScriptingObject = { - type: testNodeMetadata.metadataTypeName, - schema: testNodeMetadata.schema, - name: testNodeMetadata.name, - }; - const scriptingObject = scriptingService.getObjectFromNode(testNode); - assert.equal(scriptingObject.name, expectedScriptingObject.name); - assert.equal(scriptingObject.schema, expectedScriptingObject.schema); - assert.equal(scriptingObject.type, expectedScriptingObject.type); - }); - - test("Test Create Scripting Params", () => { - const testNodeMetadata: ObjectMetadata = { - metadataType: MetadataType.Table, - metadataTypeName: "Table", - urn: undefined, - schema: "dbo", - name: "test_table", - }; - const testNode = new TreeNodeInfo( - "test_table (System Versioned)", - undefined, - undefined, - undefined, - undefined, - "Table", - undefined, - undefined, - undefined, - undefined, - undefined, - testNodeMetadata, - ); - scriptingService = new ScriptingService(connectionManager.object); - let scriptingParams = scriptingService.createScriptingParamsFromNode( - testNode, - "test_uri", - ScriptOperation.Select, - ); - const scriptingObject = scriptingService.getObjectFromNode(testNode); - assert.notEqual(scriptingParams, undefined); - assert.equal(scriptingParams.scriptDestination, "ToEditor"); - assert.equal(scriptingParams.scriptingObjects[0].name, scriptingObject.name); - assert.equal(scriptingParams.scriptingObjects[0].schema, scriptingObject.schema); - assert.equal(scriptingParams.scriptingObjects[0].type, scriptingObject.type); - assert.equal(scriptingParams.operation, ScriptOperation.Select); - }); - - test("Test Script Select function", async () => { - const testNodeMetadata: ObjectMetadata = { - metadataType: MetadataType.Table, - metadataTypeName: "Table", - urn: undefined, - schema: "dbo", - name: "test_table", - }; - const testNode = new TreeNodeInfo( - "test_table (System Versioned)", - undefined, - undefined, - undefined, - undefined, - "Table", - undefined, - undefined, - undefined, - undefined, - undefined, - testNodeMetadata, - ); - scriptingService = new ScriptingService(connectionManager.object); - const script = await scriptingService.scriptTreeNode( - testNode, - "test_uri", - ScriptOperation.Select, - ); - assert.notEqual(script, undefined); - }); - - test("Test Script Create function", async () => { - const testNodeMetadata: ObjectMetadata = { - metadataType: MetadataType.Table, - metadataTypeName: "Table", - urn: undefined, - schema: "dbo", - name: "test_table", - }; - const testNode = new TreeNodeInfo( - "test_table (System Versioned)", - undefined, - undefined, - undefined, - undefined, - "Table", - undefined, - undefined, - undefined, - undefined, - undefined, - testNodeMetadata, - ); - scriptingService = new ScriptingService(connectionManager.object); - const script = await scriptingService.scriptTreeNode( - testNode, - "test_uri", - ScriptOperation.Create, - ); - assert.notEqual(script, undefined); - }); - - test("Test Script Execute function", async () => { - const testNodeMetadata: ObjectMetadata = { - metadataType: MetadataType.SProc, - metadataTypeName: "StoredProcedure", - urn: undefined, - schema: "dbo", - name: "test_proc", - }; - const testNode = new TreeNodeInfo( - "test_table (System Versioned)", - undefined, - undefined, - undefined, - undefined, - "Table", - undefined, - undefined, - undefined, - undefined, - undefined, - testNodeMetadata, - ); - scriptingService = new ScriptingService(connectionManager.object); - const script = await scriptingService.scriptTreeNode( - testNode, - "test_uri", - ScriptOperation.Execute, - ); - assert.notEqual(script, undefined); - }); - - test("Test Script Drop function", async () => { - const testNodeMetadata: ObjectMetadata = { - metadataType: MetadataType.Table, - metadataTypeName: "Table", - urn: undefined, - schema: "dbo", - name: "test_table", - }; - const testNode = new TreeNodeInfo( - "test_table (System Versioned)", - undefined, - undefined, - undefined, - undefined, - "Table", - undefined, - undefined, - undefined, - undefined, - undefined, - testNodeMetadata, - ); - scriptingService = new ScriptingService(connectionManager.object); - const script = await scriptingService.scriptTreeNode( - testNode, - "test_uri", - ScriptOperation.Delete, - ); - assert.notEqual(script, undefined); - }); - - test("Test Script Alter function", async () => { - const testNodeMetadata: ObjectMetadata = { - metadataType: MetadataType.SProc, - metadataTypeName: "StoredProcedure", - urn: undefined, - schema: "dbo", - name: "test_sproc", - }; - const testNode = new TreeNodeInfo( - "test_table (System Versioned)", - undefined, - undefined, - undefined, - undefined, - "StoredProcedure", - undefined, - undefined, - undefined, - undefined, - undefined, - testNodeMetadata, - ); - scriptingService = new ScriptingService(connectionManager.object); - const script = await scriptingService.scriptTreeNode( - testNode, - "test_uri", - ScriptOperation.Alter, - ); - assert.notEqual(script, undefined); - }); -}); +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as sinon from "sinon"; +import sinonChai from "sinon-chai"; +import { expect } from "chai"; +import * as chai from "chai"; +import { IServerInfo, MetadataType, ObjectMetadata } from "vscode-mssql"; +import ConnectionManager from "../../src/controllers/connectionManager"; +import SqlToolsServiceClient from "../../src/languageservice/serviceclient"; +import { + IScriptingResult, + ScriptingRequest, + ScriptOperation, +} from "../../src/models/contracts/scripting/scriptingRequest"; +import { TreeNodeInfo } from "../../src/objectExplorer/nodes/treeNodeInfo"; +import { ScriptingService } from "../../src/scripting/scriptingService"; +import { initializeIconUtils } from "./utils"; + +chai.use(sinonChai); + +suite("Scripting Service Tests", () => { + let sandbox: sinon.SinonSandbox; + let connectionManager: sinon.SinonStubbedInstance; + let client: sinon.SinonStubbedInstance; + let scriptingService: ScriptingService; + + const mockScriptResult: IScriptingResult = { + operationId: undefined, + script: "test_script", + }; + + const serverInfo: IServerInfo = { + engineEditionId: 2, + serverMajorVersion: 1, + isCloud: true, + serverMinorVersion: 0, + serverReleaseVersion: 0, + serverVersion: "", + serverLevel: "", + serverEdition: "", + azureVersion: 0, + osVersion: "", + }; + + setup(() => { + sandbox = sinon.createSandbox(); + initializeIconUtils(); + + connectionManager = sandbox.createStubInstance(ConnectionManager); + client = sandbox.createStubInstance(SqlToolsServiceClient); + + // Wire scripting service dependencies + connectionManager.client = client; + connectionManager.getServerInfo.callsFake(() => serverInfo); + client.onNotification.callsFake(() => undefined); + client.sendRequest + .withArgs(ScriptingRequest.type, sinon.match.any) + .resolves(mockScriptResult); + }); + + teardown(() => { + sandbox.restore(); + }); + + function getTableNode(): TreeNodeInfo { + const metadata: ObjectMetadata = { + metadataType: MetadataType.Table, + metadataTypeName: "Table", + urn: undefined, + schema: "dbo", + name: "test_table", + }; + return new TreeNodeInfo( + "test_table (System Versioned)", + undefined, + undefined, + undefined, + undefined, + "Table", + undefined, + undefined, + undefined, + undefined, + undefined, + metadata, + ); + } + + function getSprocNode(): TreeNodeInfo { + const metadata: ObjectMetadata = { + metadataType: MetadataType.SProc, + metadataTypeName: "StoredProcedure", + urn: undefined, + schema: "dbo", + name: "test_sproc", + }; + return new TreeNodeInfo( + "test_sproc", + undefined, + undefined, + undefined, + undefined, + "StoredProcedure", + undefined, + undefined, + undefined, + undefined, + undefined, + metadata, + ); + } + + test("Test Get Object From Node function", () => { + const testNode = getTableNode(); + scriptingService = new ScriptingService(connectionManager); + const scriptingObject = scriptingService.getObjectFromNode(testNode); + + expect(scriptingObject).to.include({ + type: "Table", + schema: "dbo", + name: "test_table", + }); + }); + + test("Test Create Scripting Params", () => { + const testNode = getTableNode(); + scriptingService = new ScriptingService(connectionManager); + + const scriptingParams = scriptingService.createScriptingParamsFromNode( + testNode, + "test_uri", + ScriptOperation.Select, + ); + const scriptingObject = scriptingService.getObjectFromNode(testNode); + + expect(scriptingParams).to.not.be.undefined; + expect(scriptingParams.scriptDestination).to.equal("ToEditor"); + expect(scriptingParams.scriptingObjects[0]).to.include(scriptingObject); + }); + + test("Test Script Select function", async () => { + const testNode = getTableNode(); + scriptingService = new ScriptingService(connectionManager); + + const script = await scriptingService.scriptTreeNode( + testNode, + "test_uri", + ScriptOperation.Select, + ); + + expect(script).to.equal("test_script"); + }); + + test("Test Script Create function", async () => { + const testNode = getTableNode(); + scriptingService = new ScriptingService(connectionManager); + + const script = await scriptingService.scriptTreeNode( + testNode, + "test_uri", + ScriptOperation.Create, + ); + + expect(script).to.equal("test_script"); + }); + + test("Test Script Execute function", async () => { + const testNode = getSprocNode(); + scriptingService = new ScriptingService(connectionManager); + + const script = await scriptingService.scriptTreeNode( + testNode, + "test_uri", + ScriptOperation.Execute, + ); + + expect(script).to.equal("test_script"); + }); + + test("Test Script Drop function", async () => { + const testNode = getTableNode(); + scriptingService = new ScriptingService(connectionManager); + + const script = await scriptingService.scriptTreeNode( + testNode, + "test_uri", + ScriptOperation.Delete, + ); + + expect(script).to.equal("test_script"); + }); + + test("Test Script Alter function", async () => { + const testNode = getSprocNode(); + scriptingService = new ScriptingService(connectionManager); + + const script = await scriptingService.scriptTreeNode( + testNode, + "test_uri", + ScriptOperation.Alter, + ); + + expect(script).to.equal("test_script"); + }); +}); From 22329014278556a6d0a1363cecec3a2b5d78e276 Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Mon, 29 Sep 2025 20:46:18 -0700 Subject: [PATCH 11/30] converted extConfig.test.ts --- test/unit/extConfig.test.ts | 247 +++++++++++++++++++----------------- 1 file changed, 128 insertions(+), 119 deletions(-) diff --git a/test/unit/extConfig.test.ts b/test/unit/extConfig.test.ts index 2f205fd230..ad8e952695 100644 --- a/test/unit/extConfig.test.ts +++ b/test/unit/extConfig.test.ts @@ -3,151 +3,160 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from "assert"; -import * as TypeMoq from "typemoq"; +import * as sinon from "sinon"; +import sinonChai from "sinon-chai"; +import { expect } from "chai"; +import * as chai from "chai"; import { IConfigUtils } from "../../src/languageservice/interfaces"; import { WorkspaceConfiguration, workspace } from "vscode"; import * as Constants from "../../src/constants/constants"; import ExtConfig from "../../src/configurations/extConfig"; import ConfigUtils from "../../src/configurations/configUtils"; +chai.use(sinonChai); + suite("ExtConfig Tests", () => { - let config: TypeMoq.IMock; - let extensionConfig: TypeMoq.IMock; - let workspaceConfig: TypeMoq.IMock; - let fromConfig = "fromConfig"; - let fromExtensionConfig = "fromExtensionConfig"; + let sandbox: sinon.SinonSandbox; + let config: sinon.SinonStubbedInstance; + let extensionConfigGet: sinon.SinonStub; + let workspaceConfigGet: sinon.SinonStub; + let extensionConfig: WorkspaceConfiguration; + let workspaceConfig: WorkspaceConfiguration; + const fromConfig = "fromConfig"; + const fromExtensionConfig = "fromExtensionConfig"; + + const toolsKey = (configKey: string): string => + `${Constants.sqlToolsServiceConfigKey}.${configKey}`; - function createExtConfigInstance( + const createExtConfigInstance = ( configKey: string, - expectedFromConfig: string, - expectedFromExtensionConfig: string, - ): ExtConfig { - let toolsConfigKey = `${Constants.sqlToolsServiceConfigKey}.${configKey}`; - config.setup((x) => x.getSqlToolsConfigValue(configKey)).returns(() => expectedFromConfig); - extensionConfig - .setup((x) => x.get(toolsConfigKey)) - .returns(() => expectedFromExtensionConfig); - let extConfig = new ExtConfig( - config.object, - extensionConfig.object, - workspaceConfig.object, - ); - return extConfig; - } + expectedFromConfig: string | undefined, + expectedFromExtension: string | undefined, + ): ExtConfig => { + config.getSqlToolsConfigValue.reset(); + config.getSqlToolsConfigValue.returns(expectedFromConfig); + + extensionConfigGet.reset(); + extensionConfigGet.returns(undefined); + if (expectedFromExtension !== undefined) { + extensionConfigGet.withArgs(toolsKey(configKey)).returns(expectedFromExtension); + } + + return new ExtConfig(config as unknown as IConfigUtils, extensionConfig, workspaceConfig); + }; setup(() => { - config = TypeMoq.Mock.ofType(ConfigUtils, TypeMoq.MockBehavior.Strict); - let configInstance = workspace.getConfiguration(); - extensionConfig = TypeMoq.Mock.ofInstance( - configInstance, - TypeMoq.MockBehavior.Strict, - ); - workspaceConfig = TypeMoq.Mock.ofInstance( - configInstance, - TypeMoq.MockBehavior.Strict, - ); + sandbox = sinon.createSandbox(); + + config = sandbox.createStubInstance(ConfigUtils); + config.getSqlToolsConfigValue.returns(undefined); + config.getSqlToolsExecutableFiles.returns([]); + config.getSqlToolsInstallDirectory?.returns?.(""); + + const baseConfig = workspace.getConfiguration(); + extensionConfigGet = sandbox.stub(); + workspaceConfigGet = sandbox.stub(); + + extensionConfig = { + ...baseConfig, + get: extensionConfigGet, + } as WorkspaceConfiguration; + workspaceConfig = { + ...baseConfig, + get: workspaceConfigGet, + } as WorkspaceConfiguration; + + extensionConfigGet.returns(undefined); + workspaceConfigGet.returns(undefined); + }); + + teardown(() => { + sandbox.restore(); }); - test("getSqlToolsServiceDownloadUrl should return value from extension config first", (done) => { - return new Promise((resolve, reject) => { - let configKey = Constants.sqlToolsServiceDownloadUrlConfigKey; - let extConfig = createExtConfigInstance(configKey, fromConfig, fromExtensionConfig); - let actual = extConfig.getSqlToolsServiceDownloadUrl(); - assert.equal(actual, fromExtensionConfig); - done(); - }); + test("getSqlToolsServiceDownloadUrl should return value from extension config first", () => { + const configKey = Constants.sqlToolsServiceDownloadUrlConfigKey; + const extConfig = createExtConfigInstance(configKey, fromConfig, fromExtensionConfig); + const actual = extConfig.getSqlToolsServiceDownloadUrl(); + expect(actual).to.equal(fromExtensionConfig); }); - test("getSqlToolsServiceDownloadUrl should return value from config.json if not exit in extension config", (done) => { - return new Promise((resolve, reject) => { - let configKey = Constants.sqlToolsServiceDownloadUrlConfigKey; - let extConfig = createExtConfigInstance(configKey, fromConfig, undefined); - let actual = extConfig.getSqlToolsServiceDownloadUrl(); - assert.equal(actual, fromConfig); - done(); - }); + test("getSqlToolsServiceDownloadUrl should return value from config.json if not exist in extension config", () => { + const configKey = Constants.sqlToolsServiceDownloadUrlConfigKey; + const extConfig = createExtConfigInstance(configKey, fromConfig, undefined); + const actual = extConfig.getSqlToolsServiceDownloadUrl(); + expect(actual).to.equal(fromConfig); }); - test("getSqlToolsConfigValue should return value from extension config first", (done) => { - return new Promise((resolve, reject) => { - let configKey = Constants.sqlToolsServiceInstallDirConfigKey; - let extConfig = createExtConfigInstance(configKey, fromConfig, fromExtensionConfig); - let actual = extConfig.getSqlToolsConfigValue(configKey); - assert.equal(actual, fromExtensionConfig); - done(); - }); + test("getSqlToolsConfigValue should return value from extension config first", () => { + const configKey = Constants.sqlToolsServiceInstallDirConfigKey; + const extConfig = createExtConfigInstance(configKey, fromConfig, fromExtensionConfig); + const actual = extConfig.getSqlToolsConfigValue(configKey); + expect(actual).to.equal(fromExtensionConfig); }); - test("getSqlToolsConfigValue should return value from config.json if not exit in extension config", (done) => { - return new Promise((resolve, reject) => { - let configKey = Constants.sqlToolsServiceInstallDirConfigKey; - let extConfig = createExtConfigInstance(configKey, fromConfig, undefined); - let actual = extConfig.getSqlToolsConfigValue(configKey); - assert.equal(actual, fromConfig); - done(); - }); + test("getSqlToolsConfigValue should return value from config.json if not exist in extension config", () => { + const configKey = Constants.sqlToolsServiceInstallDirConfigKey; + const extConfig = createExtConfigInstance(configKey, fromConfig, undefined); + const actual = extConfig.getSqlToolsConfigValue(configKey); + expect(actual).to.equal(fromConfig); }); - test("getExtensionConfig should return value from extension config", (done) => { - return new Promise((resolve, reject) => { - let configKey = "config key"; - extensionConfig.setup((x) => x.get(configKey)).returns(() => fromExtensionConfig); - let extConfig = new ExtConfig( - config.object, - extensionConfig.object, - workspaceConfig.object, - ); - let actual = extConfig.getExtensionConfig(configKey); - assert.equal(actual, fromExtensionConfig); - done(); - }); + test("getExtensionConfig should return value from extension config", () => { + const configKey = "config key"; + extensionConfigGet.reset(); + extensionConfigGet.returns(undefined); + extensionConfigGet.withArgs(configKey).returns(fromExtensionConfig); + const extConfig = new ExtConfig( + config as unknown as IConfigUtils, + extensionConfig, + workspaceConfig, + ); + const actual = extConfig.getExtensionConfig(configKey); + expect(actual).to.equal(fromExtensionConfig); }); - test("getExtensionConfig should return the default value if the extension does not have the config", (done) => { - return new Promise((resolve, reject) => { - let configKey = "config key"; - let defaultValue = "default value"; - extensionConfig.setup((x) => x.get(configKey)).returns(() => undefined); - let extConfig = new ExtConfig( - config.object, - extensionConfig.object, - workspaceConfig.object, - ); - let actual = extConfig.getExtensionConfig(configKey, defaultValue); - assert.equal(actual, defaultValue); - done(); - }); + test("getExtensionConfig should return the default value if the extension does not have the config", () => { + const configKey = "config key"; + const defaultValue = "default value"; + extensionConfigGet.reset(); + extensionConfigGet.returns(undefined); + extensionConfigGet.withArgs(configKey).returns(undefined); + const extConfig = new ExtConfig( + config as unknown as IConfigUtils, + extensionConfig, + workspaceConfig, + ); + const actual = extConfig.getExtensionConfig(configKey, defaultValue); + expect(actual).to.equal(defaultValue); }); - test("getWorkspaceConfig should return value from workspace config", (done) => { - return new Promise((resolve, reject) => { - let configKey = "config key"; - workspaceConfig.setup((x) => x.get(configKey)).returns(() => fromExtensionConfig); - let extConfig = new ExtConfig( - config.object, - extensionConfig.object, - workspaceConfig.object, - ); - let actual = extConfig.getWorkspaceConfig(configKey); - assert.equal(actual, fromExtensionConfig); - done(); - }); + test("getWorkspaceConfig should return value from workspace config", () => { + const configKey = "config key"; + workspaceConfigGet.reset(); + workspaceConfigGet.returns(undefined); + workspaceConfigGet.withArgs(configKey).returns(fromExtensionConfig); + const extConfig = new ExtConfig( + config as unknown as IConfigUtils, + extensionConfig, + workspaceConfig, + ); + const actual = extConfig.getWorkspaceConfig(configKey); + expect(actual).to.equal(fromExtensionConfig); }); - test("getWorkspaceConfig should return the default value if the workspace does not have the config", (done) => { - return new Promise((resolve, reject) => { - let configKey = "config key"; - let defaultValue = "default value"; - workspaceConfig.setup((x) => x.get(configKey)).returns(() => undefined); - let extConfig = new ExtConfig( - config.object, - extensionConfig.object, - workspaceConfig.object, - ); - let actual = extConfig.getWorkspaceConfig(configKey, defaultValue); - assert.equal(actual, defaultValue); - done(); - }); + test("getWorkspaceConfig should return the default value if the workspace does not have the config", () => { + const configKey = "config key"; + const defaultValue = "default value"; + workspaceConfigGet.reset(); + workspaceConfigGet.returns(undefined); + workspaceConfigGet.withArgs(configKey).returns(undefined); + const extConfig = new ExtConfig( + config as unknown as IConfigUtils, + extensionConfig, + workspaceConfig, + ); + const actual = extConfig.getWorkspaceConfig(configKey, defaultValue); + expect(actual).to.equal(defaultValue); }); }); From e6e8c2589a71069e18c6a467b5c1c2163ab82271 Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Mon, 29 Sep 2025 20:52:14 -0700 Subject: [PATCH 12/30] converted executionPlanWebviewController.test.ts --- .../executionPlanWebviewController.test.ts | 153 ++++++++---------- 1 file changed, 65 insertions(+), 88 deletions(-) diff --git a/test/unit/executionPlanWebviewController.test.ts b/test/unit/executionPlanWebviewController.test.ts index 88c1c09b9d..c4fd2bb780 100644 --- a/test/unit/executionPlanWebviewController.test.ts +++ b/test/unit/executionPlanWebviewController.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from "assert"; +import { expect } from "chai"; import * as sinon from "sinon"; import * as vscode from "vscode"; import { ExecutionPlanWebviewController } from "../../src/controllers/executionPlanWebviewController"; @@ -13,10 +13,10 @@ import * as ep from "../../src/sharedInterfaces/executionPlan"; import { ApiStatus } from "../../src/sharedInterfaces/webview"; import * as epUtils from "../../src/controllers/sharedExecutionPlanUtils"; import { contents } from "../resources/testsqlplan"; -import * as TypeMoq from "typemoq"; import SqlToolsServiceClient from "../../src/languageservice/serviceclient"; import { GetExecutionPlanRequest } from "../../src/models/contracts/executionPlan"; import VscodeWrapper from "../../src/controllers/vscodeWrapper"; +import { stubVscodeWrapper } from "./utils"; suite("ExecutionPlanWebviewController", () => { let sandbox: sinon.SinonSandbox; @@ -26,7 +26,7 @@ suite("ExecutionPlanWebviewController", () => { let controller: ExecutionPlanWebviewController; let mockInitialState: ep.ExecutionPlanWebviewState; let mockResultState: ep.ExecutionPlanWebviewState; - let vscodeWrapper: TypeMoq.IMock; + let vscodeWrapper: sinon.SinonStubbedInstance; const executionPlanContents = contents; const xmlPlanFileName = "testPlan.sqlplan"; @@ -41,7 +41,7 @@ suite("ExecutionPlanWebviewController", () => { mockExecutionPlanService = sandbox.createStubInstance(ExecutionPlanService); mockSqlDocumentService = sandbox.createStubInstance(SqlDocumentService); - vscodeWrapper = TypeMoq.Mock.ofType(VscodeWrapper, TypeMoq.MockBehavior.Loose); + vscodeWrapper = stubVscodeWrapper(sandbox); mockInitialState = { executionPlanState: { @@ -61,7 +61,7 @@ suite("ExecutionPlanWebviewController", () => { controller = new ExecutionPlanWebviewController( mockContext, - vscodeWrapper.object, + vscodeWrapper as unknown as VscodeWrapper, mockExecutionPlanService, mockSqlDocumentService, executionPlanContents, @@ -74,12 +74,8 @@ suite("ExecutionPlanWebviewController", () => { }); test("should initialize with correct state and webview title", () => { - assert.deepStrictEqual(controller.state, mockInitialState, "Initial state should match"); - assert.deepStrictEqual( - controller.panel.title, - xmlPlanFileName, - "Webview Title should match", - ); + expect(controller.state, "Initial state should match").to.deep.equal(mockInitialState); + expect(controller.panel.title, "Webview Title should match").to.equal(xmlPlanFileName); }); test("should call createExecutionPlanGraphs in getExecutionPlan reducer", async () => { @@ -93,27 +89,25 @@ suite("ExecutionPlanWebviewController", () => { {}, ); - assert.ok( + expect( createExecutionPlanGraphsStub.calledOnce, "createExecutionPlanGraphs should be called once", - ); + ).to.be.true; - assert.deepStrictEqual( + expect( createExecutionPlanGraphsStub.firstCall.args, - [ - mockInitialState, - controller.executionPlanService, - [controller.executionPlanContents], - "SqlplanFile", - ], "createExecutionPlanGraphs should be called with correct arguments", - ); + ).to.deep.equal([ + mockInitialState, + controller.executionPlanService, + [controller.executionPlanContents], + "SqlplanFile", + ]); - assert.deepStrictEqual( + expect( result, - mockResultState, "State should have an updated total cost, api status, and graphs", - ); + ).to.deep.equal(mockResultState); createExecutionPlanGraphsStub.restore(); }); @@ -132,15 +126,15 @@ suite("ExecutionPlanWebviewController", () => { mockPayload, ); - assert.ok(saveExecutionPlanStub.calledOnce, "saveExecutionPlan should be called once"); + expect(saveExecutionPlanStub.calledOnce, "saveExecutionPlan should be called once").to.be + .true; - assert.deepStrictEqual( + expect( saveExecutionPlanStub.firstCall.args, - [mockInitialState, mockPayload], "saveExecutionPlan should be called with correct arguments", - ); + ).to.deep.equal([mockInitialState, mockPayload]); - assert.deepStrictEqual(result, mockInitialState, "State should not be changed"); + expect(result, "State should not be changed").to.deep.equal(mockInitialState); saveExecutionPlanStub.restore(); }); @@ -157,15 +151,14 @@ suite("ExecutionPlanWebviewController", () => { mockPayload, ); - assert.ok(showPlanXmlStub.calledOnce, "showPlanXml should be called once"); + expect(showPlanXmlStub.calledOnce, "showPlanXml should be called once").to.be.true; - assert.deepStrictEqual( + expect( showPlanXmlStub.firstCall.args, - [mockInitialState, mockPayload], "showPlanXml should be called with correct arguments", - ); + ).to.deep.equal([mockInitialState, mockPayload]); - assert.deepStrictEqual(result, mockInitialState, "State should not be changed"); + expect(result, "State should not be changed").to.deep.equal(mockInitialState); showPlanXmlStub.restore(); }); @@ -182,15 +175,14 @@ suite("ExecutionPlanWebviewController", () => { mockPayload, ); - assert.ok(showQueryStub.calledOnce, "showQuery should be called once"); + expect(showQueryStub.calledOnce, "showQuery should be called once").to.be.true; - assert.deepStrictEqual( + expect( showQueryStub.firstCall.args, - [mockInitialState, mockPayload, controller.sqlDocumentService], "showQuery should be called with correct arguments", - ); + ).to.deep.equal([mockInitialState, mockPayload, controller.sqlDocumentService]); - assert.deepStrictEqual(result, mockInitialState, "State should not be changed"); + expect(result, "State should not be changed").to.deep.equal(mockInitialState); showQueryStub.restore(); }); @@ -213,15 +205,14 @@ suite("ExecutionPlanWebviewController", () => { mockPayload, ); - assert.ok(updateTotalCostStub.calledOnce, "updateTotalCost should be called once"); + expect(updateTotalCostStub.calledOnce, "updateTotalCost should be called once").to.be.true; - assert.deepStrictEqual( + expect( updateTotalCostStub.firstCall.args, - [mockInitialState, mockPayload], "showQuery should be called with correct arguments", - ); + ).to.deep.equal([mockInitialState, mockPayload]); - assert.deepStrictEqual(result, mockResultState, "State should have an updated total cost"); + expect(result, "State should have an updated total cost").to.deep.equal(mockResultState); updateTotalCostStub.restore(); }); @@ -232,7 +223,7 @@ suite("Execution Plan Utilities", () => { let mockExecutionPlanService: ExecutionPlanService; let mockSqlDocumentService: SqlDocumentService; let executionPlanContents: string; - let client: TypeMoq.IMock; + let client: sinon.SinonStubbedInstance; let mockResult: ep.GetExecutionPlanResult; let mockInitialState: ep.ExecutionPlanWebviewState; @@ -242,9 +233,9 @@ suite("Execution Plan Utilities", () => { executionPlanContents = contents; mockResult = { - graphs: TypeMoq.It.isAny(), - success: TypeMoq.It.isAny(), - errorMessage: TypeMoq.It.isAny(), + graphs: [], + success: true, + errorMessage: "", }; mockInitialState = { @@ -255,12 +246,15 @@ suite("Execution Plan Utilities", () => { }, }; - client = TypeMoq.Mock.ofType(SqlToolsServiceClient, TypeMoq.MockBehavior.Loose); - client - .setup((c) => c.sendRequest(GetExecutionPlanRequest.type, TypeMoq.It.isAny())) - .returns(() => Promise.resolve(mockResult)); + client = sandbox.createStubInstance(SqlToolsServiceClient); + client.logger = { error: sandbox.stub() } as unknown as SqlToolsServiceClient["logger"]; + client.sendRequest + .withArgs(GetExecutionPlanRequest.type, sinon.match.any) + .resolves(mockResult); - mockExecutionPlanService = new ExecutionPlanService(client.object); + mockExecutionPlanService = new ExecutionPlanService( + client as unknown as SqlToolsServiceClient, + ); mockSqlDocumentService = sandbox.createStubInstance(SqlDocumentService); }); @@ -286,7 +280,7 @@ suite("Execution Plan Utilities", () => { const result = await epUtils.saveExecutionPlan(mockInitialState, mockPayload); - assert.deepEqual(result, mockInitialState, "State should not change"); + expect(result, "State should not change").to.deep.equal(mockInitialState); // Checks the file was saved sinon.assert.calledOnce(writeFileStub); @@ -301,7 +295,7 @@ suite("Execution Plan Utilities", () => { const result = await epUtils.showPlanXml(mockInitialState, mockPayload); sinon.assert.calledOnce(openDocumentStub); - assert.strictEqual(result, mockInitialState, "The state should be returned unchanged."); + expect(result, "The state should be returned unchanged.").to.equal(mockInitialState); }); test("showQuery: should call newQuery with copyConnectionFromUri when URI is provided", async () => { @@ -317,7 +311,7 @@ suite("Execution Plan Utilities", () => { mockUri, ); - assert.strictEqual(result, mockInitialState, "The state should be returned unchanged."); + expect(result, "The state should be returned unchanged.").to.equal(mockInitialState); sinon.assert.calledOnceWithExactly(mockSqlDocumentService.newQuery as sinon.SinonStub, { content: mockPayload.query, connectionStrategy: ConnectionStrategy.CopyFromUri, @@ -336,7 +330,7 @@ suite("Execution Plan Utilities", () => { mockSqlDocumentService, ); - assert.strictEqual(result, mockInitialState, "The state should be returned unchanged."); + expect(result, "The state should be returned unchanged.").to.equal(mockInitialState); sinon.assert.calledOnceWithExactly(mockSqlDocumentService.newQuery as sinon.SinonStub, { content: mockPayload.query, connectionStrategy: ConnectionStrategy.DoNotConnect, @@ -367,12 +361,11 @@ suite("Execution Plan Utilities", () => { sinon.assert.calledOnceWithExactly(getExecutionPlanStub, planFile); - assert.notEqual(result, undefined); - assert.deepStrictEqual( + expect(result).to.not.equal(undefined); + expect( result.executionPlanState.loadState, - ApiStatus.Loaded, "The api status of the state should be properly updated", - ); + ).to.equal(ApiStatus.Loaded); }); test("createExecutionPlanGraphs: should register error and update the state", async () => { @@ -394,17 +387,14 @@ suite("Execution Plan Utilities", () => { sinon.assert.calledOnceWithExactly(getExecutionPlanStub, planFile); - assert.notDeepEqual(result, undefined, "The resulting state should be defined"); - assert.deepStrictEqual( - result.executionPlanState.loadState, + expect(result, "The resulting state should be defined").to.not.deep.equal(undefined); + expect(result.executionPlanState.loadState, "The load state should be updated").to.equal( ApiStatus.Error, - "The load state should be updated", ); - assert.deepStrictEqual( + expect( result.executionPlanState.errorMessage, - "Mock Error", "The correct error message should be updated in state", - ); + ).to.equal("Mock Error"); }); test("updateTotalCost: should call updateTotalCost with the added cost and return the updated state", async () => { @@ -412,11 +402,10 @@ suite("Execution Plan Utilities", () => { const result = await epUtils.updateTotalCost(mockInitialState, mockPayload); - assert.strictEqual( + expect( result.executionPlanState.totalCost, - 100, "The state should be returned with new cost.", - ); + ).to.equal(100); }); test("calculateTotalCost: should return 0 and set loadState to Error if executionPlanGraphs is undefined", () => { @@ -429,15 +418,9 @@ suite("Execution Plan Utilities", () => { const result = epUtils.calculateTotalCost(mockState); - assert.strictEqual( - result, - 0, - "Total cost should be 0 when executionPlanGraphs is undefined", - ); - assert.strictEqual( - mockState.executionPlanState.loadState, + expect(result, "Total cost should be 0 when executionPlanGraphs is undefined").to.equal(0); + expect(mockState.executionPlanState.loadState, "loadState should be set to Error").to.equal( ApiStatus.Error, - "loadState should be set to Error", ); }); @@ -454,26 +437,20 @@ suite("Execution Plan Utilities", () => { const result = epUtils.calculateTotalCost(mockInitialState); - assert.strictEqual( - result, + expect(result, "Total cost should correctly sum up the costs and subtree costs").to.equal( 50, - "Total cost should correctly sum up the costs and subtree costs", ); }); test("calculateTotalCost: should return 0 if executionPlanGraphs is empty", () => { const result = epUtils.calculateTotalCost(mockInitialState); - assert.strictEqual( - result, - 0, - "Total cost should be 0 for an empty executionPlanGraphs array", - ); + expect(result, "Total cost should be 0 for an empty executionPlanGraphs array").to.equal(0); }); test("formatXml: should return original xml contents if it is not a valid xml file", () => { const invalidXml = " Date: Mon, 29 Sep 2025 20:54:20 -0700 Subject: [PATCH 13/30] converted metadataService.test.ts --- test/unit/metadataService.test.ts | 95 ++++++++++++++++--------------- 1 file changed, 49 insertions(+), 46 deletions(-) diff --git a/test/unit/metadataService.test.ts b/test/unit/metadataService.test.ts index 4349defc9c..6cece79a01 100644 --- a/test/unit/metadataService.test.ts +++ b/test/unit/metadataService.test.ts @@ -1,46 +1,49 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { MetadataService } from "../../src/metadata/metadataService"; -import ConnectionManager from "../../src/controllers/connectionManager"; -import SqlToolsServiceClient from "../../src/languageservice/serviceclient"; -import { - MetadataQueryRequest, - MetadataQueryResult, -} from "../../src/models/contracts/metadata/metadataRequest"; -import { assert } from "chai"; - -suite("Metadata Service Tests", () => { - let metdataService: MetadataService; - let connectionManager: TypeMoq.IMock; - let client: TypeMoq.IMock; - - setup(() => { - let mockContext: TypeMoq.IMock = - TypeMoq.Mock.ofType(); - connectionManager = TypeMoq.Mock.ofType( - ConnectionManager, - TypeMoq.MockBehavior.Loose, - mockContext.object, - ); - connectionManager.setup((c) => c.client).returns(() => client.object); - client = TypeMoq.Mock.ofType(SqlToolsServiceClient, TypeMoq.MockBehavior.Loose); - const mockMetadata: MetadataQueryResult = { - metadata: TypeMoq.It.isAny(), - }; - client - .setup((c) => c.sendRequest(MetadataQueryRequest.type, TypeMoq.It.isAny())) - .returns(() => Promise.resolve(mockMetadata)); - connectionManager.object.client = client.object; - metdataService = new MetadataService(connectionManager.object); - }); - - test("Test getMetadata function", async () => { - let metadata = await metdataService.getMetadata("test_uri"); - assert.notEqual(metadata, undefined); - }); -}); +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { expect } from "chai"; +import * as sinon from "sinon"; +import { MetadataService } from "../../src/metadata/metadataService"; +import ConnectionManager from "../../src/controllers/connectionManager"; +import SqlToolsServiceClient from "../../src/languageservice/serviceclient"; +import { + MetadataQueryRequest, + MetadataQueryResult, +} from "../../src/models/contracts/metadata/metadataRequest"; + +suite("Metadata Service Tests", () => { + let sandbox: sinon.SinonSandbox; + let connectionManager: sinon.SinonStubbedInstance; + let client: sinon.SinonStubbedInstance; + let metadataService: MetadataService; + + setup(() => { + sandbox = sinon.createSandbox(); + connectionManager = sandbox.createStubInstance(ConnectionManager); + client = sandbox.createStubInstance(SqlToolsServiceClient); + + const mockMetadata: MetadataQueryResult = { + metadata: [], + }; + + client.sendRequest.resolves(mockMetadata); + connectionManager.client = client as unknown as SqlToolsServiceClient; + + metadataService = new MetadataService(connectionManager as unknown as ConnectionManager); + }); + + teardown(() => { + sandbox.restore(); + }); + + test("Test getMetadata function", async () => { + const metadata = await metadataService.getMetadata("test_uri"); + + expect(metadata).to.deep.equal([]); + sinon.assert.calledOnceWithExactly(client.sendRequest, MetadataQueryRequest.type, { + ownerUri: "test_uri", + }); + }); +}); From ea9740081d25402521f569a9d29574690179b5d3 Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Mon, 29 Sep 2025 20:55:54 -0700 Subject: [PATCH 14/30] converted list.test.ts --- test/unit/list.test.ts | 99 +++++++++++++++++++++--------------------- 1 file changed, 50 insertions(+), 49 deletions(-) diff --git a/test/unit/list.test.ts b/test/unit/list.test.ts index 3c816744fa..e2c2a16bc5 100644 --- a/test/unit/list.test.ts +++ b/test/unit/list.test.ts @@ -1,49 +1,50 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as TypeMoq from "typemoq"; -import ListPrompt from "../../src/prompts/list"; -import VscodeWrapper from "../../src/controllers/vscodeWrapper"; - -suite("List Prompt Tests", () => { - let listPrompt: ListPrompt; - let vscodeWrapper: TypeMoq.IMock; - const question = { - choices: [ - { name: "test1", value: "test1" }, - { name: "test2", value: "test2" }, - ], - }; - - setup(() => { - vscodeWrapper = TypeMoq.Mock.ofType(VscodeWrapper, TypeMoq.MockBehavior.Loose); - vscodeWrapper - .setup((v) => v.showQuickPickStrings(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve("test1")); - }); - - test("Test list prompt render", () => { - listPrompt = new ListPrompt(question, vscodeWrapper.object); - listPrompt.render(); - vscodeWrapper.verify( - (v) => v.showQuickPickStrings(TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.once(), - ); - }); - - // @cssuh 10/22 - commented this test because it was throwing some random undefined errors - test.skip("Test list prompt render with error", () => { - let errorWrapper = TypeMoq.Mock.ofType(VscodeWrapper, TypeMoq.MockBehavior.Loose); - errorWrapper - .setup((w) => w.showQuickPickStrings(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)); - let errorPrompt = new ListPrompt(question, errorWrapper.object); - errorPrompt.render(); - errorWrapper.verify( - (v) => v.showQuickPickStrings(TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.once(), - ); - }); -}); +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as sinon from "sinon"; +import ListPrompt from "../../src/prompts/list"; +import VscodeWrapper from "../../src/controllers/vscodeWrapper"; +import { stubVscodeWrapper } from "./utils"; + +suite("List Prompt Tests", () => { + let sandbox: sinon.SinonSandbox; + let vscodeWrapper: sinon.SinonStubbedInstance; + const question = { + choices: [ + { name: "test1", value: "test1" }, + { name: "test2", value: "test2" }, + ], + }; + + setup(() => { + sandbox = sinon.createSandbox(); + vscodeWrapper = stubVscodeWrapper(sandbox); + vscodeWrapper.showQuickPickStrings.resolves("test1"); + }); + + teardown(() => { + sandbox.restore(); + }); + + test("Test list prompt render", async () => { + const listPrompt = new ListPrompt(question, vscodeWrapper as unknown as VscodeWrapper); + await listPrompt.render(); + + sinon.assert.calledOnceWithExactly( + vscodeWrapper.showQuickPickStrings, + sinon.match.array, + sinon.match.object, + ); + }); + + // @cssuh 10/22 - commented this test because it was throwing some random undefined errors + test.skip("Test list prompt render with error", async () => { + const errorWrapper = stubVscodeWrapper(sandbox); + errorWrapper.showQuickPickStrings.resolves(undefined); + const errorPrompt = new ListPrompt(question, errorWrapper as unknown as VscodeWrapper); + await errorPrompt.render(); + sinon.assert.calledOnce(errorWrapper.showQuickPickStrings); + }); +}); From d22bb94edc0be5721c2275637fc9a3acf8fb928f Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Mon, 29 Sep 2025 20:58:42 -0700 Subject: [PATCH 15/30] converted adapter.test.ts --- test/unit/adapter.test.ts | 69 +++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/test/unit/adapter.test.ts b/test/unit/adapter.test.ts index fff7917108..7e315031a2 100644 --- a/test/unit/adapter.test.ts +++ b/test/unit/adapter.test.ts @@ -3,16 +3,23 @@ * 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 * as sinon from "sinon"; import CodeAdapter from "../../src/prompts/adapter"; import VscodeWrapper from "../../src/controllers/vscodeWrapper"; import { IQuestion } from "../../src/prompts/question"; +import { stubVscodeWrapper } from "./utils"; suite("Code Adapter Tests", () => { + let sandbox: sinon.SinonSandbox; let adapter: CodeAdapter; - let outputChannel: TypeMoq.IMock; - let vscodeWrapper: TypeMoq.IMock; + let vscodeWrapper: sinon.SinonStubbedInstance; + let outputChannel: { + append: sinon.SinonStub; + appendLine: sinon.SinonStub; + clear: sinon.SinonStub; + show: sinon.SinonStub; + }; + const testMessage = { message: "test_message", code: 123, @@ -27,53 +34,65 @@ suite("Code Adapter Tests", () => { }; setup(() => { - vscodeWrapper = TypeMoq.Mock.ofType(VscodeWrapper, TypeMoq.MockBehavior.Loose); - outputChannel = TypeMoq.Mock.ofType(); - outputChannel.setup((o) => o.appendLine(TypeMoq.It.isAnyString())); - outputChannel.setup((o) => o.clear()); - outputChannel.setup((o) => o.show()); - vscodeWrapper.setup((v) => v.outputChannel).returns(() => outputChannel.object); - vscodeWrapper.setup((v) => v.showErrorMessage(TypeMoq.It.isAnyString())); - adapter = new CodeAdapter(vscodeWrapper.object); + sandbox = sinon.createSandbox(); + vscodeWrapper = stubVscodeWrapper(sandbox); + + outputChannel = { + append: sandbox.stub(), + appendLine: sandbox.stub(), + clear: sandbox.stub(), + show: sandbox.stub(), + }; + + sandbox.stub(vscodeWrapper, "outputChannel").get(() => outputChannel as any); + vscodeWrapper.showErrorMessage.resolves(undefined); + + adapter = new CodeAdapter(vscodeWrapper as unknown as VscodeWrapper); + }); + + teardown(() => { + sandbox.restore(); }); test("logError should append message to the channel", () => { adapter.logError(testMessage); - outputChannel.verify((o) => o.appendLine(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); + sinon.assert.calledOnce(outputChannel.appendLine); }); test("log should format message and append to the channel", () => { adapter.log(testMessage); - outputChannel.verify((o) => o.appendLine(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); + sinon.assert.calledOnce(outputChannel.appendLine); }); test("clearLog should clear from output channel", () => { adapter.clearLog(); - outputChannel.verify((o) => o.clear(), TypeMoq.Times.once()); + sinon.assert.calledOnce(outputChannel.clear); }); test("showLog should show the output channel", () => { adapter.showLog(); - outputChannel.verify((o) => o.show(), TypeMoq.Times.once()); + sinon.assert.calledOnce(outputChannel.show); }); - test("promptSingle and promptCallback should call prompt", () => { - void adapter.promptSingle(testQuestion); + test("promptSingle and promptCallback should call prompt", async () => { + await adapter.promptSingle(testQuestion); adapter.promptCallback([testQuestion], () => true); // Error case - void adapter.prompt([{ type: "test", message: "test", name: "test" }]); + await adapter.prompt([{ type: "test", message: "test", name: "test" }]); }); - test("prompting a checkbox question should call fixQuestion", () => { - let formattedQuestion: IQuestion = { + test("prompting a checkbox question should call fixQuestion", async () => { + const formattedQuestion: IQuestion = { type: "checkbox", message: "test", name: "test_checkbox", choices: [{ name: "test_choice", value: "test_choice" }], }; - void adapter.promptSingle(formattedQuestion); - let question: any = Object.assign({}, formattedQuestion); - question.choices[0] = "test"; - void adapter.promptSingle(question); + await adapter.promptSingle(formattedQuestion); + const question: IQuestion = { + ...formattedQuestion, + choices: ["test"], + }; + await adapter.promptSingle(question); }); }); From 81d20d3ca2b900efdce6f882ba7b14c775cb912b Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Mon, 29 Sep 2025 21:01:19 -0700 Subject: [PATCH 16/30] converted firewallService.test.ts --- test/unit/firewallService.test.ts | 160 ++++++++++-------------------- 1 file changed, 54 insertions(+), 106 deletions(-) diff --git a/test/unit/firewallService.test.ts b/test/unit/firewallService.test.ts index 45473c6ae8..0f6e587721 100644 --- a/test/unit/firewallService.test.ts +++ b/test/unit/firewallService.test.ts @@ -3,8 +3,8 @@ * 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 { expect } from "chai"; +import * as sinon from "sinon"; import SqlToolsServiceClient from "../../src/languageservice/serviceclient"; import { FirewallService } from "../../src/firewall/firewallService"; import { AccountService } from "../../src/azure/accountService"; @@ -14,123 +14,71 @@ import { CreateFirewallRuleRequest, ICreateFirewallRuleResponse, } from "../../src/models/contracts/firewall/firewallRequest"; -import VscodeWrapper from "../../src/controllers/vscodeWrapper"; -import { assert } from "chai"; -import { IAzureSession, IAzureResourceFilter } from "../../src/models/interfaces"; -import { - AzureAuthType, - IAccount, - ITenant, - IToken, - IAzureAccountProperties, -} from "../../src/models/contracts/azure"; -import allSettings from "../../src/azure/providerSettings"; +import * as Constants from "../../src/constants/constants"; suite("Firewall Service Tests", () => { - let firewallService: TypeMoq.IMock; - let accountService: TypeMoq.IMock; - let client: TypeMoq.IMock; - let vscodeWrapper: TypeMoq.IMock; + let sandbox: sinon.SinonSandbox; + let client: sinon.SinonStubbedInstance; + let accountService: AccountService; + let firewallService: FirewallService; setup(() => { - client = TypeMoq.Mock.ofType(SqlToolsServiceClient, TypeMoq.MockBehavior.Loose); - let mockHandleFirewallResponse: IHandleFirewallRuleResponse = { + sandbox = sinon.createSandbox(); + client = sandbox.createStubInstance(SqlToolsServiceClient); + accountService = { + client: client as unknown as SqlToolsServiceClient, + } as AccountService; + firewallService = new FirewallService(accountService); + }); + + teardown(() => { + sandbox.restore(); + }); + + test("Handle Firewall Rule test", async () => { + const mockResponse: IHandleFirewallRuleResponse = { result: true, ipAddress: "128.0.0.0", }; - let mockCreateFirewallRuleResponse: ICreateFirewallRuleResponse = { - 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: IAzureSession = { - environment: undefined, - userId: "test", - tenantId: "test", - credentials: undefined, - }; - let mockSessions: IAzureSession[] = [mockSession]; - let mockFilter: IAzureResourceFilter = { - sessions: mockSessions, - subscription: undefined, - }; - let mockExtension: vscode.Extension = { - id: "", - extensionKind: undefined, - extensionPath: "", - isActive: true, - packageJSON: undefined, - activate: undefined, - extensionUri: undefined, - exports: { - sessions: mockSessions, - filters: mockFilter, - }, - }; - vscodeWrapper.setup((v) => v.azureAccountExtension).returns(() => mockExtension); - accountService = TypeMoq.Mock.ofType(AccountService, TypeMoq.MockBehavior.Loose); - firewallService = TypeMoq.Mock.ofType(FirewallService, TypeMoq.MockBehavior.Loose); - }); + client.sendResourceRequest.resolves(mockResponse); - test("Handle Firewall Rule test", async () => { - let handleResult = await firewallService.object.handleFirewallRule( - 12345, - "firewall error!", + const handleResult = await firewallService.handleFirewallRule(12345, "firewall error!"); + + expect(handleResult).to.deep.equal(mockResponse); + sinon.assert.calledOnceWithExactly( + client.sendResourceRequest, + HandleFirewallRuleRequest.type, + { + errorCode: 12345, + errorMessage: "firewall error!", + connectionTypeId: Constants.mssqlProviderName, + }, ); - assert.isNotNull(handleResult, "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"; - let mockTenant: ITenant = { - id: "1", - displayName: undefined, - }; - let properties: IAzureAccountProperties = { - tenants: [mockTenant], - azureAuthType: AzureAuthType.AuthCodeGrant, - isMsAccount: false, - owningTenant: { - id: "1", - displayName: undefined, - }, - providerSettings: allSettings, - }; - let mockAccount: IAccount = { - properties: properties, - key: undefined, - displayInfo: undefined, - isStale: undefined, - }; - let mockToken: IToken = { - key: "", - tokenType: "", - token: "", - expiresOn: 0, + const mockResponse: ICreateFirewallRuleResponse = { + result: true, + errorMessage: "", }; - accountService - .setup((v) => v.refreshToken(mockAccount, mockTenant.id)) - .returns(() => Promise.resolve(mockToken)); - accountService.object.setAccount(mockAccount); - let result = await firewallService.object.createFirewallRule({ - account: mockAccount, + client.sendResourceRequest.resolves(mockResponse); + + const request = { + account: { properties: { tenants: [] } }, firewallRuleName: "Test Rule", - startIpAddress: startIpAddress, - endIpAddress: endIpAddress, - serverName: server, - securityTokenMappings: accountService.object.createSecurityTokenMapping( - mockAccount, - mockTenant.id, - ), - }); - assert.isNotNull(result, "Create Firewall Rule request is sent successfully"); + startIpAddress: "1.2.3.1", + endIpAddress: "1.2.3.255", + serverName: "test_server", + securityTokenMappings: {}, + }; + + const result = await firewallService.createFirewallRule(request); + + expect(result).to.deep.equal(mockResponse); + sinon.assert.calledOnceWithExactly( + client.sendResourceRequest, + CreateFirewallRuleRequest.type, + request, + ); }); }); From 09ee2c482c3b16c4f789a39915c270bc718f89a9 Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Mon, 29 Sep 2025 21:06:25 -0700 Subject: [PATCH 17/30] converted databaseObjectSearchService.test.ts --- test/unit/databaseObjectSearchService.test.ts | 87 ++++++++++--------- 1 file changed, 47 insertions(+), 40 deletions(-) diff --git a/test/unit/databaseObjectSearchService.test.ts b/test/unit/databaseObjectSearchService.test.ts index 0cfda0747b..b0d6f49538 100644 --- a/test/unit/databaseObjectSearchService.test.ts +++ b/test/unit/databaseObjectSearchService.test.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { assert } from "chai"; -import * as TypeMoq from "typemoq"; +import { expect } from "chai"; +import * as sinon from "sinon"; import SqlToolsServiceClient from "../../src/languageservice/serviceclient"; import { MetadataQueryRequest, @@ -14,12 +14,18 @@ import { DatabaseObjectSearchService } from "../../src/services/databaseObjectSe import { ObjectMetadata } from "vscode-mssql"; suite("DatabaseObjectSearchService Tests", () => { - let client: TypeMoq.IMock; + let sandbox: sinon.SinonSandbox; + let client: sinon.SinonStubbedInstance; let searchService: DatabaseObjectSearchService; setup(() => { - client = TypeMoq.Mock.ofType(SqlToolsServiceClient, TypeMoq.MockBehavior.Loose); - searchService = new DatabaseObjectSearchService(client.object); + sandbox = sinon.createSandbox(); + client = sandbox.createStubInstance(SqlToolsServiceClient); + searchService = new DatabaseObjectSearchService(client as unknown as SqlToolsServiceClient); + }); + + teardown(() => { + sandbox.restore(); }); test("searchObjects filters metadata by term", async () => { @@ -47,17 +53,18 @@ suite("DatabaseObjectSearchService Tests", () => { }, ]; const mockResult: MetadataQueryResult = { metadata: md }; - client - .setup((c) => c.sendRequest(MetadataQueryRequest.type, TypeMoq.It.isAny())) - .returns(() => Promise.resolve(mockResult)); + client.sendRequest.resolves(mockResult); const result = await searchService.searchObjects("test_uri", "cust"); - assert.isTrue(result.success); - assert.equal(result.objects.length, 2, "Should match Customers and vTopCustomers"); - assert.deepEqual( - result.objects.map((o) => o.name).sort(), + + expect(result.success).to.be.true; + expect(result.objects).to.have.lengthOf(2); + expect(result.objects.map((o) => o.name).sort()).to.deep.equal( ["Customers", "vTopCustomers"].sort(), ); + sinon.assert.calledOnceWithExactly(client.sendRequest, MetadataQueryRequest.type, { + ownerUri: "test_uri", + }); }); test("warmCache caches results and subsequent calls do not re-fetch", async () => { @@ -71,15 +78,14 @@ suite("DatabaseObjectSearchService Tests", () => { }, ]; const mockResult: MetadataQueryResult = { metadata: md }; - const sendReq = client - .setup((c) => c.sendRequest(MetadataQueryRequest.type, TypeMoq.It.isAny())) - .returns(() => Promise.resolve(mockResult)); + client.sendRequest.resolves(mockResult); await searchService.warmCache("uri1"); await searchService.searchObjects("uri1", "thing"); - // Should have been called only once thanks to cache - sendReq.verifiable(TypeMoq.Times.once()); - client.verifyAll(); + + sinon.assert.calledOnceWithExactly(client.sendRequest, MetadataQueryRequest.type, { + ownerUri: "uri1", + }); }); test("clearCache removes cached metadata", async () => { @@ -93,28 +99,30 @@ suite("DatabaseObjectSearchService Tests", () => { }, ]; const mockResult: MetadataQueryResult = { metadata: md }; - const sendReq = client - .setup((c) => c.sendRequest(MetadataQueryRequest.type, TypeMoq.It.isAny())) - .returns(() => Promise.resolve(mockResult)); + client.sendRequest.resolves(mockResult); await searchService.warmCache("uri2"); DatabaseObjectSearchService.clearCache("uri2"); await searchService.searchObjects("uri2", "x"); - // Called twice because cache was cleared - sendReq.verifiable(TypeMoq.Times.exactly(2)); - client.verifyAll(); + + sinon.assert.calledTwice(client.sendRequest); + expect(client.sendRequest.firstCall.args).to.deep.equal([ + MetadataQueryRequest.type, + { ownerUri: "uri2" }, + ]); + expect(client.sendRequest.secondCall.args).to.deep.equal([ + MetadataQueryRequest.type, + { ownerUri: "uri2" }, + ]); }); test("returns error when search term is empty and does not call service", async () => { - // No setup for sendRequest; verify it's not called const result = await searchService.searchObjects("test_uri", " "); - assert.isFalse(result.success); - assert.equal(result.objects.length, 0); - assert.match(result.error || "", /Search term cannot be empty/); - client.verify( - (c) => c.sendRequest(TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.never(), - ); + + expect(result.success).to.be.false; + expect(result.objects).to.have.lengthOf(0); + expect(result.error || "").to.match(/Search term cannot be empty/); + sinon.assert.notCalled(client.sendRequest); }); test("maps metadata type names to friendly labels", async () => { @@ -149,17 +157,16 @@ suite("DatabaseObjectSearchService Tests", () => { }, ]; const mockResult: MetadataQueryResult = { metadata: md }; - client - .setup((c) => c.sendRequest(MetadataQueryRequest.type, TypeMoq.It.isAny())) - .returns(() => Promise.resolve(mockResult)); + client.sendRequest.resolves(mockResult); const result = await searchService.searchObjects("uri3", "test"); - assert.isTrue(result.success); + + expect(result.success).to.be.true; const typesByName = new Map(result.objects.map((o) => [o.name, o.type])); - assert.equal(typesByName.get("test_proc"), "Stored Procedure"); - assert.equal(typesByName.get("test_svf"), "Scalar Function"); - assert.equal(typesByName.get("test_tvf"), "Table-valued Function"); + expect(typesByName.get("test_proc")).to.equal("Stored Procedure"); + expect(typesByName.get("test_svf")).to.equal("Scalar Function"); + expect(typesByName.get("test_tvf")).to.equal("Table-valued Function"); // Unknown types pass through unchanged - assert.equal(typesByName.get("test_syn"), "Synonym"); + expect(typesByName.get("test_syn")).to.equal("Synonym"); }); }); From 7edbfa699f9d5b59386ebed2deeaad4e4a18c82b Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Tue, 30 Sep 2025 10:20:10 -0700 Subject: [PATCH 18/30] cleaned up some tests --- .../executionPlanWebviewController.test.ts | 901 +++++++++--------- test/unit/extConfig.test.ts | 2 +- test/unit/metadataService.test.ts | 4 +- 3 files changed, 448 insertions(+), 459 deletions(-) diff --git a/test/unit/executionPlanWebviewController.test.ts b/test/unit/executionPlanWebviewController.test.ts index c4fd2bb780..5db52d68ab 100644 --- a/test/unit/executionPlanWebviewController.test.ts +++ b/test/unit/executionPlanWebviewController.test.ts @@ -1,456 +1,445 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { expect } from "chai"; -import * as sinon from "sinon"; -import * as vscode from "vscode"; -import { ExecutionPlanWebviewController } from "../../src/controllers/executionPlanWebviewController"; -import SqlDocumentService, { ConnectionStrategy } from "../../src/controllers/sqlDocumentService"; -import { ExecutionPlanService } from "../../src/services/executionPlanService"; -import * as ep from "../../src/sharedInterfaces/executionPlan"; -import { ApiStatus } from "../../src/sharedInterfaces/webview"; -import * as epUtils from "../../src/controllers/sharedExecutionPlanUtils"; -import { contents } from "../resources/testsqlplan"; -import SqlToolsServiceClient from "../../src/languageservice/serviceclient"; -import { GetExecutionPlanRequest } from "../../src/models/contracts/executionPlan"; -import VscodeWrapper from "../../src/controllers/vscodeWrapper"; -import { stubVscodeWrapper } from "./utils"; - -suite("ExecutionPlanWebviewController", () => { - let sandbox: sinon.SinonSandbox; - let mockContext: vscode.ExtensionContext; - let mockExecutionPlanService: ExecutionPlanService; - let mockSqlDocumentService: SqlDocumentService; - let controller: ExecutionPlanWebviewController; - let mockInitialState: ep.ExecutionPlanWebviewState; - let mockResultState: ep.ExecutionPlanWebviewState; - let vscodeWrapper: sinon.SinonStubbedInstance; - - const executionPlanContents = contents; - const xmlPlanFileName = "testPlan.sqlplan"; - - setup(() => { - sandbox = sinon.createSandbox(); - mockContext = { - extensionUri: vscode.Uri.parse("https://localhost"), - extensionPath: "path", - } as unknown as vscode.ExtensionContext; - - mockExecutionPlanService = sandbox.createStubInstance(ExecutionPlanService); - mockSqlDocumentService = sandbox.createStubInstance(SqlDocumentService); - - vscodeWrapper = stubVscodeWrapper(sandbox); - - mockInitialState = { - executionPlanState: { - loadState: ApiStatus.Loading, - executionPlanGraphs: [], - totalCost: 0, - }, - }; - - mockResultState = { - executionPlanState: { - executionPlanGraphs: [], - loadState: ApiStatus.Loaded, - totalCost: 100, - }, - }; - - controller = new ExecutionPlanWebviewController( - mockContext, - vscodeWrapper as unknown as VscodeWrapper, - mockExecutionPlanService, - mockSqlDocumentService, - executionPlanContents, - xmlPlanFileName, - ); - }); - - teardown(() => { - sandbox.restore(); - }); - - test("should initialize with correct state and webview title", () => { - expect(controller.state, "Initial state should match").to.deep.equal(mockInitialState); - expect(controller.panel.title, "Webview Title should match").to.equal(xmlPlanFileName); - }); - - test("should call createExecutionPlanGraphs in getExecutionPlan reducer", async () => { - // Stub createExecutionPlanGraphs to mock its behavior - const createExecutionPlanGraphsStub = sandbox - .stub(epUtils, "createExecutionPlanGraphs") - .resolves(mockResultState); - - const result = await controller["_reducerHandlers"].get("getExecutionPlan")( - mockInitialState, - {}, - ); - - expect( - createExecutionPlanGraphsStub.calledOnce, - "createExecutionPlanGraphs should be called once", - ).to.be.true; - - expect( - createExecutionPlanGraphsStub.firstCall.args, - "createExecutionPlanGraphs should be called with correct arguments", - ).to.deep.equal([ - mockInitialState, - controller.executionPlanService, - [controller.executionPlanContents], - "SqlplanFile", - ]); - - expect( - result, - "State should have an updated total cost, api status, and graphs", - ).to.deep.equal(mockResultState); - - createExecutionPlanGraphsStub.restore(); - }); - - test("should call saveExecutionPlan in saveExecutionPlan reducer", async () => { - const saveExecutionPlanStub = sandbox - .stub(epUtils, "saveExecutionPlan") - .resolves(mockInitialState); - - const mockPayload = { - sqlPlanContent: executionPlanContents, - }; - - const result = await controller["_reducerHandlers"].get("saveExecutionPlan")( - mockInitialState, - mockPayload, - ); - - expect(saveExecutionPlanStub.calledOnce, "saveExecutionPlan should be called once").to.be - .true; - - expect( - saveExecutionPlanStub.firstCall.args, - "saveExecutionPlan should be called with correct arguments", - ).to.deep.equal([mockInitialState, mockPayload]); - - expect(result, "State should not be changed").to.deep.equal(mockInitialState); - - saveExecutionPlanStub.restore(); - }); - - test("should call showPlanXml in showPlanXml reducer", async () => { - const showPlanXmlStub = sandbox.stub(epUtils, "showPlanXml").resolves(mockInitialState); - - const mockPayload = { - sqlPlanContent: executionPlanContents, - }; - - const result = await controller["_reducerHandlers"].get("showPlanXml")( - mockInitialState, - mockPayload, - ); - - expect(showPlanXmlStub.calledOnce, "showPlanXml should be called once").to.be.true; - - expect( - showPlanXmlStub.firstCall.args, - "showPlanXml should be called with correct arguments", - ).to.deep.equal([mockInitialState, mockPayload]); - - expect(result, "State should not be changed").to.deep.equal(mockInitialState); - - showPlanXmlStub.restore(); - }); - - test("should call showQuery in showQuery reducer", async () => { - const showQueryStub = sandbox.stub(epUtils, "showQuery").resolves(mockInitialState); - - const mockPayload = { - query: "select * from sys.objects;", - }; - - const result = await controller["_reducerHandlers"].get("showQuery")( - mockInitialState, - mockPayload, - ); - - expect(showQueryStub.calledOnce, "showQuery should be called once").to.be.true; - - expect( - showQueryStub.firstCall.args, - "showQuery should be called with correct arguments", - ).to.deep.equal([mockInitialState, mockPayload, controller.sqlDocumentService]); - - expect(result, "State should not be changed").to.deep.equal(mockInitialState); - - showQueryStub.restore(); - }); - - test("should call updateTotalCost in updateTotalCost reducer", async () => { - const updateTotalCostStub = sandbox.stub(epUtils, "updateTotalCost").resolves({ - executionPlanState: { - executionPlanGraphs: [], - loadState: ApiStatus.Loaded, - totalCost: 100, - }, - }); - - const mockPayload = { - addedCost: 100, - }; - - const result = await controller["_reducerHandlers"].get("updateTotalCost")( - mockInitialState, - mockPayload, - ); - - expect(updateTotalCostStub.calledOnce, "updateTotalCost should be called once").to.be.true; - - expect( - updateTotalCostStub.firstCall.args, - "showQuery should be called with correct arguments", - ).to.deep.equal([mockInitialState, mockPayload]); - - expect(result, "State should have an updated total cost").to.deep.equal(mockResultState); - - updateTotalCostStub.restore(); - }); -}); - -suite("Execution Plan Utilities", () => { - let sandbox: sinon.SinonSandbox; - let mockExecutionPlanService: ExecutionPlanService; - let mockSqlDocumentService: SqlDocumentService; - let executionPlanContents: string; - let client: sinon.SinonStubbedInstance; - let mockResult: ep.GetExecutionPlanResult; - let mockInitialState: ep.ExecutionPlanWebviewState; - - setup(() => { - sandbox = sinon.createSandbox(); - - executionPlanContents = contents; - - mockResult = { - graphs: [], - success: true, - errorMessage: "", - }; - - mockInitialState = { - executionPlanState: { - loadState: ApiStatus.Loading, - executionPlanGraphs: [], - totalCost: 0, - }, - }; - - client = sandbox.createStubInstance(SqlToolsServiceClient); - client.logger = { error: sandbox.stub() } as unknown as SqlToolsServiceClient["logger"]; - client.sendRequest - .withArgs(GetExecutionPlanRequest.type, sinon.match.any) - .resolves(mockResult); - - mockExecutionPlanService = new ExecutionPlanService( - client as unknown as SqlToolsServiceClient, - ); - mockSqlDocumentService = sandbox.createStubInstance(SqlDocumentService); - }); - - teardown(() => { - sandbox.restore(); - }); - - test("saveExecutionPlan: should call saveExecutionPlan and return the state", async () => { - const mockPayload = { sqlPlanContent: executionPlanContents }; - - const mockUri = vscode.Uri.file("/plan.sqlplan"); - - const showSaveDialogStub = sinon.stub(vscode.window, "showSaveDialog").resolves(mockUri); - - const writeFileStub = sinon.stub().resolves(); - const mockFs = { - ...vscode.workspace.fs, - writeFile: writeFileStub, - }; - - // replace vscode.workspace.fs with mockfs - sandbox.replaceGetter(vscode.workspace, "fs", () => mockFs); - - const result = await epUtils.saveExecutionPlan(mockInitialState, mockPayload); - - expect(result, "State should not change").to.deep.equal(mockInitialState); - - // Checks the file was saved - sinon.assert.calledOnce(writeFileStub); - - showSaveDialogStub.restore(); - }); - - test("showXml: should call showXml and return the state", async () => { - const openDocumentStub = sandbox.stub(vscode.workspace, "openTextDocument"); - - const mockPayload = { sqlPlanContent: executionPlanContents }; - - const result = await epUtils.showPlanXml(mockInitialState, mockPayload); - sinon.assert.calledOnce(openDocumentStub); - expect(result, "The state should be returned unchanged.").to.equal(mockInitialState); - }); - - test("showQuery: should call newQuery with copyConnectionFromUri when URI is provided", async () => { - (mockSqlDocumentService.newQuery as sinon.SinonStub).resolves(); - - const mockPayload = { query: "SELECT * FROM TestTable" }; - const mockUri = "file:///test.sql"; - - const result = await epUtils.showQuery( - mockInitialState, - mockPayload, - mockSqlDocumentService, - mockUri, - ); - - expect(result, "The state should be returned unchanged.").to.equal(mockInitialState); - sinon.assert.calledOnceWithExactly(mockSqlDocumentService.newQuery as sinon.SinonStub, { - content: mockPayload.query, - connectionStrategy: ConnectionStrategy.CopyFromUri, - sourceUri: mockUri, - }); - }); - - test("showQuery: should fallback to copyLastActiveConnection when no URI is provided", async () => { - (mockSqlDocumentService.newQuery as sinon.SinonStub).resolves(); - - const mockPayload = { query: "SELECT * FROM TestTable" }; - - const result = await epUtils.showQuery( - mockInitialState, - mockPayload, - mockSqlDocumentService, - ); - - expect(result, "The state should be returned unchanged.").to.equal(mockInitialState); - sinon.assert.calledOnceWithExactly(mockSqlDocumentService.newQuery as sinon.SinonStub, { - content: mockPayload.query, - connectionStrategy: ConnectionStrategy.DoNotConnect, - sourceUri: undefined, - }); - }); - - test("createExecutionPlanGraphs: should create executionPlanGraphs correctly and return the state", async () => { - const getExecutionPlanStub = sandbox - .stub(mockExecutionPlanService, "getExecutionPlan") - .resolves({ - graphs: [], - success: true, - errorMessage: "", - }); - - const result = await epUtils.createExecutionPlanGraphs( - mockInitialState, - mockExecutionPlanService, - [executionPlanContents], - "Tests" as never, - ); - - const planFile: ep.ExecutionPlanGraphInfo = { - graphFileContent: executionPlanContents, - graphFileType: `.sqlplan`, - }; - - sinon.assert.calledOnceWithExactly(getExecutionPlanStub, planFile); - - expect(result).to.not.equal(undefined); - expect( - result.executionPlanState.loadState, - "The api status of the state should be properly updated", - ).to.equal(ApiStatus.Loaded); - }); - - test("createExecutionPlanGraphs: should register error and update the state", async () => { - const getExecutionPlanStub = sandbox - .stub(mockExecutionPlanService, "getExecutionPlan") - .rejects(new Error("Mock Error")); - - const result = await epUtils.createExecutionPlanGraphs( - mockInitialState, - mockExecutionPlanService, - [executionPlanContents], - "Tests" as never, - ); - - const planFile: ep.ExecutionPlanGraphInfo = { - graphFileContent: executionPlanContents, - graphFileType: `.sqlplan`, - }; - - sinon.assert.calledOnceWithExactly(getExecutionPlanStub, planFile); - - expect(result, "The resulting state should be defined").to.not.deep.equal(undefined); - expect(result.executionPlanState.loadState, "The load state should be updated").to.equal( - ApiStatus.Error, - ); - expect( - result.executionPlanState.errorMessage, - "The correct error message should be updated in state", - ).to.equal("Mock Error"); - }); - - test("updateTotalCost: should call updateTotalCost with the added cost and return the updated state", async () => { - const mockPayload = { addedCost: 100 }; - - const result = await epUtils.updateTotalCost(mockInitialState, mockPayload); - - expect( - result.executionPlanState.totalCost, - "The state should be returned with new cost.", - ).to.equal(100); - }); - - test("calculateTotalCost: should return 0 and set loadState to Error if executionPlanGraphs is undefined", () => { - let mockState: any = { - executionPlanState: { - executionPlanGraphs: undefined, - loadState: ApiStatus.Loading, - }, - }; - - const result = epUtils.calculateTotalCost(mockState); - - expect(result, "Total cost should be 0 when executionPlanGraphs is undefined").to.equal(0); - expect(mockState.executionPlanState.loadState, "loadState should be set to Error").to.equal( - ApiStatus.Error, - ); - }); - - test("calculateTotalCost: should correctly calculate the total cost for a valid state", () => { - const mockInitialState: any = { - executionPlanState: { - executionPlanGraphs: [ - { root: { cost: 10, subTreeCost: 20 } }, - { root: { cost: 5, subTreeCost: 15 } }, - ], - loadState: ApiStatus.Loaded, - }, - }; - - const result = epUtils.calculateTotalCost(mockInitialState); - - expect(result, "Total cost should correctly sum up the costs and subtree costs").to.equal( - 50, - ); - }); - - test("calculateTotalCost: should return 0 if executionPlanGraphs is empty", () => { - const result = epUtils.calculateTotalCost(mockInitialState); - - expect(result, "Total cost should be 0 for an empty executionPlanGraphs array").to.equal(0); - }); - - test("formatXml: should return original xml contents if it is not a valid xml file", () => { - const invalidXml = " { + let sandbox: sinon.SinonSandbox; + let mockContext: vscode.ExtensionContext; + let mockExecutionPlanService: ExecutionPlanService; + let mockSqlDocumentService: SqlDocumentService; + let controller: ExecutionPlanWebviewController; + let mockInitialState: ep.ExecutionPlanWebviewState; + let mockResultState: ep.ExecutionPlanWebviewState; + let vscodeWrapper: sinon.SinonStubbedInstance; + + const executionPlanContents = contents; + const xmlPlanFileName = "testPlan.sqlplan"; + + setup(() => { + sandbox = sinon.createSandbox(); + mockContext = { + extensionUri: vscode.Uri.parse("https://localhost"), + extensionPath: "path", + } as unknown as vscode.ExtensionContext; + + mockExecutionPlanService = sandbox.createStubInstance(ExecutionPlanService); + mockSqlDocumentService = sandbox.createStubInstance(SqlDocumentService); + + vscodeWrapper = stubVscodeWrapper(sandbox); + + mockInitialState = { + executionPlanState: { + loadState: ApiStatus.Loading, + executionPlanGraphs: [], + totalCost: 0, + }, + }; + + mockResultState = { + executionPlanState: { + executionPlanGraphs: [], + loadState: ApiStatus.Loaded, + totalCost: 100, + }, + }; + + controller = new ExecutionPlanWebviewController( + mockContext, + vscodeWrapper, + mockExecutionPlanService, + mockSqlDocumentService, + executionPlanContents, + xmlPlanFileName, + ); + }); + + teardown(() => { + sandbox.restore(); + }); + + test("should initialize with correct state and webview title", () => { + expect(controller.state, "Initial state should match").to.deep.equal(mockInitialState); + expect(controller.panel.title, "Webview Title should match").to.equal(xmlPlanFileName); + }); + + test("should call createExecutionPlanGraphs in getExecutionPlan reducer", async () => { + // Stub createExecutionPlanGraphs to mock its behavior + const createExecutionPlanGraphsStub = sandbox + .stub(epUtils, "createExecutionPlanGraphs") + .resolves(mockResultState); + + const result = await controller["_reducerHandlers"].get("getExecutionPlan")( + mockInitialState, + {}, + ); + + expect(createExecutionPlanGraphsStub).to.have.been.calledOnce; + + expect(createExecutionPlanGraphsStub).to.have.been.calledWithExactly( + mockInitialState, + controller.executionPlanService, + [controller.executionPlanContents], + "SqlplanFile", + ); + + expect( + result, + "State should have an updated total cost, api status, and graphs", + ).to.deep.equal(mockResultState); + + createExecutionPlanGraphsStub.restore(); + }); + + test("should call saveExecutionPlan in saveExecutionPlan reducer", async () => { + const saveExecutionPlanStub = sandbox + .stub(epUtils, "saveExecutionPlan") + .resolves(mockInitialState); + + const mockPayload = { + sqlPlanContent: executionPlanContents, + }; + + const result = await controller["_reducerHandlers"].get("saveExecutionPlan")( + mockInitialState, + mockPayload, + ); + + expect(saveExecutionPlanStub).to.have.been.calledOnce; + + expect(saveExecutionPlanStub).to.have.been.calledWithExactly(mockInitialState, mockPayload); + + expect(result, "State should not be changed").to.deep.equal(mockInitialState); + + saveExecutionPlanStub.restore(); + }); + + test("should call showPlanXml in showPlanXml reducer", async () => { + const showPlanXmlStub = sandbox.stub(epUtils, "showPlanXml").resolves(mockInitialState); + + const mockPayload = { + sqlPlanContent: executionPlanContents, + }; + + const result = await controller["_reducerHandlers"].get("showPlanXml")( + mockInitialState, + mockPayload, + ); + + expect(showPlanXmlStub).to.have.been.calledOnce; + + expect(showPlanXmlStub).to.have.been.calledWithExactly(mockInitialState, mockPayload); + + expect(result, "State should not be changed").to.deep.equal(mockInitialState); + + showPlanXmlStub.restore(); + }); + + test("should call showQuery in showQuery reducer", async () => { + const showQueryStub = sandbox.stub(epUtils, "showQuery").resolves(mockInitialState); + + const mockPayload = { + query: "select * from sys.objects;", + }; + + const result = await controller["_reducerHandlers"].get("showQuery")( + mockInitialState, + mockPayload, + ); + + expect(showQueryStub).to.have.been.calledOnce; + + expect(showQueryStub).to.have.been.calledWithExactly( + mockInitialState, + mockPayload, + controller.sqlDocumentService, + ); + + expect(result, "State should not be changed").to.deep.equal(mockInitialState); + + showQueryStub.restore(); + }); + + test("should call updateTotalCost in updateTotalCost reducer", async () => { + const updateTotalCostStub = sandbox.stub(epUtils, "updateTotalCost").resolves({ + executionPlanState: { + executionPlanGraphs: [], + loadState: ApiStatus.Loaded, + totalCost: 100, + }, + }); + + const mockPayload = { + addedCost: 100, + }; + + const result = await controller["_reducerHandlers"].get("updateTotalCost")( + mockInitialState, + mockPayload, + ); + + expect(updateTotalCostStub).to.have.been.calledOnce; + + expect(updateTotalCostStub).to.have.been.calledWithExactly(mockInitialState, mockPayload); + + expect(result, "State should have an updated total cost").to.deep.equal(mockResultState); + + updateTotalCostStub.restore(); + }); +}); + +suite("Execution Plan Utilities", () => { + let sandbox: sinon.SinonSandbox; + let mockExecutionPlanService: ExecutionPlanService; + let mockSqlDocumentService: SqlDocumentService; + let executionPlanContents: string; + let client: sinon.SinonStubbedInstance; + let mockResult: ep.GetExecutionPlanResult; + let mockInitialState: ep.ExecutionPlanWebviewState; + + setup(() => { + sandbox = sinon.createSandbox(); + + executionPlanContents = contents; + + mockResult = { + graphs: [], + success: true, + errorMessage: "", + }; + + mockInitialState = { + executionPlanState: { + loadState: ApiStatus.Loading, + executionPlanGraphs: [], + totalCost: 0, + }, + }; + + client = sandbox.createStubInstance(SqlToolsServiceClient); + client.sendRequest + .withArgs(GetExecutionPlanRequest.type, sinon.match.any) + .resolves(mockResult); + + mockExecutionPlanService = new ExecutionPlanService(client); + mockSqlDocumentService = sandbox.createStubInstance(SqlDocumentService); + }); + + teardown(() => { + sandbox.restore(); + }); + + test("saveExecutionPlan: should call saveExecutionPlan and return the state", async () => { + const mockPayload = { sqlPlanContent: executionPlanContents }; + + const mockUri = vscode.Uri.file("/plan.sqlplan"); + + const showSaveDialogStub = sinon.stub(vscode.window, "showSaveDialog").resolves(mockUri); + + const writeFileStub = sinon.stub().resolves(); + const mockFs = { + ...vscode.workspace.fs, + writeFile: writeFileStub, + }; + + // replace vscode.workspace.fs with mockfs + sandbox.replaceGetter(vscode.workspace, "fs", () => mockFs); + + const result = await epUtils.saveExecutionPlan(mockInitialState, mockPayload); + + expect(result, "State should not change").to.deep.equal(mockInitialState); + + expect(writeFileStub).to.have.been.calledOnce; + + showSaveDialogStub.restore(); + }); + + test("showXml: should call showXml and return the state", async () => { + const openDocumentStub = sandbox.stub(vscode.workspace, "openTextDocument"); + + const mockPayload = { sqlPlanContent: executionPlanContents }; + + const result = await epUtils.showPlanXml(mockInitialState, mockPayload); + expect(openDocumentStub).to.have.been.calledOnce; + expect(result, "The state should be returned unchanged.").to.equal(mockInitialState); + }); + + test("showQuery: should call newQuery with copyConnectionFromUri when URI is provided", async () => { + (mockSqlDocumentService.newQuery as sinon.SinonStub).resolves(); + + const mockPayload = { query: "SELECT * FROM TestTable" }; + const mockUri = "file:///test.sql"; + + const result = await epUtils.showQuery( + mockInitialState, + mockPayload, + mockSqlDocumentService, + mockUri, + ); + + expect(result, "The state should be returned unchanged.").to.equal(mockInitialState); + expect( + mockSqlDocumentService.newQuery as sinon.SinonStub, + ).to.have.been.calledOnceWithExactly({ + content: mockPayload.query, + connectionStrategy: ConnectionStrategy.CopyFromUri, + sourceUri: mockUri, + }); + }); + + test("showQuery: should fallback to copyLastActiveConnection when no URI is provided", async () => { + (mockSqlDocumentService.newQuery as sinon.SinonStub).resolves(); + + const mockPayload = { query: "SELECT * FROM TestTable" }; + + const result = await epUtils.showQuery( + mockInitialState, + mockPayload, + mockSqlDocumentService, + ); + + expect(result, "The state should be returned unchanged.").to.equal(mockInitialState); + expect( + mockSqlDocumentService.newQuery as sinon.SinonStub, + ).to.have.been.calledOnceWithExactly({ + content: mockPayload.query, + connectionStrategy: ConnectionStrategy.DoNotConnect, + sourceUri: undefined, + }); + }); + + test("createExecutionPlanGraphs: should create executionPlanGraphs correctly and return the state", async () => { + const getExecutionPlanStub = sandbox + .stub(mockExecutionPlanService, "getExecutionPlan") + .resolves({ + graphs: [], + success: true, + errorMessage: "", + }); + + const result = await epUtils.createExecutionPlanGraphs( + mockInitialState, + mockExecutionPlanService, + [executionPlanContents], + "Tests" as never, + ); + + const planFile: ep.ExecutionPlanGraphInfo = { + graphFileContent: executionPlanContents, + graphFileType: `.sqlplan`, + }; + + expect(getExecutionPlanStub).to.have.been.calledOnceWithExactly(planFile); + + expect(result).to.not.equal(undefined); + expect( + result.executionPlanState.loadState, + "The api status of the state should be properly updated", + ).to.equal(ApiStatus.Loaded); + }); + + test("createExecutionPlanGraphs: should register error and update the state", async () => { + const getExecutionPlanStub = sandbox + .stub(mockExecutionPlanService, "getExecutionPlan") + .rejects(new Error("Mock Error")); + + const result = await epUtils.createExecutionPlanGraphs( + mockInitialState, + mockExecutionPlanService, + [executionPlanContents], + "Tests" as never, + ); + + const planFile: ep.ExecutionPlanGraphInfo = { + graphFileContent: executionPlanContents, + graphFileType: `.sqlplan`, + }; + + expect(getExecutionPlanStub).to.have.been.calledOnceWithExactly(planFile); + + expect(result, "The resulting state should be defined").to.not.deep.equal(undefined); + expect(result.executionPlanState.loadState, "The load state should be updated").to.equal( + ApiStatus.Error, + ); + expect( + result.executionPlanState.errorMessage, + "The correct error message should be updated in state", + ).to.equal("Mock Error"); + }); + + test("updateTotalCost: should call updateTotalCost with the added cost and return the updated state", async () => { + const mockPayload = { addedCost: 100 }; + + const result = await epUtils.updateTotalCost(mockInitialState, mockPayload); + + expect( + result.executionPlanState.totalCost, + "The state should be returned with new cost.", + ).to.equal(100); + }); + + test("calculateTotalCost: should return 0 and set loadState to Error if executionPlanGraphs is undefined", () => { + let mockState: ep.ExecutionPlanWebviewState = { + executionPlanState: { + executionPlanGraphs: undefined, + loadState: ApiStatus.Loading, + }, + }; + + const result = epUtils.calculateTotalCost(mockState); + + expect(result, "Total cost should be 0 when executionPlanGraphs is undefined").to.equal(0); + expect(mockState.executionPlanState.loadState, "loadState should be set to Error").to.equal( + ApiStatus.Error, + ); + }); + + test("calculateTotalCost: should correctly calculate the total cost for a valid state", () => { + const mockInitialState: ep.ExecutionPlanWebviewState = { + executionPlanState: { + executionPlanGraphs: [ + { root: { cost: 10, subTreeCost: 20 } } as ep.ExecutionPlanGraph, + { root: { cost: 5, subTreeCost: 15 } } as ep.ExecutionPlanGraph, + ], + loadState: ApiStatus.Loaded, + }, + }; + + const result = epUtils.calculateTotalCost(mockInitialState); + + expect(result, "Total cost should correctly sum up the costs and subtree costs").to.equal( + 50, + ); + }); + + test("calculateTotalCost: should return 0 if executionPlanGraphs is empty", () => { + const result = epUtils.calculateTotalCost(mockInitialState); + + expect(result, "Total cost should be 0 for an empty executionPlanGraphs array").to.equal(0); + }); + + test("formatXml: should return original xml contents if it is not a valid xml file", () => { + const invalidXml = " { extensionConfigGet.withArgs(toolsKey(configKey)).returns(expectedFromExtension); } - return new ExtConfig(config as unknown as IConfigUtils, extensionConfig, workspaceConfig); + return new ExtConfig(config, extensionConfig, workspaceConfig); }; setup(() => { diff --git a/test/unit/metadataService.test.ts b/test/unit/metadataService.test.ts index 6cece79a01..c8821b924c 100644 --- a/test/unit/metadataService.test.ts +++ b/test/unit/metadataService.test.ts @@ -29,9 +29,9 @@ suite("Metadata Service Tests", () => { }; client.sendRequest.resolves(mockMetadata); - connectionManager.client = client as unknown as SqlToolsServiceClient; + connectionManager.client = client; - metadataService = new MetadataService(connectionManager as unknown as ConnectionManager); + metadataService = new MetadataService(connectionManager); }); teardown(() => { From 4886de7b25619f7263312524b85962f04718034b Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Tue, 30 Sep 2025 14:12:13 -0700 Subject: [PATCH 19/30] cleaned up list.test.ts --- test/unit/list.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/list.test.ts b/test/unit/list.test.ts index e2c2a16bc5..6659c678bf 100644 --- a/test/unit/list.test.ts +++ b/test/unit/list.test.ts @@ -29,7 +29,7 @@ suite("List Prompt Tests", () => { }); test("Test list prompt render", async () => { - const listPrompt = new ListPrompt(question, vscodeWrapper as unknown as VscodeWrapper); + const listPrompt = new ListPrompt(question, vscodeWrapper); await listPrompt.render(); sinon.assert.calledOnceWithExactly( @@ -43,7 +43,7 @@ suite("List Prompt Tests", () => { test.skip("Test list prompt render with error", async () => { const errorWrapper = stubVscodeWrapper(sandbox); errorWrapper.showQuickPickStrings.resolves(undefined); - const errorPrompt = new ListPrompt(question, errorWrapper as unknown as VscodeWrapper); + const errorPrompt = new ListPrompt(question, errorWrapper); await errorPrompt.render(); sinon.assert.calledOnce(errorWrapper.showQuickPickStrings); }); From 1be9d3f41c070e08c6e71bcc066281741bd93a8a Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Tue, 30 Sep 2025 14:43:05 -0700 Subject: [PATCH 20/30] fixed up adapter.test.ts --- test/unit/adapter.test.ts | 188 ++++++++++++++++++-------------------- test/unit/utils.ts | 140 ++++++++++++++-------------- 2 files changed, 163 insertions(+), 165 deletions(-) diff --git a/test/unit/adapter.test.ts b/test/unit/adapter.test.ts index 7e315031a2..201d3ef815 100644 --- a/test/unit/adapter.test.ts +++ b/test/unit/adapter.test.ts @@ -1,98 +1,90 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as sinon from "sinon"; -import CodeAdapter from "../../src/prompts/adapter"; -import VscodeWrapper from "../../src/controllers/vscodeWrapper"; -import { IQuestion } from "../../src/prompts/question"; -import { stubVscodeWrapper } from "./utils"; - -suite("Code Adapter Tests", () => { - let sandbox: sinon.SinonSandbox; - let adapter: CodeAdapter; - let vscodeWrapper: sinon.SinonStubbedInstance; - let outputChannel: { - append: sinon.SinonStub; - appendLine: sinon.SinonStub; - clear: sinon.SinonStub; - show: sinon.SinonStub; - }; - - const testMessage = { - message: "test_message", - code: 123, - level: "456", - id: 789, - }; - const testQuestion: IQuestion = { - type: "password", - name: "test_question", - message: "test_message", - shouldPrompt: ({}) => false, - }; - - setup(() => { - sandbox = sinon.createSandbox(); - vscodeWrapper = stubVscodeWrapper(sandbox); - - outputChannel = { - append: sandbox.stub(), - appendLine: sandbox.stub(), - clear: sandbox.stub(), - show: sandbox.stub(), - }; - - sandbox.stub(vscodeWrapper, "outputChannel").get(() => outputChannel as any); - vscodeWrapper.showErrorMessage.resolves(undefined); - - adapter = new CodeAdapter(vscodeWrapper as unknown as VscodeWrapper); - }); - - teardown(() => { - sandbox.restore(); - }); - - test("logError should append message to the channel", () => { - adapter.logError(testMessage); - sinon.assert.calledOnce(outputChannel.appendLine); - }); - - test("log should format message and append to the channel", () => { - adapter.log(testMessage); - sinon.assert.calledOnce(outputChannel.appendLine); - }); - - test("clearLog should clear from output channel", () => { - adapter.clearLog(); - sinon.assert.calledOnce(outputChannel.clear); - }); - - test("showLog should show the output channel", () => { - adapter.showLog(); - sinon.assert.calledOnce(outputChannel.show); - }); - - test("promptSingle and promptCallback should call prompt", async () => { - await adapter.promptSingle(testQuestion); - adapter.promptCallback([testQuestion], () => true); - // Error case - await adapter.prompt([{ type: "test", message: "test", name: "test" }]); - }); - - test("prompting a checkbox question should call fixQuestion", async () => { - const formattedQuestion: IQuestion = { - type: "checkbox", - message: "test", - name: "test_checkbox", - choices: [{ name: "test_choice", value: "test_choice" }], - }; - await adapter.promptSingle(formattedQuestion); - const question: IQuestion = { - ...formattedQuestion, - choices: ["test"], - }; - await adapter.promptSingle(question); - }); -}); +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { expect } from "chai"; +import * as chai from "chai"; +import sinonChai from "sinon-chai"; +import * as sinon from "sinon"; +import CodeAdapter from "../../src/prompts/adapter"; +import VscodeWrapper from "../../src/controllers/vscodeWrapper"; +import { IQuestion } from "../../src/prompts/question"; +import { stubVscodeWrapper } from "./utils"; + +chai.use(sinonChai); + +suite("Code Adapter Tests", () => { + let sandbox: sinon.SinonSandbox; + let adapter: CodeAdapter; + let mockVscodeWrapper: sinon.SinonStubbedInstance; + + const testMessage = { + message: "test_message", + code: 123, + level: "456", + id: 789, + }; + const testQuestion: IQuestion = { + type: "password", + name: "test_question", + message: "test_message", + shouldPrompt: ({}) => false, + }; + + setup(() => { + sandbox = sinon.createSandbox(); + mockVscodeWrapper = stubVscodeWrapper(sandbox); + + mockVscodeWrapper.showErrorMessage.resolves(undefined); + + adapter = new CodeAdapter(mockVscodeWrapper); + }); + + teardown(() => { + sandbox.restore(); + }); + + test("logError should append message to the channel", () => { + adapter.logError(testMessage); + expect(mockVscodeWrapper.outputChannel.appendLine).to.have.been.calledOnce; + }); + + test("log should format message and append to the channel", () => { + adapter.log(testMessage); + expect(mockVscodeWrapper.outputChannel.appendLine).to.have.been.calledOnce; + }); + + test("clearLog should clear from output channel", () => { + adapter.clearLog(); + expect(mockVscodeWrapper.outputChannel.clear).to.have.been.calledOnce; + }); + + test("showLog should show the output channel", () => { + adapter.showLog(); + expect(mockVscodeWrapper.outputChannel.show).to.have.been.calledOnce; + }); + + test("promptSingle and promptCallback should call prompt", async () => { + await adapter.promptSingle(testQuestion); + adapter.promptCallback([testQuestion], () => true); + // Error case + await adapter.prompt([{ type: "test", message: "test", name: "test" }]); + }); + + test("prompting a checkbox question should call fixQuestion", async () => { + const formattedQuestion: IQuestion = { + type: "checkbox", + message: "test", + name: "test_checkbox", + choices: [{ name: "test_choice", value: "test_choice" }], + }; + await adapter.promptSingle(formattedQuestion); + const question: IQuestion = { + ...formattedQuestion, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + choices: ["test" as any], // Intentionally wrong type to trigger fixQuestion + }; + await adapter.promptSingle(question); + }); +}); diff --git a/test/unit/utils.ts b/test/unit/utils.ts index 38a7dbd54d..174978ad1b 100644 --- a/test/unit/utils.ts +++ b/test/unit/utils.ts @@ -1,67 +1,73 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as constants from "../../src/constants/constants"; -import * as sinon from "sinon"; -import * as telemetry from "../../src/telemetry/telemetry"; -import * as vscode from "vscode"; -import { IExtension } from "vscode-mssql"; -import VscodeWrapper from "../../src/controllers/vscodeWrapper"; -import * as path from "path"; -import SqlToolsServerClient from "../../src/languageservice/serviceclient"; -import { GetCapabilitiesRequest } from "../../src/models/contracts/connection"; -import { buildCapabilitiesResult } from "./mocks"; - -// Launches and activates the extension -export async function activateExtension(): Promise { - const extension = vscode.extensions.getExtension(constants.extensionId); - return await extension.activate(); -} - -// Stubs the telemetry code -export function stubTelemetry(sandbox?: sinon.SinonSandbox): { - sendActionEvent: sinon.SinonStub; - sendErrorEvent: sinon.SinonStub; -} { - const stubber = sandbox || sinon; - return { - sendActionEvent: stubber.stub(telemetry, "sendActionEvent").callsFake(() => {}), - sendErrorEvent: stubber.stub(telemetry, "sendErrorEvent").callsFake(() => {}), - }; -} - -export function stubVscodeWrapper( - sandbox?: sinon.SinonSandbox, -): sinon.SinonStubbedInstance { - const stubber = sandbox || sinon; - - const vscodeWrapper = stubber.createStubInstance(VscodeWrapper); - const outputChannel = stubber.stub({ - append: () => stubber.stub(), - appendLine: () => stubber.stub(), - }) as unknown as vscode.OutputChannel; - - stubber.stub(vscodeWrapper, "outputChannel").get(() => { - return outputChannel; - }); - - return vscodeWrapper; -} - -export function stubGetCapabilitiesRequest( - sandbox?: sinon.SinonSandbox, -): sinon.SinonStubbedInstance { - const stubber = sandbox || sinon; - const serviceClientMock = stubber.createStubInstance(SqlToolsServerClient); - serviceClientMock.sendRequest - .withArgs(GetCapabilitiesRequest.type, sinon.match.any) - .resolves(buildCapabilitiesResult()); - return serviceClientMock; -} - -export function initializeIconUtils(): void { - const { IconUtils } = require("../../src/utils/iconUtils"); - IconUtils.initialize(vscode.Uri.file(path.join(__dirname, "..", ".."))); -} +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as constants from "../../src/constants/constants"; +import * as sinon from "sinon"; +import * as telemetry from "../../src/telemetry/telemetry"; +import * as vscode from "vscode"; +import { IExtension } from "vscode-mssql"; +import VscodeWrapper from "../../src/controllers/vscodeWrapper"; +import * as path from "path"; +import SqlToolsServerClient from "../../src/languageservice/serviceclient"; +import { GetCapabilitiesRequest } from "../../src/models/contracts/connection"; +import { buildCapabilitiesResult } from "./mocks"; + +// Launches and activates the extension +export async function activateExtension(): Promise { + const extension = vscode.extensions.getExtension(constants.extensionId); + return await extension.activate(); +} + +// Stubs the telemetry code +export function stubTelemetry(sandbox?: sinon.SinonSandbox): { + sendActionEvent: sinon.SinonStub; + sendErrorEvent: sinon.SinonStub; +} { + const stubber = sandbox || sinon; + return { + sendActionEvent: stubber.stub(telemetry, "sendActionEvent").callsFake(() => {}), + sendErrorEvent: stubber.stub(telemetry, "sendErrorEvent").callsFake(() => {}), + }; +} + +export function stubVscodeWrapper( + sandbox?: sinon.SinonSandbox, +): sinon.SinonStubbedInstance { + const stubber = sandbox || sinon; + + const vscodeWrapper = stubber.createStubInstance(VscodeWrapper); + + const outputChannel: vscode.OutputChannel = { + name: "", + append: stubber.stub(), + appendLine: stubber.stub(), + clear: stubber.stub(), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + show: stubber.stub() as any, + replace: stubber.stub(), + hide: stubber.stub(), + dispose: stubber.stub(), + }; + + stubber.stub(vscodeWrapper, "outputChannel").get(() => outputChannel); + + return vscodeWrapper; +} + +export function stubGetCapabilitiesRequest( + sandbox?: sinon.SinonSandbox, +): sinon.SinonStubbedInstance { + const stubber = sandbox || sinon; + const serviceClientMock = stubber.createStubInstance(SqlToolsServerClient); + serviceClientMock.sendRequest + .withArgs(GetCapabilitiesRequest.type, sinon.match.any) + .resolves(buildCapabilitiesResult()); + return serviceClientMock; +} + +export function initializeIconUtils(): void { + const { IconUtils } = require("../../src/utils/iconUtils"); + IconUtils.initialize(vscode.Uri.file(path.join(__dirname, "..", ".."))); +} From ac5488debef2e8ab93fb8907f8ecfdd87bf720e6 Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Tue, 30 Sep 2025 14:48:23 -0700 Subject: [PATCH 21/30] cleaned up firewallService.test.ts --- test/unit/firewallService.test.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/test/unit/firewallService.test.ts b/test/unit/firewallService.test.ts index 0f6e587721..bc54e4c7b1 100644 --- a/test/unit/firewallService.test.ts +++ b/test/unit/firewallService.test.ts @@ -13,21 +13,22 @@ import { IHandleFirewallRuleResponse, CreateFirewallRuleRequest, ICreateFirewallRuleResponse, + ICreateFirewallRuleParams, } from "../../src/models/contracts/firewall/firewallRequest"; import * as Constants from "../../src/constants/constants"; suite("Firewall Service Tests", () => { let sandbox: sinon.SinonSandbox; let client: sinon.SinonStubbedInstance; - let accountService: AccountService; + let accountService: sinon.SinonStubbedInstance; let firewallService: FirewallService; setup(() => { sandbox = sinon.createSandbox(); client = sandbox.createStubInstance(SqlToolsServiceClient); - accountService = { - client: client as unknown as SqlToolsServiceClient, - } as AccountService; + accountService = sandbox.createStubInstance(AccountService); + + sandbox.stub(accountService, "client").get(() => client); firewallService = new FirewallService(accountService); }); @@ -70,7 +71,7 @@ suite("Firewall Service Tests", () => { endIpAddress: "1.2.3.255", serverName: "test_server", securityTokenMappings: {}, - }; + } as ICreateFirewallRuleParams; const result = await firewallService.createFirewallRule(request); From 9b130ae8c1bfad3ae55764e8c6514c082cb5cb1f Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Tue, 30 Sep 2025 15:17:44 -0700 Subject: [PATCH 22/30] added test/unit AGENTS.md --- test/unit/AGENTS.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 test/unit/AGENTS.md diff --git a/test/unit/AGENTS.md b/test/unit/AGENTS.md new file mode 100644 index 0000000000..ed32a4b56b --- /dev/null +++ b/test/unit/AGENTS.md @@ -0,0 +1,29 @@ +## Instructions for writing good unit tests: + +### Rules & Expectations + + - Do not edit application/source files unless the refactor demands it. Confirm before editing files outside of /test/unit, and justify why you need to make those changes. + - Use Sinon, not TypeMoq. If easily possible, replace TypeMoq mocks/stubs/helpers with Sinon equivalents. + - Use a Sinon sandbox (setup/teardown with sinon.createSandbox()); keep helper closures (e.g., createServer) inside setup where the + sandbox is created. + - Default to chai.expect; when checking Sinon interactions, use sinon-chai. + - Avoid Object.defineProperty hacks and (if possible) fake/partial plain objects; use sandbox.createStubInstance(type) and sandbox.stub(obj, 'prop').value(...). + - Add shared Sinon helpers to test/unit/utils.ts when they’ll be reused. + - If updating preexisting tests, preserve relevant inline comments from the original tests. + - When introducing a Sinon helper to replace a TypeMoq helper (e.g., capabilities mock), follow the utils.ts pattern: accept an optional + sandbox, create stub instances, and return them. + - Avoid unnecessary casts, like `myVar as unknown as MyType` when myVar is already a sinon-stubbed instance of MyType. + - Maintain existing formatting conventions, line endings, and text encoding. + - Nest test suits as necessary to group tests in a logical manner. + +### Process + + * Write tests following the rules and expectations defined above. + * Validate the tests written by running the test suite you've edited. + * Don't commit your changes unless directly instructed. If you do create git commits, follow these rules: + * Choose a concise commit message + * Orgnize the contents of each commit with test files that make sense together. It's okay for each .test.ts file to have its own commit if they're not related. + +### Testing + +Use the provided command format: yarn test -- --grep . \ No newline at end of file From ae4e96977550a46fd0fc778ca7a07e9877683943 Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Thu, 2 Oct 2025 11:10:23 -0700 Subject: [PATCH 23/30] Adding AGENTS.md for unit test folder --- test/unit/AGENTS.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 test/unit/AGENTS.md diff --git a/test/unit/AGENTS.md b/test/unit/AGENTS.md new file mode 100644 index 0000000000..5fa0da47ce --- /dev/null +++ b/test/unit/AGENTS.md @@ -0,0 +1,29 @@ +## Instructions for writing good unit tests: + +### Rules & Expectations + +- Do not edit application/source files unless the refactor demands it. Confirm before editing files outside of /test/unit, and justify why you need to make those changes. +- Use Sinon, not TypeMoq. If easily possible, replace TypeMoq mocks/stubs/helpers with Sinon equivalents. +- Use a Sinon sandbox (setup/teardown with sinon.createSandbox()); keep helper closures (e.g., createServer) inside setup where the + sandbox is created. +- Default to chai.expect; when checking Sinon interactions, use sinon-chai. +- Avoid Object.defineProperty hacks and (if possible) fake/partial plain objects; use sandbox.createStubInstance(type) and sandbox.stub(obj, 'prop').value(...). +- Add shared Sinon helpers to test/unit/utils.ts when they’ll be reused. +- If updating preexisting tests, preserve relevant inline comments from the original tests. +- When introducing a Sinon helper to replace a TypeMoq helper (e.g., capabilities mock), follow the utils.ts pattern: accept an optional + sandbox, create stub instances, and return them. +- Avoid unnecessary casts, like `myVar as unknown as MyType` when myVar is already a sinon-stubbed instance of MyType. +- Maintain existing formatting conventions, line endings, and text encoding. +- Nest test suits as necessary to group tests in a logical manner. + +### Process + +- Write tests following the rules and expectations defined above. +- Validate the tests written by running the test suite you've edited. +- Don't commit your changes unless directly instructed. If you do create git commits, follow these rules: + - Choose a concise commit message + - Orgnize the contents of each commit with test files that make sense together. It's okay for each .test.ts file to have its own commit if they're not related. + +### Testing + +Use the provided command format: yarn test -- --grep . From 183831d2989e89a6f9b766c312feb6f7c89651d5 Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Thu, 2 Oct 2025 15:25:01 -0700 Subject: [PATCH 24/30] converted tableDesigner.test.ts --- test/unit/tableDesigner.test.ts | 108 ++++++++++++++------------------ test/unit/utils.ts | 7 +++ 2 files changed, 53 insertions(+), 62 deletions(-) diff --git a/test/unit/tableDesigner.test.ts b/test/unit/tableDesigner.test.ts index a261750d37..4f1b9c7f6c 100644 --- a/test/unit/tableDesigner.test.ts +++ b/test/unit/tableDesigner.test.ts @@ -6,7 +6,6 @@ import * as assert from "assert"; import * as sinon from "sinon"; import * as vscode from "vscode"; -import * as TypeMoq from "typemoq"; import * as tdTab from "../../src/tableDesigner/tableDesignerTabDefinition"; import { TableDesignerWebviewController } from "../../src/tableDesigner/tableDesignerWebviewController"; import VscodeWrapper from "../../src/controllers/vscodeWrapper"; @@ -15,16 +14,17 @@ import { TreeNodeInfo } from "../../src/objectExplorer/nodes/treeNodeInfo"; import { TableDesignerService } from "../../src/services/tableDesignerService"; import SqlDocumentService, { ConnectionStrategy } from "../../src/controllers/sqlDocumentService"; import ConnectionManager from "../../src/controllers/connectionManager"; +import { getMockContext } from "./utils"; suite("TableDesignerWebviewController tests", () => { let sandbox: sinon.SinonSandbox; let mockContext: vscode.ExtensionContext; - let mockVscodeWrapper: TypeMoq.IMock; + let mockVscodeWrapper: sinon.SinonStubbedInstance; let controller: TableDesignerWebviewController; - let treeNode: TypeMoq.IMock; - let mockConnectionManager: TypeMoq.IMock; - let mockTableDesignerService: TableDesignerService; - let mockSqlDocumentService: SqlDocumentService; + let treeNode: sinon.SinonStubbedInstance; + let mockConnectionManager: sinon.SinonStubbedInstance; + let mockTableDesignerService: sinon.SinonStubbedInstance; + let mockSqlDocumentService: sinon.SinonStubbedInstance; let newQueryStub: sinon.SinonStub; const tableName = "TestTable"; let mockResult: any; @@ -33,15 +33,12 @@ suite("TableDesignerWebviewController tests", () => { setup(async () => { sandbox = sinon.createSandbox(); - mockContext = { - extensionUri: vscode.Uri.parse("file://test"), - extensionPath: "path", - } as unknown as vscode.ExtensionContext; + mockContext = getMockContext(); - mockVscodeWrapper = TypeMoq.Mock.ofType(); + mockVscodeWrapper = sandbox.createStubInstance(VscodeWrapper); mockTableDesignerService = sandbox.createStubInstance(TableDesignerService); mockSqlDocumentService = sandbox.createStubInstance(SqlDocumentService); - mockConnectionManager = TypeMoq.Mock.ofType(); + mockConnectionManager = sandbox.createStubInstance(ConnectionManager); const mockConnectionDetails = { server: "localhost", @@ -50,30 +47,21 @@ suite("TableDesignerWebviewController tests", () => { authenticationType: "SqlLogin", }; - mockConnectionManager - .setup((m) => m.createConnectionDetails(TypeMoq.It.isAny())) - .returns(() => mockConnectionDetails as any); - mockConnectionManager - .setup((m) => - m.getConnectionString(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - ) - .returns(() => Promise.resolve(mockConnectionDetails.connectionString)); - mockConnectionManager - .setup((mgr) => mgr.getUriForConnection(TypeMoq.It.isAny())) - .returns(() => "localhost,1433_undefined_sa_undefined"); - - treeNode = TypeMoq.Mock.ofType(TreeNodeInfo, TypeMoq.MockBehavior.Loose); - treeNode.setup((t) => t.nodeType).returns(() => "Table"); - treeNode.setup((t) => t.nodePath).returns(() => "localhost,1433/Databases"); - treeNode.setup((t) => t.label).returns(() => tableName); - treeNode - .setup((t) => t.context) - .returns( - () => - ({ - subType: "Table", - }) as any, - ); + mockConnectionManager.createConnectionDetails.returns(mockConnectionDetails as any); + mockConnectionManager.getConnectionString.resolves(mockConnectionDetails.connectionString); + mockConnectionManager.getUriForConnection.returns("localhost,1433_undefined_sa_undefined"); + mockConnectionManager.confirmEntraTokenValidity.resolves(); + + treeNode = sandbox.createStubInstance(TreeNodeInfo); + sandbox.stub(treeNode, "nodeType").get(() => "Table"); + sandbox.stub(treeNode, "nodePath").get(() => "localhost,1433/Databases"); + treeNode.label = tableName as any; + sandbox.stub(treeNode, "context").get( + () => + ({ + subType: "Table", + }) as any, + ); // Arrange const mockConnectionProfile = { @@ -87,11 +75,11 @@ suite("TableDesignerWebviewController tests", () => { name: tableName, }; - treeNode.setup((t) => t.connectionProfile).returns(() => mockConnectionProfile as any); - treeNode.setup((t) => t.metadata).returns(() => mockMetadata as any); + sandbox.stub(treeNode, "connectionProfile").get(() => mockConnectionProfile as any); + sandbox.stub(treeNode, "metadata").get(() => mockMetadata as any); assert.deepStrictEqual( - treeNode.object.connectionProfile, + treeNode.connectionProfile, mockConnectionProfile, "Connection profile should be defined", ); @@ -110,8 +98,8 @@ suite("TableDesignerWebviewController tests", () => { }; mockTableChangeInfo = { - type: treeNode.object.nodeType, - source: treeNode.object.nodePath, + type: treeNode.nodeType, + source: treeNode.nodePath, }; mockPayload = { @@ -119,19 +107,19 @@ suite("TableDesignerWebviewController tests", () => { tableChangeInfo: mockTableChangeInfo, }; - newQueryStub = (mockSqlDocumentService.newQuery as sinon.SinonStub).resolves(); + newQueryStub = mockSqlDocumentService.newQuery.resolves(); - (mockTableDesignerService.initializeTableDesigner as sinon.SinonStub).resolves(mockResult); + mockTableDesignerService.initializeTableDesigner.resolves(mockResult); sandbox.stub(tdTab, "getDesignerView").returns({ tabs: [] }); controller = new TableDesignerWebviewController( mockContext, - mockVscodeWrapper.object, + mockVscodeWrapper, mockTableDesignerService, - mockConnectionManager.object, + mockConnectionManager, mockSqlDocumentService, - treeNode.object, + treeNode, ); controller.revealToForeground(); @@ -167,12 +155,10 @@ suite("TableDesignerWebviewController tests", () => { view: {}, viewModel: {}, isValid: true, - }; + } as td.DesignerEditResult; // First scenario: no issues, view is defined - let processTableEditStub = ( - mockTableDesignerService.processTableEdit as sinon.SinonStub - ).resolves(editResponse); + let processTableEditStub = mockTableDesignerService.processTableEdit.resolves(editResponse); const callState = (controller as any)._state; @@ -196,7 +182,10 @@ suite("TableDesignerWebviewController tests", () => { processTableEditStub.restore(); editResponse = { - issues: ["issue1", "issue2"], + issues: [ + { description: "issue1", severity: "warning" }, + { description: "issue2", severity: "error" }, + ], view: undefined, viewModel: {}, isValid: false, @@ -248,15 +237,13 @@ suite("TableDesignerWebviewController tests", () => { test("should call publishTable in publishTable reducer", async () => { let publishResponse = { issues: [], - view: {}, + view: { useAdvancedSaveMode: false }, viewModel: {}, newTableInfo: { ...mockResult.tableInfo, title: "NewTable" }, - }; + } as td.PublishChangesResult; // First scenario: no issues, view is defined - let publishChangesStub = ( - mockTableDesignerService.publishChanges as sinon.SinonStub - ).resolves(publishResponse); + let publishChangesStub = mockTableDesignerService.publishChanges.resolves(publishResponse); const mockPublishPayload = { table: mockResult.tableInfo, @@ -320,9 +307,7 @@ suite("TableDesignerWebviewController tests", () => { let scriptResponse = "CREATE TABLE Test (Id INT);"; // First scenario: no issues, view is defined - let scriptStub = (mockTableDesignerService.generateScript as sinon.SinonStub).resolves( - scriptResponse, - ); + let scriptStub = mockTableDesignerService.generateScript.resolves(scriptResponse); const mockScriptPayload = { table: mockResult.tableInfo, @@ -376,9 +361,8 @@ suite("TableDesignerWebviewController tests", () => { mimeType: "text/html", }; - const generatePreviewStub = ( - mockTableDesignerService.generatePreviewReport as sinon.SinonStub - ).resolves(previewResponse); + const generatePreviewStub = + mockTableDesignerService.generatePreviewReport.resolves(previewResponse); const mockPreviewPayload = { table: mockResult.tableInfo, diff --git a/test/unit/utils.ts b/test/unit/utils.ts index fb6d6313c8..3c0c261f16 100644 --- a/test/unit/utils.ts +++ b/test/unit/utils.ts @@ -67,6 +67,13 @@ export function stubGetCapabilitiesRequest( return serviceClientMock; } +export function getMockContext(): vscode.ExtensionContext { + return { + extensionUri: vscode.Uri.parse("file://test"), + extensionPath: "path", + } as unknown as vscode.ExtensionContext; +} + export function initializeIconUtils(): void { const { IconUtils } = require("../../src/utils/iconUtils"); IconUtils.initialize(vscode.Uri.file(path.join(__dirname, "..", ".."))); From ba44ccd6e24d1537f347cd1f5d0c54e53785ad24 Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Thu, 2 Oct 2025 16:32:30 -0700 Subject: [PATCH 25/30] swapping to correct assertions --- test/unit/tableDesigner.test.ts | 196 ++++++++++++-------------------- test/unit/utils.ts | 10 ++ 2 files changed, 83 insertions(+), 123 deletions(-) diff --git a/test/unit/tableDesigner.test.ts b/test/unit/tableDesigner.test.ts index 4f1b9c7f6c..a9023f5d9d 100644 --- a/test/unit/tableDesigner.test.ts +++ b/test/unit/tableDesigner.test.ts @@ -3,9 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from "assert"; -import * as sinon from "sinon"; import * as vscode from "vscode"; +import * as sinon from "sinon"; +import sinonChai from "sinon-chai"; +import { expect } from "chai"; +import * as chai from "chai"; import * as tdTab from "../../src/tableDesigner/tableDesignerTabDefinition"; import { TableDesignerWebviewController } from "../../src/tableDesigner/tableDesignerWebviewController"; import VscodeWrapper from "../../src/controllers/vscodeWrapper"; @@ -14,7 +16,9 @@ import { TreeNodeInfo } from "../../src/objectExplorer/nodes/treeNodeInfo"; import { TableDesignerService } from "../../src/services/tableDesignerService"; import SqlDocumentService, { ConnectionStrategy } from "../../src/controllers/sqlDocumentService"; import ConnectionManager from "../../src/controllers/connectionManager"; -import { getMockContext } from "./utils"; +import { getMockContext, stubUserSurvey } from "./utils"; + +chai.use(sinonChai); suite("TableDesignerWebviewController tests", () => { let sandbox: sinon.SinonSandbox; @@ -34,6 +38,7 @@ suite("TableDesignerWebviewController tests", () => { setup(async () => { sandbox = sinon.createSandbox(); mockContext = getMockContext(); + stubUserSurvey(sandbox); mockVscodeWrapper = sandbox.createStubInstance(VscodeWrapper); mockTableDesignerService = sandbox.createStubInstance(TableDesignerService); @@ -78,10 +83,8 @@ suite("TableDesignerWebviewController tests", () => { sandbox.stub(treeNode, "connectionProfile").get(() => mockConnectionProfile as any); sandbox.stub(treeNode, "metadata").get(() => mockMetadata as any); - assert.deepStrictEqual( - treeNode.connectionProfile, + expect(treeNode.connectionProfile, "Connection profile should be defined").to.deep.equal( mockConnectionProfile, - "Connection profile should be defined", ); mockResult = { @@ -123,10 +126,8 @@ suite("TableDesignerWebviewController tests", () => { ); controller.revealToForeground(); - assert.strictEqual( - controller.panel.title, + expect(controller.panel.title, "Panel title should be table name").to.equal( "Table Designer", - "Panel title should be table name", ); await (controller as any).initialize(); await (controller as any).registerRpcHandlers(); @@ -137,16 +138,14 @@ suite("TableDesignerWebviewController tests", () => { }); test("should initialize correctly for table edit", async () => { - assert.strictEqual( + expect( (controller as any)._state.apiState.initializeState, - td.LoadState.Loaded, "Initialize state should be loaded", - ); - assert.deepStrictEqual( + ).to.equal(td.LoadState.Loaded); + expect( (controller as any)._state.tableInfo.database, - "master", "Table Info should be loaded", - ); + ).to.equal("master"); }); test("should call processTableEdit in processTableEdit reducer", async () => { @@ -167,16 +166,12 @@ suite("TableDesignerWebviewController tests", () => { mockPayload, ); - assert.ok(processTableEditStub.calledOnce, "processTableEdit should be called once"); - assert.deepStrictEqual( - processTableEditStub.firstCall.args, - [mockPayload.table, mockPayload.tableChangeInfo], - "processTableEdit should be called with correct arguments", - ); - assert.deepStrictEqual( - result.tabStates.resultPaneTab, + expect( + processTableEditStub, + "processTableEdit should be called once with correct arguments", + ).to.have.been.calledOnceWithExactly(mockPayload.table, mockPayload.tableChangeInfo); + expect(result.tabStates.resultPaneTab, "State tab should be set to Script").to.equal( td.DesignerResultPaneTabs.Script, - "State tab should be set to Script", ); processTableEditStub.restore(); @@ -200,22 +195,18 @@ suite("TableDesignerWebviewController tests", () => { mockPayload, ); - assert.ok(secondStub.calledOnce, "processTableEdit should be called again"); - assert.deepStrictEqual( - secondStub.firstCall.args, - [mockPayload.table, mockPayload.tableChangeInfo], - "Second call should use correct arguments", - ); - assert.deepStrictEqual( + expect( + secondStub, + "processTableEdit should be called again with correct arguments", + ).to.have.been.calledOnceWithExactly(mockPayload.table, mockPayload.tableChangeInfo); + expect( result.tabStates.resultPaneTab, - td.DesignerResultPaneTabs.Issues, "Tab should be set to Issues when there are issues", - ); - assert.deepStrictEqual( + ).to.equal(td.DesignerResultPaneTabs.Issues); + expect( result.view, - callState.view, "Should retain previous view when editResponse.view is undefined", - ); + ).to.deep.equal(callState.view); secondStub.restore(); // Cleanup const errorMessage = "error message"; @@ -227,11 +218,10 @@ suite("TableDesignerWebviewController tests", () => { mockPayload, ); - assert.deepStrictEqual( - errorStub.firstCall.args, - [errorMessage], + expect( + errorStub, "Error message call should use correct arguments", - ); + ).to.have.been.calledOnceWithExactly(errorMessage); }); test("should call publishTable in publishTable reducer", async () => { @@ -256,28 +246,20 @@ suite("TableDesignerWebviewController tests", () => { mockPublishPayload, ); - assert.ok(publishChangesStub.calledOnce, "publishChanges should be called once"); - assert.deepStrictEqual( - publishChangesStub.firstCall.args, - [mockPublishPayload.table], - "publishChanges should be called with correct arguments", - ); - assert.deepStrictEqual( - result.apiState.publishState, + expect( + publishChangesStub, + "publishChanges should be called once with correct arguments", + ).to.have.been.calledOnceWithExactly(mockPublishPayload.table); + expect(result.apiState.publishState, "Publish State should be loaded").to.equal( td.LoadState.Loaded, - "Publish State should be loaded", ); - assert.deepStrictEqual( - result.apiState.previewState, + expect(result.apiState.previewState, "Preview State should be not started").to.equal( td.LoadState.NotStarted, - "Preview State should be not started", ); - assert.strictEqual( - controller.panel.title, + expect(controller.panel.title, "Panel title should be table name").to.equal( publishResponse.newTableInfo.title, - "Panel title should be table name", ); publishChangesStub.restore(); @@ -290,16 +272,12 @@ suite("TableDesignerWebviewController tests", () => { mockPublishPayload, ); - assert.deepStrictEqual( - result.publishingError, + expect(result.publishingError, "State should contain error message").to.equal( `Error: ${errorMessage}`, - "State should contain error message", ); - assert.deepStrictEqual( - result.apiState.publishState, + expect(result.apiState.publishState, "State should contain correct status").to.equal( td.LoadState.Error, - "State should contain correct status", ); }); @@ -320,35 +298,25 @@ suite("TableDesignerWebviewController tests", () => { mockScriptPayload, ); - assert.ok(scriptStub.calledOnce, "generateScript should be called once"); - assert.deepStrictEqual( - scriptStub.firstCall.args, - [mockScriptPayload.table], - "generateScript should be called with correct arguments", - ); - assert.ok(newQueryStub.calledOnce, "newQuery should be called once"); - assert.deepStrictEqual( - newQueryStub.firstCall.args, - [ - { - content: scriptResponse, - connectionStrategy: ConnectionStrategy.CopyConnectionFromInfo, - connectionInfo: undefined, - }, - ], - "newQuery should be called with the generated script", - ); + expect( + scriptStub, + "generateScript should be called once with correct arguments", + ).to.have.been.calledOnceWithExactly(mockScriptPayload.table); + expect( + newQueryStub, + "newQuery should be called once with the generated script", + ).to.have.been.calledOnceWithExactly({ + content: scriptResponse, + connectionStrategy: ConnectionStrategy.CopyConnectionFromInfo, + connectionInfo: undefined, + }); - assert.deepStrictEqual( - result.apiState.generateScriptState, + expect(result.apiState.generateScriptState, "Script State should be loaded").to.equal( td.LoadState.Loaded, - "Script State should be loaded", ); - assert.deepStrictEqual( - result.apiState.previewState, + expect(result.apiState.previewState, "Preview State should be not started").to.equal( td.LoadState.NotStarted, - "Preview State should be not started", ); scriptStub.restore(); newQueryStub.restore(); @@ -376,28 +344,22 @@ suite("TableDesignerWebviewController tests", () => { mockPreviewPayload, ); - assert.ok(generatePreviewStub.calledOnce, "generatePreviewReport should be called once"); - assert.deepStrictEqual( - generatePreviewStub.firstCall.args, - [mockPreviewPayload.table], - "generatePreviewReport should be called with correct arguments", - ); + expect( + generatePreviewStub, + "generatePreviewReport should be called once with correct arguments", + ).to.have.been.calledOnceWithExactly(mockPreviewPayload.table); - assert.deepStrictEqual( + expect( result.apiState.previewState, - td.LoadState.Loaded, "Preview state should be Loaded when no validation error", - ); - assert.deepStrictEqual( - result.apiState.publishState, + ).to.equal(td.LoadState.Loaded); + expect(result.apiState.publishState, "Publish state should remain NotStarted").to.equal( td.LoadState.NotStarted, - "Publish state should remain NotStarted", ); - assert.deepStrictEqual( + expect( result.generatePreviewReportResult, - previewResponse, "Should store the preview report result", - ); + ).to.deep.equal(previewResponse); generatePreviewStub.restore(); @@ -412,21 +374,17 @@ suite("TableDesignerWebviewController tests", () => { mockPreviewPayload, ); - assert.deepStrictEqual( - result.apiState.previewState, + expect(result.apiState.previewState, "Preview state should be Error on failure").to.equal( td.LoadState.Error, - "Preview state should be Error on failure", ); - assert.deepStrictEqual( + expect( result.apiState.publishState, - td.LoadState.NotStarted, "Publish state should remain NotStarted on failure", - ); - assert.strictEqual( + ).to.equal(td.LoadState.NotStarted); + expect( result.generatePreviewReportResult.schemaValidationError, - errorMessage, "Should include error message in result", - ); + ).to.equal(errorMessage); }); test("should set mainPaneTab in setTab reducer", async () => { @@ -435,11 +393,7 @@ suite("TableDesignerWebviewController tests", () => { const result = await controller["_reducerHandlers"].get("setTab")(state as any, { tabId }); - assert.strictEqual( - result.tabStates.mainPaneTab, - tabId, - "mainPaneTab should be set correctly", - ); + expect(result.tabStates.mainPaneTab, "mainPaneTab should be set correctly").to.equal(tabId); }); test("should set propertiesPaneData in setPropertiesComponents reducer", async () => { @@ -450,11 +404,10 @@ suite("TableDesignerWebviewController tests", () => { components: mockComponents, }); - assert.deepStrictEqual( + expect( result.propertiesPaneData, - mockComponents, "propertiesPaneData should be set correctly", - ); + ).to.deep.equal(mockComponents); }); test("should set resultPaneTab in setResultTab reducer", async () => { @@ -465,10 +418,8 @@ suite("TableDesignerWebviewController tests", () => { tabId, }); - assert.strictEqual( - result.tabStates.resultPaneTab, + expect(result.tabStates.resultPaneTab, "resultPaneTab should be set correctly").to.equal( tabId, - "resultPaneTab should be set correctly", ); }); @@ -477,10 +428,9 @@ suite("TableDesignerWebviewController tests", () => { await controller["_reducerHandlers"].get("continueEditing")(state, mockPayload); - assert.strictEqual( + expect( controller.state.apiState.publishState, - td.LoadState.NotStarted, "publishState should be set to NotStarted", - ); + ).to.equal(td.LoadState.NotStarted); }); }); diff --git a/test/unit/utils.ts b/test/unit/utils.ts index 3c0c261f16..7ba57d6525 100644 --- a/test/unit/utils.ts +++ b/test/unit/utils.ts @@ -13,6 +13,7 @@ import * as path from "path"; import SqlToolsServerClient from "../../src/languageservice/serviceclient"; import { GetCapabilitiesRequest } from "../../src/models/contracts/connection"; import { buildCapabilitiesResult } from "./mocks"; +import { UserSurvey } from "../../src/nps/userSurvey"; // Launches and activates the extension export async function activateExtension(): Promise { @@ -67,6 +68,15 @@ export function stubGetCapabilitiesRequest( return serviceClientMock; } +export function stubUserSurvey(sandbox?: sinon.SinonSandbox): sinon.SinonStub { + const stubber = sandbox || sinon; + const surveyStub = stubber.stub(UserSurvey, "getInstance").returns({ + promptUserForNPSFeedback: sandbox.stub(), + } as unknown as UserSurvey); + + return surveyStub; +} + export function getMockContext(): vscode.ExtensionContext { return { extensionUri: vscode.Uri.parse("file://test"), From 9963e3f241554b0d518f297fb15856b247aa692a Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Thu, 2 Oct 2025 17:08:40 -0700 Subject: [PATCH 26/30] reverting AGENTS and README --- AGENTS.md | 55 ++++--------------------------------------------------- README.md | 2 +- 2 files changed, 5 insertions(+), 52 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 40f3994152..f8564b0b1b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,13 +1,3 @@ -# MSSQL Extension for Visual Studio Code - -The MSSQL Extension for Visual Studio Code is a TypeScript-based VS Code extension that provides database management capabilities for SQL Server, Azure SQL, and SQL Database in Fabric. The extension includes React-based webview components, AI-powered features with GitHub Copilot integration, and comprehensive SQL development tools. - -**Always reference these instructions first** and fallback to search or bash commands only when you encounter unexpected information that does not match the info here. - -## Working Effectively - -### Bootstrap, Build, and Test the Repository - **NEVER CANCEL** any build or test commands. These operations can take significant time and should be allowed to complete. #### Initial Setup (Required once) @@ -26,11 +16,6 @@ yarn install ```bash # Full build - takes ~19 seconds. NEVER CANCEL. Set timeout to 60+ seconds. yarn build - -# Development watch mode (for active development) -yarn watch -# This runs continuous compilation and bundling. Leave running during development. -# Includes: extension TypeScript, webview React/TypeScript, and asset bundling. ``` #### Individual Build Steps (if needed) @@ -43,39 +28,20 @@ yarn build:prepare yarn build:extension # Bundle extension (~1 second) -yarn build:extension-bundle - -# Compile React webviews (~8 seconds) -yarn build:webviews - -# Bundle webviews (~2 seconds) -yarn build:webviews-bundle ``` ### Linting and Code Quality -```bash +````bash # Lint source files only - takes ~1.5 seconds yarn lint src/ test/ - -# DO NOT run 'yarn lint' without arguments - it will fail trying to lint build output -``` - ### Testing #### Unit Tests - ```bash # Unit tests require VS Code download and cannot run in sandboxed environments # This is expected behavior - tests work in CI with proper VS Code setup -yarn test -# Expected to fail with "ENOTFOUND update.code.visualstudio.com" in sandboxed environments - -# Run targeted unit tests using grep patterns -yarn test --grep "ConnectionManager" # Run tests matching "ConnectionManager" -yarn test --pattern ".*service.*" # Run tests matching service pattern -yarn test --testPattern "QueryRunner" # Alternative syntax for test filtering -``` +```` #### E2E Tests (Smoke Tests) @@ -87,21 +53,12 @@ yarn smoketest ### Packaging -```bash +````bash # Install vsce globally (if not already installed) npm install -g vsce - -# Package extension - takes ~4.5 seconds. NEVER CANCEL. Set timeout to 60+ seconds. -yarn package --online # Creates ~12MB VSIX file for online distribution -yarn package --offline # Creates platform-specific packages with embedded services -``` - -## Validation Scenarios - **Always test the following scenarios after making changes:** ### Complete Build Validation - 1. Clean install: `rm -rf node_modules && yarn install` 2. Full build: `yarn build` 3. Lint check: `yarn lint src/ test/` @@ -109,20 +66,16 @@ yarn package --offline # Creates platform-specific packages with embedded servi 5. Verify VSIX file is created (~12-15MB is normal) ### Development Workflow Validation - 1. Start watch mode: `yarn watch` 2. Make a small change to a TypeScript file in `src/` 3. Verify automatic recompilation occurs 4. Stop watch mode with Ctrl+C ### Pre-Commit Validation Workflow - ```bash # Always run these commands before committing changes: yarn build # Ensure code compiles -yarn lint src/ test/ # Ensure code meets style standards -yarn package --online # Ensure extension can be packaged -``` +```` ### Code Quality Validation diff --git a/README.md b/README.md index 296a17ca9d..43340c19c8 100644 --- a/README.md +++ b/README.md @@ -208,7 +208,7 @@ The following Visual Studio Code settings are available for the mssql extension. // Status bar { "mssql.statusBar.connectionInfoMaxLength": -1, - "mssql.statusBar.enableConnectionColor": true, + "mssql.enableConnectionColor": true, } ``` From bf7ee0cba6d1aa2da3d8bfaf133d1a3e6db373ca Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Thu, 2 Oct 2025 18:11:16 -0700 Subject: [PATCH 27/30] fixing import for test --- test/unit/azureResourceService.test.ts | 118 ++++++++++++------------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/test/unit/azureResourceService.test.ts b/test/unit/azureResourceService.test.ts index 7bd4bb590b..d5813ec9b5 100644 --- a/test/unit/azureResourceService.test.ts +++ b/test/unit/azureResourceService.test.ts @@ -17,11 +17,68 @@ import { PagedAsyncIterableIterator } from "@azure/core-paging"; import { ResourceGroup, ResourceGroups, ResourceManagementClient } from "@azure/arm-resources"; import { AzureResourceController } from "../../src/azure/azureResourceController"; import { getCloudProviderSettings } from "../../src/azure/providerSettings"; -import { AzureAuthType, IAzureAccountSession } from "vscode-mssql"; -import { IAccount } from "../../src/models/contracts/azure"; +import { IAzureAccountSession } from "vscode-mssql"; +import { AzureAuthType, IAccount } from "../../src/models/contracts/azure"; chai.use(sinonChai); +suite("Azure SQL client", function (): void { + let sandbox: sinon.SinonSandbox; + + setup(() => { + sandbox = sinon.createSandbox(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test("Should return locations successfully", async function (): Promise { + const pages = createPagedIterator(mockLocations); + const subscriptions: Subscriptions = { + listLocations: sandbox.stub().returns(pages), + list: sandbox.stub().returns(undefined), + get: sandbox.stub().returns(undefined), + }; + const subscriptionClient = { + subscriptions, + } as unknown as SubscriptionClient; + const azureSqlClient = new AzureResourceController(() => subscriptionClient); + + const result = await azureSqlClient.getLocations(primarySession); + + expect(subscriptions.listLocations).to.have.been.calledOnceWithExactly( + primarySession.subscription?.subscriptionId, + ); + expect(result).to.have.lengthOf(mockLocations.length); + }); + + test("Should return resource groups successfully", async function (): Promise { + const pages = createPagedIterator(mockGroups); + const resourceGroups: ResourceGroups = { + list: sandbox.stub().returns(pages), + get: sandbox.stub().returns(undefined), + beginDelete: sandbox.stub(), + beginDeleteAndWait: sandbox.stub(), + beginExportTemplate: sandbox.stub(), + beginExportTemplateAndWait: sandbox.stub(), + checkExistence: sandbox.stub(), + createOrUpdate: sandbox.stub(), + update: sandbox.stub(), + }; + const resourceClient = { + resourceGroups, + } as unknown as ResourceManagementClient; + const azureSqlClient = new AzureResourceController(undefined, () => resourceClient); + + const result = await azureSqlClient.getResourceGroups(primarySession); + + expect(resourceGroups.list).to.have.been.calledOnceWithExactly(); + expect(result).to.have.lengthOf(mockGroups.length); + expect(result[0].location).to.equal(mockGroups[0].location); + }); +}); + const mockAccounts: IAccount[] = [ { key: undefined!, @@ -83,60 +140,3 @@ function createPagedIterator(items: T[]): PagedAsyncIterableIterator { [Symbol.asyncIterator]: undefined!, }; } - -suite("Azure SQL client", function (): void { - let sandbox: sinon.SinonSandbox; - - setup(() => { - sandbox = sinon.createSandbox(); - }); - - teardown(() => { - sandbox.restore(); - }); - - test("Should return locations successfully", async function (): Promise { - const pages = createPagedIterator(mockLocations); - const subscriptions: Subscriptions = { - listLocations: sandbox.stub().returns(pages), - list: sandbox.stub().returns(undefined), - get: sandbox.stub().returns(undefined), - }; - const subscriptionClient = { - subscriptions, - } as unknown as SubscriptionClient; - const azureSqlClient = new AzureResourceController(() => subscriptionClient); - - const result = await azureSqlClient.getLocations(primarySession); - - expect(subscriptions.listLocations).to.have.been.calledOnceWithExactly( - primarySession.subscription?.subscriptionId, - ); - expect(result).to.have.lengthOf(mockLocations.length); - }); - - test("Should return resource groups successfully", async function (): Promise { - const pages = createPagedIterator(mockGroups); - const resourceGroups: ResourceGroups = { - list: sandbox.stub().returns(pages), - get: sandbox.stub().returns(undefined), - beginDelete: sandbox.stub(), - beginDeleteAndWait: sandbox.stub(), - beginExportTemplate: sandbox.stub(), - beginExportTemplateAndWait: sandbox.stub(), - checkExistence: sandbox.stub(), - createOrUpdate: sandbox.stub(), - update: sandbox.stub(), - }; - const resourceClient = { - resourceGroups, - } as unknown as ResourceManagementClient; - const azureSqlClient = new AzureResourceController(undefined, () => resourceClient); - - const result = await azureSqlClient.getResourceGroups(primarySession); - - expect(resourceGroups.list).to.have.been.calledOnceWithExactly(); - expect(result).to.have.lengthOf(mockGroups.length); - expect(result[0].location).to.equal(mockGroups[0].location); - }); -}); From e1cef64571e6a74122e09e07e3e4249139a3cb09 Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Wed, 15 Oct 2025 12:41:04 -0700 Subject: [PATCH 28/30] PR feedback --- test/unit/mssqlProtocolHandler.test.ts | 89 ++++++++++++++++++++-- test/unit/queryNotificationHandler.test.ts | 4 +- 2 files changed, 84 insertions(+), 9 deletions(-) diff --git a/test/unit/mssqlProtocolHandler.test.ts b/test/unit/mssqlProtocolHandler.test.ts index 2e782b937f..da0a1ba93d 100644 --- a/test/unit/mssqlProtocolHandler.test.ts +++ b/test/unit/mssqlProtocolHandler.test.ts @@ -32,6 +32,11 @@ suite("MssqlProtocolHandler Tests", () => { let openConnectionDialogStub: sinon.SinonStub; let connectProfileStub: sinon.SinonStub; + const testServer = "TestServer"; + const testDatabase = "TestDatabase"; + const tenantId = "11111"; + const accountId = `00000.${tenantId}`; + setup(() => { sandbox = sinon.createSandbox(); mockVscodeWrapper = sandbox.createStubInstance(VscodeWrapper); @@ -143,6 +148,58 @@ suite("MssqlProtocolHandler Tests", () => { expect(openConnectionDialogStub).to.not.have.been.called; }); + test("Should ignore partially-matching profile when additional specifiers do not match", async () => { + // Case 1: database specified but doesn't match + const params: Record = { + server: testServer, + database: testDatabase, + }; + + const mockConnectionManager = sandbox.createStubInstance(ConnectionManager); + + sandbox.stub(mockMainController, "connectionManager").get(() => { + return mockConnectionManager; + }); + + mockConnectionManager.findMatchingProfile.resolves({ + profile: { + server: testServer, // database not set + } as IConnectionProfile, + score: MatchScore.Server, + }); + + await mssqlProtocolHandler.handleUri( + Uri.parse( + `vscode://ms-mssql.mssql/connect?${new URLSearchParams(params).toString()}`, + ), + ); + + expect( + connectProfileStub, + "Should not have connected because database was specified, but not in best profile match", + ).to.not.have.been.called; + expect(openConnectionDialogStub).to.have.been.called; + + // Case 2: auth info specified but doesn't match + params["accountId"] = accountId; + + mockConnectionManager.findMatchingProfile.reset(); + mockConnectionManager.findMatchingProfile.resolves({ + profile: { + server: testServer, + database: testDatabase, + accountId: "00000.22222", // different accountId + } as IConnectionProfile, + score: MatchScore.ServerAndDatabase, + }); + + expect( + connectProfileStub, + "Should not have connected because accountId was specified, but didn't match with the best available profile", + ).to.not.have.been.called; + expect(openConnectionDialogStub).to.have.been.called; + }); + test("Should open connection dialog with populated parameters when no matching profile is found", async () => { const params: Record = { server: "myServer", @@ -245,14 +302,29 @@ suite("MssqlProtocolHandler Tests", () => { }); suite("readProfileFromArgs", () => { + test("Should include connection properties that aren't sourced from SQL Tools Service", async () => { + const profileName = "myProfile"; + + const connInfo = await mssqlProtocolHandler["readProfileFromArgs"]( + `server=${testServer}&database=${testDatabase}&accountId=${accountId}&tenantId=${tenantId}&profileName=${profileName}`, + ); + + expect(connInfo).to.be.an("object"); + expect(connInfo.server).to.equal(testServer); + expect(connInfo.database).to.equal(testDatabase); + expect(connInfo.profileName).to.equal(profileName); + expect(connInfo.accountId).to.equal(accountId); + expect(connInfo.tenantId).to.equal(tenantId); + }); + test("Should ignore invalid values for booleans and numbers", async () => { const connInfo = await mssqlProtocolHandler["readProfileFromArgs"]( - "server=myServer&database=dbName&trustServerCertificate=yes&connectTimeout=twenty", + `server=${testServer}&database=${testDatabase}&trustServerCertificate=yes&connectTimeout=twenty`, ); expect(connInfo).to.be.an("object"); - expect(connInfo.server).to.equal("myServer"); - expect(connInfo.database).to.equal("dbName"); + expect(connInfo.server).to.equal(testServer); + expect(connInfo.database).to.equal(testDatabase); expect( connInfo.trustServerCertificate, "trustServerCertificate should be false from an invalid value", @@ -264,17 +336,18 @@ suite("MssqlProtocolHandler Tests", () => { }); test("Should handle invalid parameter by ignoring it", async () => { + const madeUpParam = "madeUpParam"; const connInfo = await mssqlProtocolHandler["readProfileFromArgs"]( - "server=myServer&database=dbName&madeUpParam=great", + `server=${testServer}&database=${testDatabase}&${madeUpParam}=great`, ); expect(connInfo).to.be.an("object"); - expect(connInfo.server).to.equal("myServer"); - expect(connInfo.database).to.equal("dbName"); + expect(connInfo.server).to.equal(testServer); + expect(connInfo.database).to.equal(testDatabase); expect( // eslint-disable-next-line @typescript-eslint/no-explicit-any - (connInfo as any).madeUpParam, - "madeUpParam should be undefined from an invalid value", + (connInfo as any)[madeUpParam], + `${madeUpParam} should be undefined from an invalid value`, ).to.be.undefined; }); }); diff --git a/test/unit/queryNotificationHandler.test.ts b/test/unit/queryNotificationHandler.test.ts index acc24d9991..2040501198 100644 --- a/test/unit/queryNotificationHandler.test.ts +++ b/test/unit/queryNotificationHandler.test.ts @@ -20,11 +20,13 @@ suite("QueryNotificationHandler tests", () => { let runnerMock: sinon.SinonStubbedInstance; let runner: QueryRunner; + /* eslint-disable @typescript-eslint/no-explicit-any */ let batchStartHandler: NotificationHandler; let messageHandler: NotificationHandler; let resultSetCompleteHandler: NotificationHandler; let batchCompleteHandler: NotificationHandler; let queryCompleteHandler: NotificationHandler; + /* eslint-enable @typescript-eslint/no-explicit-any */ setup(() => { sandbox = sinon.createSandbox(); @@ -36,7 +38,7 @@ suite("QueryNotificationHandler tests", () => { runnerMock.setHasCompleted(); }); - runner = runnerMock as unknown as QueryRunner; + runner = runnerMock; batchStartHandler = notificationHandler.handleBatchStartNotification(); messageHandler = notificationHandler.handleMessageNotification(); From bea2ea565bbd32b700b0834977bcbac23f6a5fc1 Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Wed, 15 Oct 2025 12:44:25 -0700 Subject: [PATCH 29/30] cleanup --- test/unit/databaseObjectSearchService.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/databaseObjectSearchService.test.ts b/test/unit/databaseObjectSearchService.test.ts index b0d6f49538..feaa790765 100644 --- a/test/unit/databaseObjectSearchService.test.ts +++ b/test/unit/databaseObjectSearchService.test.ts @@ -21,7 +21,7 @@ suite("DatabaseObjectSearchService Tests", () => { setup(() => { sandbox = sinon.createSandbox(); client = sandbox.createStubInstance(SqlToolsServiceClient); - searchService = new DatabaseObjectSearchService(client as unknown as SqlToolsServiceClient); + searchService = new DatabaseObjectSearchService(client); }); teardown(() => { From 83c30427c24d21743494c6f38ca83d934462ef3f Mon Sep 17 00:00:00 2001 From: "Benjin Dubishar (from Dev Box)" Date: Wed, 15 Oct 2025 12:48:53 -0700 Subject: [PATCH 30/30] reverting root AGENTS.md --- AGENTS.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 47abc8c711..40f3994152 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1,13 @@ +# MSSQL Extension for Visual Studio Code + +The MSSQL Extension for Visual Studio Code is a TypeScript-based VS Code extension that provides database management capabilities for SQL Server, Azure SQL, and SQL Database in Fabric. The extension includes React-based webview components, AI-powered features with GitHub Copilot integration, and comprehensive SQL development tools. + +**Always reference these instructions first** and fallback to search or bash commands only when you encounter unexpected information that does not match the info here. + +## Working Effectively + +### Bootstrap, Build, and Test the Repository + **NEVER CANCEL** any build or test commands. These operations can take significant time and should be allowed to complete. #### Initial Setup (Required once) @@ -16,6 +26,11 @@ yarn install ```bash # Full build - takes ~19 seconds. NEVER CANCEL. Set timeout to 60+ seconds. yarn build + +# Development watch mode (for active development) +yarn watch +# This runs continuous compilation and bundling. Leave running during development. +# Includes: extension TypeScript, webview React/TypeScript, and asset bundling. ``` #### Individual Build Steps (if needed) @@ -28,13 +43,24 @@ yarn build:prepare yarn build:extension # Bundle extension (~1 second) +yarn build:extension-bundle + +# Compile React webviews (~8 seconds) +yarn build:webviews + +# Bundle webviews (~2 seconds) +yarn build:webviews-bundle ``` ### Linting and Code Quality -````bash +```bash # Lint source files only - takes ~1.5 seconds yarn lint src/ test/ + +# DO NOT run 'yarn lint' without arguments - it will fail trying to lint build output +``` + ### Testing #### Unit Tests @@ -42,7 +68,14 @@ yarn lint src/ test/ ```bash # Unit tests require VS Code download and cannot run in sandboxed environments # This is expected behavior - tests work in CI with proper VS Code setup -```` +yarn test +# Expected to fail with "ENOTFOUND update.code.visualstudio.com" in sandboxed environments + +# Run targeted unit tests using grep patterns +yarn test --grep "ConnectionManager" # Run tests matching "ConnectionManager" +yarn test --pattern ".*service.*" # Run tests matching service pattern +yarn test --testPattern "QueryRunner" # Alternative syntax for test filtering +``` #### E2E Tests (Smoke Tests) @@ -54,9 +87,17 @@ yarn smoketest ### Packaging -````bash +```bash # Install vsce globally (if not already installed) npm install -g vsce + +# Package extension - takes ~4.5 seconds. NEVER CANCEL. Set timeout to 60+ seconds. +yarn package --online # Creates ~12MB VSIX file for online distribution +yarn package --offline # Creates platform-specific packages with embedded services +``` + +## Validation Scenarios + **Always test the following scenarios after making changes:** ### Complete Build Validation @@ -79,7 +120,9 @@ npm install -g vsce ```bash # Always run these commands before committing changes: yarn build # Ensure code compiles -```` +yarn lint src/ test/ # Ensure code meets style standards +yarn package --online # Ensure extension can be packaged +``` ### Code Quality Validation