Files
sqltoolsservice/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Connection/ConnectionServiceTests.cs
Kevin Cunnane f3bf330da6 Connect with different properties should actually change context (#307)
* Connect with different properties should actually change context
- Up to now, calling Connect for a previously-connected URI would disconnect, then reconnect ot the original (not new) target. WIth these changes we handle changes to database name or other key properties by updating the ConnectionInfo and connecting to the new target
- Some interesting scenarios are raised by our API, notably that an empty database name maps to the default DB (which we know nothing about). This limits the new feature such that only if the DB Name is specified, we'll change the connection. Hence 2 calls to an empty DB will not result in a DB change.

Additional changes:
- After discussion with Ben, we're simplifying the cancellation logic. He had made changes to support this, so the main update is that we dispose the token in the final block after its last use (hence avoiding a disposed exception) and clean up the number of Waits required since we already have async cancellation support
- Factored some logic such that the OnConnection callback isn't invoked until after we've updated the database name in the GetConnectionCompleteParams method. Again, this supports reporting the actual DB name instead of leaving it blank for default DB requests.

* PR comment fixes
2017-04-06 11:25:59 -07:00

1215 lines
49 KiB
C#

//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Data.SqlClient;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
using Microsoft.SqlTools.ServiceLayer.Test.Common;
using Microsoft.SqlTools.ServiceLayer.UnitTests.Utility;
using Moq;
using Moq.Protected;
using Xunit;
namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Connection
{
/// <summary>
/// Tests for the ServiceHost Connection Service tests
/// </summary>
public class ConnectionServiceTests
{
/// <summary>
/// Creates a mock db command that returns a predefined result set
/// </summary>
public static DbCommand CreateTestCommand(TestResultSet[] data)
{
var commandMock = new Mock<DbCommand> { CallBase = true };
var commandMockSetup = commandMock.Protected()
.Setup<DbDataReader>("ExecuteDbDataReader", It.IsAny<CommandBehavior>());
commandMockSetup.Returns(() => new TestDbDataReader(data));
return commandMock.Object;
}
/// <summary>
/// Creates a mock db connection that returns predefined data when queried for a result set
/// </summary>
public DbConnection CreateMockDbConnection(TestResultSet[] data)
{
var connectionMock = new Mock<DbConnection> { CallBase = true };
connectionMock.Protected()
.Setup<DbCommand>("CreateDbCommand")
.Returns(CreateTestCommand(data));
return connectionMock.Object;
}
[Fact]
public void CanCancelConnectRequest()
{
const string testFile = "file:///my/test/file.sql";
// Given a connection that times out and responds to cancellation
var mockConnection = new Mock<DbConnection> { CallBase = true };
CancellationToken token;
bool ready = false;
mockConnection.Setup(x => x.OpenAsync(It.IsAny<CancellationToken>()))
.Callback<CancellationToken>(t =>
{
// Pass the token to the return handler and signal the main thread to cancel
token = t;
ready = true;
})
.Returns(() =>
{
if (TestUtils.WaitFor(() => token.IsCancellationRequested))
{
throw new OperationCanceledException();
}
return Task.FromResult(true);
});
var mockFactory = new Mock<ISqlConnectionFactory>();
mockFactory.Setup(factory => factory.CreateSqlConnection(It.IsAny<string>()))
.Returns(mockConnection.Object);
var connectionService = new ConnectionService(mockFactory.Object);
// Connect the connection asynchronously in a background thread
var connectionDetails = TestObjects.GetTestConnectionDetails();
var connectTask = Task.Run(async () => await connectionService
.Connect(new ConnectParams
{
OwnerUri = testFile,
Connection = connectionDetails
}));
// Wait for the connection to call OpenAsync()
Assert.True(TestUtils.WaitFor(() => ready));
// Send a cancellation request
var cancelResult = connectionService
.CancelConnect(new CancelConnectParams()
{
OwnerUri = testFile
});
// Wait for the connection task to finish
connectTask.Wait();
// Verify that the connection was cancelled (no connection was created)
Assert.Null(connectTask.Result.ConnectionId);
// Verify that the cancel succeeded
Assert.True(cancelResult);
}
[Fact]
public async Task CanCancelConnectRequestByConnecting()
{
const string testFile = "file:///my/test/file.sql";
// Given a connection that times out and responds to cancellation
var mockConnection = new Mock<DbConnection> { CallBase = true };
CancellationToken token;
bool ready = false;
mockConnection.Setup(x => x.OpenAsync(It.IsAny<CancellationToken>()))
.Callback<CancellationToken>(t =>
{
// Pass the token to the return handler and signal the main thread to cancel
token = t;
ready = true;
})
.Returns(() =>
{
if (TestUtils.WaitFor(() => token.IsCancellationRequested))
{
throw new OperationCanceledException();
}
return Task.FromResult(true);
});
// Given a second connection that succeeds
var mockConnection2 = new Mock<DbConnection> { CallBase = true };
mockConnection2.Setup(x => x.OpenAsync(It.IsAny<CancellationToken>()))
.Returns(() => Task.Run(() => {}));
var mockFactory = new Mock<ISqlConnectionFactory>();
mockFactory.SetupSequence(factory => factory.CreateSqlConnection(It.IsAny<string>()))
.Returns(mockConnection.Object)
.Returns(mockConnection2.Object);
var connectionService = new ConnectionService(mockFactory.Object);
// Connect the first connection asynchronously in a background thread
var connectionDetails = TestObjects.GetTestConnectionDetails();
var connectTask = Task.Run(async () => await connectionService
.Connect(new ConnectParams()
{
OwnerUri = testFile,
Connection = connectionDetails
}));
// Wait for the connection to call OpenAsync()
Assert.True(TestUtils.WaitFor(() => ready));
// Send a cancellation by trying to connect again
var connectResult = await connectionService
.Connect(new ConnectParams()
{
OwnerUri = testFile,
Connection = connectionDetails
});
// Wait for the first connection task to finish
connectTask.Wait();
// Verify that the first connection was cancelled (no connection was created)
Assert.Null(connectTask.Result.ConnectionId);
// Verify that the second connection succeeded
Assert.NotEmpty(connectResult.ConnectionId);
}
[Fact]
public void CanCancelConnectRequestByDisconnecting()
{
const string testFile = "file:///my/test/file.sql";
// Given a connection that times out and responds to cancellation
var mockConnection = new Mock<DbConnection> { CallBase = true };
CancellationToken token;
bool ready = false;
mockConnection.Setup(x => x.OpenAsync(It.IsAny<CancellationToken>()))
.Callback<CancellationToken>(t =>
{
// Pass the token to the return handler and signal the main thread to cancel
token = t;
ready = true;
})
.Returns(() =>
{
if (TestUtils.WaitFor(() => token.IsCancellationRequested))
{
throw new OperationCanceledException();
}
return Task.FromResult(true);
});
var mockFactory = new Mock<ISqlConnectionFactory>();
mockFactory.Setup(factory => factory.CreateSqlConnection(It.IsAny<string>()))
.Returns(mockConnection.Object);
var connectionService = new ConnectionService(mockFactory.Object);
// Connect the first connection asynchronously in a background thread
var connectionDetails = TestObjects.GetTestConnectionDetails();
var connectTask = Task.Run(async () => await connectionService
.Connect(new ConnectParams
{
OwnerUri = testFile,
Connection = connectionDetails
}));
// Wait for the connection to call OpenAsync()
Assert.True(TestUtils.WaitFor(() => ready));
// Send a cancellation by trying to disconnect
var disconnectResult = connectionService
.Disconnect(new DisconnectParams
{
OwnerUri = testFile
});
// Wait for the first connection task to finish
connectTask.Wait();
// Verify that the first connection was cancelled (no connection was created)
Assert.Null(connectTask.Result.ConnectionId);
// Verify that the disconnect failed (since it caused a cancellation)
Assert.False(disconnectResult);
}
/// <summary>
/// Verify that we can connect to the default database when no database name is
/// provided as a parameter.
/// </summary>
[Theory]
[InlineData(null)]
[InlineData("")]
public async Task CanConnectWithEmptyDatabaseName(string databaseName)
{
// Connect
var connectionDetails = TestObjects.GetTestConnectionDetails();
connectionDetails.DatabaseName = databaseName;
var connectionResult = await
TestObjects.GetTestConnectionService()
.Connect(new ConnectParams()
{
OwnerUri = "file:///my/test/file.sql",
Connection = connectionDetails
});
// check that a connection was created
Assert.NotEmpty(connectionResult.ConnectionId);
}
/// <summary>
/// Verify that we can connect to the default database when no database name is
/// provided as a parameter.
/// </summary>
[Theory]
[InlineData("master")]
[InlineData("nonMasterDb")]
public async Task ConnectToDefaultDatabaseRespondsWithActualDbName(string expectedDbName)
{
// Given connecting with empty database name will return the expected DB name
var connectionMock = new Mock<DbConnection> { CallBase = true };
connectionMock.Setup(c => c.Database).Returns(expectedDbName);
var mockFactory = new Mock<ISqlConnectionFactory>();
mockFactory.Setup(factory => factory.CreateSqlConnection(It.IsAny<string>()))
.Returns(connectionMock.Object);
var connectionService = new ConnectionService(mockFactory.Object);
// When I connect with an empty DB name
var connectionDetails = TestObjects.GetTestConnectionDetails();
connectionDetails.DatabaseName = string.Empty;
var connectionResult = await
connectionService
.Connect(new ConnectParams()
{
OwnerUri = "file:///my/test/file.sql",
Connection = connectionDetails
});
// Then I expect connection to succeed and the Summary to include the correct DB name
Assert.NotEmpty(connectionResult.ConnectionId);
Assert.NotNull(connectionResult.ConnectionSummary);
Assert.Equal(expectedDbName, connectionResult.ConnectionSummary.DatabaseName);
}
/// <summary>
/// Verify that when a connection is started for a URI with an already existing
/// connection, we disconnect first before connecting.
/// </summary>
[Fact]
public async Task ConnectingWhenConnectionExistCausesDisconnectThenConnect()
{
bool callbackInvoked = false;
string ownerUri = "file://my/sample/file.sql";
const string masterDbName = "master";
const string otherDbName = "other";
// Given a connection that returns the database name
var dummySqlConnection = new TestSqlConnection(null);
var mockFactory = new Mock<ISqlConnectionFactory>();
mockFactory.Setup(factory => factory.CreateSqlConnection(It.IsAny<string>()))
.Returns((string connString) =>
{
dummySqlConnection.ConnectionString = connString;
SqlConnectionStringBuilder scsb = new SqlConnectionStringBuilder(connString);
// Database name is respected. Follow heuristic where empty DB name really means Master
var dbName = string.IsNullOrEmpty(scsb.InitialCatalog) ? masterDbName : scsb.InitialCatalog;
dummySqlConnection.SetDatabase(dbName);
return dummySqlConnection;
});
var connectionService = new ConnectionService(mockFactory.Object);
// register disconnect callback
connectionService.RegisterOnDisconnectTask(
(result, uri) => {
callbackInvoked = true;
Assert.True(uri.Equals(ownerUri));
return Task.FromResult(true);
}
);
// When I connect to default
var connectionResult = await
connectionService
.Connect(new ConnectParams()
{
OwnerUri = ownerUri,
Connection = TestObjects.GetTestConnectionDetails()
});
// Then I expect to be connected to master
Assert.NotEmpty(connectionResult.ConnectionId);
// And when I then connect to another DB
var updatedConnectionDetails = TestObjects.GetTestConnectionDetails();
updatedConnectionDetails.DatabaseName = otherDbName;
connectionResult = await
connectionService
.Connect(new ConnectParams()
{
OwnerUri = ownerUri,
Connection = updatedConnectionDetails
});
// Then I expect to be disconnected from master, and connected to the new DB
// verify that the event was fired (we disconnected first before connecting)
Assert.True(callbackInvoked);
// verify that we connected again
Assert.NotEmpty(connectionResult.ConnectionId);
Assert.Equal(otherDbName, connectionResult.ConnectionSummary.DatabaseName);
}
/// <summary>
/// Verify that when connecting with invalid credentials, an error is thrown.
/// </summary>
[Fact]
public async Task ConnectingWithInvalidCredentialsYieldsErrorMessage()
{
var testConnectionDetails = TestObjects.GetTestConnectionDetails();
var invalidConnectionDetails = new ConnectionDetails
{
ServerName = testConnectionDetails.ServerName,
DatabaseName = testConnectionDetails.DatabaseName,
UserName = "invalidUsername",
Password = "invalidPassword"
};
// triggers exception when opening mock connection
// Connect to test db with invalid credentials
var connectionResult = await
TestObjects.GetTestConnectionService()
.Connect(new ConnectParams
{
OwnerUri = "file://my/sample/file.sql",
Connection = invalidConnectionDetails
});
// check that an error was caught
Assert.NotNull(connectionResult.Messages);
Assert.NotEqual(String.Empty, connectionResult.Messages);
}
/// <summary>
/// Verify that when connecting with invalid parameters, an error is thrown.
/// </summary>
[Theory]
[InlineData("SqlLogin", null, "my-server", "test", "sa", "123456")]
[InlineData("SqlLogin", "file://my/sample/file.sql", null, "test", "sa", "123456")]
[InlineData("SqlLogin", "file://my/sample/file.sql", "my-server", "test", null, "123456")]
[InlineData("SqlLogin", "file://my/sample/file.sql", "my-server", "test", "sa", null)]
[InlineData("SqlLogin", "", "my-server", "test", "sa", "123456")]
[InlineData("SqlLogin", "file://my/sample/file.sql", "", "test", "sa", "123456")]
[InlineData("SqlLogin", "file://my/sample/file.sql", "my-server", "test", "", "123456")]
[InlineData("SqlLogin", "file://my/sample/file.sql", "my-server", "test", "sa", "")]
[InlineData("Integrated", null, "my-server", "test", "sa", "123456")]
[InlineData("Integrated", "file://my/sample/file.sql", null, "test", "sa", "123456")]
[InlineData("Integrated", "", "my-server", "test", "sa", "123456")]
[InlineData("Integrated", "file://my/sample/file.sql", "", "test", "sa", "123456")]
public async Task ConnectingWithInvalidParametersYieldsErrorMessage(string authType, string ownerUri, string server, string database, string userName, string password)
{
// Connect with invalid parameters
var connectionResult = await
TestObjects.GetTestConnectionService()
.Connect(new ConnectParams()
{
OwnerUri = ownerUri,
Connection = new ConnectionDetails() {
ServerName = server,
DatabaseName = database,
UserName = userName,
Password = password,
AuthenticationType = authType
}
});
// check that an error was caught
Assert.NotNull(connectionResult.Messages);
Assert.NotEqual(String.Empty, connectionResult.Messages);
}
/// <summary>
/// Verify that when using integrated authentication, the username and/or password can be empty.
/// </summary>
[Theory]
[InlineData(null, null)]
[InlineData(null, "")]
[InlineData("", null)]
[InlineData("", "")]
[InlineData("sa", null)]
[InlineData("sa", "")]
[InlineData(null, "12345678")]
[InlineData("", "12345678")]
public async Task ConnectingWithNoUsernameOrPasswordWorksForIntegratedAuth(string userName, string password)
{
// Connect
var connectionResult = await
TestObjects.GetTestConnectionService()
.Connect(new ConnectParams()
{
OwnerUri = "file:///my/test/file.sql",
Connection = new ConnectionDetails() {
ServerName = "my-server",
DatabaseName = "test",
UserName = userName,
Password = password,
AuthenticationType = "Integrated"
}
});
// check that the connection was successful
Assert.NotEmpty(connectionResult.ConnectionId);
}
/// <summary>
/// Verify that when connecting with a null parameters object, an error is thrown.
/// </summary>
[Fact]
public async Task ConnectingWithNullParametersObjectYieldsErrorMessage()
{
// Connect with null parameters
var connectionResult = await
TestObjects.GetTestConnectionService()
.Connect(null);
// check that an error was caught
Assert.NotNull(connectionResult.Messages);
Assert.NotEqual(String.Empty, connectionResult.Messages);
}
/// <summary>
/// Verify that optional parameters can be built into a connection string for connecting.
/// </summary>
[Theory]
[InlineData("AuthenticationType", "Integrated", "Integrated Security")]
[InlineData("AuthenticationType", "SqlLogin", "")]
[InlineData("Encrypt", true, "Encrypt")]
[InlineData("Encrypt", false, "Encrypt")]
[InlineData("TrustServerCertificate", true, "TrustServerCertificate")]
[InlineData("TrustServerCertificate", false, "TrustServerCertificate")]
[InlineData("PersistSecurityInfo", true, "Persist Security Info")]
[InlineData("PersistSecurityInfo", false, "Persist Security Info")]
[InlineData("ConnectTimeout", 15, "Connect Timeout")]
[InlineData("ConnectRetryCount", 1, "ConnectRetryCount")]
[InlineData("ConnectRetryInterval", 10, "ConnectRetryInterval")]
[InlineData("ApplicationName", "vscode-mssql", "Application Name")]
[InlineData("WorkstationId", "mycomputer", "Workstation ID")]
[InlineData("ApplicationIntent", "ReadWrite", "ApplicationIntent")]
[InlineData("ApplicationIntent", "ReadOnly", "ApplicationIntent")]
[InlineData("CurrentLanguage", "test", "Current Language")]
[InlineData("Pooling", false, "Pooling")]
[InlineData("Pooling", true, "Pooling")]
[InlineData("MaxPoolSize", 100, "Max Pool Size")]
[InlineData("MinPoolSize", 0, "Min Pool Size")]
[InlineData("LoadBalanceTimeout", 0, "Load Balance Timeout")]
[InlineData("Replication", true, "Replication")]
[InlineData("Replication", false, "Replication")]
[InlineData("AttachDbFilename", "myfile", "AttachDbFilename")]
[InlineData("FailoverPartner", "partner", "Failover Partner")]
[InlineData("MultiSubnetFailover", true, "MultiSubnetFailover")]
[InlineData("MultiSubnetFailover", false, "MultiSubnetFailover")]
[InlineData("MultipleActiveResultSets", false, "MultipleActiveResultSets")]
[InlineData("MultipleActiveResultSets", true, "MultipleActiveResultSets")]
[InlineData("PacketSize", 8192, "Packet Size")]
[InlineData("TypeSystemVersion", "Latest", "Type System Version")]
public void ConnectingWithOptionalParametersBuildsConnectionString(string propertyName, object propertyValue, string connectionStringMarker)
{
// Create a test connection details object and set the property to a specific value
ConnectionDetails details = TestObjects.GetTestConnectionDetails();
PropertyInfo info = details.GetType().GetProperty(propertyName);
info.SetValue(details, propertyValue);
// Test that a connection string can be created without exceptions
string connectionString = ConnectionService.BuildConnectionString(details);
Assert.NotNull(connectionString);
Assert.NotEmpty(connectionString);
// Verify that the parameter is in the connection string
Assert.True(connectionString.Contains(connectionStringMarker));
}
/// <summary>
/// Build connection string with an invalid auth type
/// </summary>
[Fact]
public void BuildConnectionStringWithInvalidAuthType()
{
ConnectionDetails details = TestObjects.GetTestConnectionDetails();
details.AuthenticationType = "NotAValidAuthType";
Assert.Throws<ArgumentException>(() => ConnectionService.BuildConnectionString(details));
}
/// <summary>
/// Verify that a connection changed event is fired when the database context changes.
/// </summary>
[Fact]
public async Task ConnectionChangedEventIsFiredWhenDatabaseContextChanges()
{
var serviceHostMock = new Mock<IProtocolEndpoint>();
var connectionService = TestObjects.GetTestConnectionService();
connectionService.ServiceHost = serviceHostMock.Object;
// Set up an initial connection
const string ownerUri = "file://my/sample/file.sql";
var connectionResult = await
connectionService
.Connect(new ConnectParams()
{
OwnerUri = ownerUri,
Connection = TestObjects.GetTestConnectionDetails()
});
// verify that a valid connection id was returned
Assert.NotEmpty(connectionResult.ConnectionId);
ConnectionInfo info;
Assert.True(connectionService.TryFindConnection(ownerUri, out info));
// Tell the connection manager that the database change occurred
connectionService.ChangeConnectionDatabaseContext(ownerUri, "myOtherDb");
// Verify that the connection changed event was fired
serviceHostMock.Verify(x => x.SendEvent(ConnectionChangedNotification.Type, It.IsAny<ConnectionChangedParams>()), Times.Once());
}
/// <summary>
/// Verify that the SQL parser correctly detects errors in text
/// </summary>
[Fact]
public async Task ConnectToDatabaseTest()
{
// connect to a database instance
string ownerUri = "file://my/sample/file.sql";
var connectionResult = await
TestObjects.GetTestConnectionService()
.Connect(new ConnectParams()
{
OwnerUri = ownerUri,
Connection = TestObjects.GetTestConnectionDetails()
});
// verify that a valid connection id was returned
Assert.NotEmpty(connectionResult.ConnectionId);
}
/// <summary>
/// Verify that we can disconnect from an active connection successfully
/// </summary>
[Fact]
public async Task DisconnectFromDatabaseTest()
{
// first connect
string ownerUri = "file://my/sample/file.sql";
var connectionService = TestObjects.GetTestConnectionService();
var connectionResult = await
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);
}
/// <summary>
/// Test that when a disconnect is performed, the callback event is fired
/// </summary>
[Fact]
public async Task DisconnectFiresCallbackEvent()
{
bool callbackInvoked = false;
// first connect
string ownerUri = "file://my/sample/file.sql";
var connectionService = TestObjects.GetTestConnectionService();
var connectionResult = await
connectionService
.Connect(new ConnectParams()
{
OwnerUri = ownerUri,
Connection = TestObjects.GetTestConnectionDetails()
});
// verify that we are connected
Assert.NotEmpty(connectionResult.ConnectionId);
// register disconnect callback
connectionService.RegisterOnDisconnectTask(
(result, uri) => {
callbackInvoked = true;
Assert.True(uri.Equals(ownerUri));
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);
}
/// <summary>
/// Test that disconnecting an active connection removes the Owner URI -> ConnectionInfo mapping
/// </summary>
[Fact]
public async Task DisconnectRemovesOwnerMapping()
{
// first connect
string ownerUri = "file://my/sample/file.sql";
var connectionService = TestObjects.GetTestConnectionService();
var connectionResult = await
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));
}
/// <summary>
/// Test that disconnecting validates parameters and doesn't succeed when they are invalid
/// </summary>
[Theory]
[InlineData(null)]
[InlineData("")]
public async Task DisconnectValidatesParameters(string disconnectUri)
{
// first connect
string ownerUri = "file://my/sample/file.sql";
var connectionService = TestObjects.GetTestConnectionService();
var connectionResult = await
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);
}
/// <summary>
/// Verifies the the list databases operation lists database names for the server used by a connection.
/// </summary>
[Fact]
public async Task ListDatabasesOnServerForCurrentConnectionReturnsDatabaseNames()
{
// Result set for the query of database names
TestDbColumn[] cols = {new TestDbColumn("name")};
object[][] rows =
{
new object[] {"master"},
new object[] {"model"},
new object[] {"msdb"},
new object[] {"tempdb"},
new object[] {"mydatabase"}
};
TestResultSet data = new TestResultSet(cols, rows);
// Setup mock connection factory to inject query results
var mockFactory = new Mock<ISqlConnectionFactory>();
mockFactory.Setup(factory => factory.CreateSqlConnection(It.IsAny<string>()))
.Returns(CreateMockDbConnection(new[] {data}));
var connectionService = new ConnectionService(mockFactory.Object);
// connect to a database instance
string ownerUri = "file://my/sample/file.sql";
var connectionResult = await
connectionService
.Connect(new ConnectParams()
{
OwnerUri = ownerUri,
Connection = TestObjects.GetTestConnectionDetails()
});
// verify that a valid connection id was returned
Assert.NotEmpty(connectionResult.ConnectionId);
// list databases for the connection
ListDatabasesParams parameters = new ListDatabasesParams {OwnerUri = ownerUri};
var listDatabasesResult = connectionService.ListDatabases(parameters);
string[] databaseNames = listDatabasesResult.DatabaseNames;
Assert.Equal(databaseNames.Length, 5);
Assert.Equal(databaseNames[0], "master");
Assert.Equal(databaseNames[1], "model");
Assert.Equal(databaseNames[2], "msdb");
Assert.Equal(databaseNames[3], "tempdb");
Assert.Equal(databaseNames[4], "mydatabase");
}
/// <summary>
/// Verify that the SQL parser correctly detects errors in text
/// </summary>
[Fact]
public async Task OnConnectionCallbackHandlerTest()
{
bool callbackInvoked = false;
// setup connection service with callback
var connectionService = TestObjects.GetTestConnectionService();
connectionService.RegisterOnConnectionTask(
(sqlConnection) => {
callbackInvoked = true;
return Task.FromResult(true);
}
);
// connect to a database instance
await connectionService.Connect(TestObjects.GetTestConnectionParams());
// verify that a valid connection id was returned
Assert.True(callbackInvoked);
}
/// <summary>
/// Test ConnectionSummaryComparer
/// </summary>
[Fact]
public void TestConnectionSummaryComparer()
{
var summary1 = new ConnectionSummary()
{
ServerName = "localhost",
DatabaseName = "master",
UserName = "user"
};
var summary2 = new ConnectionSummary()
{
ServerName = "localhost",
DatabaseName = "master",
UserName = "user"
};
var comparer = new ConnectionSummaryComparer();
Assert.True(comparer.Equals(summary1, summary2));
summary2.DatabaseName = "tempdb";
Assert.False(comparer.Equals(summary1, summary2));
Assert.False(comparer.Equals(null, summary2));
Assert.False(summary1.GetHashCode() == summary2.GetHashCode());
}
/// <summary>
/// Verify when a connection is created that the URI -> Connection mapping is created in the connection service.
/// </summary>
[Fact]
public async Task TestConnectRequestRegistersOwner()
{
// Given a request to connect to a database
var service = TestObjects.GetTestConnectionService();
var connectParams = TestObjects.GetTestConnectionParams();
// connect to a database instance
var connectionResult = await service.Connect(connectParams);
// verify that a valid connection id was returned
Assert.NotNull(connectionResult.ConnectionId);
Assert.NotEqual(string.Empty, connectionResult.ConnectionId);
Assert.NotNull(new Guid(connectionResult.ConnectionId));
// verify that the (URI -> connection) mapping was created
ConnectionInfo info;
Assert.True(service.TryFindConnection(connectParams.OwnerUri, out info));
}
/// <summary>
/// Verify that Linux/OSX SqlExceptions thrown do not contain an error code.
/// This is a bug in .NET core (see https://github.com/dotnet/corefx/issues/12472).
/// If this test ever fails, it means that this bug has been fixed. When this is
/// the case, look at RetryPolicyUtils.cs in IsRetryableNetworkConnectivityError(),
/// and remove the code block specific to Linux/OSX.
/// </summary>
[Fact]
public void TestThatLinuxAndOsxSqlExceptionHasNoErrorCode()
{
RunIfWrapper.RunIfLinuxOrOSX(() =>
{
try
{
SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder
{
DataSource = "bad-server-name",
UserID = "sa",
Password = "bad password"
};
SqlConnection connection = new SqlConnection(builder.ConnectionString);
connection.Open(); // This should fail
}
catch (SqlException ex)
{
// Error code should be 0 due to bug
Assert.Equal(ex.Number, 0);
}
});
}
/// <summary>
/// Test that cancel connection with a null connection parameter
/// </summary>
[Fact]
public void TestCancelConnectionNullParam()
{
var service = TestObjects.GetTestConnectionService();
Assert.False(service.CancelConnect(null));
}
/// <summary>
/// Test that cancel connection with a null connection parameter
/// </summary>
[Fact]
public void TestListDatabasesInvalidParams()
{
var service = TestObjects.GetTestConnectionService();
var listParams = new ListDatabasesParams();
Assert.Throws<ArgumentException>(() => service.ListDatabases(listParams));
listParams.OwnerUri = "file://notmyfile.sql";
Assert.Throws<Exception>(() => service.ListDatabases(listParams));
}
/// <summary>
/// Test that the connection complete notification type can be created.
/// </summary>
[Fact]
public void TestConnectionCompleteNotificationIsCreated()
{
Assert.NotNull(ConnectionCompleteNotification.Type);
}
/// <summary>
/// Test that the connection summary comparer creates a hash code correctly
/// </summary>
[Theory]
[InlineData(true, null, null ,null)]
[InlineData(false, null, null, null)]
[InlineData(false, null, null, "sa")]
[InlineData(false, null, "test", null)]
[InlineData(false, null, "test", "sa")]
[InlineData(false, "server", null, null)]
[InlineData(false, "server", null, "sa")]
[InlineData(false, "server", "test", null)]
[InlineData(false, "server", "test", "sa")]
public void TestConnectionSummaryComparerHashCode(bool objectNull, string serverName, string databaseName, string userName)
{
// Given a connection summary and comparer object
ConnectionSummary summary = null;
if (!objectNull)
{
summary = new ConnectionSummary()
{
ServerName = serverName,
DatabaseName = databaseName,
UserName = userName
};
}
ConnectionSummaryComparer comparer = new ConnectionSummaryComparer();
// If I compute a hash code
int hashCode = comparer.GetHashCode(summary);
if (summary == null || (serverName == null && databaseName == null && userName == null))
{
// Then I expect it to be 31 for a null summary
Assert.Equal(31, hashCode);
}
else
{
// And not 31 otherwise
Assert.NotEqual(31, hashCode);
}
}
[Fact]
public void ConnectParamsAreInvalidIfConnectionIsNull()
{
// Given connection parameters where the connection property is null
ConnectParams parameters = new ConnectParams
{
OwnerUri = "my/sql/file.sql",
Connection = null
};
string errorMessage;
// If I check if the parameters are valid
Assert.False(parameters.IsValid(out errorMessage));
// Then I expect an error message
Assert.NotNull(errorMessage);
Assert.NotEmpty(errorMessage);
}
[Fact]
public async void ConnectingTwiceWithTheSameUriDoesNotCreateAnotherDbConnection()
{
// Setup the connect and disconnect params
var connectParamsSame1 = new ConnectParams()
{
OwnerUri = "connectParamsSame",
Connection = TestObjects.GetTestConnectionDetails()
};
var connectParamsSame2 = new ConnectParams()
{
OwnerUri = "connectParamsSame",
Connection = TestObjects.GetTestConnectionDetails()
};
var disconnectParamsSame = new DisconnectParams()
{
OwnerUri = connectParamsSame1.OwnerUri
};
var connectParamsDifferent = new ConnectParams()
{
OwnerUri = "connectParamsDifferent",
Connection = TestObjects.GetTestConnectionDetails()
};
var disconnectParamsDifferent = new DisconnectParams()
{
OwnerUri = connectParamsDifferent.OwnerUri
};
// Given a request to connect to a database, there should be no initial connections in the map
var service = TestObjects.GetTestConnectionService();
Dictionary<string, ConnectionInfo> ownerToConnectionMap = service.OwnerToConnectionMap;
Assert.Equal(0, ownerToConnectionMap.Count);
// If we connect to the service, there should be 1 connection
await service.Connect(connectParamsSame1);
Assert.Equal(1, ownerToConnectionMap.Count);
// If we connect again with the same URI, there should still be 1 connection
await service.Connect(connectParamsSame2);
Assert.Equal(1, ownerToConnectionMap.Count);
// If we connect with a different URI, there should be 2 connections
await service.Connect(connectParamsDifferent);
Assert.Equal(2, ownerToConnectionMap.Count);
// If we disconnect with the unique URI, there should be 1 connection
service.Disconnect(disconnectParamsDifferent);
Assert.Equal(1, ownerToConnectionMap.Count);
// If we disconnect with the duplicate URI, there should be 0 connections
service.Disconnect(disconnectParamsSame);
Assert.Equal(0, ownerToConnectionMap.Count);
}
[Fact]
public async void DbConnectionDoesntLeakUponDisconnect()
{
// If we connect with a single URI and 2 connection types
var connectParamsDefault = new ConnectParams()
{
OwnerUri = "connectParams",
Connection = TestObjects.GetTestConnectionDetails(),
Type = ConnectionType.Default
};
var connectParamsQuery = new ConnectParams()
{
OwnerUri = "connectParams",
Connection = TestObjects.GetTestConnectionDetails(),
Type = ConnectionType.Query
};
var disconnectParams = new DisconnectParams()
{
OwnerUri = connectParamsDefault.OwnerUri
};
var service = TestObjects.GetTestConnectionService();
await service.Connect(connectParamsDefault);
await service.Connect(connectParamsQuery);
// We should have one ConnectionInfo and 2 DbConnections
ConnectionInfo connectionInfo = service.OwnerToConnectionMap[connectParamsDefault.OwnerUri];
Assert.Equal(2, connectionInfo.CountConnections);
Assert.Equal(1, service.OwnerToConnectionMap.Count);
// If we record when the Default connecton calls Close()
bool defaultDisconnectCalled = false;
var mockDefaultConnection = new Mock<DbConnection> { CallBase = true };
mockDefaultConnection.Setup(x => x.Close())
.Callback(() =>
{
defaultDisconnectCalled = true;
});
connectionInfo.ConnectionTypeToConnectionMap[ConnectionType.Default] = mockDefaultConnection.Object;
// And when the Query connecton calls Close()
bool queryDisconnectCalled = false;
var mockQueryConnection = new Mock<DbConnection> { CallBase = true };
mockQueryConnection.Setup(x => x.Close())
.Callback(() =>
{
queryDisconnectCalled = true;
});
connectionInfo.ConnectionTypeToConnectionMap[ConnectionType.Query] = mockQueryConnection.Object;
// If we disconnect all open connections with the same URI as used above
service.Disconnect(disconnectParams);
// Close() should have gotten called for both DbConnections
Assert.True(defaultDisconnectCalled);
Assert.True(queryDisconnectCalled);
// And the maps that hold connection data should be empty
Assert.Equal(0, connectionInfo.CountConnections);
Assert.Equal(0, service.OwnerToConnectionMap.Count);
}
[Fact]
public async void ClosingQueryConnectionShouldLeaveDefaultConnectionOpen()
{
// Setup the connect and disconnect params
var connectParamsDefault = new ConnectParams()
{
OwnerUri = "connectParamsSame",
Connection = TestObjects.GetTestConnectionDetails(),
Type = ConnectionType.Default
};
var connectParamsQuery = new ConnectParams()
{
OwnerUri = connectParamsDefault.OwnerUri,
Connection = TestObjects.GetTestConnectionDetails(),
Type = ConnectionType.Query
};
var disconnectParamsQuery = new DisconnectParams()
{
OwnerUri = connectParamsDefault.OwnerUri,
Type = connectParamsQuery.Type
};
// If I connect a Default and a Query connection
var service = TestObjects.GetTestConnectionService();
await service.Connect(connectParamsDefault);
await service.Connect(connectParamsQuery);
ConnectionInfo connectionInfo = service.OwnerToConnectionMap[connectParamsDefault.OwnerUri];
// There should be 2 connections in the map
Assert.Equal(2, connectionInfo.CountConnections);
// If I Disconnect only the Query connection, there should be 1 connection in the map
service.Disconnect(disconnectParamsQuery);
Assert.Equal(1, connectionInfo.CountConnections);
// If I reconnect, there should be 2 again
await service.Connect(connectParamsQuery);
Assert.Equal(2, connectionInfo.CountConnections);
}
[Theory]
[InlineData(null)]
[InlineData("")]
public async Task GetOrOpenNullOwnerUri(string ownerUri)
{
// If: I have a connection service and I ask for a connection with an invalid ownerUri
// Then: An exception should be thrown
var service = TestObjects.GetTestConnectionService();
await Assert.ThrowsAsync<ArgumentException>(
() => service.GetOrOpenConnection(ownerUri, ConnectionType.Default));
}
[Theory]
[InlineData(null)]
[InlineData("")]
public async Task GetOrOpenNullConnectionType(string connType)
{
// If: I have a connection service and I ask for a connection with an invalid connectionType
// Then: An exception should be thrown
var service = TestObjects.GetTestConnectionService();
await Assert.ThrowsAsync<ArgumentException>(
() => service.GetOrOpenConnection(TestObjects.ScriptUri, connType));
}
[Fact]
public async Task GetOrOpenNoConnection()
{
// If: I have a connection service and I ask for a connection for an unconnected uri
// Then: An exception should be thrown
var service = TestObjects.GetTestConnectionService();
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(
() => service.GetOrOpenConnection(TestObjects.ScriptUri, ConnectionType.Query));
}
[Fact]
public async Task GetOrOpenNoDefaultConnection()
{
// Setup: Create a connection service with an empty connection info obj
var service = TestObjects.GetTestConnectionService();
var connInfo = new ConnectionInfo(null, null, null);
service.OwnerToConnectionMap[TestObjects.ScriptUri] = connInfo;
// If: I ask for a connection on a connection that doesn't have a default connection
// Then: An exception should be thrown
await Assert.ThrowsAsync<InvalidOperationException>(
() => service.GetOrOpenConnection(TestObjects.ScriptUri, ConnectionType.Query));
}
}
}