diff --git a/src/ConnectedMode.UnitTests/SlCoreConnectionAdapterTests.cs b/src/ConnectedMode.UnitTests/SlCoreConnectionAdapterTests.cs index 52f0ecf7ae..8e5a19f671 100644 --- a/src/ConnectedMode.UnitTests/SlCoreConnectionAdapterTests.cs +++ b/src/ConnectedMode.UnitTests/SlCoreConnectionAdapterTests.cs @@ -60,7 +60,7 @@ public async Task ValidateConnectionAsync_SwitchesToBackgroundThread() var threadHandlingMock = Substitute.For(); 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>>()); } @@ -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(); @@ -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(x => IsExpectedSonarQubeConnectionParams(x, token))); @@ -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(x => IsExpectedSonarQubeConnectionParams(x, username, password))); @@ -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(x => IsExpectedSonarCloudConnectionParams(x, token))); @@ -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(x => IsExpectedSonarCloudConnectionParams(x, username, password))); @@ -130,7 +130,7 @@ public async Task ValidateConnectionAsync_ReturnsResponseFromSlCore(bool success var expectedResponse = new ValidateConnectionResponse(success, message); connectionConfigurationSlCoreService.ValidateConnectionAsync(Arg.Any()).Returns(expectedResponse); - var response = await testSubject.ValidateConnectionAsync(sonarCloudConnectionInfo, "token"); + var response = await testSubject.ValidateConnectionAsync(sonarCloudConnectionInfo, new TokenCredentialsModel("myToken")); response.Success.Should().Be(success); } @@ -142,7 +142,7 @@ public async Task ValidateConnectionAsync_SlCoreValidationThrowsException_Return connectionConfigurationSlCoreService.When(x => x.ValidateConnectionAsync(Arg.Any())) .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(); diff --git a/src/ConnectedMode.UnitTests/UI/Credentials/CredentialsViewModelTests.cs b/src/ConnectedMode.UnitTests/UI/Credentials/CredentialsViewModelTests.cs index 45df33d402..cb8ec5b3ef 100644 --- a/src/ConnectedMode.UnitTests/UI/Credentials/CredentialsViewModelTests.cs +++ b/src/ConnectedMode.UnitTests/UI/Credentials/CredentialsViewModelTests.cs @@ -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(x => x.Token == testSubject.Token)); } [TestMethod] @@ -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(x => x.Username == testSubject.Username && x.Password == testSubject.Password)); } [TestMethod] @@ -368,9 +370,7 @@ public void GetCredentialsModel_SelectedAuthenticationTypeIsCredentials_ReturnsM private void MockAdapterValidateConnectionAsync(bool success = true) { - slCoreConnectionAdapter.ValidateConnectionAsync(Arg.Any(), Arg.Any()) - .Returns(new AdapterResponse(success)); - slCoreConnectionAdapter.ValidateConnectionAsync(Arg.Any(), Arg.Any(), Arg.Any()) + slCoreConnectionAdapter.ValidateConnectionAsync(Arg.Any(), Arg.Any()) .Returns(new AdapterResponse(success)); } } diff --git a/src/ConnectedMode.UnitTests/UI/OrganizationSelection/OrganizationSelectionViewModelTests.cs b/src/ConnectedMode.UnitTests/UI/OrganizationSelection/OrganizationSelectionViewModelTests.cs index cf9eca309e..d5ae228fee 100644 --- a/src/ConnectedMode.UnitTests/UI/OrganizationSelection/OrganizationSelectionViewModelTests.cs +++ b/src/ConnectedMode.UnitTests/UI/OrganizationSelection/OrganizationSelectionViewModelTests.cs @@ -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; @@ -40,6 +41,7 @@ public void TestInitialize() credentialsModel = Substitute.For(); slCoreConnectionAdapter = Substitute.For(); progressReporterViewModel = Substitute.For(); + progressReporterViewModel.ExecuteTaskWithProgressAsync(Arg.Any>()).Returns(new AdapterResponse(true)); testSubject = new(credentialsModel, slCoreConnectionAdapter, progressReporterViewModel); } @@ -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() { @@ -213,4 +221,48 @@ public void UpdateOrganizations_RaisesEvents() eventHandler.Received().Invoke(testSubject, Arg.Is(x => x.PropertyName == nameof(testSubject.NoOrganizationExists))); } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public async Task ValidateConnectionForOrganizationAsync_ReturnsResponseFromSlCore(bool success) + { + progressReporterViewModel.ExecuteTaskWithProgressAsync(Arg.Any>()).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>(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> xTaskToPerform, string organizationKey) + { + xTaskToPerform().Forget(); + slCoreConnectionAdapter.Received(1).ValidateConnectionAsync(Arg.Is(x=> x.Id == organizationKey), Arg.Any()); + return true; + } } diff --git a/src/ConnectedMode/SlCoreConnectionAdapter.cs b/src/ConnectedMode/SlCoreConnectionAdapter.cs index 02ccad79f9..ebe6912fd4 100644 --- a/src/ConnectedMode/SlCoreConnectionAdapter.cs +++ b/src/ConnectedMode/SlCoreConnectionAdapter.cs @@ -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; @@ -35,8 +34,7 @@ namespace SonarLint.VisualStudio.ConnectedMode; public interface ISlCoreConnectionAdapter { - Task ValidateConnectionAsync(ConnectionInfo connectionInfo, string token); - Task ValidateConnectionAsync(ConnectionInfo connectionInfo, string username, string password); + Task ValidateConnectionAsync(ConnectionInfo connectionInfo, ICredentialsModel credentialsModel); Task>> GetOrganizationsAsync(ICredentialsModel credentialsModel); } @@ -68,15 +66,10 @@ public SlCoreConnectionAdapter(ISLCoreServiceProvider serviceProvider, IThreadHa this.logger = logger; } - public async Task ValidateConnectionAsync(ConnectionInfo connectionInfo, string token) + public async Task ValidateConnectionAsync(ConnectionInfo connectionInfo, ICredentialsModel credentialsModel) { - var validateConnectionParams = GetValidateConnectionParams(connectionInfo, GetEitherForToken(token)); - return await ValidateConnectionAsync(validateConnectionParams); - } - - public async Task 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); } diff --git a/src/ConnectedMode/UI/Credentials/CredentialsViewModel.cs b/src/ConnectedMode/UI/Credentials/CredentialsViewModel.cs index a3e450e41b..a5c798e2fa 100644 --- a/src/ConnectedMode/UI/Credentials/CredentialsViewModel.cs +++ b/src/ConnectedMode/UI/Credentials/CredentialsViewModel.cs @@ -118,11 +118,7 @@ internal async Task ValidateConnectionAsync() internal async Task 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() diff --git a/src/ConnectedMode/UI/ManageConnections/ManageConnectionsDialog.xaml.cs b/src/ConnectedMode/UI/ManageConnections/ManageConnectionsDialog.xaml.cs index 6dfc5312a9..766205b9c9 100644 --- a/src/ConnectedMode/UI/ManageConnections/ManageConnectionsDialog.xaml.cs +++ b/src/ConnectedMode/UI/ManageConnections/ManageConnectionsDialog.xaml.cs @@ -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) diff --git a/src/ConnectedMode/UI/OrganizationSelection/OrganizationSelectionDialog.xaml.cs b/src/ConnectedMode/UI/OrganizationSelection/OrganizationSelectionDialog.xaml.cs index 3106a82661..79500cf27a 100644 --- a/src/ConnectedMode/UI/OrganizationSelection/OrganizationSelectionDialog.xaml.cs +++ b/src/ConnectedMode/UI/OrganizationSelection/OrganizationSelectionDialog.xaml.cs @@ -20,36 +20,55 @@ 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 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; } } @@ -57,5 +76,15 @@ private async void OrganizationSelectionDialog_OnLoaded(object sender, RoutedEve { await ViewModel.LoadOrganizationsAsync(); } + + private async Task UpdateFinalConnectionInfoAsync(string organizationKey) + { + var isConnectionValid = await ValidateConnectionForSelectedOrganizationAsync(organizationKey); + if (isConnectionValid) + { + ViewModel.UpdateFinalConnectionInfo(organizationKey); + DialogResult = true; + } + } } diff --git a/src/ConnectedMode/UI/OrganizationSelection/OrganizationSelectionViewModel.cs b/src/ConnectedMode/UI/OrganizationSelection/OrganizationSelectionViewModel.cs index c460d120fe..30c4695f8f 100644 --- a/src/ConnectedMode/UI/OrganizationSelection/OrganizationSelectionViewModel.cs +++ b/src/ConnectedMode/UI/OrganizationSelection/OrganizationSelectionViewModel.cs @@ -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 { @@ -43,10 +42,11 @@ public OrganizationDisplay SelectedOrganization } public bool IsValidSelectedOrganization => SelectedOrganization is { Key: var key } && !string.IsNullOrWhiteSpace(key); - public ObservableCollection Organizations { get; } = []; public bool NoOrganizationExists => Organizations.Count == 0; + private OrganizationDisplay selectedOrganization; + public void AddOrganization(OrganizationDisplay organization) { Organizations.Add(organization); @@ -76,4 +76,20 @@ internal void UpdateOrganizations(AdapterResponseWithData ValidateConnectionForOrganizationAsync(string organizationKey, string warningText) + { + var connectionInfoToValidate = new ConnectionInfo(organizationKey, ConnectionServerType.SonarCloud); + var validationParams = new TaskToPerformParams( + 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); + } } diff --git a/src/ConnectedMode/UI/Resources/UiResources.Designer.cs b/src/ConnectedMode/UI/Resources/UiResources.Designer.cs index 8d16661713..c0b01a703f 100644 --- a/src/ConnectedMode/UI/Resources/UiResources.Designer.cs +++ b/src/ConnectedMode/UI/Resources/UiResources.Designer.cs @@ -752,5 +752,14 @@ public static string ValidatingConnectionProgressText { return ResourceManager.GetString("ValidatingConnectionProgressText", resourceCulture); } } + + /// + /// Looks up a localized string similar to The connection is not valid for the chosen organization "{0}". Make sure the entered key matches exactly your organization's key.. + /// + public static string ValidatingOrganziationSelectionFailedText { + get { + return ResourceManager.GetString("ValidatingOrganziationSelectionFailedText", resourceCulture); + } + } } } diff --git a/src/ConnectedMode/UI/Resources/UiResources.resx b/src/ConnectedMode/UI/Resources/UiResources.resx index e8bbb71148..65f0828287 100644 --- a/src/ConnectedMode/UI/Resources/UiResources.resx +++ b/src/ConnectedMode/UI/Resources/UiResources.resx @@ -348,4 +348,7 @@ Loading organizations... + + The connection is not valid for the chosen organization "{0}". Make sure the entered key matches exactly your organization's key. + \ No newline at end of file