diff --git a/nuget.config b/nuget.config index a839b559..933ad9ee 100644 --- a/nuget.config +++ b/nuget.config @@ -9,7 +9,6 @@ - diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs index 83b860a8..8f430e29 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs @@ -38,17 +38,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection public ConnectionDetails ConnectionDetails { get; private set; } - public DbConnection SqlConnection { get; private set; } - - public void OpenConnection() - { - // build the connection string from the input parameters - string connectionString = ConnectionService.BuildConnectionString(ConnectionDetails); - - // create a sql connection instance - SqlConnection = Factory.CreateSqlConnection(connectionString); - SqlConnection.Open(); - } + public DbConnection SqlConnection { get; set; } } /// @@ -93,11 +83,21 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection /// public delegate Task OnConnectionHandler(ConnectionInfo info); + /// + // Callback for ondisconnect handler + /// + public delegate Task OnDisconnectHandler(ConnectionSummary summary); + /// /// List of onconnection handlers /// private readonly List onConnectionActivities = new List(); + /// + /// List of ondisconnect handlers + /// + private readonly List onDisconnectActivities = new List(); + /// /// Gets the SQL connection factory instance /// @@ -127,16 +127,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection { return this.ownerToConnectionMap.TryGetValue(ownerUri, out connectionInfo); } - - private static ConnectionSummary CopySummary(ConnectionSummary summary) - { - return new ConnectionSummary() - { - ServerName = summary.ServerName, - DatabaseName = summary.DatabaseName, - UserName = summary.UserName - }; - } /// /// Open a connection with the specified connection details @@ -153,10 +143,16 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection }; } + // Resolve if it is an existing connection + // Disconnect active connection if the URI is already connected ConnectionInfo connectionInfo; if (ownerToConnectionMap.TryGetValue(connectionParams.OwnerUri, out connectionInfo) ) { - // TODO disconnect + var disconnectParams = new DisconnectParams() + { + OwnerUri = connectionParams.OwnerUri + }; + Disconnect(disconnectParams); } connectionInfo = new ConnectionInfo(ConnectionFactory, connectionParams.OwnerUri, connectionParams.Connection); @@ -164,7 +160,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection var response = new ConnectResponse(); try { - connectionInfo.OpenConnection(); + // build the connection string from the input parameters + string connectionString = ConnectionService.BuildConnectionString(connectionInfo.ConnectionDetails); + + // create a sql connection instance + connectionInfo.SqlConnection = connectionInfo.Factory.CreateSqlConnection(connectionString); + connectionInfo.SqlConnection.Open(); } catch(Exception ex) { @@ -185,10 +186,45 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection return response; } + /// + /// Close a connection with the specified connection details. + /// + public bool Disconnect(DisconnectParams disconnectParams) + { + // Validate parameters + if (disconnectParams == null || String.IsNullOrEmpty(disconnectParams.OwnerUri)) + { + return false; + } + + // Lookup the connection owned by the URI + ConnectionInfo info; + if (!ownerToConnectionMap.TryGetValue(disconnectParams.OwnerUri, out info)) + { + return false; + } + + // Close the connection + info.SqlConnection.Close(); + + // Remove URI mapping + ownerToConnectionMap.Remove(disconnectParams.OwnerUri); + + // Invoke callback notifications + foreach (var activity in this.onDisconnectActivities) + { + activity(info.ConnectionDetails); + } + + // Success + return true; + } + public void InitializeService(IProtocolEndpoint serviceHost) { // Register request and event handlers with the Service Host serviceHost.SetRequestHandler(ConnectionRequest.Type, HandleConnectRequest); + serviceHost.SetRequestHandler(DisconnectRequest.Type, HandleDisconnectRequest); // Register the configuration update handler WorkspaceService.Instance.RegisterConfigChangeCallback(HandleDidChangeConfigurationNotification); @@ -202,6 +238,14 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection { onConnectionActivities.Add(activity); } + + /// + /// Add a new method to be called when the ondisconnect request is submitted + /// + public void RegisterOnDisconnectTask(OnDisconnectHandler activity) + { + onDisconnectActivities.Add(activity); + } /// /// Handle new connection requests @@ -226,6 +270,27 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection await requestContext.SendError(ex.Message); } } + + /// + /// Handle disconnect requests + /// + protected async Task HandleDisconnectRequest( + DisconnectParams disconnectParams, + RequestContext requestContext) + { + Logger.Write(LogLevel.Verbose, "HandleDisconnectRequest"); + + try + { + bool result = ConnectionService.Instance.Disconnect(disconnectParams); + await requestContext.SendResult(result); + } + catch(Exception ex) + { + await requestContext.SendError(ex.Message); + } + + } public Task HandleDidChangeConfigurationNotification( SqlToolsSettings newSettings, diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionMessages.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectMessages.cs similarity index 62% rename from src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionMessages.cs rename to src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectMessages.cs index baa426e2..543b18f5 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionMessages.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectMessages.cs @@ -7,85 +7,23 @@ using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts; namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts { - /// - /// Parameters for the Connect Request. - /// - public class ConnectParams - { - /// - /// A URI identifying the owner of the connection. This will most commonly be a file in the workspace - /// or a virtual file representing an object in a database. - /// - public string OwnerUri { get; set; } - /// - /// Contains the required parameters to initialize a connection to a database. - /// A connection will identified by its server name, database name and user name. - /// This may be changed in the future to support multiple connections with different - /// connection properties to the same database. - /// - public ConnectionDetails Connection { get; set; } - } - - /// - /// Parameters for the Disconnect Request. - /// - public class DisconnectParams - { - /// - /// A URI identifying the owner of the connection. This will most commonly be a file in the workspace - /// or a virtual file representing an object in a database. - /// - public string ownerUri { get; set; } - } - - /// - /// Parameters for the ConnectionChanged Notification. - /// - public class ConnectionChangedParams - { - /// - /// A URI identifying the owner of the connection. This will most commonly be a file in the workspace - /// or a virtual file representing an object in a database. - /// - public string ownerUri { get; set; } - /// - /// Contains the high-level properties about the connection, for display to the user. - /// - public ConnectionSummary Connection { get; set; } - } - - /// - /// Provides high level information about a connection. - /// - public class ConnectionSummary - { - /// - /// Gets or sets the connection server name - /// - public string ServerName { get; set; } - - /// - /// Gets or sets the connection database name - /// - public string DatabaseName { get; set; } - - /// - /// Gets or sets the connection user name - /// - public string UserName { get; set; } - } /// - /// Message format for the initial connection request + /// Parameters for the Connect Request. /// - public class ConnectionDetails : ConnectionSummary + public class ConnectParams { /// - /// Gets or sets the connection password + /// A URI identifying the owner of the connection. This will most commonly be a file in the workspace + /// or a virtual file representing an object in a database. /// - /// - public string Password { get; set; } - - // TODO Handle full set of properties + public string OwnerUri { get; set; } + /// + /// Contains the required parameters to initialize a connection to a database. + /// A connection will identified by its server name, database name and user name. + /// This may be changed in the future to support multiple connections with different + /// connection properties to the same database. + /// + public ConnectionDetails Connection { get; set; } } /// @@ -102,8 +40,43 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts /// Gets or sets any connection error messages /// public string Messages { get; set; } - } - + } + + /// + /// Provides high level information about a connection. + /// + public class ConnectionSummary + { + /// + /// Gets or sets the connection server name + /// + public string ServerName { get; set; } + + /// + /// Gets or sets the connection database name + /// + public string DatabaseName { get; set; } + + /// + /// Gets or sets the connection user name + /// + public string UserName { get; set; } + } + + /// + /// Message format for the initial connection request + /// + public class ConnectionDetails : ConnectionSummary + { + /// + /// Gets or sets the connection password + /// + /// + public string Password { get; set; } + + // TODO Handle full set of properties + } + /// /// Connect request mapping entry /// @@ -113,25 +86,4 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts RequestType Type = RequestType.Create("connection/connect"); } - - /// - /// Disconnect request mapping entry - /// - public class DisconnectRequest - { - public static readonly - RequestType Type = - RequestType.Create("connection/disconnect"); - } - - /// - /// ConnectionChanged notification mapping entry - /// - public class ConnectionChangedNotification - { - public static readonly - EventType Type = - EventType.Create("connection/connectionchanged"); - } - } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionMessagesExtensions.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectMessagesExtensions.cs similarity index 100% rename from src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionMessagesExtensions.cs rename to src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectMessagesExtensions.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionChangedMessages.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionChangedMessages.cs new file mode 100644 index 00000000..94454bc5 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionChangedMessages.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.ServiceLayer.Hosting.Protocol.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts +{ + /// + /// Parameters for the ConnectionChanged Notification. + /// + public class ConnectionChangedParams + { + /// + /// A URI identifying the owner of the connection. This will most commonly be a file in the workspace + /// or a virtual file representing an object in a database. + /// + public string OwnerUri { get; set; } + /// + /// Contains the high-level properties about the connection, for display to the user. + /// + public ConnectionSummary Connection { get; set; } + } + + /// + /// ConnectionChanged notification mapping entry + /// + public class ConnectionChangedNotification + { + public static readonly + EventType Type = + EventType.Create("connection/connectionchanged"); + } + +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/DisconnectMessages.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/DisconnectMessages.cs new file mode 100644 index 00000000..c078b308 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/DisconnectMessages.cs @@ -0,0 +1,31 @@ +// +// 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.ServiceLayer.Hosting.Protocol.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts +{ + /// + /// Parameters for the Disconnect Request. + /// + public class DisconnectParams + { + /// + /// A URI identifying the owner of the connection. This will most commonly be a file in the workspace + /// or a virtual file representing an object in a database. + /// + public string OwnerUri { get; set; } + } + + /// + /// Disconnect request mapping entry + /// + public class DisconnectRequest + { + public static readonly + RequestType Type = + RequestType.Create("connection/disconnect"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteService.cs index 7ecc7106..14148778 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteService.cs @@ -21,8 +21,13 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices // connection used to query for intellisense info private DbConnection connection; + // number of documents (URI's) that are using the cache for the same database + // the autocomplete service uses this to remove unreferenced caches + public int ReferenceCount { get; set; } + public IntellisenseCache(ISqlConnectionFactory connectionFactory, ConnectionDetails connectionDetails) { + ReferenceCount = 0; DatabaseInfo = CopySummary(connectionDetails); // TODO error handling on this. Intellisense should catch or else the service should handle @@ -130,10 +135,10 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices { public bool Equals(ConnectionSummary x, ConnectionSummary y) { - if (x == y) { return true; } - else if (x != null) + if(x == y) { return true; } + else if(x != null) { - if (y == null) { return false; } + if(y == null) { return false; } // Compare server, db, username. Note: server is case-insensitive in the driver return string.Compare(x.ServerName, y.ServerName, StringComparison.OrdinalIgnoreCase) == 0 @@ -146,9 +151,9 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices public int GetHashCode(ConnectionSummary obj) { int hashcode = 31; - if (obj != null) + if(obj != null) { - if (obj.ServerName != null) + if(obj.ServerName != null) { hashcode ^= obj.ServerName.GetHashCode(); } @@ -174,13 +179,13 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// /// Singleton service instance /// - private static Lazy instance + private static Lazy instance = new Lazy(() => new AutoCompleteService()); /// /// Gets the singleton service instance /// - public static AutoCompleteService Instance + public static AutoCompleteService Instance { get { @@ -193,16 +198,18 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// TODO: Figure out how to make this truely singleton even with dependency injection for tests /// public AutoCompleteService() - { + { } #endregion // Dictionary of unique intellisense caches for each Connection - private Dictionary caches = + private Dictionary caches = new Dictionary(new ConnectionSummaryComparer()); + private Object cachesLock = new Object(); // Used when we insert/remove something from the cache dictionary private ISqlConnectionFactory factory; + private Object factoryLock = new Object(); /// /// Internal for testing purposes only @@ -211,22 +218,30 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices { get { - // TODO consider protecting against multi-threaded access - if (factory == null) + lock(factoryLock) { - factory = new SqlConnectionFactory(); + if(factory == null) + { + factory = new SqlConnectionFactory(); + } } return factory; } set { - factory = value; + lock(factoryLock) + { + factory = value; + } } } public void InitializeService(ServiceHost serviceHost) { // Register a callback for when a connection is created ConnectionService.Instance.RegisterOnConnectionTask(UpdateAutoCompleteCache); + + // Register a callback for when a connection is closed + ConnectionService.Instance.RegisterOnDisconnectTask(RemoveAutoCompleteCacheUriReference); } private async Task UpdateAutoCompleteCache(ConnectionInfo connectionInfo) @@ -237,20 +252,50 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices } } + /// + /// Remove a reference to an autocomplete cache from a URI. If + /// it is the last URI connected to a particular connection, + /// then remove the cache. + /// + public async Task RemoveAutoCompleteCacheUriReference(ConnectionSummary summary) + { + await Task.Run( () => + { + lock(cachesLock) + { + IntellisenseCache cache; + if( caches.TryGetValue(summary, out cache) ) + { + cache.ReferenceCount--; + + // Remove unused caches + if( cache.ReferenceCount == 0 ) + { + caches.Remove(summary); + } + } + } + }); + } + + /// /// Update the cached autocomplete candidate list when the user connects to a database - /// TODO: Update with refactoring/async /// /// public async Task UpdateAutoCompleteCache(ConnectionDetails details) { IntellisenseCache cache; - if (!caches.TryGetValue(details, out cache)) + lock(cachesLock) { - cache = new IntellisenseCache(ConnectionFactory, details); - caches[cache.DatabaseInfo] = cache; + if(!caches.TryGetValue(details, out cache)) + { + cache = new IntellisenseCache(ConnectionFactory, details); + caches[cache.DatabaseInfo] = cache; + } + cache.ReferenceCount++; } - + await cache.UpdateCache(); } @@ -263,19 +308,18 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices { // Try to find a cache for the document's backing connection (if available) // If we have a connection but no cache, we don't care - assuming the OnConnect and OnDisconnect listeners - // behave well, there should be a cache for any actively connected document. This also helps skip documents + // behave well, there should be a cache for any actively connected document. This also helps skip documents // that are not backed by a SQL connection - ConnectionInfo connectionInfo; + ConnectionInfo info; IntellisenseCache cache; - if (ConnectionService.Instance.TryFindConnection(textDocumentPosition.Uri, out connectionInfo) - && caches.TryGetValue(connectionInfo.ConnectionDetails, out cache)) + if (ConnectionService.Instance.TryFindConnection(textDocumentPosition.Uri, out info) + && caches.TryGetValue((ConnectionSummary)info.ConnectionDetails, out cache)) { return cache.GetAutoCompleteItems(textDocumentPosition).ToArray(); } - + return new CompletionItem[0]; } - + } } - diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs index 35ee0ebd..6cdbd745 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs @@ -103,10 +103,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices WorkspaceService.Instance.RegisterTextDocChangeCallback(HandleDidChangeTextDocumentNotification); // Register the file open update handler - WorkspaceService.Instance.RegisterTextDocOpenCallback(HandleDidOpenTextDocumentNotification); - - // register an OnConnection callback - ConnectionService.Instance.RegisterOnConnectionTask(OnConnection); + WorkspaceService.Instance.RegisterTextDocOpenCallback(HandleDidOpenTextDocumentNotification); // Store the SqlToolsContext for future use Context = context; @@ -305,16 +302,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices CurrentSettings.ScriptAnalysis.Update(newSettings.ScriptAnalysis, CurrentWorkspace.WorkspacePath); } - /// - /// Callback for when a user connection is done processing - /// - /// - public async Task OnConnection(ConnectionInfo connectionInfo) - { - // TODO consider whether this is needed at all - currently AutoComplete service handles its own updating - await Task.FromResult(true); - } - #endregion #region Private Helpers diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/Connection/ConnectionServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/Connection/ConnectionServiceTests.cs index d0c991f8..9e3d5339 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/Connection/ConnectionServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/Connection/ConnectionServiceTests.cs @@ -19,6 +19,53 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.Connection /// public class ConnectionServiceTests { + /// + /// Verify that when a connection is started for a URI with an already existing + /// connection, we disconnect first before connecting. + /// + [Fact] + public void ConnectingWhenConnectionExistCausesDisconnectThenConnect() + { + bool callbackInvoked = false; + + // first connect + string ownerUri = "file://my/sample/file.sql"; + var connectionService = TestObjects.GetTestConnectionService(); + var connectionResult = + connectionService + .Connect(new ConnectParams() + { + OwnerUri = ownerUri, + Connection = TestObjects.GetTestConnectionDetails() + }); + + // verify that we are connected + Assert.NotEmpty(connectionResult.ConnectionId); + + // register disconnect callback + connectionService.RegisterOnDisconnectTask( + (result) => { + callbackInvoked = true; + return Task.FromResult(true); + } + ); + + // send annother connect request + connectionResult = + connectionService + .Connect(new ConnectParams() + { + OwnerUri = ownerUri, + Connection = TestObjects.GetTestConnectionDetails() + }); + + // verify that the event was fired (we disconnected first before connecting) + Assert.True(callbackInvoked); + + // verify that we connected again + Assert.NotEmpty(connectionResult.ConnectionId); + } + /// /// Verify that when connecting with invalid credentials, an error is thrown. /// @@ -117,6 +164,151 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.Connection Assert.NotEmpty(connectionResult.ConnectionId); } + /// + /// Verify that we can disconnect from an active connection succesfully + /// + [Fact] + public void DisconnectFromDatabaseTest() + { + // first connect + string ownerUri = "file://my/sample/file.sql"; + var connectionService = TestObjects.GetTestConnectionService(); + var connectionResult = + connectionService + .Connect(new ConnectParams() + { + OwnerUri = ownerUri, + Connection = TestObjects.GetTestConnectionDetails() + }); + + // verify that we are connected + Assert.NotEmpty(connectionResult.ConnectionId); + + // send disconnect request + var disconnectResult = + connectionService + .Disconnect(new DisconnectParams() + { + OwnerUri = ownerUri + }); + Assert.True(disconnectResult); + } + + /// + /// Test that when a disconnect is performed, the callback event is fired + /// + [Fact] + public void DisconnectFiresCallbackEvent() + { + bool callbackInvoked = false; + + // first connect + string ownerUri = "file://my/sample/file.sql"; + var connectionService = TestObjects.GetTestConnectionService(); + var connectionResult = + connectionService + .Connect(new ConnectParams() + { + OwnerUri = ownerUri, + Connection = TestObjects.GetTestConnectionDetails() + }); + + // verify that we are connected + Assert.NotEmpty(connectionResult.ConnectionId); + + // register disconnect callback + connectionService.RegisterOnDisconnectTask( + (result) => { + callbackInvoked = true; + return Task.FromResult(true); + } + ); + + // send disconnect request + var disconnectResult = + connectionService + .Disconnect(new DisconnectParams() + { + OwnerUri = ownerUri + }); + Assert.True(disconnectResult); + + // verify that the event was fired + Assert.True(callbackInvoked); + } + + /// + /// Test that disconnecting an active connection removes the Owner URI -> ConnectionInfo mapping + /// + [Fact] + public void DisconnectRemovesOwnerMapping() + { + // first connect + string ownerUri = "file://my/sample/file.sql"; + var connectionService = TestObjects.GetTestConnectionService(); + var connectionResult = + connectionService + .Connect(new ConnectParams() + { + OwnerUri = ownerUri, + Connection = TestObjects.GetTestConnectionDetails() + }); + + // verify that we are connected + Assert.NotEmpty(connectionResult.ConnectionId); + + // check that the owner mapping exists + ConnectionInfo info; + Assert.True(connectionService.TryFindConnection(ownerUri, out info)); + + // send disconnect request + var disconnectResult = + connectionService + .Disconnect(new DisconnectParams() + { + OwnerUri = ownerUri + }); + Assert.True(disconnectResult); + + // check that the owner mapping no longer exists + Assert.False(connectionService.TryFindConnection(ownerUri, out info)); + } + + /// + /// Test that disconnecting validates parameters and doesn't succeed when they are invalid + /// + [Theory] + [InlineDataAttribute(null)] + [InlineDataAttribute("")] + + public void DisconnectValidatesParameters(string disconnectUri) + { + // first connect + string ownerUri = "file://my/sample/file.sql"; + var connectionService = TestObjects.GetTestConnectionService(); + var connectionResult = + connectionService + .Connect(new ConnectParams() + { + OwnerUri = ownerUri, + Connection = TestObjects.GetTestConnectionDetails() + }); + + // verify that we are connected + Assert.NotEmpty(connectionResult.ConnectionId); + + // send disconnect request + var disconnectResult = + connectionService + .Disconnect(new DisconnectParams() + { + OwnerUri = disconnectUri + }); + + // verify that disconnect failed + Assert.False(disconnectResult); + } + /// /// Verify that the SQL parser correctly detects errors in text /// @@ -151,24 +343,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.Connection var service = TestObjects.GetTestConnectionService(); var connectParams = TestObjects.GetTestConnectionParams(); - //var endpoint = new Mock(); - //Func, Task> connectRequestHandler = null; - //endpoint.Setup(e => e.SetRequestHandler(ConnectionRequest.Type, It.IsAny, Task>>())) - // .Callback, Task>>(handler => connectRequestHandler = handler); - - // when I initialize the service - //service.InitializeService(endpoint.Object); - - // then I expect the handler to be captured - //Assert.NotNull(connectRequestHandler); - - // when I call the service - //var requestContext = new Mock>(); - - //connectRequestHandler(connectParams, requestContext.Object); - // then I should get a live connection - - // and then I should have // connect to a database instance var connectionResult = service.Connect(connectParams); diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestObjects.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestObjects.cs index 5ca94d2b..b1ee31bb 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestObjects.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestObjects.cs @@ -169,7 +169,7 @@ namespace Microsoft.SqlTools.Test.Utility public override void Close() { - throw new NotImplementedException(); + // No Op } public override void Open()