// // 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 Microsoft.Data.SqlClient; using System.Globalization; using System.Threading; using Microsoft.SqlServer.Management.Common; using Microsoft.SqlServer.Management.Smo; using Microsoft.SqlTools.Extensibility; using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; using Microsoft.SqlTools.ServiceLayer.ObjectExplorer; using Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Contracts; using Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes; using Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel; using Microsoft.SqlTools.ServiceLayer.UnitTests.Utility; using Moq; using Xunit; using Microsoft.SqlTools.ServiceLayer.Connection; namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ObjectExplorer { /// /// Tests covering basic operation of Node based classes /// public class NodeTests : ObjectExplorerTestBase { private string defaultOwnerUri = "objectexplorer://myserver"; private ServerInfo defaultServerInfo; private ConnectionDetails defaultConnectionDetails; private ConnectionCompleteParams defaultConnParams; private string fakeConnectionString = "Data Source=server;Initial Catalog=database;Integrated Security=False;User Id=user"; private ServerConnection serverConnection = null; public NodeTests() { defaultServerInfo = TestObjects.GetTestServerInfo(); serverConnection = new ServerConnection(new SqlConnection(fakeConnectionString)); defaultConnectionDetails = new ConnectionDetails() { DatabaseName = "master", ServerName = "localhost", UserName = "serverAdmin", Password = "..." }; defaultConnParams = new ConnectionCompleteParams() { ServerInfo = defaultServerInfo, ConnectionSummary = defaultConnectionDetails != null ? ((IConnectionSummary)defaultConnectionDetails).Clone(): null, OwnerUri = defaultOwnerUri }; // TODO can all tests use the standard service provider? ServiceProvider = ExtensionServiceProvider.CreateDefaultServiceProvider(); } [Fact] public void ServerNodeConstructorValidatesFields() { Assert.Throws(() => new ServerNode(null, ServiceProvider, serverConnection)); Assert.Throws(() => new ServerNode(defaultConnParams, null, serverConnection)); } [Fact] public void ServerNodeConstructorShouldSetValuesCorrectly() { // Given a server node with valid inputs ServerNode node = new ServerNode(defaultConnParams, ServiceProvider, serverConnection); // Then expect all fields set correctly Assert.False(node.IsAlwaysLeaf, "Server node should never be a leaf"); Assert.Equal(defaultConnectionDetails.ServerName, node.NodeValue); string expectedLabel = defaultConnectionDetails.ServerName + " (SQL Server " + defaultServerInfo.ServerVersion + " - " + defaultConnectionDetails.UserName + ")"; Assert.Equal(expectedLabel, node.Label); Assert.Equal(NodeTypes.Server.ToString(), node.NodeType); string[] nodePath = node.GetNodePath().Split(TreeNode.PathPartSeperator); Assert.Equal(1, nodePath.Length); Assert.Equal(defaultConnectionDetails.ServerName, nodePath[0]); } [Fact] public void ServerNodeLabelShouldIgnoreUserNameIfEmptyOrNull() { // Given no username set ConnectionSummary integratedAuthSummary = new ConnectionSummary() { DatabaseName = defaultConnectionDetails.DatabaseName, ServerName = defaultConnectionDetails.ServerName, UserName = null }; ConnectionCompleteParams connParams = new ConnectionCompleteParams() { ConnectionSummary = integratedAuthSummary, ServerInfo = defaultServerInfo, OwnerUri = defaultOwnerUri }; // When querying label string label = new ServerNode(connParams, ServiceProvider, serverConnection).Label; // Then only server name and version shown string expectedLabel = defaultConnectionDetails.ServerName + " (SQL Server " + defaultServerInfo.ServerVersion + ")"; Assert.Equal(expectedLabel, label); } [Fact] public void ServerNodeConstructorShouldShowDbNameForCloud() { defaultServerInfo.IsCloud = true; // Given a server node for a cloud DB, with master name ServerNode node = new ServerNode(defaultConnParams, ServiceProvider, serverConnection); // Then expect label to not include db name string expectedLabel = defaultConnectionDetails.ServerName + " (SQL Server " + defaultServerInfo.ServerVersion + " - " + defaultConnectionDetails.UserName + ")"; Assert.Equal(expectedLabel, node.Label); // But given a server node for a cloud DB that's not master defaultConnectionDetails.DatabaseName = "NotMaster"; defaultConnParams.ConnectionSummary.DatabaseName = defaultConnectionDetails.DatabaseName; node = new ServerNode(defaultConnParams, ServiceProvider, serverConnection); // Then expect label to include db name expectedLabel = defaultConnectionDetails.ServerName + " (SQL Server " + defaultServerInfo.ServerVersion + " - " + defaultConnectionDetails.UserName + ", " + defaultConnectionDetails.DatabaseName + ")"; Assert.Equal(expectedLabel, node.Label); } [Fact] public void ToNodeInfoIncludeAllFields() { // Given a server connection ServerNode node = new ServerNode(defaultConnParams, ServiceProvider, serverConnection); // When converting to NodeInfo NodeInfo info = node.ToNodeInfo(); // Then all fields should match Assert.Equal(node.IsAlwaysLeaf, info.IsLeaf); Assert.Equal(node.Label, info.Label); Assert.Equal(node.NodeType, info.NodeType); string[] nodePath = node.GetNodePath().Split(TreeNode.PathPartSeperator); string[] nodeInfoPathParts = info.NodePath.Split(TreeNode.PathPartSeperator); Assert.Equal(nodePath.Length, nodeInfoPathParts.Length); for (int i = 0; i < nodePath.Length; i++) { Assert.Equal(nodePath[i], nodeInfoPathParts[i]); } } [Fact] public void AddChildShouldSetParent() { TreeNode parent = new TreeNode("parent"); TreeNode child = new TreeNode("child"); Assert.Null(child.Parent); parent.AddChild(child); Assert.Equal(parent, child.Parent); } [Fact] public void GetChildrenShouldReturnReadonlyList() { TreeNode node = new TreeNode("parent"); IList children = node.GetChildren(); Assert.Throws(() => children.Add(new TreeNode("child"))); } [Fact] public void GetChildrenShouldReturnAddedNodesInOrder() { TreeNode parent = new TreeNode("parent"); TreeNode[] expectedKids = new TreeNode[] { new TreeNode("1"), new TreeNode("2") }; foreach (TreeNode child in expectedKids) { parent.AddChild(child); } IList children = parent.GetChildren(); Assert.Equal(expectedKids.Length, children.Count); for (int i = 0; i < expectedKids.Length; i++) { Assert.Equal(expectedKids[i], children[i]); } } [Fact] public void MultiLevelTreeShouldFormatPath() { TreeNode root = new TreeNode("root"); Assert.Equal("root" , root.GetNodePath()); TreeNode level1Child1 = new TreeNode("L1C1 (with extra info)"); level1Child1.NodePathName = "L1C1"; TreeNode level1Child2 = new TreeNode("L1C2"); root.AddChild(level1Child1); root.AddChild(level1Child2); Assert.Equal("root/L1C1" , level1Child1.GetNodePath()); Assert.Equal("root/L1C2", level1Child2.GetNodePath()); TreeNode level2Child1 = new TreeNode("L2C2"); level1Child1.AddChild(level2Child1); Assert.Equal("root/L1C1/L2C2", level2Child1.GetNodePath()); } [Fact] public void ServerNodeContextShouldIncludeServer() { // given a successful Server creation Server smoServer = new Server(new ServerConnection(new SqlConnection(fakeConnectionString))); ServerNode node = SetupServerNodeWithServer(smoServer); // When I get the context for a ServerNode var context = node.GetContextAs(); // Then I expect it to contain the server I created Assert.NotNull(context); Assert.Equal(smoServer, context.Server); // And the server should be the parent Assert.Equal(smoServer, context.Parent); Assert.Null(context.Database); } [Fact] public void ServerNodeContextShouldSetErrorMessageIfSqlConnectionIsNull() { // given a connectionInfo with no SqlConnection to use for queries Server smoServer = null; ServerNode node = SetupServerNodeWithServer(smoServer); // When I get the context for a ServerNode var context = node.GetContextAs(); // Then I expect it to be in an error state Assert.Null(context); } [Fact] public void ServerNodeContextShouldSetErrorMessageIfConnFailureExceptionThrown() { // given a connectionInfo with no SqlConnection to use for queries Server smoServer = new Server(new ServerConnection(new SqlConnection(fakeConnectionString))); string expectedMsg = "ConnFailed!"; ServerNode node = SetupServerNodeWithExceptionCreator(new ConnectionFailureException(expectedMsg)); // When I get the context for a ServerNode var context = node.GetContextAs(); // Then I expect it to be in an error state Assert.Null(context); Assert.Equal( string.Format(CultureInfo.CurrentCulture, SR.TreeNodeError, expectedMsg), node.ErrorStateMessage); } [Fact] public void ServerNodeContextShouldSetErrorMessageIfExceptionThrown() { // given a connectionInfo with no SqlConnection to use for queries Server smoServer = new Server(new ServerConnection(new SqlConnection(fakeConnectionString))); string expectedMsg = "Failed!"; ServerNode node = SetupServerNodeWithExceptionCreator(new Exception(expectedMsg)); // When I get the context for a ServerNode var context = node.GetContextAs(); // Then I expect it to be in an error state Assert.Null(context); Assert.Equal( string.Format(CultureInfo.CurrentCulture, SR.TreeNodeError, expectedMsg), node.ErrorStateMessage); } [Fact] public void QueryContextShouldNotCallOpenOnAlreadyOpenConnection() { // given a server connection that will state its connection is open Server smoServer = new Server(new ServerConnection(new SqlConnection(fakeConnectionString))); Mock wrapper = SetupSmoWrapperForIsOpenTest(smoServer, isOpen: true); SmoQueryContext context = new SmoQueryContext(smoServer, ServiceProvider, wrapper.Object); // when I access the Server property Server actualServer = context.Server; // Then I do not expect to have open called Assert.NotNull(actualServer); wrapper.Verify(c => c.OpenConnection(It.IsAny()), Times.Never); } private Mock SetupSmoWrapperForIsOpenTest(Server smoServer, bool isOpen) { Mock wrapper = new Mock(); int count = 0; wrapper.Setup(c => c.CreateServer(It.IsAny())) .Returns(() => smoServer); wrapper.Setup(c => c.IsConnectionOpen(It.IsAny())) .Returns(() => isOpen); wrapper.Setup(c => c.OpenConnection(It.IsAny())) .Callback(() => count++) .Verifiable(); return wrapper; } [Fact] public void QueryContextShouldReopenClosedConnectionWhenGettingServer() { // given a server connection that will state its connection is closed Server smoServer = new Server(new ServerConnection(new SqlConnection(fakeConnectionString))); Mock wrapper = SetupSmoWrapperForIsOpenTest(smoServer, isOpen: false); SmoQueryContext context = new SmoQueryContext(smoServer, ServiceProvider, wrapper.Object); // when I access the Server property Server actualServer = context.Server; // Then I expect to have open called Assert.NotNull(actualServer); wrapper.Verify(c => c.OpenConnection(It.IsAny()), Times.Once); } [Fact] public void QueryContextShouldReopenClosedConnectionWhenGettingParent() { // given a server connection that will state its connection is closed Server smoServer = new Server(new ServerConnection(new SqlConnection(fakeConnectionString))); Mock wrapper = SetupSmoWrapperForIsOpenTest(smoServer, isOpen: false); SmoQueryContext context = new SmoQueryContext(smoServer, ServiceProvider, wrapper.Object); context.Parent = smoServer; // when I access the Parent property SmoObjectBase actualParent = context.Parent; // Then I expect to have open called Assert.NotNull(actualParent); wrapper.Verify(c => c.OpenConnection(It.IsAny()), Times.Once); } private ConnectionService SetupAndRegisterTestConnectionService() { ConnectionService connService = TestObjects.GetTestConnectionService(); ConnectionInfo connectionInfo = new ConnectionInfo(TestObjects.GetTestSqlConnectionFactory(), defaultOwnerUri, defaultConnectionDetails); connectionInfo.AddConnection("Default", new SqlConnection()); connService.OwnerToConnectionMap.Add(defaultOwnerUri, connectionInfo); ServiceProvider.RegisterSingleService(connService); return connService; } private ServerNode SetupServerNodeWithServer(Server smoServer) { Mock creator = new Mock(); creator.Setup(c => c.CreateServer(It.IsAny())) .Returns(() => smoServer); creator.Setup(c => c.IsConnectionOpen(It.IsAny())) .Returns(() => true); ServerNode node = SetupServerNodeWithCreator(creator.Object); return node; } private ServerNode SetupServerNodeWithExceptionCreator(Exception ex) { Mock creator = new Mock(); creator.Setup(c => c.CreateServer(It.IsAny())) .Throws(ex); creator.Setup(c => c.IsConnectionOpen(It.IsAny())) .Returns(() => false); ServerNode node = SetupServerNodeWithCreator(creator.Object); return node; } private ServerNode SetupServerNodeWithCreator(SmoWrapper creator) { ServerNode node = new ServerNode(defaultConnParams, ServiceProvider, new ServerConnection(new SqlConnection(fakeConnectionString))); node.SmoWrapper = creator; return node; } [Fact] public void ServerNodeChildrenShouldIncludeFoldersAndDatabases() { // Given a server with 1 database SetupAndRegisterTestConnectionService(); ServiceProvider.RegisterSingleService(new ObjectExplorerService()); string dbName = "DB1"; Mock smoObjectMock = new Mock(); smoObjectMock.SetupGet(s => s.Name).Returns(dbName); Mock querierMock = new Mock(); querierMock.Setup(q => q.Query(It.IsAny(), It.IsAny(), false, It.IsAny>())) .Returns(smoObjectMock.Object.SingleItemAsEnumerable()); ServiceProvider.Register(() => new[] { querierMock.Object }); Server smoServer = new Server(new ServerConnection(new SqlConnection(fakeConnectionString))); ServerNode node = SetupServerNodeWithServer(smoServer); // When I populate its children IList children = node.Expand(new CancellationToken()); // Then I expect it to contain server-level folders Assert.Equal(3, children.Count); VerifyTreeNode(children[0], "Folder", SR.SchemaHierarchy_Databases); VerifyTreeNode(children[1], "Folder", SR.SchemaHierarchy_Security); VerifyTreeNode(children[2], "Folder", SR.SchemaHierarchy_ServerObjects); // And the database is contained under it TreeNode databases = children[0]; IList dbChildren = databases.Expand(new CancellationToken()); Assert.Equal(2, dbChildren.Count); Assert.Equal(SR.SchemaHierarchy_SystemDatabases, dbChildren[0].NodeValue); TreeNode dbNode = dbChildren[1]; Assert.Equal(dbName, dbNode.NodeValue); Assert.Equal(dbName, dbNode.Label); Assert.False(dbNode.IsAlwaysLeaf); // Note: would like to verify Database in the context, but cannot since it's a Sealed class and isn't easily mockable } private void VerifyTreeNode(TreeNode node, string nodeType, string folderValue) where T : TreeNode { T nodeAsT = node as T; Assert.NotNull(nodeAsT); Assert.Equal(nodeType, nodeAsT.NodeType); Assert.Equal(folderValue, nodeAsT.NodeValue); } } }