Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SLVS-1379 Binding Dialog: Implement Use Shared Binding Configuration #5697

Merged
merged 10 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<ConnectedModeBindingServices, IConnectedModeBindingServices>(
MefTestHelpers.CreateExport<IBindingController>(),
MefTestHelpers.CreateExport<ISolutionInfoProvider>(),
MefTestHelpers.CreateExport<ISharedBindingConfigProvider>());
}
}
3 changes: 1 addition & 2 deletions src/ConnectedMode.UnitTests/UI/ConnectedModeServicesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -37,8 +36,8 @@ public void MefCtor_CheckIsExported()
MefTestHelpers.CreateExport<IThreadHandling>(),
MefTestHelpers.CreateExport<ISlCoreConnectionAdapter>(),
MefTestHelpers.CreateExport<IConfigurationProvider>(),
MefTestHelpers.CreateExport<ISharedBindingConfigProvider>(),
MefTestHelpers.CreateExport<IServerConnectionsRepositoryAdapter>(),
MefTestHelpers.CreateExport<IMessageBox>(),
MefTestHelpers.CreateExport<ILogger>());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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<IConnectedModeServices>();
bindingController = Substitute.For<IBindingController>();
solutionInfoProvider = Substitute.For<ISolutionInfoProvider>();
progressReporterViewModel = Substitute.For<IProgressReporterViewModel>();
testSubject = new ManageBindingViewModel(connectedModeServices, bindingController, solutionInfoProvider, progressReporterViewModel);
connectedModeBindingServices = Substitute.For<IConnectedModeBindingServices>();

testSubject = new ManageBindingViewModel(connectedModeServices, connectedModeBindingServices, progressReporterViewModel);

MockServices();
}
Expand Down Expand Up @@ -98,6 +105,8 @@ public void BoundProject_Set_RaisesEvents()
Arg.Is<PropertyChangedEventArgs>(x => x.PropertyName == nameof(testSubject.IsSelectProjectButtonEnabled)));
eventHandler.Received().Invoke(testSubject,
Arg.Is<PropertyChangedEventArgs>(x => x.PropertyName == nameof(testSubject.IsExportButtonEnabled)));
eventHandler.Received().Invoke(testSubject,
Arg.Is<PropertyChangedEventArgs>(x => x.PropertyName == nameof(testSubject.IsUseSharedBindingButtonVisible)));
}

[TestMethod]
Expand Down Expand Up @@ -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<PropertyChangedEventHandler>();
testSubject.PropertyChanged += eventHandler;
eventHandler.ReceivedCalls().Should().BeEmpty();

testSubject.IsSharedBindingConfigurationDetected = true;
testSubject.SharedBindingConfigModel = new SharedBindingConfigModel();

eventHandler.Received().Invoke(testSubject,
Arg.Is<PropertyChangedEventArgs>(x => x.PropertyName == nameof(testSubject.IsSharedBindingConfigurationDetected)));
eventHandler.Received().Invoke(testSubject,
Arg.Is<PropertyChangedEventArgs>(x => x.PropertyName == nameof(testSubject.IsUseSharedBindingButtonEnabled)));
Arg.Is<PropertyChangedEventArgs>(x => x.PropertyName == nameof(testSubject.IsUseSharedBindingButtonVisible)));
}

[TestMethod]
Expand Down Expand Up @@ -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<TaskToPerformParams<AdapterResponse>>(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<BoundServerProject>(proj =>
proj.ServerProjectKey == testSubject.SharedBindingConfigModel.ProjectKey), Arg.Any<CancellationToken>());
}

[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<BoundServerProject>(proj =>
proj.ServerProjectKey == testSubject.SharedBindingConfigModel.ProjectKey), Arg.Any<CancellationToken>());
}

[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<BoundServerProject>(proj => proj.ServerConnection == expectedServerConnection), Arg.Any<CancellationToken>());
}

[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<BoundServerProject>(proj => proj.ServerConnection == expectedServerConnection), Arg.Any<CancellationToken>());
}

[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<BoundServerProject>(proj =>
proj.ServerProjectKey == testSubject.SharedBindingConfigModel.ProjectKey), Arg.Any<CancellationToken>());
}

[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<BoundServerProject>(proj =>
proj.ServerProjectKey == testSubject.SharedBindingConfigModel.ProjectKey), Arg.Any<CancellationToken>());
}

[TestMethod]
public async Task UseSharedBindingAsync_BindingFails_ReturnsFalse()
{
MockTryGetServerConnectionId();
bindingController.When(x => x.BindAsync(Arg.Any<BoundServerProject>(), Arg.Any<CancellationToken>()))
.Do(_ => throw new Exception());
testSubject.SharedBindingConfigModel = sonarCloudSharedBindingConfigModel;

var response = await testSubject.UseSharedBindingAsync();

response.Success.Should().BeFalse();
}

private void MockServices()
{
serverConnectionsRepositoryAdapter = Substitute.For<IServerConnectionsRepositoryAdapter>();
threadHandling = Substitute.For<IThreadHandling>();
logger = Substitute.For<ILogger>();
messageBox = Substitute.For<IMessageBox>();

connectedModeServices.ServerConnectionsRepositoryAdapter.Returns(serverConnectionsRepositoryAdapter);
connectedModeServices.ThreadHandling.Returns(threadHandling);
connectedModeServices.Logger.Returns(logger);
connectedModeServices.MessageBox.Returns(messageBox);

bindingController = Substitute.For<IBindingController>();
solutionInfoProvider = Substitute.For<ISolutionInfoProvider>();
sharedBindingConfigProvider = Substitute.For<ISharedBindingConfigProvider>();
connectedModeBindingServices.BindingController.Returns(bindingController);
connectedModeBindingServices.SolutionInfoProvider.Returns(solutionInfoProvider);
connectedModeBindingServices.SharedBindingConfigProvider.Returns(sharedBindingConfigProvider);

MockTryGetAllConnectionsInfo([]);
}
Expand Down Expand Up @@ -760,16 +910,21 @@ private void SetupBoundProject(ServerConnection serverConnection, ServerProject
var configurationProvider = Substitute.For<IConfigurationProvider>();
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<string>(), out _).Returns(callInfo =>
{
callInfo[1] = serverConnection;
return true;
});
solutionInfoProvider.GetSolutionNameAsync().Returns(ALocalProjectKey);

MockGetServerProjectByKey(true, expectedServerProject);
}

private void SetupUnboundProject()
{
var configurationProvider = Substitute.For<IConfigurationProvider>();
Expand Down
10 changes: 10 additions & 0 deletions src/ConnectedMode/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/ConnectedMode/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -319,4 +319,7 @@
<data name="Binding_Fails" xml:space="preserve">
<value>[ConnectedMode] Binding failed due to: {0}</value>
</data>
<data name="UseSharedBinding_ConnectionNotFound" xml:space="preserve">
<value>[ConnectedMode/UseSharedBinding] Connection to server {0} could not be found</value>
</data>
</root>
Loading