diff --git a/src/ConnectedMode.UnitTests/UI/ConnectedModeBindingServicesTests.cs b/src/ConnectedMode.UnitTests/UI/ConnectedModeBindingServicesTests.cs new file mode 100644 index 000000000..61b741097 --- /dev/null +++ b/src/ConnectedMode.UnitTests/UI/ConnectedModeBindingServicesTests.cs @@ -0,0 +1,40 @@ +/* + * 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.Core; +using SonarLint.VisualStudio.TestInfrastructure; +using SonarLint.VisualStudio.ConnectedMode.UI; +using SonarLint.VisualStudio.ConnectedMode.Shared; +using SonarLint.VisualStudio.ConnectedMode.Binding; + +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.UI; + +[TestClass] +public class ConnectedModeBindingServicesTests +{ + [TestMethod] + public void MefCtor_CheckIsExported() + { + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + } +} diff --git a/src/ConnectedMode.UnitTests/UI/ConnectedModeServicesTests.cs b/src/ConnectedMode.UnitTests/UI/ConnectedModeServicesTests.cs index 9dd7fab8b..333fd1d92 100644 --- a/src/ConnectedMode.UnitTests/UI/ConnectedModeServicesTests.cs +++ b/src/ConnectedMode.UnitTests/UI/ConnectedModeServicesTests.cs @@ -22,7 +22,6 @@ using SonarLint.VisualStudio.TestInfrastructure; using SonarLint.VisualStudio.ConnectedMode.UI; using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.ConnectedMode.Shared; namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.UI; @@ -37,8 +36,8 @@ public void MefCtor_CheckIsExported() MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport()); } } diff --git a/src/ConnectedMode.UnitTests/UI/ManageBinding/ManageBindingViewModelTests.cs b/src/ConnectedMode.UnitTests/UI/ManageBinding/ManageBindingViewModelTests.cs index ab33e7557..4557d178b 100644 --- a/src/ConnectedMode.UnitTests/UI/ManageBinding/ManageBindingViewModelTests.cs +++ b/src/ConnectedMode.UnitTests/UI/ManageBinding/ManageBindingViewModelTests.cs @@ -20,9 +20,11 @@ using System.ComponentModel; using System.Security; +using System.Windows; using NSubstitute.ExceptionExtensions; using SonarLint.VisualStudio.ConnectedMode.Binding; using SonarLint.VisualStudio.ConnectedMode.Persistence; +using SonarLint.VisualStudio.ConnectedMode.Shared; using SonarLint.VisualStudio.ConnectedMode.UI; using SonarLint.VisualStudio.ConnectedMode.UI.ManageBinding; using SonarLint.VisualStudio.ConnectedMode.UI.ProjectSelection; @@ -41,7 +43,9 @@ public class ManageBindingViewModelTests private readonly ConnectionInfo sonarQubeConnectionInfo = new ("http://localhost:9000", ConnectionServerType.SonarQube); private readonly ConnectionInfo sonarCloudConnectionInfo = new ("organization", ConnectionServerType.SonarCloud); private readonly BasicAuthCredentials validCredentials = new ("TOKEN", new SecureString()); - + private readonly SharedBindingConfigModel sonarQubeSharedBindingConfigModel = new() { Uri = new Uri("http://localhost:9000"), ProjectKey = "myProj" }; + private readonly SharedBindingConfigModel sonarCloudSharedBindingConfigModel = new() { Organization = "myOrg", ProjectKey = "myProj" }; + private ManageBindingViewModel testSubject; private IServerConnectionsRepositoryAdapter serverConnectionsRepositoryAdapter; private IConnectedModeServices connectedModeServices; @@ -50,15 +54,18 @@ public class ManageBindingViewModelTests private IProgressReporterViewModel progressReporterViewModel; private IThreadHandling threadHandling; private ILogger logger; + private IConnectedModeBindingServices connectedModeBindingServices; + private ISharedBindingConfigProvider sharedBindingConfigProvider; + private IMessageBox messageBox; [TestInitialize] public void TestInitialize() { connectedModeServices = Substitute.For(); - bindingController = Substitute.For(); - solutionInfoProvider = Substitute.For(); progressReporterViewModel = Substitute.For(); - testSubject = new ManageBindingViewModel(connectedModeServices, bindingController, solutionInfoProvider, progressReporterViewModel); + connectedModeBindingServices = Substitute.For(); + + testSubject = new ManageBindingViewModel(connectedModeServices, connectedModeBindingServices, progressReporterViewModel); MockServices(); } @@ -98,6 +105,8 @@ public void BoundProject_Set_RaisesEvents() Arg.Is(x => x.PropertyName == nameof(testSubject.IsSelectProjectButtonEnabled))); eventHandler.Received().Invoke(testSubject, Arg.Is(x => x.PropertyName == nameof(testSubject.IsExportButtonEnabled))); + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.IsUseSharedBindingButtonVisible))); } [TestMethod] @@ -252,38 +261,24 @@ public void IsManageConnectionsButtonEnabled_ReturnsTrueOnlyWhenNoBindingIsInPro [TestMethod] [DataRow(true, false)] [DataRow(false, true)] - public void IsUseSharedBindingButtonEnabled_SharedBindingConfigurationIsDetected_ReturnsTrueOnlyWhenNoBindingIsInProgress(bool isBindingInProgress, bool expectedResult) + public void IsUseSharedBindingButtonEnabled_ReturnsTrueOnlyWhenNoBindingIsInProgress(bool isBindingInProgress, bool expectedResult) { - testSubject.IsSharedBindingConfigurationDetected = true; progressReporterViewModel.IsOperationInProgress.Returns(isBindingInProgress); testSubject.IsUseSharedBindingButtonEnabled.Should().Be(expectedResult); } [TestMethod] - [DataRow(true)] - [DataRow(null)] - public void IsUseSharedBindingButtonEnabled_SharedBindingConfigurationIsNotDetected_ReturnsFalse(bool isBindingInProgress) - { - testSubject.IsSharedBindingConfigurationDetected = false; - progressReporterViewModel.IsOperationInProgress.Returns(isBindingInProgress); - - testSubject.IsUseSharedBindingButtonEnabled.Should().BeFalse(); - } - - [TestMethod] - public void IsSharedBindingConfigurationDetected_Set_RaisesEvents() + public void SharedBindingConfigModel_Set_RaisesEvents() { var eventHandler = Substitute.For(); testSubject.PropertyChanged += eventHandler; eventHandler.ReceivedCalls().Should().BeEmpty(); - testSubject.IsSharedBindingConfigurationDetected = true; + testSubject.SharedBindingConfigModel = new SharedBindingConfigModel(); eventHandler.Received().Invoke(testSubject, - Arg.Is(x => x.PropertyName == nameof(testSubject.IsSharedBindingConfigurationDetected))); - eventHandler.Received().Invoke(testSubject, - Arg.Is(x => x.PropertyName == nameof(testSubject.IsUseSharedBindingButtonEnabled))); + Arg.Is(x => x.PropertyName == nameof(testSubject.IsUseSharedBindingButtonVisible))); } [TestMethod] @@ -723,15 +718,170 @@ public async Task BindAsync_WhenBindingCompletesSuccessfully_SetsBoundProjectToS testSubject.BoundProject.Should().BeEquivalentTo(serverProject); } + [TestMethod] + public void DetectSharedBinding_CurrentProjectBound_DoesNothing() + { + testSubject.BoundProject = serverProject; + + testSubject.DetectSharedBinding(); + + sharedBindingConfigProvider.DidNotReceive().GetSharedBinding(); + } + + [TestMethod] + public void DetectSharedBinding_CurrentProjectNotBound_UpdatesSharedBindingConfigModel() + { + testSubject.BoundProject = null; + var sharedBindingModel = new SharedBindingConfigModel(); + sharedBindingConfigProvider.GetSharedBinding().Returns(sharedBindingModel); + + testSubject.DetectSharedBinding(); + + sharedBindingConfigProvider.Received(1).GetSharedBinding(); + testSubject.SharedBindingConfigModel.Should().Be(sharedBindingModel); + } + + [TestMethod] + public async Task UseSharedBindingWithProgressAsync_SharedBindingExistsAndValid_BindsProjectAndReportsProgress() + { + testSubject.SharedBindingConfigModel = sonarQubeSharedBindingConfigModel; + + await testSubject.UseSharedBindingWithProgressAsync(); + + await progressReporterViewModel.Received(1) + .ExecuteTaskWithProgressAsync( + Arg.Is>(x => + x.TaskToPerform == testSubject.UseSharedBindingAsync && + x.ProgressStatus == UiResources.BindingInProgressText && + x.WarningText == UiResources.BindingFailedText && + x.AfterProgressUpdated == testSubject.OnProgressUpdated)); + } + + [TestMethod] + public async Task UseSharedBindingAsync_SharedBindingForSonarQubeConnection_BindsWithTheCorrectProjectKey() + { + testSubject.SelectedProject = serverProject; // this is to make sure the SelectedProject is ignored and the shared config is used instead + testSubject.SharedBindingConfigModel = sonarQubeSharedBindingConfigModel; + SetupBoundProject(new ServerConnection.SonarQube(testSubject.SharedBindingConfigModel.Uri)); + + var response = await testSubject.UseSharedBindingAsync(); + + response.Success.Should().BeTrue(); + await bindingController.Received(1) + .BindAsync(Arg.Is(proj => + proj.ServerProjectKey == testSubject.SharedBindingConfigModel.ProjectKey), Arg.Any()); + } + + [TestMethod] + public async Task UseSharedBindingAsync_SharedBindingForSonarCloudConnection_BindsWithTheCorrectProjectKey() + { + testSubject.SelectedConnectionInfo = sonarQubeConnectionInfo; // this is to make sure the SelectedConnectionInfo is ignored and the shared config is used instead + testSubject.SharedBindingConfigModel = sonarCloudSharedBindingConfigModel; + SetupBoundProject(new ServerConnection.SonarCloud(testSubject.SharedBindingConfigModel.Organization)); + + var response = await testSubject.UseSharedBindingAsync(); + + response.Success.Should().BeTrue(); + await bindingController.Received(1) + .BindAsync(Arg.Is(proj => + proj.ServerProjectKey == testSubject.SharedBindingConfigModel.ProjectKey), Arg.Any()); + } + + [TestMethod] + public async Task UseSharedBindingAsync_SharedBindingForExistSonarQubeConnection_BindsWithTheCorrectConnectionId() + { + testSubject.SelectedConnectionInfo = sonarCloudConnectionInfo; // this is to make sure the SelectedConnectionInfo is ignored and the shared config is used instead + testSubject.SharedBindingConfigModel = sonarQubeSharedBindingConfigModel; + var expectedServerConnection = new ServerConnection.SonarQube(testSubject.SharedBindingConfigModel.Uri); + SetupBoundProject(expectedServerConnection); + + var response = await testSubject.UseSharedBindingAsync(); + + response.Success.Should().BeTrue(); + serverConnectionsRepositoryAdapter.Received(1).TryGetServerConnectionById(testSubject.SharedBindingConfigModel.Uri.ToString(), out _); + await bindingController.Received(1) + .BindAsync(Arg.Is(proj => proj.ServerConnection == expectedServerConnection), Arg.Any()); + } + + [TestMethod] + public async Task UseSharedBindingAsync_SharedBindingForExistingSonarCloudConnection_BindsWithTheCorrectConnectionId() + { + testSubject.SelectedProject = serverProject; // this is to make sure the SelectedProject is ignored and the shared config is used instead + testSubject.SharedBindingConfigModel = sonarCloudSharedBindingConfigModel; + var expectedServerConnection = new ServerConnection.SonarCloud(testSubject.SharedBindingConfigModel.Organization); + SetupBoundProject(expectedServerConnection); + + var response = await testSubject.UseSharedBindingAsync(); + + response.Success.Should().BeTrue(); + serverConnectionsRepositoryAdapter.Received(1).TryGetServerConnectionById(testSubject.SharedBindingConfigModel.Organization, out _); + await bindingController.Received(1) + .BindAsync(Arg.Is(proj => proj.ServerConnection == expectedServerConnection), Arg.Any()); + } + + [TestMethod] + public async Task UseSharedBindingAsync_SharedBindingForNonExistingSonarQubeConnection_ReturnsFalseAndLogsAndInformsUser() + { + testSubject.SelectedProject = serverProject; // this is to make sure the SelectedProject is ignored and the shared config is used instead + testSubject.SharedBindingConfigModel = sonarQubeSharedBindingConfigModel; + + var response = await testSubject.UseSharedBindingAsync(); + + response.Success.Should().BeFalse(); + messageBox.Received(1).Show(UiResources.NotFoundConnectionForSharedBindingMessageBoxText, UiResources.NotFoundConnectionForSharedBindingMessageBoxCaption, MessageBoxButton.OK, MessageBoxImage.Warning); + logger.WriteLine(Resources.UseSharedBinding_ConnectionNotFound, testSubject.SharedBindingConfigModel.Uri); + await bindingController.DidNotReceive() + .BindAsync(Arg.Is(proj => + proj.ServerProjectKey == testSubject.SharedBindingConfigModel.ProjectKey), Arg.Any()); + } + + [TestMethod] + public async Task UseSharedBindingAsync_SharedBindingForNonExistingSonarCloudConnection_ReturnsFalseAndLogsAndInformsUser() + { + testSubject.SelectedProject = serverProject; // this is to make sure the SelectedProject is ignored and the shared config is used instead + testSubject.SharedBindingConfigModel = sonarCloudSharedBindingConfigModel; + + var response = await testSubject.UseSharedBindingAsync(); + + response.Success.Should().BeFalse(); + logger.WriteLine(Resources.UseSharedBinding_ConnectionNotFound, testSubject.SharedBindingConfigModel.Organization); + messageBox.Received(1).Show(UiResources.NotFoundConnectionForSharedBindingMessageBoxText, UiResources.NotFoundConnectionForSharedBindingMessageBoxCaption, MessageBoxButton.OK, MessageBoxImage.Warning); + await bindingController.DidNotReceive() + .BindAsync(Arg.Is(proj => + proj.ServerProjectKey == testSubject.SharedBindingConfigModel.ProjectKey), Arg.Any()); + } + + [TestMethod] + public async Task UseSharedBindingAsync_BindingFails_ReturnsFalse() + { + MockTryGetServerConnectionId(); + bindingController.When(x => x.BindAsync(Arg.Any(), Arg.Any())) + .Do(_ => throw new Exception()); + testSubject.SharedBindingConfigModel = sonarCloudSharedBindingConfigModel; + + var response = await testSubject.UseSharedBindingAsync(); + + response.Success.Should().BeFalse(); + } + private void MockServices() { serverConnectionsRepositoryAdapter = Substitute.For(); threadHandling = Substitute.For(); logger = Substitute.For(); + messageBox = Substitute.For(); connectedModeServices.ServerConnectionsRepositoryAdapter.Returns(serverConnectionsRepositoryAdapter); connectedModeServices.ThreadHandling.Returns(threadHandling); connectedModeServices.Logger.Returns(logger); + connectedModeServices.MessageBox.Returns(messageBox); + + bindingController = Substitute.For(); + solutionInfoProvider = Substitute.For(); + sharedBindingConfigProvider = Substitute.For(); + connectedModeBindingServices.BindingController.Returns(bindingController); + connectedModeBindingServices.SolutionInfoProvider.Returns(solutionInfoProvider); + connectedModeBindingServices.SharedBindingConfigProvider.Returns(sharedBindingConfigProvider); MockTryGetAllConnectionsInfo([]); } @@ -760,16 +910,21 @@ private void SetupBoundProject(ServerConnection serverConnection, ServerProject var configurationProvider = Substitute.For(); configurationProvider.GetConfiguration().Returns(new BindingConfiguration(boundServerProject, SonarLintMode.Connected, "binding-dir")); connectedModeServices.ConfigurationProvider.Returns(configurationProvider); - serverConnectionsRepositoryAdapter.TryGetServerConnectionById(serverConnection.Id, out _).Returns(callInfo => + MockTryGetServerConnectionId(serverConnection); + solutionInfoProvider.GetSolutionNameAsync().Returns(ALocalProjectKey); + + MockGetServerProjectByKey(true, expectedServerProject); + } + + private void MockTryGetServerConnectionId(ServerConnection serverConnection = null) + { + serverConnectionsRepositoryAdapter.TryGetServerConnectionById(serverConnection?.Id ?? Arg.Any(), out _).Returns(callInfo => { callInfo[1] = serverConnection; return true; }); - solutionInfoProvider.GetSolutionNameAsync().Returns(ALocalProjectKey); - - MockGetServerProjectByKey(true, expectedServerProject); } - + private void SetupUnboundProject() { var configurationProvider = Substitute.For(); diff --git a/src/ConnectedMode/Resources.Designer.cs b/src/ConnectedMode/Resources.Designer.cs index 04f9adddd..4704aa2ab 100644 --- a/src/ConnectedMode/Resources.Designer.cs +++ b/src/ConnectedMode/Resources.Designer.cs @@ -1,6 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. +// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -635,6 +636,15 @@ internal static string TimedUpdateTriggered { } } + /// + /// Looks up a localized string similar to [ConnectedMode/UseSharedBinding] Connection to server {0} could not be found. + /// + internal static string UseSharedBinding_ConnectionNotFound { + get { + return ResourceManager.GetString("UseSharedBinding_ConnectionNotFound", resourceCulture); + } + } + /// /// Looks up a localized string similar to [ConnectedMode] Validating credentials failed. /// diff --git a/src/ConnectedMode/Resources.resx b/src/ConnectedMode/Resources.resx index 10ace0365..2a87e5d08 100644 --- a/src/ConnectedMode/Resources.resx +++ b/src/ConnectedMode/Resources.resx @@ -319,4 +319,7 @@ [ConnectedMode] Binding failed due to: {0} + + [ConnectedMode/UseSharedBinding] Connection to server {0} could not be found + \ No newline at end of file diff --git a/src/ConnectedMode/UI/ConnectedModeBindingServices.cs b/src/ConnectedMode/UI/ConnectedModeBindingServices.cs new file mode 100644 index 000000000..79e48054c --- /dev/null +++ b/src/ConnectedMode/UI/ConnectedModeBindingServices.cs @@ -0,0 +1,47 @@ +/* + * 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 System.ComponentModel.Composition; +using SonarLint.VisualStudio.ConnectedMode.Binding; +using SonarLint.VisualStudio.ConnectedMode.Shared; +using SonarLint.VisualStudio.Core; + +namespace SonarLint.VisualStudio.ConnectedMode.UI; + +public interface IConnectedModeBindingServices +{ + public IBindingController BindingController { get; } + public ISolutionInfoProvider SolutionInfoProvider { get; } + public ISharedBindingConfigProvider SharedBindingConfigProvider { get; } +} + +[Export(typeof(IConnectedModeBindingServices))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +public class ConnectedModeBindingServices( + IBindingController bindingController, + ISolutionInfoProvider solutionInfoProvider, + ISharedBindingConfigProvider sharedBindingConfigProvider) + : IConnectedModeBindingServices +{ + public IBindingController BindingController { get; } = bindingController; + public ISolutionInfoProvider SolutionInfoProvider { get; } = solutionInfoProvider; + public ISharedBindingConfigProvider SharedBindingConfigProvider { get; } = sharedBindingConfigProvider; +} diff --git a/src/ConnectedMode/UI/ConnectedModeServices.cs b/src/ConnectedMode/UI/ConnectedModeServices.cs index fa62c5457..0bdd1ee90 100644 --- a/src/ConnectedMode/UI/ConnectedModeServices.cs +++ b/src/ConnectedMode/UI/ConnectedModeServices.cs @@ -27,13 +27,13 @@ namespace SonarLint.VisualStudio.ConnectedMode.UI; public interface IConnectedModeServices { - public ISharedBindingConfigProvider SharedBindingConfigProvider { get; } public IBrowserService BrowserService { get; } public IThreadHandling ThreadHandling { get; } public ILogger Logger { get; } public ISlCoreConnectionAdapter SlCoreConnectionAdapter { get; } public IConfigurationProvider ConfigurationProvider { get; } public IServerConnectionsRepositoryAdapter ServerConnectionsRepositoryAdapter { get; } + public IMessageBox MessageBox { get; } } [Export(typeof(IConnectedModeServices))] @@ -44,13 +44,13 @@ public class ConnectedModeServices( IThreadHandling threadHandling, ISlCoreConnectionAdapter slCoreConnectionAdapter, IConfigurationProvider configurationProvider, - ISharedBindingConfigProvider sharedBindingConfigProvider, IServerConnectionsRepositoryAdapter serverConnectionsRepositoryAdapter, + IMessageBox messageBox, ILogger logger) : IConnectedModeServices { - public ISharedBindingConfigProvider SharedBindingConfigProvider { get; } = sharedBindingConfigProvider; public IServerConnectionsRepositoryAdapter ServerConnectionsRepositoryAdapter { get; } = serverConnectionsRepositoryAdapter; + public IMessageBox MessageBox { get; } = messageBox; public IBrowserService BrowserService { get; } = browserService; public IThreadHandling ThreadHandling { get; } = threadHandling; public ILogger Logger { get; } = logger; diff --git a/src/ConnectedMode/UI/ManageBinding/ManageBindingDialog.xaml b/src/ConnectedMode/UI/ManageBinding/ManageBindingDialog.xaml index 0323f7376..5c6292a94 100644 --- a/src/ConnectedMode/UI/ManageBinding/ManageBindingDialog.xaml +++ b/src/ConnectedMode/UI/ManageBinding/ManageBindingDialog.xaml @@ -57,7 +57,9 @@