diff --git a/extensions/mssql/src/publishProject/projectUtils.ts b/extensions/mssql/src/publishProject/projectUtils.ts index 16e7dc4cc9..76c39ee8ed 100644 --- a/extensions/mssql/src/publishProject/projectUtils.ts +++ b/extensions/mssql/src/publishProject/projectUtils.ts @@ -140,9 +140,12 @@ export async function readProjectProperties( // Calculate DACPAC output path const projectDir = path.dirname(projectFilePath); const projectName = path.basename(projectFilePath, path.extname(projectFilePath)); - const outputPath = path.isAbsolute(result.outputPath) - ? result.outputPath - : path.join(projectDir, result.outputPath); + // Normalize path separators for cross-platform compatibility + // path.normalize doesn't convert backslashes, so using a regex replace here + const normalizedOutputPath = result.outputPath.replace(/\\/g, "/"); + const outputPath = path.isAbsolute(normalizedOutputPath) + ? normalizedOutputPath + : path.join(projectDir, normalizedOutputPath); const dacpacOutputPath = path.join( outputPath, `${projectName}${constants.DacpacExtension}`, diff --git a/extensions/mssql/test/unit/projectUtils.test.ts b/extensions/mssql/test/unit/projectUtils.test.ts new file mode 100644 index 0000000000..ed7d1b48d7 --- /dev/null +++ b/extensions/mssql/test/unit/projectUtils.test.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * 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 path from "path"; +import { readProjectProperties } from "../../src/publishProject/projectUtils"; +import { SqlProjectsService } from "../../src/services/sqlProjectsService"; + +suite("projectUtils Tests", () => { + let sandbox: sinon.SinonSandbox; + let mockSqlProjectsService: sinon.SinonStubbedInstance; + + setup(() => { + sandbox = sinon.createSandbox(); + mockSqlProjectsService = sandbox.createStubInstance(SqlProjectsService); + }); + + teardown(() => { + sandbox.restore(); + }); + + test("readProjectProperties normalizes Windows backslashes in outputPath on Unix", async () => { + const projectPath = "/home/user/project/TestProject.sqlproj"; + + // Mock getProjectProperties to return Windows-style path with backslashes + mockSqlProjectsService.getProjectProperties.resolves({ + success: true, + outputPath: "bin\\Debug", // Windows-style path with backslashes + databaseSchemaProvider: "Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider", + } as any); + + const result = await readProjectProperties(mockSqlProjectsService, projectPath); + + expect(result).to.exist; + expect(result?.dacpacOutputPath).to.exist; + + // The dacpac path should use forward slashes, not backslashes + expect(result?.dacpacOutputPath).to.not.include("\\"); + expect(result?.dacpacOutputPath).to.include("/"); + + // Verify the path is constructed correctly with forward slashes + const expectedPath = path.join("/home/user/project", "bin/Debug", "TestProject.dacpac"); + expect(result?.dacpacOutputPath).to.equal(expectedPath); + }); + + test("readProjectProperties handles absolute paths", async () => { + const projectPath = "/home/user/project/TestProject.sqlproj"; + const absoluteOutputPath = "/absolute/output/path"; + + mockSqlProjectsService.getProjectProperties.resolves({ + success: true, + outputPath: absoluteOutputPath, + databaseSchemaProvider: "Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider", + } as any); + + const result = await readProjectProperties(mockSqlProjectsService, projectPath); + + expect(result).to.exist; + expect(result?.dacpacOutputPath).to.exist; + + // For absolute paths, should use the absolute path directly + const expectedPath = path.join(absoluteOutputPath, "TestProject.dacpac"); + expect(result?.dacpacOutputPath).to.equal(expectedPath); + }); + + test("readProjectProperties normalizes Windows backslashes in absolute paths", async () => { + const projectPath = "/home/user/project/TestProject.sqlproj"; + // Absolute path with Windows-style backslashes (edge case but should be handled) + const absoluteOutputPath = "C:\\absolute\\output\\path"; + + mockSqlProjectsService.getProjectProperties.resolves({ + success: true, + outputPath: absoluteOutputPath, + databaseSchemaProvider: "Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider", + } as any); + + const result = await readProjectProperties(mockSqlProjectsService, projectPath); + + expect(result).to.exist; + expect(result?.dacpacOutputPath).to.exist; + + // Even for absolute paths, backslashes should be normalized + expect(result?.dacpacOutputPath).to.not.include("\\"); + }); + + test("readProjectProperties handles relative paths with forward slashes", async () => { + const projectPath = "/home/user/project/TestProject.sqlproj"; + + mockSqlProjectsService.getProjectProperties.resolves({ + success: true, + outputPath: "bin/Debug", // Unix-style path + databaseSchemaProvider: "Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider", + } as any); + + const result = await readProjectProperties(mockSqlProjectsService, projectPath); + + expect(result).to.exist; + expect(result?.dacpacOutputPath).to.exist; + + const expectedPath = path.join("/home/user/project", "bin/Debug", "TestProject.dacpac"); + expect(result?.dacpacOutputPath).to.equal(expectedPath); + }); +});