diff --git a/src/Microsoft.SqlTools.Credentials/Microsoft.SqlTools.Credentials.csproj b/src/Microsoft.SqlTools.Credentials/Microsoft.SqlTools.Credentials.csproj index 0e34726e..803a0086 100644 --- a/src/Microsoft.SqlTools.Credentials/Microsoft.SqlTools.Credentials.csproj +++ b/src/Microsoft.SqlTools.Credentials/Microsoft.SqlTools.Credentials.csproj @@ -3,7 +3,7 @@ netcoreapp2.2 MicrosoftSqlToolsCredentials - Exe + Exe false false false @@ -16,21 +16,24 @@ win7-x64;win7-x86;ubuntu.14.04-x64;ubuntu.16.04-x64;centos.7-x64;rhel.7.2-x64;debian.8-x64;fedora.23-x64;opensuse.13.2-x64;osx.10.11-x64;linux-x64 - + - + + + - + + - + - + diff --git a/src/Microsoft.SqlTools.ServiceLayer/DacFx/Contracts/DacFxRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/DacFx/Contracts/DacFxRequest.cs new file mode 100644 index 00000000..f5a1d56e --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/DacFx/Contracts/DacFxRequest.cs @@ -0,0 +1,44 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +using Microsoft.SqlTools.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.TaskServices; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.DacFx.Contracts +{ + /// + /// Parameters for a DacFx request. + /// + public abstract class DacFxParams : IScriptableRequestParams + { + /// + /// Gets or sets package filepath + /// + public string PackageFilePath { get; set; } + + /// + /// Gets or sets name for database + /// + public string DatabaseName { get; set; } + + /// + /// Connection uri + /// + public string OwnerUri { get; set; } + + /// + /// Executation mode for the operation. Default is execution + /// + public TaskExecutionMode TaskExecutionMode { get; set; } + } + + /// + /// Parameters returned from a DacFx request. + /// + public class DacFxResult : ResultStatus + { + public string OperationId { get; set; } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/DacFx/Contracts/DeployRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/DacFx/Contracts/DeployRequest.cs new file mode 100644 index 00000000..8ec1a529 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/DacFx/Contracts/DeployRequest.cs @@ -0,0 +1,30 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +using Microsoft.SqlTools.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.TaskServices; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.DacFx.Contracts +{ + /// + /// Parameters for a DacFx deploy request. + /// + public class DeployParams : DacFxParams + { + /// + /// Gets or sets if upgrading existing database + /// + public bool UpgradeExisting { get; set; } + } + + /// + /// Defines the DacFx deploy request type + /// + class DeployRequest + { + public static readonly RequestType Type = + RequestType.Create("dacfx/deploy"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/DacFx/Contracts/ExportRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/DacFx/Contracts/ExportRequest.cs new file mode 100644 index 00000000..c78e0149 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/DacFx/Contracts/ExportRequest.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +using Microsoft.SqlTools.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.TaskServices; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.DacFx.Contracts +{ + /// + /// Parameters for a DacFx export request. + /// + public class ExportParams : DacFxParams + { + } + + /// + /// Defines the DacFx export request type + /// + class ExportRequest + { + public static readonly RequestType Type = + RequestType.Create("dacfx/export"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/DacFx/Contracts/ExtractRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/DacFx/Contracts/ExtractRequest.cs new file mode 100644 index 00000000..6f904d17 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/DacFx/Contracts/ExtractRequest.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +using Microsoft.SqlTools.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.TaskServices; +using Microsoft.SqlTools.ServiceLayer.Utility; +using System; + +namespace Microsoft.SqlTools.ServiceLayer.DacFx.Contracts +{ + /// + /// Parameters for a DacFx extract request. + /// + public class ExtractParams : DacFxParams + { + /// + /// Gets or sets the string identifier for the DAC application + /// + public string ApplicationName { get; set; } + + /// + /// Gets or sets the version of the DAC application + /// + public Version ApplicationVersion { get; set; } + } + + /// + /// Defines the DacFx extract request type + /// + class ExtractRequest + { + public static readonly RequestType Type = + RequestType.Create("dacfx/extract"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/DacFx/Contracts/ImportRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/DacFx/Contracts/ImportRequest.cs new file mode 100644 index 00000000..69c83f69 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/DacFx/Contracts/ImportRequest.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +using Microsoft.SqlTools.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.TaskServices; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.DacFx.Contracts +{ + /// + /// Parameters for a DacFx import request. + /// + public class ImportParams : DacFxParams + { + } + + + /// + /// Defines the DacFx import request type + /// + class ImportRequest + { + public static readonly RequestType Type = + RequestType.Create("dacfx/import"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/DacFx/DacFxOperation.cs b/src/Microsoft.SqlTools.ServiceLayer/DacFx/DacFxOperation.cs new file mode 100644 index 00000000..151760f1 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/DacFx/DacFxOperation.cs @@ -0,0 +1,93 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +using Microsoft.SqlServer.Dac; +using Microsoft.SqlTools.ServiceLayer.TaskServices; +using Microsoft.SqlTools.Utility; +using System; +using System.Data.SqlClient; +using System.Diagnostics; +using System.Threading; + +namespace Microsoft.SqlTools.ServiceLayer.DacFx +{ + /// + /// Base class for DacFx operations + /// + abstract class DacFxOperation : ITaskOperation + { + private CancellationTokenSource cancellation = new CancellationTokenSource(); + private bool disposed = false; + + /// + /// Gets the unique id associated with this instance. + /// + public string OperationId { get; private set; } + + public SqlTask SqlTask { get; set; } + + protected SqlConnection SqlConnection { get; private set; } + + protected DacServices DacServices { get; private set; } + + protected DacFxOperation(SqlConnection sqlConnection) + { + Validate.IsNotNull("sqlConnection", sqlConnection); + this.SqlConnection = sqlConnection; + this.OperationId = Guid.NewGuid().ToString(); + } + + protected CancellationToken CancellationToken { get { return this.cancellation.Token; } } + + /// + /// The error occurred during operation + /// + public string ErrorMessage { get; } + + /// + /// Cancel operation + /// + public void Cancel() + { + if (!this.cancellation.IsCancellationRequested) + { + Logger.Write(TraceEventType.Verbose, string.Format("Cancel invoked for OperationId {0}", this.OperationId)); + this.cancellation.Cancel(); + } + } + + /// + /// Disposes the operation. + /// + public void Dispose() + { + if (!disposed) + { + this.Cancel(); + disposed = true; + } + } + + public void Execute(TaskExecutionMode mode) + { + if (this.CancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException(this.CancellationToken); + } + + try + { + this.DacServices = new DacServices(this.SqlConnection.ConnectionString); + Execute(); + } + catch (Exception e) + { + Logger.Write(TraceEventType.Error, string.Format("DacFx import operation {0} failed with exception {1}", this.OperationId, e)); + throw; + } + } + + public abstract void Execute(); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/DacFx/DacFxService.cs b/src/Microsoft.SqlTools.ServiceLayer/DacFx/DacFxService.cs new file mode 100644 index 00000000..5e7609b8 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/DacFx/DacFxService.cs @@ -0,0 +1,215 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.DacFx.Contracts; +using Microsoft.SqlTools.ServiceLayer.Hosting; +using Microsoft.SqlTools.ServiceLayer.TaskServices; +using System; +using System.Collections.Concurrent; +using System.Data.SqlClient; +using System.Threading.Tasks; + +namespace Microsoft.SqlTools.ServiceLayer.DacFx +{ + /// + /// Main class for DacFx service + /// + class DacFxService + { + private static ConnectionService connectionService = null; + private SqlTaskManager sqlTaskManagerInstance = null; + private static readonly Lazy instance = new Lazy(() => new DacFxService()); + private readonly Lazy> operations = + new Lazy>(() => new ConcurrentDictionary()); + + /// + /// Gets the singleton instance object + /// + public static DacFxService Instance + { + get { return instance.Value; } + } + + /// + /// Initializes the service instance + /// + /// + public void InitializeService(ServiceHost serviceHost) + { + serviceHost.SetRequestHandler(ExportRequest.Type, this.HandleExportRequest); + serviceHost.SetRequestHandler(ImportRequest.Type, this.HandleImportRequest); + serviceHost.SetRequestHandler(ExtractRequest.Type, this.HandleExtractRequest); + serviceHost.SetRequestHandler(DeployRequest.Type, this.HandleDeployRequest); + } + + /// + /// The collection of active operations + /// + internal ConcurrentDictionary ActiveOperations => operations.Value; + + /// + /// Handles request to export a bacpac + /// + /// + public async Task HandleExportRequest(ExportParams parameters, RequestContext requestContext) + { + try + { + ConnectionInfo connInfo; + ConnectionServiceInstance.TryFindConnection( + parameters.OwnerUri, + out connInfo); + if (connInfo != null) + { + SqlConnection sqlConn = ConnectionService.OpenSqlConnection(connInfo, "Export"); + ExportOperation operation = new ExportOperation(parameters, sqlConn); + await ExecuteOperation(operation, parameters, "Export bacpac", requestContext); + } + } + catch (Exception e) + { + await requestContext.SendError(e); + } + } + + /// + /// Handles request to import a bacpac + /// + /// + public async Task HandleImportRequest(ImportParams parameters, RequestContext requestContext) + { + try + { + ConnectionInfo connInfo; + ConnectionServiceInstance.TryFindConnection( + parameters.OwnerUri, + out connInfo); + if (connInfo != null) + { + SqlConnection sqlConn = ConnectionService.OpenSqlConnection(connInfo, "Import"); + ImportOperation operation = new ImportOperation(parameters, sqlConn); + await ExecuteOperation(operation, parameters, "Import bacpac", requestContext); + } + } + catch (Exception e) + { + await requestContext.SendError(e); + } + } + + /// + /// Handles request to extract a dacpac + /// + /// + public async Task HandleExtractRequest(ExtractParams parameters, RequestContext requestContext) + { + try + { + ConnectionInfo connInfo; + ConnectionServiceInstance.TryFindConnection( + parameters.OwnerUri, + out connInfo); + if (connInfo != null) + { + SqlConnection sqlConn = ConnectionService.OpenSqlConnection(connInfo, "Extract"); + ExtractOperation operation = new ExtractOperation(parameters, sqlConn); + await ExecuteOperation(operation, parameters, "Extract dacpac", requestContext); + } + } + catch (Exception e) + { + await requestContext.SendError(e); + } + } + + /// + /// Handles request to deploy a dacpac + /// + /// + public async Task HandleDeployRequest(DeployParams parameters, RequestContext requestContext) + { + try + { + ConnectionInfo connInfo; + ConnectionServiceInstance.TryFindConnection( + parameters.OwnerUri, + out connInfo); + if (connInfo != null) + { + SqlConnection sqlConn = ConnectionService.OpenSqlConnection(connInfo, "Deploy"); + DeployOperation operation = new DeployOperation(parameters, sqlConn); + await ExecuteOperation(operation, parameters, "Deploy dacpac", requestContext); + } + } + catch (Exception e) + { + await requestContext.SendError(e); + } + } + + private async Task ExecuteOperation(DacFxOperation operation, DacFxParams parameters, string taskName, RequestContext requestContext) + { + SqlTask sqlTask = null; + TaskMetadata metadata = TaskMetadata.Create(parameters, taskName, operation, ConnectionServiceInstance); + + // put appropriate database name since connection passed was to master + metadata.DatabaseName = parameters.DatabaseName; + + sqlTask = SqlTaskManagerInstance.CreateAndRun(metadata); + + await requestContext.SendResult(new DacFxResult() + { + OperationId = operation.OperationId, + Success = true, + ErrorMessage = "" + }); + } + + private SqlTaskManager SqlTaskManagerInstance + { + get + { + if (sqlTaskManagerInstance == null) + { + sqlTaskManagerInstance = SqlTaskManager.Instance; + } + return sqlTaskManagerInstance; + } + set + { + sqlTaskManagerInstance = value; + } + } + + /// + /// Internal for testing purposes only + /// + internal static ConnectionService ConnectionServiceInstance + { + get + { + if (connectionService == null) + { + connectionService = ConnectionService.Instance; + } + return connectionService; + } + set + { + connectionService = value; + } + } + + /// + /// For testing purpose only + /// + /// + internal void PerformOperation(DacFxOperation operation) + { + operation.Execute(TaskExecutionMode.Execute); + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/DacFx/DeployOperation.cs b/src/Microsoft.SqlTools.ServiceLayer/DacFx/DeployOperation.cs new file mode 100644 index 00000000..b636adcc --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/DacFx/DeployOperation.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +using Microsoft.SqlServer.Dac; +using Microsoft.SqlTools.ServiceLayer.DacFx.Contracts; +using Microsoft.SqlTools.ServiceLayer.TaskServices; +using Microsoft.SqlTools.Utility; +using System; +using System.Data.SqlClient; +using System.Diagnostics; + +namespace Microsoft.SqlTools.ServiceLayer.DacFx +{ + /// + /// Class to represent an in-progress deploy operation + /// + class DeployOperation : DacFxOperation + { + public DeployParams Parameters { get; } + + public DeployOperation(DeployParams parameters, SqlConnection sqlConnection) : base(sqlConnection) + { + Validate.IsNotNull("parameters", parameters); + this.Parameters = parameters; + } + + public override void Execute() + { + DacPackage dacpac = DacPackage.Load(this.Parameters.PackageFilePath); + this.DacServices.Deploy(dacpac, this.Parameters.DatabaseName, this.Parameters.UpgradeExisting, null, this.CancellationToken); + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/DacFx/ExportOperation.cs b/src/Microsoft.SqlTools.ServiceLayer/DacFx/ExportOperation.cs new file mode 100644 index 00000000..6cda9068 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/DacFx/ExportOperation.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +using Microsoft.SqlServer.Dac; +using Microsoft.SqlTools.ServiceLayer.DacFx.Contracts; +using Microsoft.SqlTools.ServiceLayer.TaskServices; +using Microsoft.SqlTools.Utility; +using System; +using System.Data.SqlClient; +using System.Diagnostics; + +namespace Microsoft.SqlTools.ServiceLayer.DacFx +{ + /// + /// Class to represent an in-progress export operation + /// + class ExportOperation : DacFxOperation + { + public ExportParams Parameters { get; } + + public ExportOperation(ExportParams parameters, SqlConnection sqlConnection): base(sqlConnection) + { + Validate.IsNotNull("parameters", parameters); + this.Parameters = parameters; + } + + public override void Execute() + { + this.DacServices.ExportBacpac(this.Parameters.PackageFilePath, this.Parameters.DatabaseName, null, this.CancellationToken); + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/DacFx/ExtractOperation.cs b/src/Microsoft.SqlTools.ServiceLayer/DacFx/ExtractOperation.cs new file mode 100644 index 00000000..09b6b130 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/DacFx/ExtractOperation.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +using Microsoft.SqlServer.Dac; +using Microsoft.SqlTools.ServiceLayer.DacFx.Contracts; +using Microsoft.SqlTools.ServiceLayer.TaskServices; +using Microsoft.SqlTools.Utility; +using System; +using System.Data.SqlClient; +using System.Diagnostics; + +namespace Microsoft.SqlTools.ServiceLayer.DacFx +{ + /// + /// Class to represent an in-progress extract operation + /// + class ExtractOperation : DacFxOperation + { + public ExtractParams Parameters { get; } + + public ExtractOperation(ExtractParams parameters, SqlConnection sqlConnection): base(sqlConnection) + { + Validate.IsNotNull("parameters", parameters); + this.Parameters = parameters; + } + + public override void Execute() + { + this.DacServices.Extract(this.Parameters.PackageFilePath, this.Parameters.DatabaseName, this.Parameters.ApplicationName, this.Parameters.ApplicationVersion, null, null, null, this.CancellationToken); + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/DacFx/ImportOperation.cs b/src/Microsoft.SqlTools.ServiceLayer/DacFx/ImportOperation.cs new file mode 100644 index 00000000..fbc51347 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/DacFx/ImportOperation.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +using Microsoft.SqlServer.Dac; +using Microsoft.SqlTools.ServiceLayer.DacFx.Contracts; +using Microsoft.SqlTools.ServiceLayer.TaskServices; +using Microsoft.SqlTools.Utility; +using System; +using System.Data.SqlClient; +using System.Diagnostics; + +namespace Microsoft.SqlTools.ServiceLayer.DacFx +{ + /// + /// Class to represent an in-progress import operation + /// + class ImportOperation : DacFxOperation + { + public ImportParams Parameters { get; } + + public ImportOperation(ImportParams parameters, SqlConnection sqlConnection): base(sqlConnection) + { + Validate.IsNotNull("parameters", parameters); + this.Parameters = parameters; + } + + public override void Execute() + { + BacPackage bacpac = BacPackage.Load(this.Parameters.PackageFilePath); + this.DacServices.ImportBacpac(bacpac, this.Parameters.DatabaseName, this.CancellationToken); + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs b/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs index 790574ea..d9a12994 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs @@ -11,6 +11,7 @@ using Microsoft.SqlTools.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.Admin; using Microsoft.SqlTools.ServiceLayer.Agent; using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.DacFx; using Microsoft.SqlTools.ServiceLayer.DisasterRecovery; using Microsoft.SqlTools.ServiceLayer.EditData; using Microsoft.SqlTools.ServiceLayer.FileBrowser; @@ -110,6 +111,9 @@ namespace Microsoft.SqlTools.ServiceLayer SecurityService.Instance.InitializeService(serviceHost); serviceProvider.RegisterSingleService(SecurityService.Instance); + DacFxService.Instance.InitializeService(serviceHost); + serviceProvider.RegisterSingleService(DacFxService.Instance); + InitializeHostedServices(serviceProvider, serviceHost); serviceHost.ServiceProvider = serviceProvider; diff --git a/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj b/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj index 4d8484cd..7ea2aae8 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj +++ b/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj @@ -16,13 +16,14 @@ win7-x64;win7-x86;ubuntu.14.04-x64;ubuntu.16.04-x64;centos.7-x64;rhel.7.2-x64;debian.8-x64;fedora.23-x64;opensuse.13.2-x64;osx.10.11-x64;linux-x64 - - true - + + true + + diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DacFx/DacFxserviceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DacFx/DacFxserviceTests.cs new file mode 100644 index 00000000..612bda07 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DacFx/DacFxserviceTests.cs @@ -0,0 +1,276 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.DacFx; +using Microsoft.SqlTools.ServiceLayer.DacFx.Contracts; +using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; +using Microsoft.SqlTools.ServiceLayer.Test.Common; +using Moq; +using System; +using System.Data.SqlClient; +using System.IO; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.DacFx +{ + public class DacFxServiceTests + { + + private LiveConnectionHelper.TestConnectionResult GetLiveAutoCompleteTestObjects() + { + var result = LiveConnectionHelper.InitLiveConnectionInfo(); + return result; + } + + private async Task>> SendAndValidateExportRequest() + { + var result = GetLiveAutoCompleteTestObjects(); + var requestContext = new Mock>(); + requestContext.Setup(x => x.SendResult(It.IsAny())).Returns(Task.FromResult(new object())); + + SqlTestDb testdb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, null, "DacFxExportTest"); + string folderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "DacFxTest"); + Directory.CreateDirectory(folderPath); + + var exportParams = new ExportParams + { + DatabaseName = testdb.DatabaseName, + PackageFilePath = Path.Combine(folderPath, string.Format("{0}.bacpac", testdb.DatabaseName)) + }; + + SqlConnection sqlConn = ConnectionService.OpenSqlConnection(result.ConnectionInfo, "Export"); + DacFxService service = new DacFxService(); + ExportOperation operation = new ExportOperation(exportParams, sqlConn); + service.PerformOperation(operation); + + // cleanup + VerifyAndCleanup(exportParams.PackageFilePath); + testdb.Cleanup(); + + return requestContext; + } + + private async Task>> SendAndValidateImportRequest() + { + // first export a bacpac + var result = GetLiveAutoCompleteTestObjects(); + var exportRequestContext = new Mock>(); + exportRequestContext.Setup(x => x.SendResult(It.IsAny())).Returns(Task.FromResult(new object())); + + SqlTestDb sourceDb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, null, "DacFxImportTest"); + string folderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "DacFxTest"); + Directory.CreateDirectory(folderPath); + + var exportParams = new ExportParams + { + DatabaseName = sourceDb.DatabaseName, + PackageFilePath = Path.Combine(folderPath, string.Format("{0}.bacpac", sourceDb.DatabaseName)) + }; + + SqlConnection sqlConn = ConnectionService.OpenSqlConnection(result.ConnectionInfo, "Import"); + DacFxService service = new DacFxService(); + ExportOperation exportOperation = new ExportOperation(exportParams, sqlConn); + service.PerformOperation(exportOperation); + + // import the created bacpac + var importRequestContext = new Mock>(); + importRequestContext.Setup(x => x.SendResult(It.IsAny())).Returns(Task.FromResult(new object())); + + var importParams = new ImportParams + { + PackageFilePath = exportParams.PackageFilePath, + DatabaseName = string.Concat(sourceDb.DatabaseName, "-imported") + }; + + ImportOperation importOperation = new ImportOperation(importParams, sqlConn); + service.PerformOperation(importOperation); + SqlTestDb targetDb = SqlTestDb.CreateFromExisting(importParams.DatabaseName); + + // cleanup + VerifyAndCleanup(exportParams.PackageFilePath); + sourceDb.Cleanup(); + targetDb.Cleanup(); + + return importRequestContext; + } + + private async Task>> SendAndValidateExtractRequest() + { + var result = GetLiveAutoCompleteTestObjects(); + var requestContext = new Mock>(); + requestContext.Setup(x => x.SendResult(It.IsAny())).Returns(Task.FromResult(new object())); + + SqlTestDb testdb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, null, "DacFxExtractTest"); + string folderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "DacFxTest"); + Directory.CreateDirectory(folderPath); + + var extractParams = new ExtractParams + { + DatabaseName = testdb.DatabaseName, + PackageFilePath = Path.Combine(folderPath, string.Format("{0}.dacpac", testdb.DatabaseName)), + ApplicationName = "test", + ApplicationVersion = new Version(1, 0) + }; + + SqlConnection sqlConn = ConnectionService.OpenSqlConnection(result.ConnectionInfo, "Extract"); + DacFxService service = new DacFxService(); + ExtractOperation operation = new ExtractOperation(extractParams, sqlConn); + service.PerformOperation(operation); + + // cleanup + VerifyAndCleanup(extractParams.PackageFilePath); + testdb.Cleanup(); + + return requestContext; + } + + private async Task>> SendAndValidateDeployRequest() + { + // first extract a db to have a dacpac to import later + var result = GetLiveAutoCompleteTestObjects(); + var extractRequestContext = new Mock>(); + extractRequestContext.Setup(x => x.SendResult(It.IsAny())).Returns(Task.FromResult(new object())); + + SqlTestDb sourceDb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, null, "DacFxDeployTest"); + string folderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "DacFxTest"); + Directory.CreateDirectory(folderPath); + + var extractParams = new ExtractParams + { + DatabaseName = sourceDb.DatabaseName, + PackageFilePath = Path.Combine(folderPath, string.Format("{0}.dacpac", sourceDb.DatabaseName)), + ApplicationName = "test", + ApplicationVersion = new Version(1, 0) + }; + + SqlConnection sqlConn = ConnectionService.OpenSqlConnection(result.ConnectionInfo, "Deploy"); + DacFxService service = new DacFxService(); + ExtractOperation extractOperation = new ExtractOperation(extractParams, sqlConn); + service.PerformOperation(extractOperation); + + // deploy the created dacpac + var deployRequestContext = new Mock>(); + deployRequestContext.Setup(x => x.SendResult(It.IsAny())).Returns(Task.FromResult(new object())); + + + var deployParams = new DeployParams + { + PackageFilePath = extractParams.PackageFilePath, + DatabaseName = string.Concat(sourceDb.DatabaseName, "-deployed"), + UpgradeExisting = false + }; + + DeployOperation deployOperation = new DeployOperation(deployParams, sqlConn); + service.PerformOperation(deployOperation); SqlTestDb targetDb = SqlTestDb.CreateFromExisting(deployParams.DatabaseName); + + // cleanup + VerifyAndCleanup(extractParams.PackageFilePath); + sourceDb.Cleanup(); + targetDb.Cleanup(); + + return deployRequestContext; + } + + private async Task>> ValidateExportCancellation() + { + var result = GetLiveAutoCompleteTestObjects(); + var requestContext = new Mock>(); + requestContext.Setup(x => x.SendResult(It.IsAny())).Returns(Task.FromResult(new object())); + + SqlTestDb testdb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, null, "DacFxExportTest"); + string folderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "DacFxTest"); + Directory.CreateDirectory(folderPath); + + var exportParams = new ExportParams + { + DatabaseName = testdb.DatabaseName, + PackageFilePath = Path.Combine(folderPath, string.Format("{0}.bacpac", testdb.DatabaseName)) + }; + + SqlConnection sqlConn = ConnectionService.OpenSqlConnection(result.ConnectionInfo, "Export"); + DacFxService service = new DacFxService(); + ExportOperation operation = new ExportOperation(exportParams, sqlConn); + + // set cancellation token to cancel + operation.Cancel(); + OperationCanceledException expectedException = null; + + try + { + service.PerformOperation(operation); + } + catch(OperationCanceledException canceledException) + { + expectedException = canceledException; + } + + Assert.NotNull(expectedException); + + // cleanup + testdb.Cleanup(); + + return requestContext; + } + + /// + /// Verify the export bacpac request + /// + [Fact] + public async void ExportBacpac() + { + Assert.NotNull(await SendAndValidateExportRequest()); + } + + /// + /// Verify the export request being cancelled + /// + [Fact] + public async void ExportBacpacCancellationTest() + { + Assert.NotNull(await ValidateExportCancellation()); + } + + /// + /// Verify the import bacpac request + /// + [Fact] + public async void ImportBacpac() + { + Assert.NotNull(await SendAndValidateImportRequest()); + } + + /// + /// Verify the extract dacpac request + /// + [Fact] + public async void ExtractDacpac() + { + Assert.NotNull(await SendAndValidateExtractRequest()); + } + + /// + /// Verify the deploy dacpac request + /// + [Fact] + public async void DeployDacpac() + { + Assert.NotNull(await SendAndValidateDeployRequest()); + } + + private void VerifyAndCleanup(string filePath) + { + // Verify it was created + Assert.True(File.Exists(filePath)); + + // Remove the file + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Microsoft.SqlTools.ServiceLayer.IntegrationTests.csproj b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Microsoft.SqlTools.ServiceLayer.IntegrationTests.csproj index b1b1f5de..23877549 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Microsoft.SqlTools.ServiceLayer.IntegrationTests.csproj +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Microsoft.SqlTools.ServiceLayer.IntegrationTests.csproj @@ -35,6 +35,7 @@ + diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test.Common/SqlTestDb.cs b/test/Microsoft.SqlTools.ServiceLayer.Test.Common/SqlTestDb.cs index bdc0796f..40d0cc53 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test.Common/SqlTestDb.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test.Common/SqlTestDb.cs @@ -110,6 +110,27 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.Common return CreateNew(serverType, false, null, null); } + /// + /// Represents a test Database that was created in a test + /// + public static SqlTestDb CreateFromExisting( + string dbName, + TestServerType serverType = TestServerType.OnPrem, + bool doNotCleanupDb = false) + { + SqlTestDb testDb = new SqlTestDb(); + + if (string.IsNullOrEmpty(dbName)) + { + throw new ArgumentOutOfRangeException("dbName"); + } + + testDb.DatabaseName = dbName; + testDb.ServerType = serverType; + + return testDb; + } + /// /// Returns a mangled name that unique based on Prefix + Machine + Process ///