Skip to content

Commit

Permalink
SLVS-1373 Validate credentials in OrganizationSelectionDialog (#5634)
Browse files Browse the repository at this point in the history
  • Loading branch information
1 parent 898bae0 commit 6a6ab7c
Show file tree
Hide file tree
Showing 10 changed files with 140 additions and 45 deletions.
16 changes: 8 additions & 8 deletions src/ConnectedMode.UnitTests/SlCoreConnectionAdapterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public async Task ValidateConnectionAsync_SwitchesToBackgroundThread()
var threadHandlingMock = Substitute.For<IThreadHandling>();
var slCoreConnectionAdapter = new SlCoreConnectionAdapter(slCoreServiceProvider, threadHandlingMock, logger);

await slCoreConnectionAdapter.ValidateConnectionAsync(sonarQubeConnectionInfo, "myToken");
await slCoreConnectionAdapter.ValidateConnectionAsync(sonarQubeConnectionInfo, new TokenCredentialsModel("myToken"));

await threadHandlingMock.Received(1).RunOnBackgroundThread(Arg.Any<Func<Task<AdapterResponse>>>());
}
Expand All @@ -70,7 +70,7 @@ public async Task ValidateConnectionAsync_GettingConnectionConfigurationSLCoreSe
{
slCoreServiceProvider.TryGetTransientService(out IConnectionConfigurationSLCoreService _).Returns(false);

var response = await testSubject.ValidateConnectionAsync(sonarQubeConnectionInfo, "myToken");
var response = await testSubject.ValidateConnectionAsync(sonarQubeConnectionInfo, new TokenCredentialsModel("myToken"));

logger.Received(1).LogVerbose($"[{nameof(IConnectionConfigurationSLCoreService)}] {SLCoreStrings.ServiceProviderNotInitialized}");
response.Success.Should().BeFalse();
Expand All @@ -81,7 +81,7 @@ public async Task ValidateConnectionAsync_ConnectionToSonarQubeWithToken_CallsVa
{
var token = "myToken";

await testSubject.ValidateConnectionAsync(sonarQubeConnectionInfo, token);
await testSubject.ValidateConnectionAsync(sonarQubeConnectionInfo, new TokenCredentialsModel(token));

await connectionConfigurationSlCoreService.Received(1)
.ValidateConnectionAsync(Arg.Is<ValidateConnectionParams>(x => IsExpectedSonarQubeConnectionParams(x, token)));
Expand All @@ -93,7 +93,7 @@ public async Task ValidateConnectionAsync_ConnectionToSonarQubeWithCredentials_C
var username = "username";
var password = "password";

await testSubject.ValidateConnectionAsync(sonarQubeConnectionInfo, username, password);
await testSubject.ValidateConnectionAsync(sonarQubeConnectionInfo, new UsernamePasswordModel(username, password));

await connectionConfigurationSlCoreService.Received(1)
.ValidateConnectionAsync(Arg.Is<ValidateConnectionParams>(x => IsExpectedSonarQubeConnectionParams(x, username, password)));
Expand All @@ -104,7 +104,7 @@ public async Task ValidateConnectionAsync_ConnectionToSonarCloudWithToken_CallsV
{
var token = "myToken";

await testSubject.ValidateConnectionAsync(sonarCloudConnectionInfo, token);
await testSubject.ValidateConnectionAsync(sonarCloudConnectionInfo, new TokenCredentialsModel(token));

await connectionConfigurationSlCoreService.Received(1)
.ValidateConnectionAsync(Arg.Is<ValidateConnectionParams>(x => IsExpectedSonarCloudConnectionParams(x, token)));
Expand All @@ -116,7 +116,7 @@ public async Task ValidateConnectionAsync_ConnectionToSonarCloudWithCredentials_
var username = "username";
var password = "password";

await testSubject.ValidateConnectionAsync(sonarCloudConnectionInfo, username, password);
await testSubject.ValidateConnectionAsync(sonarCloudConnectionInfo, new UsernamePasswordModel(username, password));

await connectionConfigurationSlCoreService.Received(1)
.ValidateConnectionAsync(Arg.Is<ValidateConnectionParams>(x => IsExpectedSonarCloudConnectionParams(x, username, password)));
Expand All @@ -130,7 +130,7 @@ public async Task ValidateConnectionAsync_ReturnsResponseFromSlCore(bool success
var expectedResponse = new ValidateConnectionResponse(success, message);
connectionConfigurationSlCoreService.ValidateConnectionAsync(Arg.Any<ValidateConnectionParams>()).Returns(expectedResponse);

var response = await testSubject.ValidateConnectionAsync(sonarCloudConnectionInfo, "token");
var response = await testSubject.ValidateConnectionAsync(sonarCloudConnectionInfo, new TokenCredentialsModel("myToken"));

response.Success.Should().Be(success);
}
Expand All @@ -142,7 +142,7 @@ public async Task ValidateConnectionAsync_SlCoreValidationThrowsException_Return
connectionConfigurationSlCoreService.When(x => x.ValidateConnectionAsync(Arg.Any<ValidateConnectionParams>()))
.Do(x => throw new Exception(exceptionMessage));

var response = await testSubject.ValidateConnectionAsync(sonarCloudConnectionInfo, "token");
var response = await testSubject.ValidateConnectionAsync(sonarCloudConnectionInfo, new TokenCredentialsModel("token"));

logger.Received(1).LogVerbose($"{Resources.ValidateCredentials_Fails}: {exceptionMessage}");
response.Success.Should().BeFalse();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,8 @@ public async Task AdapterValidateConnectionAsync_TokenIsProvided_ShouldValidateC

await testSubject.AdapterValidateConnectionAsync();

await slCoreConnectionAdapter.Received(1).ValidateConnectionAsync(testSubject.ConnectionInfo, testSubject.Token);
await slCoreConnectionAdapter.Received(1).ValidateConnectionAsync(testSubject.ConnectionInfo,
Arg.Is<TokenCredentialsModel>(x => x.Token == testSubject.Token));
}

[TestMethod]
Expand All @@ -297,7 +298,8 @@ public async Task AdapterValidateConnectionAsync_CredentialsAreProvided_ShouldVa

await testSubject.AdapterValidateConnectionAsync();

await slCoreConnectionAdapter.Received(1).ValidateConnectionAsync(testSubject.ConnectionInfo, testSubject.Username, testSubject.Password);
await slCoreConnectionAdapter.Received(1).ValidateConnectionAsync(testSubject.ConnectionInfo,
Arg.Is<UsernamePasswordModel>(x => x.Username == testSubject.Username && x.Password == testSubject.Password));
}

[TestMethod]
Expand Down Expand Up @@ -368,9 +370,7 @@ public void GetCredentialsModel_SelectedAuthenticationTypeIsCredentials_ReturnsM

private void MockAdapterValidateConnectionAsync(bool success = true)
{
slCoreConnectionAdapter.ValidateConnectionAsync(Arg.Any<ConnectionInfo>(), Arg.Any<string>())
.Returns(new AdapterResponse(success));
slCoreConnectionAdapter.ValidateConnectionAsync(Arg.Any<ConnectionInfo>(), Arg.Any<string>(), Arg.Any<string>())
slCoreConnectionAdapter.ValidateConnectionAsync(Arg.Any<ConnectionInfo>(), Arg.Any<ICredentialsModel>())
.Returns(new AdapterResponse(success));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
*/

using System.ComponentModel;
using Microsoft.VisualStudio.Threading;
using SonarLint.VisualStudio.ConnectedMode.UI;
using SonarLint.VisualStudio.ConnectedMode.UI.Credentials;
using SonarLint.VisualStudio.ConnectedMode.UI.OrganizationSelection;
Expand All @@ -40,6 +41,7 @@ public void TestInitialize()
credentialsModel = Substitute.For<ICredentialsModel>();
slCoreConnectionAdapter = Substitute.For<ISlCoreConnectionAdapter>();
progressReporterViewModel = Substitute.For<IProgressReporterViewModel>();
progressReporterViewModel.ExecuteTaskWithProgressAsync(Arg.Any<ITaskToPerformParams<AdapterResponse>>()).Returns(new AdapterResponse(true));

testSubject = new(credentialsModel, slCoreConnectionAdapter, progressReporterViewModel);
}
Expand All @@ -61,6 +63,12 @@ public void Ctor_OrganizationList_SetsPropertyValue()
testSubject.Organizations[0].Should().Be(organization);
}

[TestMethod]
public void FinalConnectionInfo_SetByDefaultToNull()
{
testSubject.FinalConnectionInfo.Should().BeNull();
}

[TestMethod]
public void SelectedOrganization_NotSet_ValueIsNull()
{
Expand Down Expand Up @@ -213,4 +221,48 @@ public void UpdateOrganizations_RaisesEvents()
eventHandler.Received().Invoke(testSubject,
Arg.Is<PropertyChangedEventArgs>(x => x.PropertyName == nameof(testSubject.NoOrganizationExists)));
}

[TestMethod]
[DataRow(true)]
[DataRow(false)]
public async Task ValidateConnectionForOrganizationAsync_ReturnsResponseFromSlCore(bool success)
{
progressReporterViewModel.ExecuteTaskWithProgressAsync(Arg.Any<ITaskToPerformParams<AdapterResponse>>()).Returns(new AdapterResponse(success));

var response = await testSubject.ValidateConnectionForOrganizationAsync("key","warning");

response.Should().Be(success);
}

[TestMethod]
public async Task ValidateConnectionForOrganizationAsync_CallsExecuteTaskWithProgressAsync()
{
var organizationKey = "key";
var warningText = "warning";

await testSubject.ValidateConnectionForOrganizationAsync(organizationKey, warningText);

await progressReporterViewModel.Received(1)
.ExecuteTaskWithProgressAsync(Arg.Is<ITaskToPerformParams<AdapterResponse>>(x =>
IsExpectedSlCoreAdapterValidateConnectionAsync(x.TaskToPerform, organizationKey) &&
x.ProgressStatus == UiResources.ValidatingConnectionProgressText &&
x.WarningText == warningText));
}

[TestMethod]
public void UpdateFinalConnectionInfo_ValueChanges_UpdatesConnectionInfo()
{
testSubject.UpdateFinalConnectionInfo("newKey");

testSubject.FinalConnectionInfo.Should().NotBeNull();
testSubject.FinalConnectionInfo.Id.Should().Be("newKey");
testSubject.FinalConnectionInfo.ServerType.Should().Be(ConnectionServerType.SonarCloud);
}

private bool IsExpectedSlCoreAdapterValidateConnectionAsync(Func<Task<AdapterResponse>> xTaskToPerform, string organizationKey)
{
xTaskToPerform().Forget();
slCoreConnectionAdapter.Received(1).ValidateConnectionAsync(Arg.Is<ConnectionInfo>(x=> x.Id == organizationKey), Arg.Any<ICredentialsModel>());
return true;
}
}
15 changes: 4 additions & 11 deletions src/ConnectedMode/SlCoreConnectionAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
using SonarLint.VisualStudio.ConnectedMode.UI;
using SonarLint.VisualStudio.ConnectedMode.UI.Credentials;
using SonarLint.VisualStudio.ConnectedMode.UI.OrganizationSelection;
using SonarLint.VisualStudio.ConnectedMode.UI.Resources;
using SonarLint.VisualStudio.Core;
using SonarLint.VisualStudio.SLCore;
using SonarLint.VisualStudio.SLCore.Common.Models;
Expand All @@ -35,8 +34,7 @@ namespace SonarLint.VisualStudio.ConnectedMode;

public interface ISlCoreConnectionAdapter
{
Task<AdapterResponse> ValidateConnectionAsync(ConnectionInfo connectionInfo, string token);
Task<AdapterResponse> ValidateConnectionAsync(ConnectionInfo connectionInfo, string username, string password);
Task<AdapterResponse> ValidateConnectionAsync(ConnectionInfo connectionInfo, ICredentialsModel credentialsModel);
Task<AdapterResponseWithData<List<OrganizationDisplay>>> GetOrganizationsAsync(ICredentialsModel credentialsModel);
}

Expand Down Expand Up @@ -68,15 +66,10 @@ public SlCoreConnectionAdapter(ISLCoreServiceProvider serviceProvider, IThreadHa
this.logger = logger;
}

public async Task<AdapterResponse> ValidateConnectionAsync(ConnectionInfo connectionInfo, string token)
public async Task<AdapterResponse> ValidateConnectionAsync(ConnectionInfo connectionInfo, ICredentialsModel credentialsModel)
{
var validateConnectionParams = GetValidateConnectionParams(connectionInfo, GetEitherForToken(token));
return await ValidateConnectionAsync(validateConnectionParams);
}

public async Task<AdapterResponse> ValidateConnectionAsync(ConnectionInfo connectionInfo, string username, string password)
{
var validateConnectionParams = GetValidateConnectionParams(connectionInfo, GetEitherForUsernamePassword(username, password));
var credentials = GetCredentialsDto(credentialsModel);
var validateConnectionParams = GetValidateConnectionParams(connectionInfo, credentials);
return await ValidateConnectionAsync(validateConnectionParams);
}

Expand Down
6 changes: 1 addition & 5 deletions src/ConnectedMode/UI/Credentials/CredentialsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,7 @@ internal async Task<bool> ValidateConnectionAsync()

internal async Task<AdapterResponse> AdapterValidateConnectionAsync()
{
if (IsTokenAuthentication)
{
return await slCoreConnectionAdapter.ValidateConnectionAsync(ConnectionInfo, Token);
}
return await slCoreConnectionAdapter.ValidateConnectionAsync(ConnectionInfo, Username, Password);
return await slCoreConnectionAdapter.ValidateConnectionAsync(ConnectionInfo, GetCredentialsModel());
}

internal void AfterProgressStatusUpdated()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,8 @@ private ConnectionInfo FinalizeConnection(ConnectionInfo newConnectionInfo, Cred
}

var organizationSelectionDialog = new OrganizationSelectionDialog(connectedModeServices, credentialsDialog.ViewModel.GetCredentialsModel());
if (organizationSelectionDialog.ShowDialog(this) == true)
{
return newConnectionInfo with { Id = organizationSelectionDialog.ViewModel.SelectedOrganization.Key };
}
return null;

return organizationSelectionDialog.ShowDialog(this) == true ? organizationSelectionDialog.ViewModel.FinalConnectionInfo : null;
}

private void ManageConnectionsWindow_OnInitialized(object sender, EventArgs e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,42 +20,71 @@

using System.Diagnostics.CodeAnalysis;
using System.Windows;
using Microsoft.VisualStudio;
using SonarLint.VisualStudio.ConnectedMode.UI.Credentials;
using SonarLint.VisualStudio.ConnectedMode.UI.Resources;

namespace SonarLint.VisualStudio.ConnectedMode.UI.OrganizationSelection;

[ExcludeFromCodeCoverage]
public partial class OrganizationSelectionDialog : Window
{
private readonly IConnectedModeServices connectedModeServices;

public OrganizationSelectionDialog(IConnectedModeServices connectedModeServices, ICredentialsModel credentialsModel)
{
this.connectedModeServices = connectedModeServices;
ViewModel = new OrganizationSelectionViewModel(credentialsModel, connectedModeServices.SlCoreConnectionAdapter, new ProgressReporterViewModel());
InitializeComponent();
}

public OrganizationSelectionViewModel ViewModel { get; }

private void OkButton_OnClick(object sender, RoutedEventArgs e)
private async void OkButton_OnClick(object sender, RoutedEventArgs e)
{
DialogResult = true;
await UpdateFinalConnectionInfoAsync(ViewModel.SelectedOrganization.Key);
}

private void ChooseAnotherOrganizationButton_OnClick(object sender, RoutedEventArgs e)
private async void ChooseAnotherOrganizationButton_OnClick(object sender, RoutedEventArgs e)
{
ViewModel.SelectedOrganization = null;
var manualOrganizationSelectionDialog = new ManualOrganizationSelectionDialog();
manualOrganizationSelectionDialog.Owner = this;
var manualSelection = manualOrganizationSelectionDialog.ShowDialog(this);
ViewModel.SelectedOrganization =new OrganizationDisplay(manualOrganizationSelectionDialog.ViewModel.OrganizationKey, null);
if (manualSelection is true)
var manualSelectionDialogSucceeded = manualOrganizationSelectionDialog.ShowDialog(this);
if(manualSelectionDialogSucceeded is not true)
{
return;
}

await UpdateFinalConnectionInfoAsync(manualOrganizationSelectionDialog.ViewModel.OrganizationKey);
}

private async Task<bool> ValidateConnectionForSelectedOrganizationAsync(string selectedOrganizationKey)
{
try
{
DialogResult = true;
var organizationSelectionInvalidMsg = string.Format(UiResources.ValidatingOrganziationSelectionFailedText, selectedOrganizationKey);
return await ViewModel.ValidateConnectionForOrganizationAsync(selectedOrganizationKey, organizationSelectionInvalidMsg);
}
catch (Exception e) when (!ErrorHandler.IsCriticalException(e))
{
connectedModeServices.Logger.WriteLine(e.ToString());
return false;
}
}

private async void OrganizationSelectionDialog_OnLoaded(object sender, RoutedEventArgs e)
{
await ViewModel.LoadOrganizationsAsync();
}

private async Task UpdateFinalConnectionInfoAsync(string organizationKey)
{
var isConnectionValid = await ValidateConnectionForSelectedOrganizationAsync(organizationKey);
if (isConnectionValid)
{
ViewModel.UpdateFinalConnectionInfo(organizationKey);
DialogResult = true;
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,13 @@
using SonarLint.VisualStudio.ConnectedMode.UI.Credentials;
using SonarLint.VisualStudio.ConnectedMode.UI.Resources;
using SonarLint.VisualStudio.Core.WPF;
using static SonarLint.VisualStudio.ConnectedMode.UI.ProgressReporterViewModel;

namespace SonarLint.VisualStudio.ConnectedMode.UI.OrganizationSelection;

public class OrganizationSelectionViewModel(ICredentialsModel credentialsModel, ISlCoreConnectionAdapter connectionAdapter, IProgressReporterViewModel progressReporterViewModel) : ViewModelBase
{
public ConnectionInfo FinalConnectionInfo { get; private set; }
public IProgressReporterViewModel ProgressReporterViewModel { get; } = progressReporterViewModel;
private OrganizationDisplay selectedOrganization;

public OrganizationDisplay SelectedOrganization
{
Expand All @@ -43,10 +42,11 @@ public OrganizationDisplay SelectedOrganization
}

public bool IsValidSelectedOrganization => SelectedOrganization is { Key: var key } && !string.IsNullOrWhiteSpace(key);

public ObservableCollection<OrganizationDisplay> Organizations { get; } = [];
public bool NoOrganizationExists => Organizations.Count == 0;

private OrganizationDisplay selectedOrganization;

public void AddOrganization(OrganizationDisplay organization)
{
Organizations.Add(organization);
Expand Down Expand Up @@ -76,4 +76,20 @@ internal void UpdateOrganizations(AdapterResponseWithData<List<OrganizationDispl
responseWithData.ResponseData.ForEach(AddOrganization);
RaisePropertyChanged(nameof(NoOrganizationExists));
}

internal async Task<bool> ValidateConnectionForOrganizationAsync(string organizationKey, string warningText)
{
var connectionInfoToValidate = new ConnectionInfo(organizationKey, ConnectionServerType.SonarCloud);
var validationParams = new TaskToPerformParams<AdapterResponse>(
async () => await connectionAdapter.ValidateConnectionAsync(connectionInfoToValidate, credentialsModel),
UiResources.ValidatingConnectionProgressText,
warningText);
var adapterResponse = await ProgressReporterViewModel.ExecuteTaskWithProgressAsync(validationParams);
return adapterResponse.Success;
}

public void UpdateFinalConnectionInfo(string organizationKey)
{
FinalConnectionInfo = new ConnectionInfo(organizationKey, ConnectionServerType.SonarCloud);
}
}
9 changes: 9 additions & 0 deletions src/ConnectedMode/UI/Resources/UiResources.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/UI/Resources/UiResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -348,4 +348,7 @@
<data name="LoadingOrganizationsProgressText" xml:space="preserve">
<value>Loading organizations...</value>
</data>
<data name="ValidatingOrganziationSelectionFailedText" xml:space="preserve">
<value>The connection is not valid for the chosen organization "{0}". Make sure the entered key matches exactly your organization's key.</value>
</data>
</root>

0 comments on commit 6a6ab7c

Please sign in to comment.