diff --git a/src/ConnectedMode.UnitTests/Persistence/SolutionBindingRepositoryTests.cs b/src/ConnectedMode.UnitTests/Persistence/SolutionBindingRepositoryTests.cs index 72d5b7ce8..7ad191e61 100644 --- a/src/ConnectedMode.UnitTests/Persistence/SolutionBindingRepositoryTests.cs +++ b/src/ConnectedMode.UnitTests/Persistence/SolutionBindingRepositoryTests.cs @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using System.Windows; using SonarLint.VisualStudio.ConnectedMode.Binding; using SonarLint.VisualStudio.ConnectedMode.Persistence; using SonarLint.VisualStudio.Core; @@ -297,14 +298,37 @@ public void DeleteBinding_DeletesBindingDirectoryOfBindingFile() [DataRow(false)] public void DeleteBinding_ReturnsResultOfDeleteBindingDirectory(bool expectedResult) { - unintrusiveBindingPathProvider.GetBindingPath(LocalBindingKey).Returns(MockFilePath); - solutionBindingFileLoader.DeleteBindingDirectory(MockFilePath).Returns(expectedResult); + MockDeletingBindingDirectory(LocalBindingKey, expectedResult); var result = testSubject.DeleteBinding(LocalBindingKey); result.Should().Be(expectedResult); } + [TestMethod] + public void DeleteBinding_DirectoryNotDeleted_EventNotTriggered() + { + var eventHandler = Substitute.For>(); + testSubject.BindingDeleted += eventHandler; + MockDeletingBindingDirectory(LocalBindingKey, deleted:false); + + testSubject.DeleteBinding(LocalBindingKey); + + eventHandler.DidNotReceiveWithAnyArgs().Invoke(default, default); + } + + [TestMethod] + public void DeleteBinding_DirectoryDeleted_EventTriggered() + { + var eventHandler = Substitute.For>(); + testSubject.BindingDeleted += eventHandler; + MockDeletingBindingDirectory(LocalBindingKey, deleted: true); + + testSubject.DeleteBinding(LocalBindingKey); + + eventHandler.Received(1).Invoke(testSubject, Arg.Is(x => x.LocalBindingKey == LocalBindingKey)); + } + private BoundServerProject SetUpBinding(string solution, ServerConnection connection, string bindingConfig) { var dto = new BindingJsonModel { ServerConnectionId = connection?.Id }; @@ -329,4 +353,10 @@ private void SetUpConnections(params ServerConnection[] connections) => }); private void SetUpUnintrusiveBindingPathProvider(params string[] bindigFolders) => unintrusiveBindingPathProvider.GetBindingPaths().Returns(bindigFolders); + + private void MockDeletingBindingDirectory(string localBindingKey, bool deleted) + { + unintrusiveBindingPathProvider.GetBindingPath(localBindingKey).Returns(MockFilePath); + solutionBindingFileLoader.DeleteBindingDirectory(MockFilePath).Returns(deleted); + } } diff --git a/src/ConnectedMode.UnitTests/UI/DeleteConnection/DeleteConnectionDialogViewModelTests.cs b/src/ConnectedMode.UnitTests/UI/DeleteConnection/DeleteConnectionDialogViewModelTests.cs index 887bfedd3..bb9a54282 100644 --- a/src/ConnectedMode.UnitTests/UI/DeleteConnection/DeleteConnectionDialogViewModelTests.cs +++ b/src/ConnectedMode.UnitTests/UI/DeleteConnection/DeleteConnectionDialogViewModelTests.cs @@ -31,7 +31,7 @@ public class DeleteConnectionDialogViewModelTests [TestMethod] public void Ctor_SetsProperties() { - var projectsToUnbind = Substitute.For>(); + var projectsToUnbind = Substitute.For>(); var connectionInfo = new ConnectionInfo(default, default); var testSubject = new DeleteConnectionDialogViewModel(projectsToUnbind, connectionInfo); @@ -42,7 +42,7 @@ public void Ctor_SetsProperties() [DataTestMethod] public void DisplayProjectList_MultipleProjectsToUnbind_ReturnsTrue() { - var projects = new[] { new ConnectedModeProject(new ServerProject("proj key", "proj name"), new SolutionInfoModel("my sol", SolutionType.Folder)) }; + var projects = new[] { "proj key", "my sol" }; var testSubject = new DeleteConnectionDialogViewModel(projects, new ConnectionInfo(default, default)); testSubject.DisplayProjectList.Should().BeTrue(); diff --git a/src/ConnectedMode.UnitTests/UI/DeleteConnection/PreventDeleteConnectionViewModelTests.cs b/src/ConnectedMode.UnitTests/UI/DeleteConnection/PreventDeleteConnectionViewModelTests.cs deleted file mode 100644 index 76e22efa6..000000000 --- a/src/ConnectedMode.UnitTests/UI/DeleteConnection/PreventDeleteConnectionViewModelTests.cs +++ /dev/null @@ -1,65 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2024 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.UI.DeleteConnection; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.UI.DeleteConnection; - -[TestClass] -public class PreventDeleteConnectionViewModelTests -{ - [TestMethod] - public void Ctor_SetsProperties() - { - var projectsToUnbind = Substitute.For>(); - var connectionInfo = new ConnectionInfo(default, default); - - var testSubject = new PreventDeleteConnectionViewModel(projectsToUnbind, connectionInfo); - - testSubject.ConnectionInfo.Should().BeSameAs(connectionInfo); - testSubject.ProjectsToUnbind.Should().BeSameAs(projectsToUnbind); - } - - [DataTestMethod] - public void DisplayProjectList_MultipleProjectsToUnbind_ReturnsTrue() - { - var projects = new[] { "binding1", "binding2"}; - - var testSubject = new PreventDeleteConnectionViewModel(projects, new ConnectionInfo(default, default)); - - testSubject.DisplayProjectList.Should().BeTrue(); - } - - [DataTestMethod] - public void DisplayProjectList_ProjectsIsNull_ReturnsFalse() - { - var testSubject = new PreventDeleteConnectionViewModel(null, new ConnectionInfo(default, default)); - - testSubject.DisplayProjectList.Should().BeFalse(); - } - - [DataTestMethod] - public void DisplayProjectList_NoProjectsToUnbind_ReturnsFalse() - { - var testSubject = new PreventDeleteConnectionViewModel([], new ConnectionInfo(default, default)); - - testSubject.DisplayProjectList.Should().BeFalse(); - } -} diff --git a/src/ConnectedMode.UnitTests/UI/ManageConnections/ManageConnectionsViewModelTest.cs b/src/ConnectedMode.UnitTests/UI/ManageConnections/ManageConnectionsViewModelTest.cs index cb1354674..b30f36fec 100644 --- a/src/ConnectedMode.UnitTests/UI/ManageConnections/ManageConnectionsViewModelTest.cs +++ b/src/ConnectedMode.UnitTests/UI/ManageConnections/ManageConnectionsViewModelTest.cs @@ -43,6 +43,8 @@ public class ManageConnectionsViewModelTest private IBindingController bindingController; private IConnectedModeBindingServices connectedModeBindingServices; private ISolutionBindingRepository solutionBindingRepository; + private const string LocalBindingKey1 = "solution name 1"; + private const string LocalBindingKey2 = "solution name 2"; [TestInitialize] public void TestInitialize() @@ -81,7 +83,7 @@ public void InitializeConnectionViewModels_InitializesConnectionsCorrectly() [TestMethod] public async Task RemoveConnectionWithProgressAsync_InitializesDataAndReportsProgress() { - await testSubject.RemoveConnectionWithProgressAsync(new ConnectionViewModel(new Connection(new ConnectionInfo("myOrg", ConnectionServerType.SonarCloud)))); + await testSubject.RemoveConnectionWithProgressAsync([], new ConnectionViewModel(new Connection(new ConnectionInfo("myOrg", ConnectionServerType.SonarCloud)))); await progressReporterViewModel.Received(1) .ExecuteTaskWithProgressAsync( @@ -99,7 +101,7 @@ public void RemoveConnectionViewModel_ReturnsStatusFromSlCore(bool expectedStatu var connectionToRemove = testSubject.ConnectionViewModels[0]; serverConnectionsRepositoryAdapter.TryRemoveConnection(connectionToRemove.Connection.Info).Returns(expectedStatus); - var succeeded = testSubject.RemoveConnectionViewModel(connectionToRemove); + var succeeded = testSubject.RemoveConnectionViewModel([], connectionToRemove); succeeded.Should().Be(expectedStatus); serverConnectionsRepositoryAdapter.Received(1).TryRemoveConnection(connectionToRemove.Connection.Info); @@ -112,7 +114,7 @@ public void RemoveConnection_ConnectionWasRemoved_RemovesProvidedConnectionViewM var connectionToRemove = testSubject.ConnectionViewModels[0]; serverConnectionsRepositoryAdapter.TryRemoveConnection(connectionToRemove.Connection.Info).Returns(true); - testSubject.RemoveConnectionViewModel(connectionToRemove); + testSubject.RemoveConnectionViewModel([], connectionToRemove); testSubject.ConnectionViewModels.Count.Should().Be(twoConnections.Count - 1); testSubject.ConnectionViewModels.Should().NotContain(connectionToRemove); @@ -125,7 +127,7 @@ public void RemoveConnectionViewModel_ConnectionWasNotRemoved_DoesNotRemoveProvi var connectionToRemove = testSubject.ConnectionViewModels[0]; serverConnectionsRepositoryAdapter.TryRemoveConnection(connectionToRemove.Connection.Info).Returns(false); - testSubject.RemoveConnectionViewModel(connectionToRemove); + testSubject.RemoveConnectionViewModel([], connectionToRemove); testSubject.ConnectionViewModels.Count.Should().Be(twoConnections.Count); testSubject.ConnectionViewModels.Should().Contain(connectionToRemove); @@ -139,7 +141,7 @@ public void RemoveConnection_ConnectionWasRemoved_RaisesEvents() var eventHandler = Substitute.For(); testSubject.PropertyChanged += eventHandler; - testSubject.RemoveConnectionViewModel(testSubject.ConnectionViewModels[0]); + testSubject.RemoveConnectionViewModel([], testSubject.ConnectionViewModels[0]); eventHandler.Received().Invoke(testSubject, Arg.Is(x => x.PropertyName == nameof(testSubject.NoConnectionExists))); } @@ -152,11 +154,80 @@ public void RemoveConnectionViewModel_ConnectionWasNotRemoved_DoesNotRaiseEvents var eventHandler = Substitute.For(); testSubject.PropertyChanged += eventHandler; - testSubject.RemoveConnectionViewModel(testSubject.ConnectionViewModels[0]); + testSubject.RemoveConnectionViewModel([], testSubject.ConnectionViewModels[0]); eventHandler.DidNotReceive().Invoke(testSubject, Arg.Any()); } + [TestMethod] + public void RemoveConnectionViewModel_TwoBindingsExistForConnection_RemovesBindingsAndThenConnection() + { + InitializeTwoConnections(); + MockDeleteBinding(LocalBindingKey1, true); + MockDeleteBinding(LocalBindingKey2, true); + + testSubject.RemoveConnectionViewModel([LocalBindingKey1, LocalBindingKey2], testSubject.ConnectionViewModels[0]); + + Received.InOrder(() => + { + solutionBindingRepository.DeleteBinding(LocalBindingKey1); + solutionBindingRepository.DeleteBinding(LocalBindingKey2); + serverConnectionsRepositoryAdapter.TryRemoveConnection(testSubject.ConnectionViewModels[0].Connection.Info); + }); + } + + [TestMethod] + public void RemoveConnectionViewModel_TwoBindingsExistForConnection_DeletingOneBindingFails_DoesNotRemoveConnection() + { + InitializeTwoConnections(); + MockDeleteBinding(LocalBindingKey1, true); + MockDeleteBinding(LocalBindingKey2, false); + + testSubject.RemoveConnectionViewModel([LocalBindingKey1, LocalBindingKey2], testSubject.ConnectionViewModels[0]); + + Received.InOrder(() => + { + solutionBindingRepository.DeleteBinding(LocalBindingKey1); + solutionBindingRepository.DeleteBinding(LocalBindingKey2); + logger.WriteLine(UiResources.DeleteConnection_DeleteBindingFails, LocalBindingKey2); + }); + serverConnectionsRepositoryAdapter.DidNotReceive().TryRemoveConnection(testSubject.ConnectionViewModels[0].Connection.Info); + } + + [TestMethod] + public void RemoveConnectionViewModel_TwoBindingsExistForConnection_OneBindingIsForCurrentSolution_CallsUnbind() + { + InitializeTwoConnections(); + InitializeCurrentSolution(LocalBindingKey2); + MockDeleteBinding(LocalBindingKey1, true); + MockUnbind(LocalBindingKey2, true); + + testSubject.RemoveConnectionViewModel([LocalBindingKey1, LocalBindingKey2], testSubject.ConnectionViewModels[0]); + + Received.InOrder(() => + { + solutionBindingRepository.DeleteBinding(LocalBindingKey1); + bindingController.Unbind(LocalBindingKey2); + serverConnectionsRepositoryAdapter.TryRemoveConnection(testSubject.ConnectionViewModels[0].Connection.Info); + }); + solutionBindingRepository.DidNotReceive().DeleteBinding(LocalBindingKey2); + } + + [TestMethod] + public void RemoveConnectionViewModel_TwoBindingsExistForConnection_OneBindingIsForCurrentSolution_UnbindFails_DoesNotRemoveConnection() + { + InitializeTwoConnections(); + InitializeCurrentSolution(LocalBindingKey2); + MockDeleteBinding(LocalBindingKey1, true); + MockUnbind(LocalBindingKey2, false); + + testSubject.RemoveConnectionViewModel([LocalBindingKey1, LocalBindingKey2], testSubject.ConnectionViewModels[0]); + + solutionBindingRepository.Received(1).DeleteBinding(LocalBindingKey1); + bindingController.Received(1).Unbind(LocalBindingKey2); + serverConnectionsRepositoryAdapter.DidNotReceive().TryRemoveConnection(testSubject.ConnectionViewModels[0].Connection.Info); + } + [TestMethod] public async Task SafeExecuteActionAsync_LoadsConnectionsOnUIThread() { @@ -610,4 +681,10 @@ private static ServerConnection.SonarQube CreateSonarQubeServerConnection(Connec { return new ServerConnection.SonarQube(new Uri(sonarQube.Info.Id)); } + + private void MockDeleteBinding(string localBindingKey, bool success) => connectedModeBindingServices.SolutionBindingRepository.DeleteBinding(localBindingKey).Returns(success); + + private void MockUnbind(string localBindingKey, bool success) => connectedModeBindingServices.BindingController.Unbind(localBindingKey).Returns(success); + + private void InitializeCurrentSolution(string solutionName) => connectedModeBindingServices.SolutionInfoProvider.GetSolutionName().Returns(solutionName); } diff --git a/src/ConnectedMode/ConnectedMode.csproj b/src/ConnectedMode/ConnectedMode.csproj index f21c6d4b6..a455c17db 100644 --- a/src/ConnectedMode/ConnectedMode.csproj +++ b/src/ConnectedMode/ConnectedMode.csproj @@ -56,9 +56,6 @@ - - MSBuild:Compile - MSBuild:Compile diff --git a/src/ConnectedMode/Persistence/SolutionBindingRepository.cs b/src/ConnectedMode/Persistence/SolutionBindingRepository.cs index e67db54d4..16ae5d4be 100644 --- a/src/ConnectedMode/Persistence/SolutionBindingRepository.cs +++ b/src/ConnectedMode/Persistence/SolutionBindingRepository.cs @@ -103,10 +103,16 @@ public bool Write(string configFilePath, BoundServerProject binding) public bool DeleteBinding(string localBindingKey) { var bindingPath = unintrusiveBindingPathProvider.GetBindingPath(localBindingKey); - return solutionBindingFileLoader.DeleteBindingDirectory(bindingPath); + if (!solutionBindingFileLoader.DeleteBindingDirectory(bindingPath)) + { + return false; + } + BindingDeleted?.Invoke(this, new LocalBindingKeyEventArgs(localBindingKey)); + return true; } public event EventHandler BindingUpdated; + public event EventHandler BindingDeleted; public IEnumerable List() { diff --git a/src/ConnectedMode/UI/DeleteConnection/DeleteConnectionDialog.xaml b/src/ConnectedMode/UI/DeleteConnection/DeleteConnectionDialog.xaml index fba84dea3..a2a17703f 100644 --- a/src/ConnectedMode/UI/DeleteConnection/DeleteConnectionDialog.xaml +++ b/src/ConnectedMode/UI/DeleteConnection/DeleteConnectionDialog.xaml @@ -57,18 +57,6 @@ Margin="0, 10" ItemContainerStyle="{StaticResource NoSelectionListBoxItemStyle}" ItemsSource="{Binding ProjectsToUnbind}"> - - - - - - - - - - diff --git a/src/ConnectedMode/UI/DeleteConnection/DeleteConnectionDialog.xaml.cs b/src/ConnectedMode/UI/DeleteConnection/DeleteConnectionDialog.xaml.cs index add410566..35e306436 100644 --- a/src/ConnectedMode/UI/DeleteConnection/DeleteConnectionDialog.xaml.cs +++ b/src/ConnectedMode/UI/DeleteConnection/DeleteConnectionDialog.xaml.cs @@ -28,7 +28,7 @@ public partial class DeleteConnectionDialog : Window { public DeleteConnectionDialogViewModel ViewModel { get; } - public DeleteConnectionDialog(IReadOnlyList projectsToUnbind, ConnectionInfo connectionInfo) + public DeleteConnectionDialog(IReadOnlyList projectsToUnbind, ConnectionInfo connectionInfo) { ViewModel = new DeleteConnectionDialogViewModel(projectsToUnbind, connectionInfo); InitializeComponent(); diff --git a/src/ConnectedMode/UI/DeleteConnection/DeleteConnectionDialogViewModel.cs b/src/ConnectedMode/UI/DeleteConnection/DeleteConnectionDialogViewModel.cs index 1f11bd0f0..29039b2b3 100644 --- a/src/ConnectedMode/UI/DeleteConnection/DeleteConnectionDialogViewModel.cs +++ b/src/ConnectedMode/UI/DeleteConnection/DeleteConnectionDialogViewModel.cs @@ -24,13 +24,13 @@ namespace SonarLint.VisualStudio.ConnectedMode.UI.DeleteConnection; public class DeleteConnectionDialogViewModel : ViewModelBase { - public DeleteConnectionDialogViewModel(IReadOnlyList projectsToUnbind, ConnectionInfo connectionInfo) + public DeleteConnectionDialogViewModel(IReadOnlyList projectsToUnbind, ConnectionInfo connectionInfo) { ProjectsToUnbind = projectsToUnbind; ConnectionInfo = connectionInfo; } - public IReadOnlyList ProjectsToUnbind { get; } + public IReadOnlyList ProjectsToUnbind { get; } public ConnectionInfo ConnectionInfo { get; } public bool DisplayProjectList => ProjectsToUnbind is not null && ProjectsToUnbind.Count > 0; diff --git a/src/ConnectedMode/UI/DeleteConnection/PreventDeleteConnectionDialog.xaml b/src/ConnectedMode/UI/DeleteConnection/PreventDeleteConnectionDialog.xaml deleted file mode 100644 index b8f789213..000000000 --- a/src/ConnectedMode/UI/DeleteConnection/PreventDeleteConnectionDialog.xaml +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - -