Skip to content

SLVS-1373 Validate credentials in OrganizationSelectionDialog #5634

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

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be made private, see other comment. The warning text does not need to be parametrized as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it does. For usability purposes.

{
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>