From 5b110aefadc7ee2bf16cc0fcc4a825284adca8b3 Mon Sep 17 00:00:00 2001 From: Gabriela Trutan Date: Wed, 11 Dec 2024 09:44:24 +0100 Subject: [PATCH] SLVS-1377 Implement unbind in Binding Dialog (#5880) --- .../UnintrusiveBindingControllerTests.cs | 107 +++++++++++------- .../ManageBindingViewModelTests.cs | 77 ++++++++++++- .../Binding/IUnintrusiveBindingController.cs | 16 ++- .../UI/ManageBinding/ManageBindingDialog.xaml | 4 +- .../ManageBinding/ManageBindingDialog.xaml.cs | 4 +- .../ManageBinding/ManageBindingViewModel.cs | 75 ++++++++---- src/ConnectedMode/UI/Resources/Styles.xaml | 2 +- .../UI/Resources/UiResources.Designer.cs | 27 +++++ .../UI/Resources/UiResources.resx | 9 ++ 9 files changed, 249 insertions(+), 72 deletions(-) diff --git a/src/ConnectedMode.UnitTests/Binding/UnintrusiveBindingControllerTests.cs b/src/ConnectedMode.UnitTests/Binding/UnintrusiveBindingControllerTests.cs index 66e60b69e..2192a3228 100644 --- a/src/ConnectedMode.UnitTests/Binding/UnintrusiveBindingControllerTests.cs +++ b/src/ConnectedMode.UnitTests/Binding/UnintrusiveBindingControllerTests.cs @@ -21,7 +21,6 @@ using System.Security; using SonarLint.VisualStudio.ConnectedMode.Binding; using SonarLint.VisualStudio.ConnectedMode.Persistence; -using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; using SonarLint.VisualStudio.TestInfrastructure; using SonarQube.Client; @@ -35,58 +34,66 @@ namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Binding; public class UnintrusiveBindingControllerTests { private static readonly CancellationToken ACancellationToken = CancellationToken.None; - private static readonly BasicAuthCredentials ValidToken = new ("TOKEN", new SecureString()); - private static readonly BoundServerProject AnyBoundProject = new ("any", "any", new ServerConnection.SonarCloud("any", credentials: ValidToken)); + private static readonly BasicAuthCredentials ValidToken = new("TOKEN", new SecureString()); + private static readonly BoundServerProject AnyBoundProject = new("any", "any", new ServerConnection.SonarCloud("any", credentials: ValidToken)); + private IActiveSolutionChangedHandler activeSolutionChangedHandler; + private IBindingProcess bindingProcess; + private IBindingProcessFactory bindingProcessFactory; + private ISonarQubeService sonarQubeService; + private UnintrusiveBindingController testSubject; + private ISolutionBindingRepository solutionBindingRepository; + + [TestInitialize] + public void TestInitialize() + { + CreateBindingProcessFactory(); + sonarQubeService = Substitute.For(); + activeSolutionChangedHandler = Substitute.For(); + solutionBindingRepository = Substitute.For(); + testSubject = new UnintrusiveBindingController(bindingProcessFactory, sonarQubeService, activeSolutionChangedHandler, solutionBindingRepository); + } [TestMethod] - public void MefCtor_CheckTypeIsNonShared() - => MefTestHelpers.CheckIsNonSharedMefComponent(); + public void MefCtor_CheckTypeIsNonShared() => MefTestHelpers.CheckIsNonSharedMefComponent(); [TestMethod] - public void MefCtor_IUnintrusiveBindingController_CheckIsExported() - { + public void MefCtor_IUnintrusiveBindingController_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported( MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + [TestMethod] - public void MefCtor_IBindingController_CheckIsExported() - { + public void MefCtor_IBindingController_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported( MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); [TestMethod] public async Task BindAsync_EstablishesConnection() { - var sonarQubeService = Substitute.For(); var projectToBind = new BoundServerProject( - "local-key", + "local-key", "server-key", new ServerConnection.SonarCloud("organization", credentials: ValidToken)); - var testSubject = CreateTestSubject(sonarQubeService: sonarQubeService); - + await testSubject.BindAsync(projectToBind, ACancellationToken); await sonarQubeService .Received() .ConnectAsync( - Arg.Is(x => x.ServerUri.Equals("https://sonarcloud.io/") + Arg.Is(x => x.ServerUri.Equals("https://sonarcloud.io/") && x.UserName.Equals(ValidToken.UserName) && string.IsNullOrEmpty(x.Password.ToUnsecureString())), ACancellationToken); } - + [TestMethod] public async Task BindAsync_NotifiesBindingChanged() { - var activeSolutionChangedHandler = Substitute.For(); - var testSubject = CreateTestSubject(activeSolutionChangedHandler: activeSolutionChangedHandler); - await testSubject.BindAsync(AnyBoundProject, ACancellationToken); activeSolutionChangedHandler @@ -98,10 +105,7 @@ public async Task BindAsync_NotifiesBindingChanged() public async Task BindAsync_CallsBindingProcessInOrder() { var cancellationToken = CancellationToken.None; - var bindingProcess = Substitute.For(); - var bindingProcessFactory = CreateBindingProcessFactory(bindingProcess); - var testSubject = CreateTestSubject(bindingProcessFactory); - + await testSubject.BindAsync(AnyBoundProject, null, cancellationToken); Received.InOrder(() => @@ -111,25 +115,50 @@ public async Task BindAsync_CallsBindingProcessInOrder() bindingProcess.SaveServerExclusionsAsync(cancellationToken); }); } - - private UnintrusiveBindingController CreateTestSubject(IBindingProcessFactory bindingProcessFactory = null, - ISonarQubeService sonarQubeService = null, - IActiveSolutionChangedHandler activeSolutionChangedHandler = null) + + [TestMethod] + public void Unbind_BindingDeletionSucceeded_HandlesBindingChangesAndDisconnects() + { + solutionBindingRepository.DeleteBinding(AnyBoundProject.LocalBindingKey).Returns(true); + + testSubject.Unbind(AnyBoundProject.LocalBindingKey); + + Received.InOrder(() => + { + solutionBindingRepository.DeleteBinding(AnyBoundProject.LocalBindingKey); + sonarQubeService.Disconnect(); + activeSolutionChangedHandler.HandleBindingChange(true); + }); + } + + [TestMethod] + public void Unbind_BindingDeletionFailed_DoesNotCallHandleBindingChange() { - var testSubject = new UnintrusiveBindingController(bindingProcessFactory ?? CreateBindingProcessFactory(), - sonarQubeService ?? Substitute.For(), - activeSolutionChangedHandler ?? Substitute.For()); + solutionBindingRepository.DeleteBinding(AnyBoundProject.LocalBindingKey).Returns(false); + + testSubject.Unbind(AnyBoundProject.LocalBindingKey); - return testSubject; + solutionBindingRepository.Received(1).DeleteBinding(AnyBoundProject.LocalBindingKey); + activeSolutionChangedHandler.DidNotReceive().HandleBindingChange(true); } - private static IBindingProcessFactory CreateBindingProcessFactory(IBindingProcess bindingProcess = null) + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void Unbind_ReturnsResultOfDeletedBinding(bool expectedResult) + { + solutionBindingRepository.DeleteBinding(AnyBoundProject.LocalBindingKey).Returns(expectedResult); + + var result = testSubject.Unbind(AnyBoundProject.LocalBindingKey); + + result.Should().Be(expectedResult); + } + + private void CreateBindingProcessFactory() { bindingProcess ??= Substitute.For(); - var bindingProcessFactory = Substitute.For(); + bindingProcessFactory = Substitute.For(); bindingProcessFactory.Create(Arg.Any()).Returns(bindingProcess); - - return bindingProcessFactory; } } diff --git a/src/ConnectedMode.UnitTests/UI/ManageBinding/ManageBindingViewModelTests.cs b/src/ConnectedMode.UnitTests/UI/ManageBinding/ManageBindingViewModelTests.cs index 9c9d52f88..51fd618ad 100644 --- a/src/ConnectedMode.UnitTests/UI/ManageBinding/ManageBindingViewModelTests.cs +++ b/src/ConnectedMode.UnitTests/UI/ManageBinding/ManageBindingViewModelTests.cs @@ -32,6 +32,7 @@ using SonarLint.VisualStudio.ConnectedMode.UI.Resources; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; +using SonarLint.VisualStudio.TestInfrastructure; namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.UI.ManageBinding; @@ -199,22 +200,84 @@ public void SelectedConnection_Set_RaisesEvents() } [TestMethod] - public void Unbind_SetsBoundProjectToNull() + public async Task UnbindWithProgressAsync_BindsProjectAndReportsProgress() + { + await testSubject.UnbindWithProgressAsync(); + + await progressReporterViewModel.Received(1) + .ExecuteTaskWithProgressAsync( + Arg.Is>(x => + x.TaskToPerform == testSubject.UnbindAsync && + x.ProgressStatus == UiResources.UnbindingInProgressText && + x.WarningText == UiResources.UnbindingFailedText && + x.AfterProgressUpdated == testSubject.OnProgressUpdated && + x.AfterSuccess == testSubject.AfterUnbind)); + } + + [TestMethod] + public async Task UnbindAsync_UnbindsOnUIThread() + { + await testSubject.UnbindAsync(); + + await threadHandling.Received(1).RunOnUIThreadAsync(Arg.Any()); + } + + [TestMethod] + public async Task UnbindAsync_UnbindsCurrentSolution() + { + await InitializeBoundProject(); + connectedModeServices.ThreadHandling.Returns(new NoOpThreadHandler()); + + await testSubject.UnbindAsync(); + + connectedModeBindingServices.BindingController.Received(1).Unbind(testSubject.SolutionInfo.Name); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public async Task UnbindAsync_ReturnsResponseOfUnbinding(bool expectedResponse) + { + await InitializeBoundProject(); + connectedModeServices.ThreadHandling.Returns(new NoOpThreadHandler()); + connectedModeBindingServices.BindingController.Unbind(Arg.Any()).Returns(expectedResponse); + + var adapterResponse = await testSubject.UnbindAsync(); + + adapterResponse.Success.Should().Be(expectedResponse); + } + + [TestMethod] + public async Task UnbindAsync_UnbindingThrows_ReturnsFalse() + { + var exceptionMsg = "Failed to load connections"; + var mockedThreadHandling = Substitute.For(); + connectedModeServices.ThreadHandling.Returns(mockedThreadHandling); + mockedThreadHandling.When(x => x.RunOnUIThreadAsync(Arg.Any())).Do(_ => throw new Exception(exceptionMsg)); + + var adapterResponse = await testSubject.UnbindAsync(); + + adapterResponse.Success.Should().BeFalse(); + logger.Received(1).WriteLine(exceptionMsg); + } + + [TestMethod] + public void AfterUnbind_SetsBoundProjectToNull() { testSubject.BoundProject = serverProject; - testSubject.Unbind(); + testSubject.AfterUnbind(new AdapterResponse(true)); testSubject.BoundProject.Should().BeNull(); } [TestMethod] - public void Unbind_SetsConnectionInfoToNull() + public void AfterUnbind_SetsConnectionInfoToNull() { testSubject.SelectedConnectionInfo = sonarQubeConnectionInfo; testSubject.SelectedProject = serverProject; - testSubject.Unbind(); + testSubject.AfterUnbind(new AdapterResponse(true)); testSubject.SelectedConnectionInfo.Should().BeNull(); testSubject.SelectedProject.Should().BeNull(); @@ -1025,4 +1088,10 @@ private void MockGetServerProjectByKey(bool success, ServerProject responseData) .Returns(Task.FromResult(new AdapterResponseWithData(success, responseData))); connectedModeServices.SlCoreConnectionAdapter.Returns(slCoreConnectionAdapter); } + + private async Task InitializeBoundProject() + { + SetupBoundProject(new ServerConnection.SonarCloud("my org"), serverProject); + await testSubject.DisplayBindStatusAsync(); + } } diff --git a/src/ConnectedMode/Binding/IUnintrusiveBindingController.cs b/src/ConnectedMode/Binding/IUnintrusiveBindingController.cs index ef1b2b575..96d243289 100644 --- a/src/ConnectedMode/Binding/IUnintrusiveBindingController.cs +++ b/src/ConnectedMode/Binding/IUnintrusiveBindingController.cs @@ -28,6 +28,7 @@ namespace SonarLint.VisualStudio.ConnectedMode.Binding public interface IBindingController { Task BindAsync(BoundServerProject project, CancellationToken cancellationToken); + bool Unbind(string localBindingKey); } internal interface IUnintrusiveBindingController @@ -43,13 +44,15 @@ internal class UnintrusiveBindingController : IUnintrusiveBindingController, IBi private readonly IBindingProcessFactory bindingProcessFactory; private readonly ISonarQubeService sonarQubeService; private readonly IActiveSolutionChangedHandler activeSolutionChangedHandler; + private readonly ISolutionBindingRepository solutionBindingRepository; [ImportingConstructor] - public UnintrusiveBindingController(IBindingProcessFactory bindingProcessFactory, ISonarQubeService sonarQubeService, IActiveSolutionChangedHandler activeSolutionChangedHandler) + public UnintrusiveBindingController(IBindingProcessFactory bindingProcessFactory, ISonarQubeService sonarQubeService, IActiveSolutionChangedHandler activeSolutionChangedHandler, ISolutionBindingRepository solutionBindingRepository) { this.bindingProcessFactory = bindingProcessFactory; this.sonarQubeService = sonarQubeService; this.activeSolutionChangedHandler = activeSolutionChangedHandler; + this.solutionBindingRepository = solutionBindingRepository; } public async Task BindAsync(BoundServerProject project, CancellationToken cancellationToken) @@ -67,6 +70,17 @@ public async Task BindAsync(BoundServerProject project, IProgress