Skip to content

Commit

Permalink
SLVS-1377 Implement unbind in Binding Dialog (#5880)
Browse files Browse the repository at this point in the history
  • Loading branch information
gabriela-trutan-sonarsource authored and vnaskos-sonar committed Dec 18, 2024
1 parent d12293f commit 5b110ae
Show file tree
Hide file tree
Showing 9 changed files with 249 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<ISonarQubeService>();
activeSolutionChangedHandler = Substitute.For<IActiveSolutionChangedHandler>();
solutionBindingRepository = Substitute.For<ISolutionBindingRepository>();
testSubject = new UnintrusiveBindingController(bindingProcessFactory, sonarQubeService, activeSolutionChangedHandler, solutionBindingRepository);
}

[TestMethod]
public void MefCtor_CheckTypeIsNonShared()
=> MefTestHelpers.CheckIsNonSharedMefComponent<UnintrusiveBindingController>();
public void MefCtor_CheckTypeIsNonShared() => MefTestHelpers.CheckIsNonSharedMefComponent<UnintrusiveBindingController>();

[TestMethod]
public void MefCtor_IUnintrusiveBindingController_CheckIsExported()
{
public void MefCtor_IUnintrusiveBindingController_CheckIsExported() =>
MefTestHelpers.CheckTypeCanBeImported<UnintrusiveBindingController, IUnintrusiveBindingController>(
MefTestHelpers.CreateExport<IBindingProcessFactory>(),
MefTestHelpers.CreateExport<ISonarQubeService>(),
MefTestHelpers.CreateExport<IActiveSolutionChangedHandler>());
}
MefTestHelpers.CreateExport<IActiveSolutionChangedHandler>(),
MefTestHelpers.CreateExport<ISolutionBindingRepository>());

[TestMethod]
public void MefCtor_IBindingController_CheckIsExported()
{
public void MefCtor_IBindingController_CheckIsExported() =>
MefTestHelpers.CheckTypeCanBeImported<UnintrusiveBindingController, IBindingController>(
MefTestHelpers.CreateExport<IBindingProcessFactory>(),
MefTestHelpers.CreateExport<ISonarQubeService>(),
MefTestHelpers.CreateExport<IActiveSolutionChangedHandler>());
}
MefTestHelpers.CreateExport<IActiveSolutionChangedHandler>(),
MefTestHelpers.CreateExport<ISolutionBindingRepository>());

[TestMethod]
public async Task BindAsync_EstablishesConnection()
{
var sonarQubeService = Substitute.For<ISonarQubeService>();
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<ConnectionInformation>(x => x.ServerUri.Equals("https://sonarcloud.io/")
Arg.Is<ConnectionInformation>(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<IActiveSolutionChangedHandler>();
var testSubject = CreateTestSubject(activeSolutionChangedHandler: activeSolutionChangedHandler);

await testSubject.BindAsync(AnyBoundProject, ACancellationToken);

activeSolutionChangedHandler
Expand All @@ -98,10 +105,7 @@ public async Task BindAsync_NotifiesBindingChanged()
public async Task BindAsync_CallsBindingProcessInOrder()
{
var cancellationToken = CancellationToken.None;
var bindingProcess = Substitute.For<IBindingProcess>();
var bindingProcessFactory = CreateBindingProcessFactory(bindingProcess);
var testSubject = CreateTestSubject(bindingProcessFactory);


await testSubject.BindAsync(AnyBoundProject, null, cancellationToken);

Received.InOrder(() =>
Expand All @@ -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<ISonarQubeService>(),
activeSolutionChangedHandler ?? Substitute.For<IActiveSolutionChangedHandler>());
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<IBindingProcess>();

var bindingProcessFactory = Substitute.For<IBindingProcessFactory>();
bindingProcessFactory = Substitute.For<IBindingProcessFactory>();
bindingProcessFactory.Create(Arg.Any<BindCommandArgs>()).Returns(bindingProcess);

return bindingProcessFactory;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<TaskToPerformParams<AdapterResponse>>(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<Action>());
}

[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<string>()).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<IThreadHandling>();
connectedModeServices.ThreadHandling.Returns(mockedThreadHandling);
mockedThreadHandling.When(x => x.RunOnUIThreadAsync(Arg.Any<Action>())).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();
Expand Down Expand Up @@ -1025,4 +1088,10 @@ private void MockGetServerProjectByKey(bool success, ServerProject responseData)
.Returns(Task.FromResult(new AdapterResponseWithData<ServerProject>(success, responseData)));
connectedModeServices.SlCoreConnectionAdapter.Returns(slCoreConnectionAdapter);
}

private async Task InitializeBoundProject()
{
SetupBoundProject(new ServerConnection.SonarCloud("my org"), serverProject);
await testSubject.DisplayBindStatusAsync();
}
}
16 changes: 15 additions & 1 deletion src/ConnectedMode/Binding/IUnintrusiveBindingController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -67,6 +70,17 @@ public async Task BindAsync(BoundServerProject project, IProgress<FixedStepsProg
await bindingProcess.SaveServerExclusionsAsync(token);
}

public bool Unbind(string localBindingKey)
{
var bindingDeleted = solutionBindingRepository.DeleteBinding(localBindingKey);
if (bindingDeleted)
{
sonarQubeService.Disconnect();
activeSolutionChangedHandler.HandleBindingChange(true);
}
return bindingDeleted;
}

private IBindingProcess CreateBindingProcess(BoundServerProject project)
{
var bindingProcess = bindingProcessFactory.Create(new BindCommandArgs(project));
Expand Down
4 changes: 2 additions & 2 deletions src/ConnectedMode/UI/ManageBinding/ManageBindingDialog.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
</Grid.ColumnDefinitions>

<Button Grid.Column="0" Content="{x:Static res:UiResources.UnbindButton}" IsEnabled="{Binding Path=IsUnbindButtonEnabled}"
Visibility="Collapsed" VerticalAlignment="Center"
VerticalAlignment="Center"
Click="Unbind_OnClick"/>
<Button Grid.Column="1" Visibility="Collapsed" Content="{x:Static res:UiResources.ShareConfigurationButton}" ToolTip="{x:Static res:UiResources.ExportBindingConfigurationTooltip}"
IsEnabled="{Binding Path=IsExportButtonEnabled}" VerticalAlignment="Center"
Expand Down Expand Up @@ -170,7 +170,7 @@

<Button Grid.Column="0" Content="{x:Static res:UiResources.BindButton}" IsDefault="True" IsEnabled="{Binding Path=IsBindButtonEnabled}"
Click="Binding_OnClick"/>
<Button Grid.Column="1" Content="{x:Static res:UiResources.CancelButton}" IsCancel="True"/>
<Button Grid.Column="1" Content="{x:Static res:UiResources.CloseButton}" IsCancel="True"/>
</Grid>

<Grid Grid.Row="4" HorizontalAlignment="Right" Visibility="{Binding Path=IsCurrentProjectBound, Converter={StaticResource TrueToVisibleConverter}}">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ private async void ManageBindingDialog_OnInitialized(object sender, EventArgs e)
}
}

private void Unbind_OnClick(object sender, RoutedEventArgs e)
private async void Unbind_OnClick(object sender, RoutedEventArgs e)
{
ViewModel.Unbind();
await ViewModel.UnbindWithProgressAsync();
}

private async void UseSharedBinding_OnClick(object sender, RoutedEventArgs e)
Expand Down
Loading

0 comments on commit 5b110ae

Please sign in to comment.