diff --git a/src/ConnectedMode.UnitTests/Binding/BindingProcessFactoryTests.cs b/src/ConnectedMode.UnitTests/Binding/BindingProcessFactoryTests.cs index 4f20a5b0cb..a3c8894821 100644 --- a/src/ConnectedMode.UnitTests/Binding/BindingProcessFactoryTests.cs +++ b/src/ConnectedMode.UnitTests/Binding/BindingProcessFactoryTests.cs @@ -23,6 +23,7 @@ using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Analysis; +using SonarLint.VisualStudio.Core.Binding; using SonarLint.VisualStudio.TestInfrastructure; using SonarQube.Client; using SonarQube.Client.Models; @@ -45,7 +46,7 @@ public void MefCtor_CheckIsExported() [TestMethod] public void Create_ReturnsProcessImpl() { - var bindingArgs = new BindCommandArgs("proj key", "proj name", new ConnectionInformation(new Uri("http://localhost"))); + var bindingArgs = new BindCommandArgs(new BoundServerProject("any", "any", new ServerConnection.SonarCloud("any"))); var testSubject = CreateTestSubject(); diff --git a/src/ConnectedMode.UnitTests/Binding/BindingProcessImplTests.cs b/src/ConnectedMode.UnitTests/Binding/BindingProcessImplTests.cs index 9705c5ecff..50af0a34b6 100644 --- a/src/ConnectedMode.UnitTests/Binding/BindingProcessImplTests.cs +++ b/src/ConnectedMode.UnitTests/Binding/BindingProcessImplTests.cs @@ -43,7 +43,7 @@ public class BindingProcessImplTests [TestMethod] public void Ctor_ArgChecks() { - var bindingArgs = CreateBindCommandArgs(connection: new ConnectionInformation(new Uri("http://server"))); + var bindingArgs = CreateBindCommandArgs(); var exclusionSettingsStorage = Mock.Of(); var qpDownloader = Mock.Of(); var sonarQubeService = Mock.Of(); @@ -150,9 +150,7 @@ public async Task DownloadQualityProfile_CreatesBoundProjectAndCallsQPDownloader var qpDownloader = new Mock(); var progress = Mock.Of>(); - var connectionInfo = new ConnectionInformation(new Uri("http://theServer")); - connectionInfo.Organization = new SonarQubeOrganization("the org key", "the org name"); - var bindingArgs = CreateBindCommandArgs("the project key", "the project name", connectionInfo); + var bindingArgs = CreateBindCommandArgs("the project key", "http://theServer"); var testSubject = CreateTestSubject(bindingArgs, qpDownloader: qpDownloader.Object); @@ -162,61 +160,15 @@ public async Task DownloadQualityProfile_CreatesBoundProjectAndCallsQPDownloader result.Should().BeTrue(); - qpDownloader.Verify(x => x.UpdateAsync(It.IsAny(), progress, It.IsAny()), + qpDownloader.Verify(x => x.UpdateAsync(It.IsAny(), progress, It.IsAny()), Times.Once); - var actualProject = (BoundSonarQubeProject)qpDownloader.Invocations[0].Arguments[0]; + var actualProject = (BoundServerProject)qpDownloader.Invocations[0].Arguments[0]; // Check the bound project was correctly constructed from the BindCommandArgs actualProject.Should().NotBeNull(); - actualProject.ServerUri.Should().Be("http://theServer"); - actualProject.ProjectKey.Should().Be("the project key"); - actualProject.ProjectName.Should().Be("the project name"); - actualProject.Organization.Key.Should().Be("the org key"); - actualProject.Organization.Name.Should().Be("the org name"); - } - - [TestMethod] - [DataRow("the user name", null)] - [DataRow("the user name", "a password")] - [DataRow(null, null)] - [DataRow(null, "should be ignored")] - public async Task DownloadQualityProfile_HandlesBoundProjectCredentialsCorrectly(string userName, string rawPassword) - { - var qpDownloader = new Mock(); - var password = rawPassword == null ? null : rawPassword.ToSecureString(); - - var connectionInfo = new ConnectionInformation(new Uri("http://any"), userName, password); - var bindingArgs = CreateBindCommandArgs(connection: connectionInfo); - - var testSubject = CreateTestSubject(bindingArgs, - qpDownloader: qpDownloader.Object); - - // Act - var result = await testSubject.DownloadQualityProfileAsync(Mock.Of>(), CancellationToken.None); - - result.Should().BeTrue(); - - qpDownloader.Verify(x => x.UpdateAsync(It.IsAny(), - It.IsAny>(), - It.IsAny()), - Times.Once); - - var actualProject = (BoundSonarQubeProject)qpDownloader.Invocations[0].Arguments[0]; - - // Check the credentials were handled correctly - if (userName == null) - { - actualProject.Credentials.Should().BeNull(); - } - else - { - actualProject.Credentials.Should().BeOfType(); - var actualCreds = (BasicAuthCredentials)actualProject.Credentials; - - actualCreds.UserName.Should().Be(userName); - CheckIsExpectedPassword(rawPassword, actualCreds.Password); - } + actualProject.ServerConnection.ServerUri.Should().Be("http://theServer"); + actualProject.ServerProjectKey.Should().Be("the project key"); } [TestMethod] @@ -225,7 +177,7 @@ public async Task DownloadQualityProfile_HandlesInvalidOperationException() var qpDownloader = new Mock(); qpDownloader .Setup(x => - x.UpdateAsync(It.IsAny(), + x.UpdateAsync(It.IsAny(), It.IsAny>(), It.IsAny())) .Throws(new InvalidOperationException()); @@ -238,7 +190,7 @@ public async Task DownloadQualityProfile_HandlesInvalidOperationException() await testSubject.DownloadQualityProfileAsync(Mock.Of>(), CancellationToken.None); result.Should().BeFalse(); - qpDownloader.Verify(x => x.UpdateAsync(It.IsAny(), + qpDownloader.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny>(), It.IsAny()), Times.Once); @@ -267,10 +219,9 @@ private BindingProcessImpl CreateTestSubject(BindCommandArgs bindingArgs = null, logger); } - private BindCommandArgs CreateBindCommandArgs(string projectKey = "key", string projectName = "name", ConnectionInformation connection = null) + private BindCommandArgs CreateBindCommandArgs(string projectKey = "key", string serverUri = "http://any") { - connection = connection ?? new ConnectionInformation(new Uri("http://connected")); - return new BindCommandArgs(projectKey, projectName, connection); + return new BindCommandArgs(new BoundServerProject("any", projectKey, new ServerConnection.SonarQube(new Uri(serverUri)))); } private static void CheckIsExpectedPassword(string expectedRawPassword, SecureString actualPassword) diff --git a/src/ConnectedMode.UnitTests/Binding/CSharpVBBindingConfigProviderTests.cs b/src/ConnectedMode.UnitTests/Binding/CSharpVBBindingConfigProviderTests.cs index e6a93c8547..f95f5b03ac 100644 --- a/src/ConnectedMode.UnitTests/Binding/CSharpVBBindingConfigProviderTests.cs +++ b/src/ConnectedMode.UnitTests/Binding/CSharpVBBindingConfigProviderTests.cs @@ -185,7 +185,6 @@ public async Task GetConfig_ReturnsCorrectAdditionalFile() public async Task GetConfig_HasActiveInactiveAndUnsupportedRules_ReturnsValidBindingConfig() { // Arrange - const string expectedProjectName = "my project"; const string expectedServerUrl = "http://myhost:123/"; var properties = new SonarQubeProperty[] @@ -207,7 +206,7 @@ public async Task GetConfig_HasActiveInactiveAndUnsupportedRules_ReturnsValidBin InactiveRuleWithUnsupportedSeverity }; - var builder = new TestEnvironmentBuilder(validQualityProfile, Language.CSharp, expectedProjectName, expectedServerUrl) + var builder = new TestEnvironmentBuilder(validQualityProfile, Language.CSharp, expectedServerUrl) { ActiveRulesResponse = activeRules, InactiveRulesResponse = inactiveRules, @@ -278,17 +277,14 @@ private class TestEnvironmentBuilder private readonly SonarQubeQualityProfile profile; private readonly Language language; - private readonly string projectName; private readonly string serverUrl; private const string ExpectedProjectKey = "fixed.project.key"; - public TestEnvironmentBuilder(SonarQubeQualityProfile profile, Language language, - string projectName = "any", string serverUrl = "http://any") + public TestEnvironmentBuilder(SonarQubeQualityProfile profile, Language language, string serverUrl = "http://any") { this.profile = profile; this.language = language; - this.projectName = projectName; this.serverUrl = serverUrl; Logger = new TestLogger(); @@ -342,8 +338,10 @@ public CSharpVBBindingConfigProvider CreateTestSubject() CapturedRulesPassedToGlobalConfigGenerator = rules; }); - BindingConfiguration = new BindingConfiguration(new BoundSonarQubeProject(new Uri(serverUrl), ExpectedProjectKey, projectName), - SonarLintMode.Connected, "c:\\test\\"); + BindingConfiguration = new BindingConfiguration( + new BoundServerProject("solution", ExpectedProjectKey, new ServerConnection.SonarQube(new Uri(serverUrl))), + SonarLintMode.Connected, + "c:\\test\\"); var sonarProperties = PropertiesResponse.ToDictionary(x => x.Key, y => y.Value); sonarLintConfigGeneratorMock diff --git a/src/ConnectedMode.UnitTests/Binding/CompositeBindingConfigProviderTests.cs b/src/ConnectedMode.UnitTests/Binding/CompositeBindingConfigProviderTests.cs index 4d16c594ec..3548b74c0f 100644 --- a/src/ConnectedMode.UnitTests/Binding/CompositeBindingConfigProviderTests.cs +++ b/src/ConnectedMode.UnitTests/Binding/CompositeBindingConfigProviderTests.cs @@ -129,7 +129,8 @@ public DummyProvider(IBindingConfig configToReturn = null, params Language[] sup #region IBindingConfigProvider implementation - public Task GetConfigurationAsync(SonarQubeQualityProfile qualityProfile, Language language, BindingConfiguration bindingConfiguration, CancellationToken cancellationToken) + public Task GetConfigurationAsync(SonarQubeQualityProfile qualityProfile, Language language, + BindingConfiguration bindingConfiguration, CancellationToken cancellationToken) { return Task.FromResult(ConfigToReturn); } diff --git a/src/ConnectedMode.UnitTests/Binding/NonRoslynBindingConfigProviderTests.cs b/src/ConnectedMode.UnitTests/Binding/NonRoslynBindingConfigProviderTests.cs index 125cdcba69..a373b04a37 100644 --- a/src/ConnectedMode.UnitTests/Binding/NonRoslynBindingConfigProviderTests.cs +++ b/src/ConnectedMode.UnitTests/Binding/NonRoslynBindingConfigProviderTests.cs @@ -129,7 +129,7 @@ public async Task GetRules_Success() serviceMock.Setup(x => x.GetRulesAsync(true, It.IsAny(), It.IsAny())) .ReturnsAsync(() => rules); - var bindingConfiguration = new BindingConfiguration(new BoundSonarQubeProject { ProjectKey = "test" }, SonarLintMode.Connected, "c:\\"); + var bindingConfiguration = new BindingConfiguration(new BoundServerProject("any", "any", new ServerConnection.SonarCloud("any")), SonarLintMode.Connected, "c:\\"); var testSubject = CreateTestSubject(serviceMock, testLogger); @@ -176,7 +176,7 @@ public async Task GetRules_NoData_EmptyResultReturned() serviceMock.Setup(x => x.GetRulesAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(() => new List()); - var bindingConfiguration = new BindingConfiguration(new BoundSonarQubeProject { ProjectKey = "test" }, SonarLintMode.Connected, "c:\\"); + var bindingConfiguration = new BindingConfiguration(new BoundServerProject("any", "any", new ServerConnection.SonarCloud("any")), SonarLintMode.Connected, "c:\\"); var testSubject = CreateTestSubject(serviceMock, testLogger); diff --git a/src/ConnectedMode.UnitTests/Binding/UnintrusiveBindingControllerTests.cs b/src/ConnectedMode.UnitTests/Binding/UnintrusiveBindingControllerTests.cs index 8f1f9627a5..66e60b69e8 100644 --- a/src/ConnectedMode.UnitTests/Binding/UnintrusiveBindingControllerTests.cs +++ b/src/ConnectedMode.UnitTests/Binding/UnintrusiveBindingControllerTests.cs @@ -18,81 +18,118 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Collections.Generic; -using System.Threading; +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; +using SonarQube.Client.Helpers; +using SonarQube.Client.Models; using Task = System.Threading.Tasks.Task; -namespace SonarLint.VisualStudio.ConnectedMode.Binding.UnitTests +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Binding; + +[TestClass] +public class UnintrusiveBindingControllerTests { - [TestClass] - public class UnintrusiveBindingControllerTests - { - private static readonly BoundSonarQubeProject AnyBoundProject = new BoundSonarQubeProject(new Uri("http://localhost:9000"), "any-key", "any-name"); + 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)); - [TestMethod] - public void MefCtor_CheckTypeIsNonShared() - => MefTestHelpers.CheckIsNonSharedMefComponent(); + [TestMethod] + public void MefCtor_CheckTypeIsNonShared() + => MefTestHelpers.CheckIsNonSharedMefComponent(); - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport()); - } + [TestMethod] + public void MefCtor_IUnintrusiveBindingController_CheckIsExported() + { + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + } + + [TestMethod] + public void MefCtor_IBindingController_CheckIsExported() + { + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + } - [TestMethod] - public async Task BindAsnyc_GetsBindingProcessFromFactory() - { - var bindingProcessFactory = CreateBindingProcessFactory(); + [TestMethod] + public async Task BindAsync_EstablishesConnection() + { + var sonarQubeService = Substitute.For(); + var projectToBind = new BoundServerProject( + "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(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(); + var testSubject = CreateTestSubject(activeSolutionChangedHandler: activeSolutionChangedHandler); + + await testSubject.BindAsync(AnyBoundProject, ACancellationToken); + + activeSolutionChangedHandler + .Received(1) + .HandleBindingChange(false); + } - var testSubject = CreateTestSubject(bindingProcessFactory: bindingProcessFactory.Object); - await testSubject.BindAsync(AnyBoundProject, null, CancellationToken.None); + [TestMethod] + public async Task BindAsync_CallsBindingProcessInOrder() + { + var cancellationToken = CancellationToken.None; + var bindingProcess = Substitute.For(); + var bindingProcessFactory = CreateBindingProcessFactory(bindingProcess); + var testSubject = CreateTestSubject(bindingProcessFactory); - var args = bindingProcessFactory.Invocations[0].Arguments[0] as BindCommandArgs; + await testSubject.BindAsync(AnyBoundProject, null, cancellationToken); - args.ProjectName.Should().Be(AnyBoundProject.ProjectName); - args.ProjectKey.Should().Be(AnyBoundProject.ProjectKey); - args.Connection.ServerUri.Should().Be(AnyBoundProject.ServerUri); - - bindingProcessFactory.Verify(x => x.Create(It.IsAny()), Times.Once); - } - - [TestMethod] - public async Task BindAsnyc_CallsBindingProcessInOrder() - { - var calls = new List(); - var cancellationToken = CancellationToken.None; - - var bindingProcess = new Mock(); - bindingProcess.Setup(x => x.DownloadQualityProfileAsync(null, cancellationToken)).Callback(() => calls.Add("DownloadQualityProfiles")); - bindingProcess.Setup(x => x.SaveServerExclusionsAsync(cancellationToken)).Callback(() => calls.Add("SaveServerExclusionsAsync")); - - var testSubject = CreateTestSubject(bindingProcessFactory: CreateBindingProcessFactory(bindingProcess.Object).Object); - await testSubject.BindAsync(AnyBoundProject, null, cancellationToken); - - calls.Should().ContainInOrder("DownloadQualityProfiles", "SaveServerExclusionsAsync"); - } - - private UnintrusiveBindingController CreateTestSubject(IBindingProcessFactory bindingProcessFactory = null) + Received.InOrder(() => { - bindingProcessFactory ??= CreateBindingProcessFactory().Object; - - var testSubject = new UnintrusiveBindingController(bindingProcessFactory); + bindingProcessFactory.Create(Arg.Is(b => b.ProjectToBind == AnyBoundProject)); + bindingProcess.DownloadQualityProfileAsync(null, cancellationToken); + bindingProcess.SaveServerExclusionsAsync(cancellationToken); + }); + } + + private UnintrusiveBindingController CreateTestSubject(IBindingProcessFactory bindingProcessFactory = null, + ISonarQubeService sonarQubeService = null, + IActiveSolutionChangedHandler activeSolutionChangedHandler = null) + { + var testSubject = new UnintrusiveBindingController(bindingProcessFactory ?? CreateBindingProcessFactory(), + sonarQubeService ?? Substitute.For(), + activeSolutionChangedHandler ?? Substitute.For()); - return testSubject; - } + return testSubject; + } - private Mock CreateBindingProcessFactory(IBindingProcess bindingProcess = null) - { - bindingProcess ??= Mock.Of(); + private static IBindingProcessFactory CreateBindingProcessFactory(IBindingProcess bindingProcess = null) + { + bindingProcess ??= Substitute.For(); - var bindingProcessFactory = new Mock(); - bindingProcessFactory.Setup(x => x.Create(It.IsAny())).Returns(bindingProcess); + var bindingProcessFactory = Substitute.For(); + bindingProcessFactory.Create(Arg.Any()).Returns(bindingProcess); - return bindingProcessFactory; - } + return bindingProcessFactory; } } diff --git a/src/ConnectedMode.UnitTests/Binding/UnintrusiveBindingPathProviderTests.cs b/src/ConnectedMode.UnitTests/Binding/UnintrusiveBindingPathProviderTests.cs index 3c568d609b..2ca1f06ddb 100644 --- a/src/ConnectedMode.UnitTests/Binding/UnintrusiveBindingPathProviderTests.cs +++ b/src/ConnectedMode.UnitTests/Binding/UnintrusiveBindingPathProviderTests.cs @@ -31,61 +31,59 @@ public class UnintrusiveBindingPathProviderTests { [TestMethod] public void MefCtor_CheckIsExported() - => MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport()); + => MefTestHelpers.CheckTypeCanBeImported(); [TestMethod] public void MefCtor_CheckIsNonSharedMefComponent() => MefTestHelpers.CheckIsNonSharedMefComponent(); [TestMethod] - public void Ctor_DoesNotCallServices() + public void Get_NoOpenSolution_ReturnsNull() { - // The constructor should be free-threaded i.e. run entirely on the calling thread - // -> should not call services that swtich threads - var solutionInfoProvider = new Mock(); - - _ = CreateTestSubject(solutionInfoProvider.Object); + var testSubject = CreateTestSubject(); - solutionInfoProvider.Invocations.Should().BeEmpty(); + var actual = testSubject.GetBindingPath(null); + actual.Should().BeNull(); } [TestMethod] - public void Get_NoOpenSolution_ReturnsNull() + public void Get_HasOpenSolution_ReturnsExpectedValue() { - var serviceProvider = CreateSolutionInfoProvider(null); - var testSubject = CreateTestSubject(serviceProvider.Object); + const string solutionName = "mysolutionName"; + const string rootFolderName = @"x:\users\foo\"; - var actual = testSubject.GetCurrentBindingPath(); - actual.Should().BeNull(); + var envVars = CreateEnvVars(rootFolderName); + + var testSubject = CreateTestSubject(envVars); + + var actual = testSubject.GetBindingPath(solutionName); + + actual.Should().Be($@"{rootFolderName}SonarLint for Visual Studio\Bindings\{solutionName}\binding.config"); } [TestMethod] - public void Get_HasOpenSolution_ReturnsExpectedValue() + public void GetBindingKeyFromPath_ReturnsBindingFolderName() { const string solutionName = "mysolutionName"; const string rootFolderName = @"x:\users\foo\"; - var solutionInfoProvider = CreateSolutionInfoProvider(solutionName); var envVars = CreateEnvVars(rootFolderName); - var testSubject = CreateTestSubject(solutionInfoProvider.Object, envVars); + var testSubject = CreateTestSubject(envVars); - var actual = testSubject.GetCurrentBindingPath(); + var actual = testSubject.GetBindingKeyFromPath($@"{rootFolderName}SonarLint for Visual Studio\Bindings\{solutionName}\binding.config"); - actual.Should().Be($@"{rootFolderName}SonarLint for Visual Studio\Bindings\{solutionName}\binding.config"); + actual.Should().Be(solutionName); } [TestMethod] public void GetBindingFolders_NoBindingFolder_ReturnsEmpy() { - const string solutionName = "mysolutionName"; const string rootFolderName = @"x:\users\foo\"; - var solutionInfoProvider = CreateSolutionInfoProvider(solutionName); var envVars = CreateEnvVars(rootFolderName); - var testSubject = CreateTestSubject(solutionInfoProvider.Object, envVars); + var testSubject = CreateTestSubject(envVars); var actual = testSubject.GetBindingPaths(); @@ -133,21 +131,11 @@ public void GetBindingFolders_ReturnsBindings() actual.Should().BeEquivalentTo(bindingFolders); } - private static UnintrusiveBindingPathProvider CreateTestSubject(ISolutionInfoProvider solutionInfoProvider = null, - IEnvironmentVariableProvider envVars = null, IFileSystem fileSystem = null) + private static UnintrusiveBindingPathProvider CreateTestSubject(IEnvironmentVariableProvider envVars = null, IFileSystem fileSystem = null) { - solutionInfoProvider ??= CreateSolutionInfoProvider(null).Object; fileSystem ??= new MockFileSystem(); envVars ??= CreateEnvVars("any"); - return new UnintrusiveBindingPathProvider(solutionInfoProvider, envVars, fileSystem); - } - - private static Mock CreateSolutionInfoProvider(string solutionName = null) - { - var solutionInfoProvider = new Mock(); - solutionInfoProvider.Setup(x => x.GetSolutionName()).Returns(solutionName); - - return solutionInfoProvider; + return new UnintrusiveBindingPathProvider(envVars, fileSystem); } private static IEnvironmentVariableProvider CreateEnvVars(string rootInstallPath) diff --git a/src/ConnectedMode.UnitTests/Binding/UnintrusiveConfigurationProviderTests.cs b/src/ConnectedMode.UnitTests/Binding/UnintrusiveConfigurationProviderTests.cs index e0bc2de8c9..75bf71cee7 100644 --- a/src/ConnectedMode.UnitTests/Binding/UnintrusiveConfigurationProviderTests.cs +++ b/src/ConnectedMode.UnitTests/Binding/UnintrusiveConfigurationProviderTests.cs @@ -18,11 +18,12 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using SonarLint.VisualStudio.ConnectedMode.Persistence; +using SonarLint.VisualStudio.ConnectedMode.Binding; +using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; using SonarLint.VisualStudio.TestInfrastructure; -namespace SonarLint.VisualStudio.ConnectedMode.Binding +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Binding { [TestClass] public class UnintrusiveConfigurationProviderTests @@ -32,16 +33,37 @@ public void MefCtor_CheckIsExported() { MefTestHelpers.CheckTypeCanBeImported( MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + } + + [TestMethod] + public void GetConfig_NoActiveSolution_ReturnsStandalone() + { + // Arrange + var (pathProvider, solutionInfoProvider) = SetUpConfiguration(null,null); + var configRepository = Substitute.For(); + var testSubject = CreateTestSubject(pathProvider, solutionInfoProvider, configRepository); + + // Act + var actual = testSubject.GetConfiguration(); + + // Assert + actual.Should().NotBeNull(); + actual.Project.Should().BeNull(); + actual.Mode.Should().Be(SonarLintMode.Standalone); + configRepository.ReceivedCalls().Should().BeEmpty(); + pathProvider.ReceivedCalls().Should().BeEmpty(); + solutionInfoProvider.Received().GetSolutionName(); } [TestMethod] public void GetConfig_NoConfig_ReturnsStandalone() { // Arrange - var pathProvider = CreatePathProvider(null); - var configRepository = new Mock(); - var testSubject = CreateTestSubject(pathProvider, configRepository.Object); + var (pathProvider, solutionInfoProvider) = SetUpConfiguration("solution123",null); + var configRepository = Substitute.For(); + var testSubject = CreateTestSubject(pathProvider, solutionInfoProvider, configRepository); // Act var actual = testSubject.GetConfiguration(); @@ -50,68 +72,80 @@ public void GetConfig_NoConfig_ReturnsStandalone() actual.Should().NotBeNull(); actual.Project.Should().BeNull(); actual.Mode.Should().Be(SonarLintMode.Standalone); - configRepository.Invocations.Should().BeEmpty(); + configRepository.ReceivedCalls().Should().BeEmpty(); + solutionInfoProvider.Received().GetSolutionName(); + pathProvider.Received().GetBindingPath("solution123"); } [TestMethod] public void GetConfig_Bound_ReturnsExpectedConfig() { // Arrange - var expectedProject = new BoundSonarQubeProject(); + var expectedProject = new BoundServerProject("solution123", "project123", new ServerConnection.SonarCloud("org")); - var pathProvider = CreatePathProvider("c:\\users\\foo\\bindings\\xxx.config"); - var configReader = CreateRepo(expectedProject); - var testSubject = CreateTestSubject(pathProvider, configReader.Object); + var (pathProvider, solutionInfoProvider) = SetUpConfiguration("solution123", "c:\\users\\foo\\bindings\\xxx.config"); + var bindingRepository = CreateRepo(expectedProject); + var testSubject = CreateTestSubject(pathProvider, solutionInfoProvider, bindingRepository); // Act var actual = testSubject.GetConfiguration(); // Assert - CheckExpectedFileRead(configReader, "c:\\users\\foo\\bindings\\xxx.config"); + CheckExpectedFileRead(bindingRepository, "c:\\users\\foo\\bindings\\xxx.config"); actual.Should().NotBeNull(); - actual.Project.Should().BeSameAs(expectedProject); + actual.Project.Should().BeEquivalentTo(expectedProject); actual.Mode.Should().Be(SonarLintMode.Connected); actual.BindingConfigDirectory.Should().Be("c:\\users\\foo\\bindings"); + Received.InOrder(() => + { + solutionInfoProvider.GetSolutionName(); + pathProvider.GetBindingPath("solution123"); + bindingRepository.Read("c:\\users\\foo\\bindings\\xxx.config"); + }); } [TestMethod] public void GetConfig_ConfigReaderReturnsNull_ReturnsStandalone() { // Arrange - var pathProvider = CreatePathProvider("c:\\users\\foo\\bindings\\xxx.config"); - var configReader = CreateRepo(null); - var testSubject = CreateTestSubject(pathProvider, configReader.Object); + var expectedProject = new BoundServerProject("solution123", "project123", new ServerConnection.SonarCloud("org")); + + var (pathProvider, solutionInfoProvider) = SetUpConfiguration("solution123", "c:\\users\\foo\\bindings\\xxx.config"); + var bindingRepository = CreateRepo(null); + var testSubject = CreateTestSubject(pathProvider, solutionInfoProvider, bindingRepository); // Act var actual = testSubject.GetConfiguration(); // Assert - CheckExpectedFileRead(configReader, "c:\\users\\foo\\bindings\\xxx.config"); + CheckExpectedFileRead(bindingRepository, "c:\\users\\foo\\bindings\\xxx.config"); actual.Should().BeSameAs(BindingConfiguration.Standalone); } private static UnintrusiveConfigurationProvider CreateTestSubject(IUnintrusiveBindingPathProvider pathProvider, - ISolutionBindingRepository configRepo = null) - { - configRepo ??= Mock.Of(); - return new UnintrusiveConfigurationProvider(pathProvider, configRepo); - } + ISolutionInfoProvider solutionInfoProvider = null, + ISolutionBindingRepository configRepo = null) => + new(pathProvider, + solutionInfoProvider ?? Substitute.For(), + configRepo ?? Substitute.For()); - private static IUnintrusiveBindingPathProvider CreatePathProvider(string pathToReturn) + private static (IUnintrusiveBindingPathProvider, ISolutionInfoProvider) SetUpConfiguration(string localBindingKey, string pathToReturn) { - var pathProvider = new Mock(); - pathProvider.Setup(x => x.GetCurrentBindingPath()).Returns(() => pathToReturn); - return pathProvider.Object; + var solutionInfoProvider = Substitute.For(); + solutionInfoProvider.GetSolutionName().Returns(localBindingKey); + var pathProvider = Substitute.For(); + pathProvider.GetBindingPath(localBindingKey).Returns(pathToReturn); + return (pathProvider, solutionInfoProvider); } - private static Mock CreateRepo(BoundSonarQubeProject projectToReturn) + private static ISolutionBindingRepository CreateRepo(BoundServerProject projectToReturn) { - var repo = new Mock(); - repo.Setup(x => x.Read(It.IsAny())).Returns(projectToReturn); + var repo = Substitute.For(); + repo.Read(Arg.Any()).Returns(projectToReturn); return repo; } - private static void CheckExpectedFileRead(Mock configReader, string expectedFilePath) - => configReader.Verify(x => x.Read(expectedFilePath), Times.Once); + private static void CheckExpectedFileRead(ISolutionBindingRepository configReader, string expectedFilePath) + => configReader.Received().Read(expectedFilePath); } } diff --git a/src/ConnectedMode.UnitTests/ConnectionInfoTests.cs b/src/ConnectedMode.UnitTests/ConnectionInfoTests.cs new file mode 100644 index 0000000000..7b59fa77ec --- /dev/null +++ b/src/ConnectedMode.UnitTests/ConnectionInfoTests.cs @@ -0,0 +1,86 @@ +/* + * 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.ConnectedMode.UI.Resources; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Binding; + +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests; + +[TestClass] +public class ConnectionInfoTests +{ + [TestMethod] + public void FromServerConnection_ShouldReturnConnectionInfoWithSameId() + { + var sonarCloudServerConnection = new ServerConnection.SonarCloud("organization"); + + var connectionInfo = ConnectionInfo.From(sonarCloudServerConnection); + + connectionInfo.Id.Should().Be("organization"); + } + + [TestMethod] + public void FromServerConnection_WithSonarCloud_ShouldReturnConnectionInfo() + { + var sonarCloudServerConnection = new ServerConnection.SonarCloud("organization"); + + var connectionInfo = ConnectionInfo.From(sonarCloudServerConnection); + + connectionInfo.ServerType.Should().Be(ConnectionServerType.SonarCloud); + } + + [TestMethod] + public void FromServerConnection_WithSonarQube_ShouldReturnConnectionInfo() + { + var sonarQubeServerConnection = new ServerConnection.SonarQube(new Uri("http://localhost:9000")); + + var connectionInfo = ConnectionInfo.From(sonarQubeServerConnection); + + connectionInfo.ServerType.Should().Be(ConnectionServerType.SonarQube); + } + + [TestMethod] + public void GetIdForTransientConnection_SonarCloudWithNullId_ReturnsSonarCloudUrl() + { + var connectionInfo = new ConnectionInfo(null, ConnectionServerType.SonarCloud); + + connectionInfo.GetIdForTransientConnection().Should().Be(CoreStrings.SonarCloudUrl); + } + + [TestMethod] + public void GetIdForTransientConnection_SonarCloudWithNotNullId_ReturnsId() + { + var id = "my org"; + var connectionInfo = new ConnectionInfo(id, ConnectionServerType.SonarCloud); + + connectionInfo.GetIdForTransientConnection().Should().Be(id); + } + + [TestMethod] + [DataRow(null)] + [DataRow("http://localhost:9000")] + public void GetIdForTransientConnection_SonarQube_ReturnsId(string id) + { + var connectionInfo = new ConnectionInfo(id, ConnectionServerType.SonarQube); + + connectionInfo.GetIdForTransientConnection().Should().Be(id); + } +} diff --git a/src/ConnectedMode.UnitTests/Install/ImportBeforeInstallTriggerTests.cs b/src/ConnectedMode.UnitTests/Install/ImportBeforeInstallTriggerTests.cs index aba256d087..d9b1098773 100644 --- a/src/ConnectedMode.UnitTests/Install/ImportBeforeInstallTriggerTests.cs +++ b/src/ConnectedMode.UnitTests/Install/ImportBeforeInstallTriggerTests.cs @@ -177,7 +177,7 @@ private Mock CreateActiveSolutionBoundTrackerWihtBi private BindingConfiguration CreateBindingConfiguration(SonarLintMode mode) { - return new BindingConfiguration(new BoundSonarQubeProject(new Uri("http://localhost"), "test", ""), mode, ""); + return new BindingConfiguration(new BoundServerProject("test", "test", new ServerConnection.SonarQube(new Uri("http://localhost"))), mode, ""); } private ImportBeforeInstallTrigger CreateTestSubject(IActiveSolutionBoundTracker activeSolutionBoundTracker, IImportBeforeFileGenerator importBeforeFileGenerator = null, IThreadHandling threadHandling = null) diff --git a/src/ConnectedMode.UnitTests/Migration/BindingToConnectionMigrationTests.cs b/src/ConnectedMode.UnitTests/Migration/BindingToConnectionMigrationTests.cs new file mode 100644 index 0000000000..be465df1fa --- /dev/null +++ b/src/ConnectedMode.UnitTests/Migration/BindingToConnectionMigrationTests.cs @@ -0,0 +1,257 @@ +/* + * 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.ConnectedMode.Binding; +using SonarLint.VisualStudio.ConnectedMode.Migration; +using SonarLint.VisualStudio.ConnectedMode.Persistence; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Binding; +using SonarLint.VisualStudio.TestInfrastructure; +using SonarQube.Client.Helpers; + +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Migration; + +[TestClass] +public class BindingToConnectionMigrationTests +{ + private BindingToConnectionMigration testSubject; + private IServerConnectionsRepository serverConnectionsRepository; + private ILegacySolutionBindingRepository legacyBindingRepository; + private IUnintrusiveBindingPathProvider unintrusiveBindingPathProvider; + private IThreadHandling threadHandling; + private ILogger logger; + private ISolutionBindingRepository solutionBindingRepository; + + [TestInitialize] + public void TestInitialize() + { + serverConnectionsRepository = Substitute.For(); + legacyBindingRepository = Substitute.For(); + solutionBindingRepository = Substitute.For(); + unintrusiveBindingPathProvider = Substitute.For(); + logger = Substitute.For(); + threadHandling = new NoOpThreadHandler(); + + testSubject = new BindingToConnectionMigration( + serverConnectionsRepository, + legacyBindingRepository, + solutionBindingRepository, + unintrusiveBindingPathProvider, + threadHandling, + logger); + } + + [TestMethod] + public void MefCtor_CheckExports() + { + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + } + + [TestMethod] + public void Mef_CheckIsSingleton() + { + MefTestHelpers.CheckIsSingletonMefComponent(); + } + + [TestMethod] + public async Task MigrateBindingToServerConnectionIfNeeded_RunsOnBackgroundThread() + { + var mockedThreadHandling = Substitute.For(); + var migrateBindingToServer = new BindingToConnectionMigration( + serverConnectionsRepository, + legacyBindingRepository, + solutionBindingRepository, + unintrusiveBindingPathProvider, + mockedThreadHandling, + logger); + + await migrateBindingToServer.MigrateAllBindingsToServerConnectionsIfNeededAsync(); + + mockedThreadHandling.ReceivedCalls().Should().Contain(call => call.GetMethodInfo().Name == nameof(IThreadHandling.RunOnBackgroundThread)); + } + + [TestMethod] + public async Task MigrateBindingToServerConnectionIfNeeded_ConnectionsStorageFileExists_ShouldNotMigrate() + { + serverConnectionsRepository.ConnectionsFileExists().Returns(true); + + await testSubject.MigrateAllBindingsToServerConnectionsIfNeededAsync(); + + serverConnectionsRepository.Received(1).ConnectionsFileExists(); + serverConnectionsRepository.DidNotReceiveWithAnyArgs().TryAdd(default); + unintrusiveBindingPathProvider.DidNotReceive().GetBindingPaths(); + } + + [TestMethod] + public async Task MigrateBindingToServerConnectionIfNeeded_ConnectionsStorageDoesNotExists_PerformsMigration() + { + CreateTwoBindingPathsToMockedBoundProject(); + + await testSubject.MigrateAllBindingsToServerConnectionsIfNeededAsync(); + + Received.InOrder(() => + { + serverConnectionsRepository.ConnectionsFileExists(); + logger.WriteLine(MigrationStrings.ConnectionMigration_StartMigration); + unintrusiveBindingPathProvider.GetBindingPaths(); + serverConnectionsRepository.TryAdd(Arg.Any()); + serverConnectionsRepository.TryAdd(Arg.Any()); + }); + } + + [TestMethod] + public async Task MigrateBindingToServerConnectionIfNeeded_MigrationIsExecuted_CreatesServerConnectionsFromBinding() + { + var boundProjects = CreateTwoBindingPathsToMockedBoundProject().Values.ToList(); + + await testSubject.MigrateAllBindingsToServerConnectionsIfNeededAsync(); + + serverConnectionsRepository.Received(1).TryAdd(Arg.Is(conn => conn.Id == boundProjects[0].ServerUri.ToString())); + serverConnectionsRepository.Received(1).TryAdd(Arg.Is(conn => conn.Id == boundProjects[1].ServerUri.ToString())); + } + + [TestMethod] + public async Task MigrateBindingToServerConnectionIfNeeded_MigrationIsExecuted_UpdatesBindingWithServerConnection() + { + var bindingPathToBoundProjectDictionary = CreateTwoBindingPathsToMockedBoundProject(); + + await testSubject.MigrateAllBindingsToServerConnectionsIfNeededAsync(); + + CheckBindingsWereMigrated(bindingPathToBoundProjectDictionary); + } + + [TestMethod] + public async Task MigrateBindingToServerConnectionIfNeeded_MigrationIsExecutedForTwoBindingsWithTheSameConnections_MigratesServerConnectionOnceAndUpdatesBothBindings() + { + var bindingPathToBoundProjectDictionary = CreateTwoBindingPathsToMockedBoundProject(); + var boundProjects = bindingPathToBoundProjectDictionary.Values.ToList(); + var expectedServerConnectionId = boundProjects[0].ServerUri.ToString(); + serverConnectionsRepository.TryGet(expectedServerConnectionId, out _).Returns(true); + + + await testSubject.MigrateAllBindingsToServerConnectionsIfNeededAsync(); + + logger.Received(1).WriteLine(string.Format(MigrationStrings.ConnectionMigration_ExistingServerConnectionNotMigrated, expectedServerConnectionId)); + serverConnectionsRepository.DidNotReceive().TryAdd(Arg.Is(conn => conn.Id == expectedServerConnectionId)); + CheckBindingsWereMigrated(bindingPathToBoundProjectDictionary); + } + + [TestMethod] + public async Task MigrateBindingToServerConnectionIfNeeded_ReadingBindingReturnsNull_SkipsAndLogs() + { + var boundProjects = CreateTwoBindingPathsToMockedBoundProject(); + var bindingPathToExclude = boundProjects.Keys.First(); + legacyBindingRepository.Read(Arg.Is(path => path == bindingPathToExclude)).Returns((BoundSonarQubeProject)null); + + await testSubject.MigrateAllBindingsToServerConnectionsIfNeededAsync(); + + logger.Received(1).WriteLine(string.Format(MigrationStrings.ConnectionMigration_BindingNotMigrated, bindingPathToExclude, "legacyBoundProject was not found")); + serverConnectionsRepository.DidNotReceive().TryAdd(Arg.Is(conn => IsExpectedServerConnection(conn, boundProjects.First().Value))); + solutionBindingRepository.DidNotReceive().Write(bindingPathToExclude, Arg.Any()); + } + + [TestMethod] + public async Task MigrateBindingToServerConnectionIfNeeded_MigratingConnectionFails_SkipsAndLogs() + { + var boundPathToBoundProject = CreateTwoBindingPathsToMockedBoundProject().First(); + serverConnectionsRepository.TryAdd(Arg.Is(conn => IsExpectedServerConnection(conn, boundPathToBoundProject.Value))).Returns(false); + + await testSubject.MigrateAllBindingsToServerConnectionsIfNeededAsync(); + + logger.Received(1).WriteLine(string.Format(MigrationStrings.ConnectionMigration_ServerConnectionNotMigrated, boundPathToBoundProject.Value.ServerUri)); + serverConnectionsRepository.Received(1).TryAdd(Arg.Is(conn => IsExpectedServerConnection(conn, boundPathToBoundProject.Value))); + solutionBindingRepository.DidNotReceive().Write(boundPathToBoundProject.Key, Arg.Any()); + } + + [TestMethod] + public async Task MigrateBindingToServerConnectionIfNeeded_MigrationIsExecuted_CredentialsAreMigrated() + { + var boundProjects = CreateTwoBindingPathsToMockedBoundProject().Values.ToList(); + + await testSubject.MigrateAllBindingsToServerConnectionsIfNeededAsync(); + + CheckCredentialsAreLoaded(boundProjects[0]); + CheckCredentialsAreLoaded(boundProjects[1]); + } + + [TestMethod] + public async Task MigrateBindingToServerConnectionIfNeeded_MigrationThrowsException_ErrorIsLogged() + { + var boundProjects = CreateTwoBindingPathsToMockedBoundProject(); + var errorMessage = "loading failed"; + solutionBindingRepository.When(repo => repo.Write(Arg.Any(), Arg.Any())).Throw(new Exception(errorMessage)); + + await testSubject.MigrateAllBindingsToServerConnectionsIfNeededAsync(); + + logger.Received(1).WriteLine(string.Format(MigrationStrings.ConnectionMigration_BindingNotMigrated, boundProjects.First().Key, errorMessage)); + logger.Received(1).WriteLine(string.Format(MigrationStrings.ConnectionMigration_BindingNotMigrated, boundProjects.Last().Key, errorMessage)); + } + + private Dictionary CreateTwoBindingPathsToMockedBoundProject() + { + Dictionary pathToBindings = new() + { + {"bindings/proj1/binding.config", CreateBoundProject("http://server1", "proj1")}, + {"bindings/proj2/binding.config", CreateBoundProject("http://server2", "proj2")} + }; + unintrusiveBindingPathProvider.GetBindingPaths().Returns(pathToBindings.Select(kvp => kvp.Key)); + foreach (var kvp in pathToBindings) + { + MockValidBinding(kvp.Key, kvp.Value); + } + return pathToBindings; + } + + private void MockValidBinding(string bindingPath, BoundSonarQubeProject sonarQubeProject) + { + legacyBindingRepository.Read(bindingPath).Returns(sonarQubeProject); + serverConnectionsRepository.TryAdd(Arg.Is(conn => IsExpectedServerConnection(conn, sonarQubeProject))).Returns(true); + } + + private static BoundSonarQubeProject CreateBoundProject(string url, string projectKey) + { + return new BoundSonarQubeProject(new Uri(url), projectKey, "projectName", credentials: new BasicAuthCredentials("admin", "admin".ToSecureString())); + } + + private static bool IsExpectedServerConnection(ServerConnection serverConnection, BoundSonarQubeProject boundProject) + { + return serverConnection.Id == boundProject.ServerUri.ToString(); + } + + private void CheckCredentialsAreLoaded(BoundSonarQubeProject boundProject) + { + serverConnectionsRepository.Received(1).TryAdd(Arg.Is(proj => + IsExpectedServerConnection(proj, boundProject) && proj.Credentials == boundProject.Credentials)); + } + + private void CheckBindingsWereMigrated(Dictionary bindingPathToBoundProjectDictionary) + { + foreach (var boundSonarQubeProject in bindingPathToBoundProjectDictionary) + { + solutionBindingRepository.Received(1).Write(boundSonarQubeProject.Key, + Arg.Is(proj => IsExpectedServerConnection(proj.ServerConnection, boundSonarQubeProject.Value))); + } + } +} diff --git a/src/ConnectedMode.UnitTests/Migration/ConnectedModeMigrationTests.cs b/src/ConnectedMode.UnitTests/Migration/ConnectedModeMigrationTests.cs index bc2d75b22a..c786ff72ae 100644 --- a/src/ConnectedMode.UnitTests/Migration/ConnectedModeMigrationTests.cs +++ b/src/ConnectedMode.UnitTests/Migration/ConnectedModeMigrationTests.cs @@ -18,10 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting.Logging; using SonarLint.VisualStudio.ConnectedMode.Binding; using SonarLint.VisualStudio.ConnectedMode.Migration; using SonarLint.VisualStudio.ConnectedMode.Shared; @@ -59,7 +56,10 @@ public void MefCtor_CheckIsExported() MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); } [TestMethod] @@ -261,12 +261,194 @@ public async Task Migrate_CallBindAsync() { var unintrusiveBindingController = new Mock(); var cancellationToken = CancellationToken.None; - var migrationProgress = Mock.Of>(); - var testSubject = CreateTestSubject(unintrusiveBindingController: unintrusiveBindingController.Object); - await testSubject.MigrateAsync(AnyBoundProject, migrationProgress, false, cancellationToken); - unintrusiveBindingController.Verify(x => x.BindAsync(AnyBoundProject, It.IsAny>(), cancellationToken), Times.Once); + await testSubject.MigrateAsync(AnyBoundProject, Mock.Of>(), false, cancellationToken); + + unintrusiveBindingController.Verify( + x => x.BindAsync( + It.Is(proj => IsExpectedBoundServerProject(proj)), + It.IsAny>(), cancellationToken), Times.Once); + } + + [TestMethod] + public async Task Migrate_BoundProjectCanNotBeConvertedToServerConnection_LogsAndDoesB() + { + var storedConnection = new ServerConnection.SonarQube(AnyBoundProject.ServerUri); + var unintrusiveBindingControllerMock = new Mock(); + var serverConnectionsRepositoryMock = CreateServerConnectionsRepositoryMock(); + MockIServerConnectionsRepositoryTryGet(serverConnectionsRepositoryMock, storedConnection.Id, storedConnection); + var solutionInfoProviderMock = CreateSolutionInfoProviderMock(); + var testSubject = CreateTestSubject(unintrusiveBindingController: unintrusiveBindingControllerMock.Object, + serverConnectionsRepository: serverConnectionsRepositoryMock.Object, solutionInfoProvider: solutionInfoProviderMock.Object); + + await testSubject.MigrateAsync(AnyBoundProject, Mock.Of>(), false, CancellationToken.None); + + serverConnectionsRepositoryMock.Verify(mock => mock.TryGet(storedConnection.Id, out It.Ref.IsAny), Times.Once); + serverConnectionsRepositoryMock.Verify(mock => mock.TryAdd(IsExpectedServerConnection(It.IsAny())), Times.Never); + solutionInfoProviderMock.Verify(mock => mock.GetSolutionNameAsync(), Times.Once); + unintrusiveBindingControllerMock.Verify( + x => x.BindAsync( + It.Is(proj => IsExpectedBoundServerProject(proj)), + It.IsAny>(), It.IsAny()), Times.Once); + } + + [TestMethod] + public async Task Migrate_ConnectionExists_EstablishesBinding() + { + var storedConnection = new ServerConnection.SonarQube(AnyBoundProject.ServerUri); + var unintrusiveBindingControllerMock = new Mock(); + var serverConnectionsRepositoryMock = CreateServerConnectionsRepositoryMock(); + MockIServerConnectionsRepositoryTryGet(serverConnectionsRepositoryMock, storedConnection.Id, storedConnection); + var solutionInfoProviderMock = CreateSolutionInfoProviderMock(); + var testSubject = CreateTestSubject(unintrusiveBindingController: unintrusiveBindingControllerMock.Object, + serverConnectionsRepository: serverConnectionsRepositoryMock.Object, solutionInfoProvider: solutionInfoProviderMock.Object); + + await testSubject.MigrateAsync(AnyBoundProject, Mock.Of>(), false, CancellationToken.None); + + serverConnectionsRepositoryMock.Verify(mock => mock.TryGet(storedConnection.Id, out It.Ref.IsAny), Times.Once); + serverConnectionsRepositoryMock.Verify(mock => mock.TryAdd(IsExpectedServerConnection(It.IsAny())), Times.Never); + solutionInfoProviderMock.Verify(mock => mock.GetSolutionNameAsync(), Times.Once); + unintrusiveBindingControllerMock.Verify( + x => x.BindAsync( + It.Is(proj => IsExpectedBoundServerProject(proj)), + It.IsAny>(), It.IsAny()), Times.Once); + } + + [TestMethod] + public async Task Migrate_ConnectionDoesNotExist_AddsConnectionAndEstablishesBinding() + { + var unintrusiveBindingControllerMock = new Mock(); + var convertedConnection = ServerConnection.FromBoundSonarQubeProject(AnyBoundProject); + var serverConnectionsRepositoryMock = CreateServerConnectionsRepositoryMock(); + var solutionInfoProviderMock = CreateSolutionInfoProviderMock(); + var testSubject = CreateTestSubject(unintrusiveBindingController: unintrusiveBindingControllerMock.Object, + serverConnectionsRepository: serverConnectionsRepositoryMock.Object, solutionInfoProvider: solutionInfoProviderMock.Object); + serverConnectionsRepositoryMock.Setup(x => x.TryAdd(IsExpectedServerConnection(convertedConnection))).Returns(true); + + await testSubject.MigrateAsync(AnyBoundProject, Mock.Of>(), false, CancellationToken.None); + + serverConnectionsRepositoryMock.Verify(mock => mock.TryGet(convertedConnection.Id, out It.Ref.IsAny), Times.Once); + serverConnectionsRepositoryMock.Verify(mock => mock.TryAdd(IsExpectedServerConnection(convertedConnection)), Times.Once); + solutionInfoProviderMock.Verify(mock => mock.GetSolutionNameAsync(), Times.Once); + unintrusiveBindingControllerMock.Verify( + x => x.BindAsync( + It.Is(proj => IsExpectedBoundServerProject(proj)), + It.IsAny>(), It.IsAny()), Times.Once); + } + + /// + /// If the connection can not be added in the new format (for example, credentials do not exist or are invalid), the old migration should still succeed. + /// The user can add manually add the connection at a later time. + /// + [TestMethod] + public async Task Migrate_ConnectionDoesNotExist_CannotAdd_DoesNotCreateConnection() + { + var unintrusiveBindingController = new Mock(); + var convertedConnection = ServerConnection.FromBoundSonarQubeProject(AnyBoundProject); + var serverConnectionsRepositoryMock = new Mock(); + serverConnectionsRepositoryMock.Setup(x => x.ConnectionsFileExists()).Returns(true); + serverConnectionsRepositoryMock.Setup(x => x.TryAdd(convertedConnection)).Returns(false); + var testSubject = CreateTestSubject(unintrusiveBindingController: unintrusiveBindingController.Object, serverConnectionsRepository: serverConnectionsRepositoryMock.Object); + + await testSubject.MigrateAsync(AnyBoundProject, Mock.Of>(), false, CancellationToken.None); + + serverConnectionsRepositoryMock.Verify(mock => mock.TryGet(convertedConnection.Id, out It.Ref.IsAny), Times.Once); + serverConnectionsRepositoryMock.Verify(mock => mock.TryAdd(IsExpectedServerConnection(convertedConnection)), Times.Once); + unintrusiveBindingController.Verify( + x => x.BindAsync( + It.Is(proj => IsExpectedBoundServerProject(proj)), + It.IsAny>(), It.IsAny()), Times.Once); + } + + /// + /// If the connection can not be added in the new format for whatever reason, the old migration should still succeed. + /// The user can add manually add the connection at a later time. + /// + [TestMethod] + public async Task Migrate_ConnectionDoesNotExist_Throws_DoesNotCreateConnection() + { + var unintrusiveBindingController = new Mock(); + var convertedConnection = ServerConnection.FromBoundSonarQubeProject(AnyBoundProject); + var serverConnectionsRepositoryMock = new Mock(); + serverConnectionsRepositoryMock.Setup(x => x.ConnectionsFileExists()).Returns(true); + serverConnectionsRepositoryMock.Setup(x => x.TryAdd(convertedConnection)).Throws(new Exception()); + var testSubject = CreateTestSubject(unintrusiveBindingController: unintrusiveBindingController.Object, serverConnectionsRepository: serverConnectionsRepositoryMock.Object); + + await testSubject.MigrateAsync(AnyBoundProject, Mock.Of>(), false, CancellationToken.None); + + serverConnectionsRepositoryMock.Verify(mock => mock.TryGet(convertedConnection.Id, out It.Ref.IsAny), Times.Once); + serverConnectionsRepositoryMock.Verify(mock => mock.TryAdd(IsExpectedServerConnection(convertedConnection)), Times.Once); + unintrusiveBindingController.Verify( + x => x.BindAsync( + It.Is(proj => IsExpectedBoundServerProject(proj)), + It.IsAny>(), It.IsAny()), Times.Once); + } + + /// + /// If bindings exist in the new format (in the Bindings folder), it means that the old migration is being executed before the new migration. But we expect it the other way around + /// + [TestMethod] + public async Task Migrate_ConnectionsJsonFileDoesNotExistAndNewBindingsExist_DoesNotMigrateServerConnection() + { + var unintrusiveBindingControllerMock = new Mock(); + var serverConnectionsRepositoryMock = new Mock(); + var bindingPathProvider = new Mock(); + var logger = new Mock(); + var testSubject = CreateTestSubject( + serverConnectionsRepository: serverConnectionsRepositoryMock.Object, + unintrusiveBindingController: unintrusiveBindingControllerMock.Object, + unintrusiveBindingPathProvider: bindingPathProvider.Object, + logger:logger.Object); + serverConnectionsRepositoryMock.Setup(mock => mock.ConnectionsFileExists()).Returns(false); + bindingPathProvider.Setup(mock => mock.GetBindingPaths()).Returns(["binding1"]); + + await testSubject.MigrateAsync(AnyBoundProject, Mock.Of>(), false, CancellationToken.None); + + logger.Verify(x=> x.WriteLine(MigrationStrings.ConnectionsJson_DoesNotExist), Times.Once); + serverConnectionsRepositoryMock.Verify(mock => mock.TryAdd(It.IsAny()), Times.Never); + unintrusiveBindingControllerMock.Verify( + x => x.BindAsync( + It.Is(proj => IsExpectedBoundServerProject(proj)), + It.IsAny>(), It.IsAny()), Times.Once); + } + + /// + /// If no bindings exist in the new format (in the Bindings folder), even if the new migration is executed first no connections.json will be created, so we can safely proceed with the old migration + /// + [TestMethod] + public async Task Migrate_ConnectionsJsonFileDoesNotExistAndNoNewBindingsExist_MigratesConnection() + { + var unintrusiveBindingControllerMock = new Mock(); + var serverConnectionsRepositoryMock = new Mock(); + var bindingPathProvider = new Mock(); + var logger = new Mock(); + var testSubject = CreateTestSubject( + serverConnectionsRepository: serverConnectionsRepositoryMock.Object, + unintrusiveBindingController: unintrusiveBindingControllerMock.Object, + unintrusiveBindingPathProvider:bindingPathProvider.Object, + logger: logger.Object); + serverConnectionsRepositoryMock.Setup(mock => mock.ConnectionsFileExists()).Returns(false); + bindingPathProvider.Setup(mock => mock.GetBindingPaths()).Returns([]); + + await testSubject.MigrateAsync(AnyBoundProject, Mock.Of>(), false, CancellationToken.None); + + logger.Verify(x => x.WriteLine(MigrationStrings.ConnectionsJson_DoesNotExist), Times.Never); + serverConnectionsRepositoryMock.Verify(mock => mock.TryAdd(It.IsAny()), Times.Once); + unintrusiveBindingControllerMock.Verify( + x => x.BindAsync( + It.Is(proj => IsExpectedBoundServerProject(proj)), + It.IsAny>(), It.IsAny()), Times.Once); + } + + [TestMethod] + public void Migrate_InvalidServerInformation_Throws() + { + var testSubject = CreateTestSubject(); + + Func act = async () => await testSubject.MigrateAsync(new BoundSonarQubeProject(), Mock.Of>(), false, CancellationToken.None); + + act.Should().Throw(); } [TestMethod] @@ -303,7 +485,10 @@ private static ConnectedModeMigration CreateTestSubject( ISuppressionIssueStoreUpdater suppressionIssueStoreUpdater = null, ISharedBindingConfigProvider sharedBindingConfigProvider = null, ILogger logger = null, - IThreadHandling threadHandling = null) + IThreadHandling threadHandling = null, + ISolutionInfoProvider solutionInfoProvider = null, + IServerConnectionsRepository serverConnectionsRepository = null, + IUnintrusiveBindingPathProvider unintrusiveBindingPathProvider = null) { fileProvider ??= Mock.Of(); fileCleaner ??= Mock.Of(); @@ -313,11 +498,26 @@ private static ConnectedModeMigration CreateTestSubject( suppressionIssueStoreUpdater ??= Mock.Of(); settingsProvider ??= CreateSettingsProvider(DefaultTestLegacySettings).Object; sharedBindingConfigProvider ??= Mock.Of(); + solutionInfoProvider ??= CreateSolutionInfoProviderMock().Object; + serverConnectionsRepository ??= CreateServerConnectionsRepositoryMock().Object; + unintrusiveBindingPathProvider ??= Mock.Of(); logger ??= new TestLogger(logToConsole: true); threadHandling ??= new NoOpThreadHandler(); - return new ConnectedModeMigration(settingsProvider, fileProvider, fileCleaner, fileSystem, sonarQubeService, unintrusiveBindingController, suppressionIssueStoreUpdater, sharedBindingConfigProvider, logger, threadHandling); + return new ConnectedModeMigration(settingsProvider, + fileProvider, + fileCleaner, + fileSystem, + sonarQubeService, + unintrusiveBindingController, + suppressionIssueStoreUpdater, + sharedBindingConfigProvider, + logger, + threadHandling, + solutionInfoProvider, + serverConnectionsRepository, + unintrusiveBindingPathProvider); } private static Mock CreateFileProvider(params string[] filesToReturn) @@ -345,6 +545,42 @@ private static Mock CreateSettingsProvider(LegacySet settingsProvider.Setup(x => x.GetAsync(It.IsAny())).Returns(Task.FromResult(settingsToReturn)); return settingsProvider; } + + private static Mock CreateServerConnectionsRepositoryMock() + { + var serverConnectionsRepositoryMock = new Mock(); + serverConnectionsRepositoryMock.Setup(x => x.TryAdd(It.IsAny())).Returns(true); + serverConnectionsRepositoryMock.Setup(x => x.ConnectionsFileExists()).Returns(true); + + return serverConnectionsRepositoryMock; + } + + private static void MockIServerConnectionsRepositoryTryGet(Mock serverConnectionsRepositoryMock, string id = null, ServerConnection.SonarQube storedConnection = null) + { + serverConnectionsRepositoryMock.Setup(service => service.TryGet(id ?? It.IsAny(), out It.Ref.IsAny)) + .Returns((string _, out ServerConnection value) => + { + value = storedConnection; + return storedConnection != null; + }); + } + + private static Mock CreateSolutionInfoProviderMock() + { + var createSolutionInfoProviderMock = new Mock(); + createSolutionInfoProviderMock.Setup(mock => mock.GetSolutionNameAsync()).ReturnsAsync("solution"); + return createSolutionInfoProviderMock; + } + + private static bool IsExpectedBoundServerProject(BoundServerProject proj) + { + return proj.ServerProjectKey == AnyBoundProject.ProjectKey && proj.ServerConnection.ServerUri == AnyBoundProject.ServerUri; + } + + private static ServerConnection IsExpectedServerConnection(ServerConnection convertedConnection) + { + return It.Is(conn => conn.Id == convertedConnection.Id); + } } // Extension methods to make the mocks easier to work with. diff --git a/src/ConnectedMode.UnitTests/Migration/MigrationCheckerTests.cs b/src/ConnectedMode.UnitTests/Migration/MigrationCheckerTests.cs index 311865dd4a..03efd345d3 100644 --- a/src/ConnectedMode.UnitTests/Migration/MigrationCheckerTests.cs +++ b/src/ConnectedMode.UnitTests/Migration/MigrationCheckerTests.cs @@ -78,7 +78,7 @@ public async Task Migrate_CorrectProjectPassed() var configurationProvider = CreateNewConfigProvider(SonarLintMode.Standalone); var obsoleteConfigurationProvider = new Mock(); - var oldConfiguration = CreateBindingConfiguration(SonarLintMode.Connected); + var oldConfiguration = CreateLegacyBindingConfiguration(SonarLintMode.Connected); obsoleteConfigurationProvider.Setup(x => x.GetConfiguration()).Returns(oldConfiguration); var testSubject = CreateTestSubject(Mock.Of(), migrationPrompt.Object, configurationProvider.Object, obsoleteConfigurationProvider.Object); @@ -131,16 +131,25 @@ private static Mock CreateNewConfigProvider(SonarLintMod private static Mock CreateObsoleteConfigProvider(SonarLintMode? sonarLintMode) { var provider = new Mock(); - provider.Setup(x => x.GetConfiguration()).Returns(CreateBindingConfiguration(sonarLintMode)); + provider.Setup(x => x.GetConfiguration()).Returns(CreateLegacyBindingConfiguration(sonarLintMode)); return provider; } + private static LegacyBindingConfiguration CreateLegacyBindingConfiguration(SonarLintMode? mode) + { + if (mode.HasValue) + { + return new LegacyBindingConfiguration(new BoundSonarQubeProject(new Uri("http://localhost"), "test", ""), mode.Value, ""); + } + return null; + } + private static BindingConfiguration CreateBindingConfiguration(SonarLintMode? mode) { if (mode.HasValue) { - return new BindingConfiguration(new BoundSonarQubeProject(new Uri("http://localhost"), "test", ""), mode.Value, ""); + return new BindingConfiguration(new BoundServerProject("solution", "project", new ServerConnection.SonarCloud("org")), mode.Value, ""); } return null; } @@ -160,7 +169,7 @@ private static MigrationChecker CreateTestSubject(IActiveSolutionTracker activeS if (configurationProvider == null) { - var configurationProviderMock = new Mock(); + var configurationProviderMock = new Mock(); configurationProviderMock.Setup(x => x.GetConfiguration()).Returns(CreateBindingConfiguration(SonarLintMode.Standalone)); configurationProvider = configurationProviderMock.Object; @@ -169,7 +178,7 @@ private static MigrationChecker CreateTestSubject(IActiveSolutionTracker activeS if (obsoleteConfigurationProvider == null) { var obsoleteConfigurationProviderMock = new Mock(); - obsoleteConfigurationProviderMock.Setup(x => x.GetConfiguration()).Returns(CreateBindingConfiguration(SonarLintMode.Connected)); + obsoleteConfigurationProviderMock.Setup(x => x.GetConfiguration()).Returns(CreateLegacyBindingConfiguration(SonarLintMode.Connected)); obsoleteConfigurationProvider = obsoleteConfigurationProviderMock.Object; } diff --git a/src/ConnectedMode.UnitTests/Migration/ObsoleteConfigurationProviderTests.cs b/src/ConnectedMode.UnitTests/Migration/ObsoleteConfigurationProviderTests.cs index 20319c405e..4ec684a426 100644 --- a/src/ConnectedMode.UnitTests/Migration/ObsoleteConfigurationProviderTests.cs +++ b/src/ConnectedMode.UnitTests/Migration/ObsoleteConfigurationProviderTests.cs @@ -24,193 +24,192 @@ using SonarLint.VisualStudio.Core.Binding; using SonarLint.VisualStudio.TestInfrastructure; -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Migration +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Migration; + +[TestClass] +public class ObsoleteConfigurationProviderTests { - [TestClass] - public class ObsoleteConfigurationProviderTests + [TestMethod] + public void MefCtor_CheckIsExported() + => MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_CheckIsSingleton() + => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public void Ctor_DoesNotCallServices() + { + // The constructor should be free-threaded i.e. run entirely on the calling thread + // -> should not call services that swtich threads + var legacyProvider = Substitute.For(); + var connectedProvider = Substitute.For(); + var slnDataRepository = Substitute.For(); + + _ = CreateTestSubject(legacyProvider, connectedProvider, slnDataRepository); + + legacyProvider.ReceivedCalls().Should().BeEmpty(); + connectedProvider.ReceivedCalls().Should().BeEmpty(); + slnDataRepository.ReceivedCalls().Should().BeEmpty(); + } + + [TestMethod] + public void GetConfig_NoConfig_ReturnsStandalone() + { + // Arrange + var legacyPathProvider = CreatePathProvider(null); + var newPathProvider = CreatePathProvider(null); + + var testSubject = CreateTestSubject(legacyPathProvider, newPathProvider); + + // Act + var actual = testSubject.GetConfiguration(); + + // Assert + actual.Should().NotBeNull(); + actual.Project.Should().BeNull(); + actual.Mode.Should().Be(SonarLintMode.Standalone); + } + + [TestMethod] + public void GetConfig_NewConfigOnly_ReturnsConnected() + { + // Arrange + var legacyPathProvider = CreatePathProvider(null); + var newPathProvider = CreatePathProvider("c:\\new"); + + var expectedProject = new BoundSonarQubeProject(); + var reader = CreateRpo("c:\\new", expectedProject); + + var testSubject = CreateTestSubject(legacyPathProvider, newPathProvider, reader); + + // Act + var actual = testSubject.GetConfiguration(); + + // Assert + actual.Should().NotBeNull(); + actual.Project.Should().NotBeNull(); + actual.Project.Should().BeSameAs(expectedProject); + actual.Mode.Should().Be(SonarLintMode.Connected); + } + + [TestMethod] + public void GetConfig_LegacyConfigOnly_ReturnsLegacy() + { + // Arrange + var legacyPathProvider = CreatePathProvider("c:\\old"); + + var expectedProject = new BoundSonarQubeProject(); + var reader = CreateRpo("c:\\old", expectedProject); + + var testSubject = CreateTestSubject(legacyPathProvider, null, reader); + + // Act + var actual = testSubject.GetConfiguration(); + + // Assert + actual.Should().NotBeNull(); + actual.Project.Should().NotBeNull(); + actual.Project.Should().BeSameAs(expectedProject); + actual.Mode.Should().Be(SonarLintMode.LegacyConnected); + } + + [TestMethod] + public void GetConfig_NoLegacyProjectAtFileLocation_NoConnectedProjectAtFileLocation_ReturnsStandalone() + { + // Arrange + var legacyPathProvider = CreatePathProvider("c:\\legacy"); + var newPathProvider = CreatePathProvider("c:\\new"); + + var repo = Substitute.For(); + repo.Read("c:\\legacy").Returns(null as BoundSonarQubeProject); + repo.Read("c:\\new").Returns(null as BoundSonarQubeProject); + + var testSubject = CreateTestSubject(legacyPathProvider, newPathProvider, repo); + + // Act + var actual = testSubject.GetConfiguration(); + + // Assert + actual.Should().NotBeNull(); + actual.Project.Should().BeNull(); + actual.Mode.Should().Be(SonarLintMode.Standalone); + } + + [TestMethod] + public void GetConfig_NoLegacyProjectAtFileLocation_ConnectedProjectAtFileLocation_ReturnsConnected() + { + // Arrange + var legacyPathProvider = CreatePathProvider("c:\\legacy"); + var newPathProvider = CreatePathProvider("c:\\new"); + + var repo = Substitute.For(); + var expectedProject = new BoundSonarQubeProject(); + repo.Read("c:\\legacy").Returns(null as BoundSonarQubeProject); + repo.Read("c:\\new").Returns(expectedProject); + + var testSubject = CreateTestSubject(legacyPathProvider, newPathProvider, repo); + + // Act + var actual = testSubject.GetConfiguration(); + + // Assert + actual.Should().NotBeNull(); + actual.Project.Should().BeSameAs(expectedProject); + actual.Mode.Should().Be(SonarLintMode.Connected); + } + + [TestMethod] + public void GetConfig_LegacyProjectAtFileLocation_ConnectedProjectAtFileLocation_ReturnsLegacy() + { + // Note that this should not happen in practice - we only expect the legacys + // or new bindings to present. However, the legacy should take priority. + + // Arrange + var legacyPathProvider = CreatePathProvider("c:\\legacy"); + var newPathProvider = CreatePathProvider("c:\\new"); + + var reader = Substitute.For(); + var legacyProject = new BoundSonarQubeProject(); + var newProject = new BoundSonarQubeProject(); + reader.Read("c:\\legacy").Returns(legacyProject); + reader.Read("c:\\new").Returns(newProject); + + var testSubject = CreateTestSubject(legacyPathProvider, newPathProvider, reader); + + // Act + var actual = testSubject.GetConfiguration(); + + // Assert + actual.Should().NotBeNull(); + actual.Project.Should().BeSameAs(legacyProject); + actual.Mode.Should().Be(SonarLintMode.LegacyConnected); + } + + private static ISolutionBindingPathProvider CreatePathProvider(string pathToReturn) + { + var provider = new Mock(); + provider.Setup(x => x.Get()).Returns((string)pathToReturn); + return provider.Object; + } + + private static ILegacySolutionBindingRepository CreateRpo(string inputPath, BoundSonarQubeProject projectToReturn) + { + var repo = Substitute.For(); + repo.Read(inputPath).Returns(projectToReturn); + return repo; + } + + private static ObsoleteConfigurationProvider CreateTestSubject(ISolutionBindingPathProvider legacyProvider = null, + ISolutionBindingPathProvider connectedModePathProvider = null, + ILegacySolutionBindingRepository slnDataRepo = null) { - [TestMethod] - public void MefCtor_CheckIsExported() - => MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - - [TestMethod] - public void MefCtor_CheckIsSingleton() - => MefTestHelpers.CheckIsSingletonMefComponent(); - - [TestMethod] - public void Ctor_DoesNotCallServices() - { - // The constructor should be free-threaded i.e. run entirely on the calling thread - // -> should not call services that swtich threads - var legacyProvider = new Mock(); - var connectedProvider = new Mock(); - var slnDataRepository = new Mock(); - - _ = CreateTestSubject(legacyProvider.Object, connectedProvider.Object, slnDataRepository.Object); - - legacyProvider.Invocations.Should().BeEmpty(); - connectedProvider.Invocations.Should().BeEmpty(); - slnDataRepository.Invocations.Should().BeEmpty(); - } - - [TestMethod] - public void GetConfig_NoConfig_ReturnsStandalone() - { - // Arrange - var legacyPathProvider = CreatePathProvider(null); - var newPathProvider = CreatePathProvider(null); - - var testSubject = CreateTestSubject(legacyPathProvider, newPathProvider); - - // Act - var actual = testSubject.GetConfiguration(); - - // Assert - actual.Should().NotBeNull(); - actual.Project.Should().BeNull(); - actual.Mode.Should().Be(SonarLintMode.Standalone); - } - - [TestMethod] - public void GetConfig_NewConfigOnly_ReturnsConnected() - { - // Arrange - var legacyPathProvider = CreatePathProvider(null); - var newPathProvider = CreatePathProvider("c:\\new"); - - var expectedProject = new BoundSonarQubeProject(); - var reader = CreateRpo("c:\\new", expectedProject); - - var testSubject = CreateTestSubject(legacyPathProvider, newPathProvider, reader); - - // Act - var actual = testSubject.GetConfiguration(); - - // Assert - actual.Should().NotBeNull(); - actual.Project.Should().NotBeNull(); - actual.Project.Should().BeSameAs(expectedProject); - actual.Mode.Should().Be(SonarLintMode.Connected); - } - - [TestMethod] - public void GetConfig_LegacyConfigOnly_ReturnsLegacy() - { - // Arrange - var legacyPathProvider = CreatePathProvider("c:\\old"); - - var expectedProject = new BoundSonarQubeProject(); - var reader = CreateRpo("c:\\old", expectedProject); - - var testSubject = CreateTestSubject(legacyPathProvider, null, reader); - - // Act - var actual = testSubject.GetConfiguration(); - - // Assert - actual.Should().NotBeNull(); - actual.Project.Should().NotBeNull(); - actual.Project.Should().BeSameAs(expectedProject); - actual.Mode.Should().Be(SonarLintMode.LegacyConnected); - } - - [TestMethod] - public void GetConfig_NoLegacyProjectAtFileLocation_NoConnectedProjectAtFileLocation_ReturnsStandalone() - { - // Arrange - var legacyPathProvider = CreatePathProvider("c:\\legacy"); - var newPathProvider = CreatePathProvider("c:\\new"); - - var repo = new Mock(); - repo.Setup(x => x.Read("c:\\legacy")).Returns(null as BoundSonarQubeProject); - repo.Setup(x => x.Read("c:\\new")).Returns(null as BoundSonarQubeProject); - - var testSubject = CreateTestSubject(legacyPathProvider, newPathProvider, repo.Object); - - // Act - var actual = testSubject.GetConfiguration(); - - // Assert - actual.Should().NotBeNull(); - actual.Project.Should().BeNull(); - actual.Mode.Should().Be(SonarLintMode.Standalone); - } - - [TestMethod] - public void GetConfig_NoLegacyProjectAtFileLocation_ConnectedProjectAtFileLocation_ReturnsConnected() - { - // Arrange - var legacyPathProvider = CreatePathProvider("c:\\legacy"); - var newPathProvider = CreatePathProvider("c:\\new"); - - var repo = new Mock(); - var expectedProject = new BoundSonarQubeProject(); - repo.Setup(x => x.Read("c:\\legacy")).Returns(null as BoundSonarQubeProject); - repo.Setup(x => x.Read("c:\\new")).Returns(expectedProject); - - var testSubject = CreateTestSubject(legacyPathProvider, newPathProvider, repo.Object); - - // Act - var actual = testSubject.GetConfiguration(); - - // Assert - actual.Should().NotBeNull(); - actual.Project.Should().BeSameAs(expectedProject); - actual.Mode.Should().Be(SonarLintMode.Connected); - } - - [TestMethod] - public void GetConfig_LegacyProjectAtFileLocation_ConnectedProjectAtFileLocation_ReturnsLegacy() - { - // Note that this should not happen in practice - we only expect the legacys - // or new bindings to present. However, the legacy should take priority. - - // Arrange - var legacyPathProvider = CreatePathProvider("c:\\legacy"); - var newPathProvider = CreatePathProvider("c:\\new"); - - var reader = new Mock(); - var legacyProject = new BoundSonarQubeProject(); - var newProject = new BoundSonarQubeProject(); - reader.Setup(x => x.Read("c:\\legacy")).Returns(legacyProject); - reader.Setup(x => x.Read("c:\\new")).Returns(newProject); - - var testSubject = CreateTestSubject(legacyPathProvider, newPathProvider, reader.Object); - - // Act - var actual = testSubject.GetConfiguration(); - - // Assert - actual.Should().NotBeNull(); - actual.Project.Should().BeSameAs(legacyProject); - actual.Mode.Should().Be(SonarLintMode.LegacyConnected); - } - - private static ISolutionBindingPathProvider CreatePathProvider(string pathToReturn) - { - var provider = new Mock(); - provider.Setup(x => x.Get()).Returns((string)pathToReturn); - return provider.Object; - } - - private static ISolutionBindingRepository CreateRpo(string inputPath, BoundSonarQubeProject projectToReturn) - { - var repo = new Mock(); - repo.Setup(x => x.Read(inputPath)).Returns(projectToReturn); - return repo.Object; - } - - private static ObsoleteConfigurationProvider CreateTestSubject(ISolutionBindingPathProvider legacyProvider = null, - ISolutionBindingPathProvider connectedModePathProvider = null, - ISolutionBindingRepository slnDataRepo = null) - { - var testSubject = new ObsoleteConfigurationProvider( - legacyProvider ?? Mock.Of(), - connectedModePathProvider ?? Mock.Of(), - slnDataRepo ?? Mock.Of()); - return testSubject; - } + var testSubject = new ObsoleteConfigurationProvider( + legacyProvider ?? Substitute.For(), + connectedModePathProvider ?? Substitute.For(), + slnDataRepo ?? Substitute.For()); + return testSubject; } } diff --git a/src/ConnectedMode.UnitTests/Persistence/BindingDtoConverterTests.cs b/src/ConnectedMode.UnitTests/Persistence/BindingDtoConverterTests.cs new file mode 100644 index 0000000000..a26a6fa6e6 --- /dev/null +++ b/src/ConnectedMode.UnitTests/Persistence/BindingDtoConverterTests.cs @@ -0,0 +1,134 @@ +/* + * 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.ConnectedMode.Persistence; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Binding; +using SonarLint.VisualStudio.TestInfrastructure; +using SonarQube.Client.Models; + +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Persistence; + +[TestClass] +public class BindingDtoConverterTests +{ + private BindingDtoConverter testSubject; + + [TestInitialize] + public void TestInitialize() + { + testSubject = new BindingDtoConverter(); + } + + [TestMethod] + public void MefCtor_CheckIsExported() + { + MefTestHelpers.CheckTypeCanBeImported(); + } + + [TestMethod] + public void MefCtor_CheckIsSingleton() + { + MefTestHelpers.CheckIsSingletonMefComponent(); + } + + [TestMethod] + public void ConvertFromDto_ConvertsCorrectly() + { + const string localBindingKey = "solution123"; + var bindingDto = new BindingDto + { + ProjectKey = "project123", + Profiles = new Dictionary(), + ProjectName = "ignored", + Organization = new SonarQubeOrganization("ignored", "ignored"), + ServerUri = new Uri("http://ignored"), + ServerConnectionId = "ignored", // only used for connection extraction, delegates to the connection object during the conversion + }; + var connection = new ServerConnection.SonarCloud("myorg"); + + var boundServerProject = testSubject.ConvertFromDto(bindingDto, connection, localBindingKey); + + boundServerProject.ServerConnection.Should().BeSameAs(connection); + boundServerProject.LocalBindingKey.Should().BeSameAs(localBindingKey); + boundServerProject.ServerProjectKey.Should().BeSameAs(bindingDto.ProjectKey); + boundServerProject.Profiles.Should().BeSameAs(bindingDto.Profiles); + } + + [TestMethod] + public void ConvertToDto_SonarCloudConnection_ConvertsCorrectly() + { + var boundServerProject = new BoundServerProject("localBinding", "serverProject", new ServerConnection.SonarCloud("myorg")) + { + Profiles = new Dictionary() + }; + + var bindingDto = testSubject.ConvertToDto(boundServerProject); + + bindingDto.ProjectKey.Should().BeSameAs(boundServerProject.ServerProjectKey); + bindingDto.ProjectName.Should().BeNull(); + bindingDto.ServerUri.Should().BeEquivalentTo(boundServerProject.ServerConnection.ServerUri); + bindingDto.Organization.Key.Should().BeSameAs(((ServerConnection.SonarCloud)boundServerProject.ServerConnection).OrganizationKey); + bindingDto.ServerConnectionId.Should().BeSameAs(boundServerProject.ServerConnection.Id); + bindingDto.Profiles.Should().BeSameAs(boundServerProject.Profiles); + } + + [TestMethod] + public void ConvertToDto_SonarQubeConnection_ConvertsCorrectly() + { + var boundServerProject = new BoundServerProject("localBinding", "serverProject", new ServerConnection.SonarQube(new Uri("http://mysq"))) + { + Profiles = new Dictionary() + }; + + var bindingDto = testSubject.ConvertToDto(boundServerProject); + + bindingDto.ProjectKey.Should().BeSameAs(boundServerProject.ServerProjectKey); + bindingDto.ProjectName.Should().BeNull(); + bindingDto.ServerUri.Should().BeEquivalentTo(boundServerProject.ServerConnection.ServerUri); + bindingDto.Organization.Should().BeNull(); + bindingDto.ServerConnectionId.Should().BeSameAs(boundServerProject.ServerConnection.Id); + bindingDto.Profiles.Should().BeSameAs(boundServerProject.Profiles); + } + + [TestMethod] + public void ConvertFromDtoToLegacy_ConvertsCorrectly() + { + var credentials = Substitute.For(); + var bindingDto = new BindingDto + { + Organization = new SonarQubeOrganization("org", "my org"), + ServerUri = new Uri("http://localhost"), + ProjectKey = "project123", + ProjectName = "project 123", + Profiles = new Dictionary(), + ServerConnectionId = "ignored", + }; + + var legacyBinding = testSubject.ConvertFromDtoToLegacy(bindingDto, credentials); + + legacyBinding.ProjectKey.Should().BeSameAs(bindingDto.ProjectKey); + legacyBinding.ProjectName.Should().BeSameAs(bindingDto.ProjectName); + legacyBinding.ServerUri.Should().BeSameAs(bindingDto.ServerUri); + legacyBinding.Organization.Should().BeSameAs(bindingDto.Organization); + legacyBinding.Profiles.Should().BeSameAs(legacyBinding.Profiles); + legacyBinding.Credentials.Should().BeSameAs(credentials); + } +} diff --git a/src/ConnectedMode.UnitTests/Persistence/BindingDtoSerializationTests.cs b/src/ConnectedMode.UnitTests/Persistence/BindingDtoSerializationTests.cs new file mode 100644 index 0000000000..47b75c6f44 --- /dev/null +++ b/src/ConnectedMode.UnitTests/Persistence/BindingDtoSerializationTests.cs @@ -0,0 +1,231 @@ +/* + * 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 Newtonsoft.Json; +using SonarLint.VisualStudio.ConnectedMode.Persistence; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Binding; +using SonarQube.Client.Models; + +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Persistence; + +[TestClass] +public class BindingDtoSerializationTests +{ + private static readonly DateTime Date = new DateTime(2020, 12, 31, 23, 59, 59); + + private static readonly Dictionary QualityProfiles = new() + { + { Language.C, new ApplicableQualityProfile { ProfileKey = "qpkey", ProfileTimestamp = Date } } + }; + private readonly BoundSonarQubeProject boundSonarQubeProject = new(new Uri("http://next.sonarqube.com/sonarqube"), + "my_project_123", + "My Project", + /* ignored */ null, + new SonarQubeOrganization("org_key_123", "My Org")) + { + Profiles = QualityProfiles + }; + + private readonly BindingDto bindingDto = new() + { + ServerUri = new Uri("http://next.sonarqube.com/sonarqube"), + ProjectKey = "my_project_123", + ProjectName = "My Project", + Organization = new SonarQubeOrganization("org_key_123", "My Org"), + ServerConnectionId = "some_connection_id_123", + Profiles = QualityProfiles + }; + + private readonly BoundServerProject boundSonarCloudServerProject = new("solution123", "my_project_123", new ServerConnection.SonarCloud("org_key_123")){ Profiles = QualityProfiles }; + private readonly BoundServerProject boundSonarQubeServerProject = new("solution123", "my_project_123", new ServerConnection.SonarQube(new Uri("http://next.sonarqube.com/sonarqube"))){ Profiles = QualityProfiles }; + + private readonly BindingDtoConverter bindingDtoConverter = new(); + + [TestMethod] + public void Dto_SerializedAsExpected() + { + var serializeObject = JsonConvert.SerializeObject(bindingDto, Formatting.Indented); + + serializeObject.Should().BeEquivalentTo( + """ + { + "ServerConnectionId": "some_connection_id_123", + "ServerUri": "http://next.sonarqube.com/sonarqube", + "Organization": { + "Key": "org_key_123", + "Name": "My Org" + }, + "ProjectKey": "my_project_123", + "ProjectName": "My Project", + "Profiles": { + "C": { + "ProfileKey": "qpkey", + "ProfileTimestamp": "2020-12-31T23:59:59" + } + } + } + """); + } + + [TestMethod] + public void Dto_FromSonarCloudBinding_SerializedAsExpected() + { + var serializeObject = JsonConvert.SerializeObject(bindingDtoConverter.ConvertToDto(boundSonarCloudServerProject), Formatting.Indented); + + serializeObject.Should().BeEquivalentTo( + """ + { + "ServerConnectionId": "https://sonarcloud.io/organizations/org_key_123", + "ServerUri": "https://sonarcloud.io", + "Organization": { + "Key": "org_key_123", + "Name": null + }, + "ProjectKey": "my_project_123", + "Profiles": { + "C": { + "ProfileKey": "qpkey", + "ProfileTimestamp": "2020-12-31T23:59:59" + } + } + } + """); + } + + [TestMethod] + public void Dto_FromSonarQubeBinding_SerializedAsExpected() + { + var serializeObject = JsonConvert.SerializeObject(bindingDtoConverter.ConvertToDto(boundSonarQubeServerProject), Formatting.Indented); + + serializeObject.Should().BeEquivalentTo( + """ + { + "ServerConnectionId": "http://next.sonarqube.com/sonarqube", + "ServerUri": "http://next.sonarqube.com/sonarqube", + "ProjectKey": "my_project_123", + "Profiles": { + "C": { + "ProfileKey": "qpkey", + "ProfileTimestamp": "2020-12-31T23:59:59" + } + } + } + """); + } + + [TestMethod] + public void Legacy_ToJson_ToDto_ToLegacy_IsCorrect() + { + var serialized = JsonConvert.SerializeObject(boundSonarQubeProject); + var deserializeBindingDto = JsonConvert.DeserializeObject(serialized); + + var convertedFromDtoToLegacy = bindingDtoConverter.ConvertFromDtoToLegacy(deserializeBindingDto, null); + + convertedFromDtoToLegacy.Should().BeEquivalentTo(boundSonarQubeProject); + } + + [TestMethod] + public void Legacy_ToJson_ToLegacyDirectly_And_ToDto_ToLegacy_IsCorrect() + { + var serializedLegacy = JsonConvert.SerializeObject(boundSonarQubeProject); + var legacyDirect = JsonConvert.DeserializeObject(serializedLegacy); + var deserializedBindingDto = JsonConvert.DeserializeObject(serializedLegacy); + + var convertedFromDtoToLegacy = bindingDtoConverter.ConvertFromDtoToLegacy(deserializedBindingDto, null); + + convertedFromDtoToLegacy.Should().BeEquivalentTo(legacyDirect); + } + + [TestMethod] + public void Dto_ToJson_ToLegacy_IsCorrect() + { + var serializedBindingDto = JsonConvert.SerializeObject(bindingDto); + var convertedFromDtoToLegacy = JsonConvert.DeserializeObject(serializedBindingDto); + + convertedFromDtoToLegacy.Should().BeEquivalentTo(boundSonarQubeProject); + } + + [TestMethod] + public void Dto_ToJson_ToDto_ToLegacy_IsCorrect() + { + var serializedBindingDto = JsonConvert.SerializeObject(bindingDto); + var deserializedBindingDto = JsonConvert.DeserializeObject(serializedBindingDto); + + var convertedFromDtoToLegacy = bindingDtoConverter.ConvertFromDtoToLegacy(deserializedBindingDto, null); + + convertedFromDtoToLegacy.Should().BeEquivalentTo(boundSonarQubeProject); + } + + [TestMethod] + public void CurrentSonarCloud_ToDto_ToJson_ToDto_ToLegacy_IsCorrect() + { + var convertedBindingDtoFromLegacy = bindingDtoConverter.ConvertToDto(boundSonarCloudServerProject); + var serializedBindingDto = JsonConvert.SerializeObject(convertedBindingDtoFromLegacy); + var deserializedBindingDto = JsonConvert.DeserializeObject(serializedBindingDto); + + var legacyBinding = bindingDtoConverter.ConvertFromDtoToLegacy(deserializedBindingDto, null); + + legacyBinding.Should().BeEquivalentTo(boundSonarQubeProject, options => options.Excluding(x => x.ProjectName).Excluding(x => x.ServerUri).Excluding(x => x.Organization.Name)); + legacyBinding.ServerUri.Should().BeEquivalentTo(boundSonarCloudServerProject.ServerConnection.ServerUri); + legacyBinding.ProjectName.Should().BeNull(); + legacyBinding.Organization.Name.Should().BeNull(); + } + + [TestMethod] + public void CurrentSonarQube_ToDto_ToJson_ToDto_ToLegacy_IsCorrect() + { + var convertedBindingDtoFromLegacy = bindingDtoConverter.ConvertToDto(boundSonarQubeServerProject); + var serializedBindingDto = JsonConvert.SerializeObject(convertedBindingDtoFromLegacy); + var deserializedBindingDto = JsonConvert.DeserializeObject(serializedBindingDto); + + var legacyBinding = bindingDtoConverter.ConvertFromDtoToLegacy(deserializedBindingDto, null); + + legacyBinding.Should().BeEquivalentTo(boundSonarQubeProject, options => options.Excluding(x => x.ProjectName).Excluding(x => x.Organization)); + legacyBinding.ProjectName.Should().BeNull(); + legacyBinding.Organization.Should().BeNull(); + } + + [TestMethod] + public void CurrentSonarCloud_ToDto_ToJson_ToDto_ToCurrent_IsCorrect() + { + var convertedBindingDtoFromLegacy = bindingDtoConverter.ConvertToDto(boundSonarCloudServerProject); + var serializedBindingDto = JsonConvert.SerializeObject(convertedBindingDtoFromLegacy); + var deserializedBindingDto = JsonConvert.DeserializeObject(serializedBindingDto); + deserializedBindingDto.ServerConnectionId.Should().BeEquivalentTo(boundSonarCloudServerProject.ServerConnection.Id); + + var binding = bindingDtoConverter.ConvertFromDto(deserializedBindingDto, boundSonarCloudServerProject.ServerConnection, "solution123"); + + binding.Should().BeEquivalentTo(boundSonarCloudServerProject); + } + + [TestMethod] + public void CurrentSonarQube_ToDto_ToJson_ToDto_ToCurrent_IsCorrect() + { + var convertedBindingDtoFromLegacy = bindingDtoConverter.ConvertToDto(boundSonarQubeServerProject); + var serializedBindingDto = JsonConvert.SerializeObject(convertedBindingDtoFromLegacy); + var deserializedBindingDto = JsonConvert.DeserializeObject(serializedBindingDto); + deserializedBindingDto.ServerConnectionId.Should().BeEquivalentTo(boundSonarQubeServerProject.ServerConnection.Id); + + var binding = bindingDtoConverter.ConvertFromDto(deserializedBindingDto, boundSonarQubeServerProject.ServerConnection, "solution123"); + + binding.Should().BeEquivalentTo(boundSonarQubeServerProject); + } +} diff --git a/src/ConnectedMode.UnitTests/Persistence/BoundSonarQubeProjectExtensionsTests.cs b/src/ConnectedMode.UnitTests/Persistence/BoundSonarQubeProjectExtensionsTests.cs index afd9e20c3b..00dcacdc8c 100644 --- a/src/ConnectedMode.UnitTests/Persistence/BoundSonarQubeProjectExtensionsTests.cs +++ b/src/ConnectedMode.UnitTests/Persistence/BoundSonarQubeProjectExtensionsTests.cs @@ -18,77 +18,129 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; using SonarLint.VisualStudio.ConnectedMode.Persistence; using SonarLint.VisualStudio.Core.Binding; using SonarLint.VisualStudio.TestInfrastructure; using SonarQube.Client.Helpers; using SonarQube.Client.Models; -namespace SonarLint.VisualStudio.Integration.UnitTests +namespace SonarLint.VisualStudio.Integration.UnitTests; + +[TestClass] +public class BoundSonarQubeProjectExtensionsTests { - [TestClass] - public class BoundSonarQubeProjectExtensionsTests + [TestMethod] + public void BoundSonarQubeProject_CreateConnectionInformation_ArgCheck() + { + Exceptions.Expect(() => BoundSonarQubeProjectExtensions.CreateConnectionInformation((BoundSonarQubeProject)null)); + } + + [TestMethod] + public void BoundSonarQubeProject_CreateConnectionInformation_NoCredentials() + { + // Arrange + var input = new BoundSonarQubeProject(new Uri("http://server"), "ProjectKey", "projectName", + organization: new SonarQubeOrganization("org_key", "org_name")); + + // Act + ConnectionInformation conn = input.CreateConnectionInformation(); + + // Assert + conn.ServerUri.Should().Be(input.ServerUri); + conn.UserName.Should().BeNull(); + conn.Password.Should().BeNull(); + conn.Organization.Key.Should().Be("org_key"); + conn.Organization.Name.Should().Be("org_name"); + } + + [TestMethod] + public void BoundSonarQubeProject_CreateConnectionInformation_BasicAuthCredentials() { - [TestMethod] - public void CreateConnectionInformation_ArgCheck() - { - Exceptions.Expect(() => BoundSonarQubeProjectExtensions.CreateConnectionInformation(null)); - } - - [TestMethod] - public void CreateConnectionInformation_NoCredentials() - { - // Arrange - var input = new BoundSonarQubeProject(new Uri("http://server"), "ProjectKey", "projectName", - organization: new SonarQubeOrganization("org_key", "org_name")); - - // Act - ConnectionInformation conn = input.CreateConnectionInformation(); - - // Assert - conn.ServerUri.Should().Be(input.ServerUri); - conn.UserName.Should().BeNull(); - conn.Password.Should().BeNull(); - conn.Organization.Key.Should().Be("org_key"); - conn.Organization.Name.Should().Be("org_name"); - } - - [TestMethod] - public void CreateConnectionInformation_BasicAuthCredentials() - { - // Arrange - var creds = new BasicAuthCredentials("UserName", "password".ToSecureString()); - var input = new BoundSonarQubeProject(new Uri("http://server"), "ProjectKey", "projectName", creds, - new SonarQubeOrganization("org_key", "org_name")); - - // Act - ConnectionInformation conn = input.CreateConnectionInformation(); - - // Assert - conn.ServerUri.Should().Be(input.ServerUri); - conn.UserName.Should().Be(creds.UserName); - conn.Password.ToUnsecureString().Should().Be(creds.Password.ToUnsecureString()); - conn.Organization.Key.Should().Be("org_key"); - conn.Organization.Name.Should().Be("org_name"); - } - - [TestMethod] - public void CreateConnectionInformation_NoOrganizationNoAuth() - { - // Arrange - var input = new BoundSonarQubeProject(new Uri("http://server"), "ProjectKey", "projectName"); - - // Act - ConnectionInformation conn = input.CreateConnectionInformation(); - - // Assert - conn.ServerUri.Should().Be(input.ServerUri); - conn.UserName.Should().BeNull(); - conn.Password.Should().BeNull(); - conn.Organization.Should().BeNull(); - } + // Arrange + var creds = new BasicAuthCredentials("UserName", "password".ToSecureString()); + var input = new BoundSonarQubeProject(new Uri("http://server"), "ProjectKey", "projectName", creds, + new SonarQubeOrganization("org_key", "org_name")); + + // Act + ConnectionInformation conn = input.CreateConnectionInformation(); + + // Assert + conn.ServerUri.Should().Be(input.ServerUri); + conn.UserName.Should().Be(creds.UserName); + conn.Password.ToUnsecureString().Should().Be(creds.Password.ToUnsecureString()); + conn.Organization.Key.Should().Be("org_key"); + conn.Organization.Name.Should().Be("org_name"); + } + + [TestMethod] + public void BoundSonarQubeProject_CreateConnectionInformation_NoOrganizationNoAuth() + { + // Arrange + var input = new BoundSonarQubeProject(new Uri("http://server"), "ProjectKey", "projectName"); + + // Act + ConnectionInformation conn = input.CreateConnectionInformation(); + + // Assert + conn.ServerUri.Should().Be(input.ServerUri); + conn.UserName.Should().BeNull(); + conn.Password.Should().BeNull(); + conn.Organization.Should().BeNull(); + } + + [TestMethod] + public void BoundServerProject_CreateConnectionInformation_ArgCheck() + { + Exceptions.Expect(() => BoundSonarQubeProjectExtensions.CreateConnectionInformation((BoundServerProject)null)); + } + + [TestMethod] + public void BoundServerProject_CreateConnectionInformation_NoCredentials() + { + // Arrange + var input = new BoundServerProject("solution", "ProjectKey", new ServerConnection.SonarCloud("org_key")); + + // Act + ConnectionInformation conn = input.CreateConnectionInformation(); + + // Assert + conn.ServerUri.Should().Be(input.ServerConnection.ServerUri); + conn.UserName.Should().BeNull(); + conn.Password.Should().BeNull(); + conn.Organization.Key.Should().Be("org_key"); + } + + + [TestMethod] + public void BoundServerProject_CreateConnectionInformation_BasicAuthCredentials() + { + // Arrange + var creds = new BasicAuthCredentials("UserName", "password".ToSecureString()); + var input = new BoundServerProject("solution", "ProjectKey", new ServerConnection.SonarCloud("org_key", credentials: creds)); + + // Act + ConnectionInformation conn = input.CreateConnectionInformation(); + + // Assert + conn.ServerUri.Should().Be(input.ServerConnection.ServerUri); + conn.UserName.Should().Be(creds.UserName); + conn.Password.ToUnsecureString().Should().Be(creds.Password.ToUnsecureString()); + conn.Organization.Key.Should().Be("org_key"); + } + + [TestMethod] + public void BoundServerProject_CreateConnectionInformation_NoOrganizationNoAuth() + { + // Arrange + var input = new BoundServerProject("solution", "ProjectKey", new ServerConnection.SonarQube(new Uri("http://localhost"))); + + // Act + ConnectionInformation conn = input.CreateConnectionInformation(); + + // Assert + conn.ServerUri.Should().Be(input.ServerConnection.ServerUri); + conn.UserName.Should().BeNull(); + conn.Password.Should().BeNull(); + conn.Organization.Should().BeNull(); } } diff --git a/src/ConnectedMode.UnitTests/Persistence/BoundSonarQubeProjectTests.cs b/src/ConnectedMode.UnitTests/Persistence/BoundSonarQubeProjectTests.cs index a31d7fb848..378e342202 100644 --- a/src/ConnectedMode.UnitTests/Persistence/BoundSonarQubeProjectTests.cs +++ b/src/ConnectedMode.UnitTests/Persistence/BoundSonarQubeProjectTests.cs @@ -48,5 +48,23 @@ public void BoundProject_Serialization() deserialized.ServerUri.Should().Be(testSubject.ServerUri); deserialized.Credentials.Should().BeNull(); } + + [TestMethod] + public void BoundProject_BindingDto_Serialization() + { + // Arrange + var serverUri = new Uri("https://finding-nemo.org"); + var projectKey = "MyProject Key"; + var testSubject = new BoundSonarQubeProject(serverUri, projectKey, "projectName", new BasicAuthCredentials("used", "pwd".ToSecureString())); + + // Act (serialize + de-serialize) + string data = JsonHelper.Serialize(testSubject); + BindingDto deserialized = JsonHelper.Deserialize(data); + + // Assert + deserialized.Should().NotBe(testSubject); + deserialized.ProjectKey.Should().Be(testSubject.ProjectKey); + deserialized.ServerUri.Should().Be(testSubject.ServerUri); + } } } diff --git a/src/ConnectedMode.UnitTests/Persistence/ServerConnectionModelMapperTest.cs b/src/ConnectedMode.UnitTests/Persistence/ServerConnectionModelMapperTest.cs new file mode 100644 index 0000000000..f578b15413 --- /dev/null +++ b/src/ConnectedMode.UnitTests/Persistence/ServerConnectionModelMapperTest.cs @@ -0,0 +1,239 @@ +/* + * 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.ConnectedMode.Persistence; +using SonarLint.VisualStudio.Core.Binding; +using SonarLint.VisualStudio.TestInfrastructure; +using static SonarLint.VisualStudio.ConnectedMode.Persistence.ServerConnectionModelMapper; + +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Persistence; + +[TestClass] +public class ServerConnectionModelMapperTest +{ + private ServerConnectionModelMapper testSubject; + + [TestInitialize] + public void TestInitialize() + { + testSubject = new ServerConnectionModelMapper(); + } + + [TestMethod] + public void MefCtor_CheckExports() + { + MefTestHelpers.CheckTypeCanBeImported(); + } + + [TestMethod] + public void Mef_CheckIsSingleton() + { + MefTestHelpers.CheckIsSingletonMefComponent(); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void GetServerConnection_SonarCloud_ReturnsSonarCloudConnection(bool isSmartNotificationsEnabled) + { + var connectionsModel = GetSonarCloudJsonModel("myOrg", isSmartNotificationsEnabled); + + var serverConnection = testSubject.GetServerConnection(connectionsModel); + + IsExpectedSonarCloudConnection(serverConnection, connectionsModel); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void GetServerConnection_SonarQube_ReturnsSonarQubeConnection(bool isSmartNotificationsEnabled) + { + var connectionsModel = GetSonarQubeJsonModel("http://localhost:9000", isSmartNotificationsEnabled); + + var serverConnection = testSubject.GetServerConnection(connectionsModel); + + IsExpectedSonarQubeConnection(serverConnection, connectionsModel); + } + + [TestMethod] + public void GetServerConnection_BothOrganizationKeyAndServerUriAreSet_ThrowsException() + { + var connectionsModel = GetSonarCloudJsonModel("myOrg"); + connectionsModel.ServerUri = "http://localhost:9000"; + + Action act = () => testSubject.GetServerConnection(connectionsModel); + + act.Should().Throw($"Invalid {nameof(ServerConnectionJsonModel)}. {nameof(ServerConnection)} could not be created"); + } + + [TestMethod] + public void GetServerConnection_BothOrganizationKeyAndServerUriAreNull_ThrowsException() + { + var connectionsModel = GetSonarCloudJsonModel("myOrg"); + connectionsModel.OrganizationKey = null; + + Action act = () => testSubject.GetServerConnection(connectionsModel); + + act.Should().Throw($"Invalid {nameof(ServerConnectionJsonModel)}. {nameof(ServerConnection)} could not be created"); + } + + [TestMethod] + [DataRow("org", null, true)] + [DataRow(null, "http://localhost", false)] + [DataRow(null, null, false)] + [DataRow("org", "http://localhost", false)] + public void IsServerConnectionForSonarCloud_OrganizationKeySetAndServerUriNotSet_ReturnsTrue(string organizationKey, string serverUi, bool expectedResult) + { + var connectionsModel = GetSonarCloudJsonModel(organizationKey); + connectionsModel.ServerUri = serverUi; + + var isSonarCloud = IsServerConnectionForSonarCloud(connectionsModel); + + isSonarCloud.Should().Be(expectedResult); + } + + [TestMethod] + [DataRow("org", null, false)] + [DataRow(null, "http://localhost", true)] + [DataRow(null, null, false)] + [DataRow("org", "http://localhost", false)] + public void IsServerConnectionForSonarCloud_OrganizationKeyNotSetAndServerUriSet_ReturnsTrue(string organizationKey, string serverUi, bool expectedResult) + { + var connectionsModel = GetSonarCloudJsonModel(organizationKey); + connectionsModel.ServerUri = serverUi; + + var isSonarQube = IsServerConnectionForSonarQube(connectionsModel); + + isSonarQube.Should().Be(expectedResult); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void GetServerConnectionsListJsonModel_OneSonarCloudConnection_ReturnsServerConnectionModelForSonarCloud(bool isSmartNotifications) + { + var sonarCloud = new ServerConnection.SonarCloud("myOrg", new ServerConnectionSettings(isSmartNotifications)); + + var model = testSubject.GetServerConnectionsListJsonModel([sonarCloud]); + + model.Should().NotBeNull(); + model.ServerConnections.Count.Should().Be(1); + model.ServerConnections[0].Should().BeEquivalentTo(new ServerConnectionJsonModel + { + Id = sonarCloud.Id, + OrganizationKey = sonarCloud.OrganizationKey, + Settings = sonarCloud.Settings, + ServerUri = null + }); + } + + [TestMethod] + public void GetServerConnectionsListJsonModel_OneSonarCloudConnectionWithNullSettings_ThrowsExceptions() + { + var sonarCloud = new ServerConnection.SonarCloud("myOrg") + { + Settings = null + }; + + Action act = () => testSubject.GetServerConnectionsListJsonModel([sonarCloud]); + + act.Should().Throw($"{nameof(ServerConnection.Settings)} can not be null"); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void GetServerConnectionsListJsonModel_OneSonarQubeConnection_ReturnsServerConnectionModelForSonarQube(bool isSmartNotifications) + { + var sonarQube = new ServerConnection.SonarQube(new Uri("http://localhost"), new ServerConnectionSettings(isSmartNotifications)); + + var model = testSubject.GetServerConnectionsListJsonModel([sonarQube]); + + model.Should().NotBeNull(); + model.ServerConnections.Count.Should().Be(1); + model.ServerConnections[0].Should().BeEquivalentTo(new ServerConnectionJsonModel + { + Id = sonarQube.Id, + OrganizationKey = null, + ServerUri = sonarQube.ServerUri.ToString(), + Settings = sonarQube.Settings + }); + } + + [TestMethod] + public void GetServerConnectionsListJsonModel_OneSonarQubeConnectionWithNullSettings_ThrowsExceptions() + { + var sonarQube = new ServerConnection.SonarQube(new Uri("http://localhost")) + { + Settings = null + }; + + Action act = () => testSubject.GetServerConnectionsListJsonModel([sonarQube]); + + act.Should().Throw($"{nameof(ServerConnection.Settings)} can not be null"); + } + + [TestMethod] + public void GetServerConnectionsListJsonModel_NoConnection_ReturnsServerConnectionModelWithNoConnection() + { + var model = testSubject.GetServerConnectionsListJsonModel([]); + + model.Should().NotBeNull(); + model.ServerConnections.Should().BeEmpty(); + } + + private static ServerConnectionJsonModel GetSonarCloudJsonModel(string id, bool isSmartNotificationsEnabled = false) + { + return new ServerConnectionJsonModel + { + Id = id, + OrganizationKey = id, + Settings = new ServerConnectionSettings(isSmartNotificationsEnabled) + }; + } + + private static ServerConnectionJsonModel GetSonarQubeJsonModel(string id, bool isSmartNotificationsEnabled = false) + { + return new ServerConnectionJsonModel + { + Id = id, + ServerUri = id, + Settings = new ServerConnectionSettings(isSmartNotificationsEnabled) + }; + } + + private static void IsExpectedSonarCloudConnection(ServerConnection serverConnection, ServerConnectionJsonModel connectionsModel) + { + serverConnection.Should().BeOfType(); + serverConnection.Id.Should().Be(serverConnection.Id); + serverConnection.Settings.Should().NotBeNull(); + serverConnection.Settings.IsSmartNotificationsEnabled.Should().Be(connectionsModel.Settings.IsSmartNotificationsEnabled); + ((ServerConnection.SonarCloud)serverConnection).OrganizationKey.Should().Be(connectionsModel.OrganizationKey); + } + + private static void IsExpectedSonarQubeConnection(ServerConnection serverConnection, ServerConnectionJsonModel connectionsModel) + { + serverConnection.Should().BeOfType(); + serverConnection.Id.Should().Be(serverConnection.Id); + serverConnection.Settings.Should().NotBeNull(); + serverConnection.Settings.IsSmartNotificationsEnabled.Should().Be(connectionsModel.Settings.IsSmartNotificationsEnabled); + ((ServerConnection.SonarQube)serverConnection).ServerUri.Should().Be(connectionsModel.ServerUri); + } +} diff --git a/src/ConnectedMode.UnitTests/Persistence/ServerConnectionsRepositoryTests.cs b/src/ConnectedMode.UnitTests/Persistence/ServerConnectionsRepositoryTests.cs new file mode 100644 index 0000000000..bc214603e0 --- /dev/null +++ b/src/ConnectedMode.UnitTests/Persistence/ServerConnectionsRepositoryTests.cs @@ -0,0 +1,613 @@ +/* + * 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 System.IO; +using System.IO.Abstractions; +using SonarLint.VisualStudio.ConnectedMode.Binding; +using SonarLint.VisualStudio.ConnectedMode.Persistence; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Binding; +using SonarLint.VisualStudio.Core.Persistence; +using SonarLint.VisualStudio.TestInfrastructure; +using static SonarLint.VisualStudio.Core.Binding.ServerConnection; + +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Persistence; + +[TestClass] +public class ServerConnectionsRepositoryTests +{ + private ServerConnectionsRepository testSubject; + private IJsonFileHandler jsonFileHandler; + private ILogger logger; + private IEnvironmentVariableProvider environmentVariableProvider; + private IServerConnectionModelMapper serverConnectionModelMapper; + private ISolutionBindingCredentialsLoader credentialsLoader; + private readonly SonarCloud sonarCloudServerConnection = new("myOrganization", new ServerConnectionSettings(true), Substitute.For()); + private readonly ServerConnection.SonarQube sonarQubeServerConnection = new(new Uri("http://localhost"), new ServerConnectionSettings(true), Substitute.For()); + private IFileSystem fileSystem; + + [TestInitialize] + public void TestInitialize() + { + jsonFileHandler = Substitute.For(); + serverConnectionModelMapper = Substitute.For(); + credentialsLoader = Substitute.For(); + environmentVariableProvider = Substitute.For(); + logger = Substitute.For(); + fileSystem = Substitute.For(); + + testSubject = new ServerConnectionsRepository(jsonFileHandler, serverConnectionModelMapper, credentialsLoader, environmentVariableProvider, fileSystem, logger); + } + + [TestMethod] + public void MefCtor_CheckExports() + { + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + } + + [TestMethod] + public void Mef_CheckIsSingleton() + { + MefTestHelpers.CheckIsSingletonMefComponent(); + } + + [TestMethod] + public void TryGet_FileDoesNotExist_ReturnsFalse() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.When(x => x.ReadFile(Arg.Any())).Do(x => throw new FileNotFoundException()); + + var succeeded = testSubject.TryGet("myId", out ServerConnection serverConnection); + + succeeded.Should().BeFalse(); + serverConnection.Should().BeNull(); + jsonFileHandler.Received(1).ReadFile(Arg.Any()); + } + + [TestMethod] + public void TryGet_FileCanNotBeRead_ReturnsFalse() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.When(x => x.ReadFile(Arg.Any())).Do(x => throw new Exception()); + + var succeeded = testSubject.TryGet("myId", out ServerConnection serverConnection); + + succeeded.Should().BeFalse(); + serverConnection.Should().BeNull(); + jsonFileHandler.Received(1).ReadFile(Arg.Any()); + } + + [TestMethod] + public void TryGet_FileExistsAndConnectionDoesNotExist_ReturnsFalse() + { + MockFileWithOneSonarCloudConnection(); + + var succeeded = testSubject.TryGet("non-existing connectionId", out ServerConnection serverConnection); + + succeeded.Should().BeFalse(); + serverConnection.Should().BeNull(); + } + + [TestMethod] + public void TryGet_FileExistsAndConnectionIsSonarCloud_ReturnsSonarCloudConnection() + { + var sonarCloudModel = GetSonarCloudJsonModel(); + var expectedConnection = new SonarCloud(sonarCloudModel.OrganizationKey); + MockReadingFile(new ServerConnectionsListJsonModel { ServerConnections = [sonarCloudModel] }); + serverConnectionModelMapper.GetServerConnection(sonarCloudModel).Returns(expectedConnection); + + var succeeded = testSubject.TryGet(sonarCloudModel.Id, out ServerConnection serverConnection); + + succeeded.Should().BeTrue(); + serverConnection.Should().Be(expectedConnection); + } + + [TestMethod] + public void TryGet_FileExistsAndConnectionIsSonarCloud_FillsCredentials() + { + var expectedConnection = MockFileWithOneSonarCloudConnection(); + var credentials = Substitute.For(); + credentialsLoader.Load(expectedConnection.CredentialsUri).Returns(credentials); + + var succeeded = testSubject.TryGet(expectedConnection.Id, out ServerConnection serverConnection); + + succeeded.Should().BeTrue(); + serverConnection.Should().Be(expectedConnection); + serverConnection.Credentials.Should().Be(credentials); + credentialsLoader.Received(1).Load(expectedConnection.CredentialsUri); + } + + [TestMethod] + public void TryGet_FileExistsAndConnectionIsSonarQube_ReturnsSonarQubeConnection() + { + var sonarQubeModel = GetSonarQubeJsonModel(new Uri("http://localhost:9000")); + var expectedConnection = new ServerConnection.SonarQube(new Uri(sonarQubeModel.ServerUri)); + MockReadingFile(new ServerConnectionsListJsonModel { ServerConnections = [sonarQubeModel] }); + serverConnectionModelMapper.GetServerConnection(sonarQubeModel).Returns(expectedConnection); + + var succeeded = testSubject.TryGet(sonarQubeModel.Id, out ServerConnection serverConnection); + + succeeded.Should().BeTrue(); + serverConnection.Should().Be(expectedConnection); + } + + [TestMethod] + public void TryGet_FileExistsAndConnectionIsSonarQube_FillsCredentials() + { + var expectedConnection = MockFileWithOneSonarQubeConnection(); + var credentials = Substitute.For(); + credentialsLoader.Load(expectedConnection.CredentialsUri).Returns(credentials); + + var succeeded = testSubject.TryGet(expectedConnection.Id, out ServerConnection serverConnection); + + succeeded.Should().BeTrue(); + serverConnection.Should().Be(expectedConnection); + serverConnection.Credentials.Should().Be(credentials); + credentialsLoader.Received(1).Load(expectedConnection.CredentialsUri); + } + + [TestMethod] + public void TryGetAll_FileDoesNotExist_ReturnsEmptyList() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.When(x => x.ReadFile(Arg.Any())).Do(x => throw new FileNotFoundException()); + + testSubject.TryGetAll(out var connections); + + connections.Should().BeEmpty(); + } + + [TestMethod] + public void TryGetAll_FileCouldNotBeRead_ThrowsException() + { + var exceptionMsg = "failed"; + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.When(x => x.ReadFile(Arg.Any())).Do(x => throw new Exception(exceptionMsg)); + + var succeeded = testSubject.TryGetAll(out var connections); + + succeeded.Should().BeFalse(); + } + + [TestMethod] + public void TryGetAll_FileExistsAndIsEmpty_ReturnsEmptyList() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + + testSubject.TryGetAll(out var connections); + + connections.Should().BeEmpty(); + } + + [TestMethod] + public void TryGetAll_FileExistsAndHasConnection_MapsModel() + { + var cloudModel = GetSonarCloudJsonModel(); + MockReadingFile(new ServerConnectionsListJsonModel { ServerConnections = [cloudModel] }); + + testSubject.TryGetAll(out _); + + serverConnectionModelMapper.Received(1).GetServerConnection(cloudModel); + } + + [TestMethod] + public void TryGetAll_ConnectionsExist_DoesNotFillCredentials() + { + MockFileWithOneSonarCloudConnection(); + + testSubject.TryGetAll(out _); + + credentialsLoader.DidNotReceive().Load(Arg.Any()); + } + + [TestMethod] + public void TryAdd_FileCouldNotBeRead_DoesNotAddConnection() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + + var succeeded = testSubject.TryAdd(sonarCloudServerConnection); + + succeeded.Should().BeFalse(); + Received.InOrder(() => + { + jsonFileHandler.ReadFile(Arg.Any()); + serverConnectionModelMapper.GetServerConnectionsListJsonModel(Arg.Is>(x => x.Contains(sonarCloudServerConnection))); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()); + }); + } + + [TestMethod] + public void TryAdd_FileExistsAndConnectionIsNew_AddsConnection() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(true); + + var succeeded = testSubject.TryAdd(sonarCloudServerConnection); + + succeeded.Should().BeTrue(); + Received.InOrder(() => + { + jsonFileHandler.ReadFile(Arg.Any()); + serverConnectionModelMapper.GetServerConnectionsListJsonModel(Arg.Is>(x => x.Contains(sonarCloudServerConnection))); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()); + }); + } + + [TestMethod] + public void TryAdd_SonarCloudConnectionIsAddedAndCredentialsAreNotNull_SavesCredentials() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(true); + + var succeeded = testSubject.TryAdd(sonarCloudServerConnection); + + succeeded.Should().BeTrue(); + credentialsLoader.Received(1).Save(sonarCloudServerConnection.Credentials, sonarCloudServerConnection.CredentialsUri); + } + + [TestMethod] + public void TryAdd_SonarQubeConnectionIsAddedAndCredentialsAreNotNull_SavesCredentials() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(true); + + var succeeded = testSubject.TryAdd(sonarQubeServerConnection); + + succeeded.Should().BeTrue(); + credentialsLoader.Received(1).Save(sonarQubeServerConnection.Credentials, sonarQubeServerConnection.CredentialsUri); + } + + [TestMethod] + public void TryAdd_ConnectionIsAddedAndCredentialsAreNull_ReturnsFalse() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(true); + sonarCloudServerConnection.Credentials = null; + + var succeeded = testSubject.TryAdd(sonarCloudServerConnection); + + succeeded.Should().BeFalse(); + credentialsLoader.DidNotReceive().Save(Arg.Any(), Arg.Any()); + } + + [TestMethod] + public void TryAdd_ConnectionIsNotAdded_DoesNotSaveCredentials() + { + var sonarCloud = MockFileWithOneSonarCloudConnection(); + + var succeeded = testSubject.TryAdd(sonarCloud); + + succeeded.Should().BeFalse(); + credentialsLoader.DidNotReceive().Save(Arg.Any(), Arg.Any()); + } + + [TestMethod] + public void TryAdd_FileExistsAndConnectionIsDuplicate_DoesNotAddConnection() + { + var sonarCloud = MockFileWithOneSonarCloudConnection(); + + var succeeded = testSubject.TryAdd(sonarCloud); + + succeeded.Should().BeFalse(); + jsonFileHandler.DidNotReceive().TryWriteToFile(Arg.Any(), Arg.Any()); + } + + [TestMethod] + public void TryAdd_WritingToFileFails_DoesNotAddConnection() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(false); + var sonarCloud = new SonarCloud("myOrg"); + + var succeeded = testSubject.TryAdd(sonarCloud); + + succeeded.Should().BeFalse(); + } + + [TestMethod] + public void TryAdd_WritingThrowsException_DoesNotUpdateConnectionAndWritesLog() + { + var exceptionMsg = "IO exception"; + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.When(x => x.TryWriteToFile(Arg.Any(), Arg.Any())).Do(x => throw new Exception(exceptionMsg)); + + var succeeded = testSubject.TryAdd(sonarCloudServerConnection); + + succeeded.Should().BeFalse(); + logger.Received(1).WriteLine($"Failed updating the {ServerConnectionsRepository.ConnectionsFileName}: {exceptionMsg}"); + } + + [TestMethod] + public void TryDelete_FileCouldNotBeRead_ReturnsFalse() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.When(x => x.ReadFile(Arg.Any())).Do(x => throw new Exception()); + + var succeeded = testSubject.TryDelete("myOrg"); + + succeeded.Should().BeFalse(); + jsonFileHandler.DidNotReceive().TryWriteToFile(Arg.Any(), Arg.Any()); + } + + [TestMethod] + public void TryDelete_FileExistsAndHasNoConnection_ReturnsFalse() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + + var succeeded = testSubject.TryDelete("myOrg"); + + succeeded.Should().BeFalse(); + jsonFileHandler.DidNotReceive().TryWriteToFile(Arg.Any(), Arg.Any()); + } + + [TestMethod] + public void TryDelete_FileExistsAndConnectionExists_RemovesConnection() + { + var sonarCloud = MockFileWithOneSonarCloudConnection(); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(true); + + var succeeded = testSubject.TryDelete(sonarCloud.Id); + + succeeded.Should().BeTrue(); + Received.InOrder(() => + { + jsonFileHandler.ReadFile(Arg.Any()); + serverConnectionModelMapper.GetServerConnectionsListJsonModel(Arg.Is>(x => !x.Any())); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()); + credentialsLoader.DeleteCredentials(sonarCloud.CredentialsUri); + }); + } + + [TestMethod] + public void TryDelete_SonarCloudConnectionWasRemoved_RemovesCredentials() + { + var sonarCloud = MockFileWithOneSonarCloudConnection(); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(true); + + var succeeded = testSubject.TryDelete(sonarCloud.Id); + + succeeded.Should().BeTrue(); + credentialsLoader.Received(1).DeleteCredentials(sonarCloud.CredentialsUri); + } + + [TestMethod] + public void TryDelete_SonarQubeConnectionWasRemoved_RemovesCredentials() + { + var sonarQube = MockFileWithOneSonarQubeConnection(); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(true); + + var succeeded = testSubject.TryDelete(sonarQube.Id); + + succeeded.Should().BeTrue(); + credentialsLoader.Received(1).DeleteCredentials(sonarQube.CredentialsUri); + } + + [TestMethod] + public void TryDelete_ConnectionWasNotRemoved_DoesNotRemoveCredentials() + { + var sonarCloud = MockFileWithOneSonarCloudConnection(); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(false); + + var succeeded = testSubject.TryDelete(sonarCloud.Id); + + succeeded.Should().BeFalse(); + credentialsLoader.DidNotReceive().DeleteCredentials(Arg.Any()); + } + + [TestMethod] + public void TryDelete_WritingToFileFails_ReturnsFalse() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(false); + + var succeeded = testSubject.TryDelete("myOrg"); + + succeeded.Should().BeFalse(); + } + + [TestMethod] + public void TryDelete_WritingThrowsException_DoesNotUpdateConnectionAndWritesLog() + { + var exceptionMsg = "IO exception"; + var sonarCloud = MockFileWithOneSonarCloudConnection(); + jsonFileHandler.When(x => x.TryWriteToFile(Arg.Any(), Arg.Any())).Do(x => throw new Exception(exceptionMsg)); + + var succeeded = testSubject.TryDelete(sonarCloud.Id); + + succeeded.Should().BeFalse(); + logger.Received(1).WriteLine($"Failed updating the {ServerConnectionsRepository.ConnectionsFileName}: {exceptionMsg}"); + } + + [TestMethod] + public void TryUpdateSettingsById_FileCouldNotBeRead_ReturnsFalse() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.When(x => x.ReadFile(Arg.Any())).Do(x => throw new Exception()); + + var succeeded = testSubject.TryUpdateSettingsById("myOrg", new ServerConnectionSettings(true)); + + succeeded.Should().BeFalse(); + jsonFileHandler.DidNotReceive().TryWriteToFile(Arg.Any(), Arg.Any()); + } + + [TestMethod] + public void TryUpdateSettingsById_FileExistsAndHasNoConnection_ReturnsFalse() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + + var succeeded = testSubject.TryUpdateSettingsById("myOrg", new ServerConnectionSettings(true)); + + succeeded.Should().BeFalse(); + jsonFileHandler.DidNotReceive().TryWriteToFile(Arg.Any(), Arg.Any()); + } + + [TestMethod] + [DataRow(false, true)] + [DataRow(true, false)] + public void TryUpdateSettingsById_FileExistsAndConnectionExists_UpdatesSettings(bool oldSmartNotifications, bool newSmartNotifications) + { + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(true); + MockFileWithOneSonarCloudConnection(oldSmartNotifications); + + var succeeded = testSubject.TryUpdateSettingsById("https://sonarcloud.io/organizations/myOrg", new ServerConnectionSettings(newSmartNotifications)); + + succeeded.Should().BeTrue(); + Received.InOrder(() => + { + jsonFileHandler.ReadFile(Arg.Any()); + serverConnectionModelMapper.GetServerConnectionsListJsonModel(Arg.Is>(x => x.Count() == 1 && x.Single().Settings.IsSmartNotificationsEnabled == newSmartNotifications)); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()); + }); + } + + [TestMethod] + public void TryUpdateSettingsById_WritingToFileFails_DoesNotUpdateConnection() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(false); + + var succeeded = testSubject.TryUpdateSettingsById("myOrg", new ServerConnectionSettings(true)); + + succeeded.Should().BeFalse(); + } + + [TestMethod] + public void TryUpdateSettingsById_WritingThrowsException_DoesNotUpdateConnectionAndWritesLog() + { + var exceptionMsg = "IO exception"; + var sonarCloud = MockFileWithOneSonarCloudConnection(); + jsonFileHandler.When(x => x.TryWriteToFile(Arg.Any(), Arg.Any())).Do(x => throw new Exception(exceptionMsg)); + + var succeeded = testSubject.TryUpdateSettingsById(sonarCloud.Id, new ServerConnectionSettings(true)); + + succeeded.Should().BeFalse(); + logger.Received(1).WriteLine($"Failed updating the {ServerConnectionsRepository.ConnectionsFileName}: {exceptionMsg}"); + } + + [TestMethod] + public void TryUpdateCredentialsById_ConnectionDoesNotExist_DoesNotUpdateCredentials() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + + var succeeded = testSubject.TryUpdateCredentialsById("myConn", Substitute.For()); + + succeeded.Should().BeFalse(); + credentialsLoader.DidNotReceive().Save(Arg.Any(), Arg.Any()); + } + + [TestMethod] + public void TryUpdateCredentialsById_SonarCloudConnectionExists_UpdatesCredentials() + { + var sonarCloud = MockFileWithOneSonarCloudConnection(); + var newCredentials = Substitute.For(); + + var succeeded = testSubject.TryUpdateCredentialsById(sonarCloud.Id, newCredentials); + + succeeded.Should().BeTrue(); + credentialsLoader.Received(1).Save(newCredentials, sonarCloud.CredentialsUri); + } + + [TestMethod] + public void TryUpdateCredentialsById_SonarQubeConnectionExists_UpdatesCredentials() + { + var sonarQube = MockFileWithOneSonarQubeConnection(); + var newCredentials = Substitute.For(); + + var succeeded = testSubject.TryUpdateCredentialsById(sonarQube.Id, newCredentials); + + succeeded.Should().BeTrue(); + credentialsLoader.Received(1).Save(newCredentials, sonarQube.ServerUri); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void ConnectionsFileExists_ReturnsTrueOnlyIfTheConnectionsFileExists(bool fileExists) + { + fileSystem.File.Exists(Arg.Any()).Returns(fileExists); + + var result = testSubject.ConnectionsFileExists(); + + result.Should().Be(fileExists); + } + + [TestMethod] + public void TryUpdateCredentialsById_SavingCredentialsThrows_ReturnsFalseAndLogs() + { + var exceptionMsg = "failed"; + var connection = MockFileWithOneSonarCloudConnection(); + credentialsLoader.When(x => x.Save(Arg.Any(), Arg.Any())).Do(x => throw new Exception(exceptionMsg)); + + var succeeded = testSubject.TryUpdateCredentialsById(connection.Id, Substitute.For()); + + succeeded.Should().BeFalse(); + logger.Received(1).WriteLine($"Failed updating credentials: {exceptionMsg}"); + } + + private SonarCloud MockFileWithOneSonarCloudConnection(bool isSmartNotificationsEnabled = true) + { + var sonarCloudModel = GetSonarCloudJsonModel(isSmartNotificationsEnabled); + var sonarCloud = new SonarCloud(sonarCloudModel.OrganizationKey, sonarCloudModel.Settings, Substitute.For()); + MockReadingFile(new ServerConnectionsListJsonModel { ServerConnections = [sonarCloudModel] }); + serverConnectionModelMapper.GetServerConnection(sonarCloudModel).Returns(sonarCloud); + + return sonarCloud; + } + + private ServerConnection.SonarQube MockFileWithOneSonarQubeConnection(bool isSmartNotificationsEnabled = true) + { + var sonarQubeModel = GetSonarQubeJsonModel(new Uri("http://localhost"), isSmartNotificationsEnabled); + var sonarQube = new ServerConnection.SonarQube(new Uri(sonarQubeModel.ServerUri), sonarQubeModel.Settings, Substitute.For()); + MockReadingFile(new ServerConnectionsListJsonModel { ServerConnections = [sonarQubeModel] }); + serverConnectionModelMapper.GetServerConnection(sonarQubeModel).Returns(sonarQube); + + return sonarQube; + } + + private void MockReadingFile(ServerConnectionsListJsonModel modelToReturn) + { + jsonFileHandler.ReadFile(Arg.Any()).Returns(modelToReturn); + } + + private static ServerConnectionJsonModel GetSonarCloudJsonModel(bool isSmartNotificationsEnabled = false) + { + return new ServerConnectionJsonModel + { + Id = "https://sonarcloud.io/organizations/myOrg", + OrganizationKey = "myOrg", + Settings = new ServerConnectionSettings(isSmartNotificationsEnabled) + }; + } + + private static ServerConnectionJsonModel GetSonarQubeJsonModel(Uri id, bool isSmartNotificationsEnabled = false) + { + return new ServerConnectionJsonModel + { + Id = id.ToString(), + ServerUri = id.ToString(), + Settings = new ServerConnectionSettings(isSmartNotificationsEnabled) + }; + } + +} diff --git a/src/ConnectedMode.UnitTests/Persistence/SolutionBindingCredentialsLoaderTests.cs b/src/ConnectedMode.UnitTests/Persistence/SolutionBindingCredentialsLoaderTests.cs index f7f8a9de1e..d92a4bd945 100644 --- a/src/ConnectedMode.UnitTests/Persistence/SolutionBindingCredentialsLoaderTests.cs +++ b/src/ConnectedMode.UnitTests/Persistence/SolutionBindingCredentialsLoaderTests.cs @@ -18,10 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; using Microsoft.Alm.Authentication; using SonarLint.VisualStudio.ConnectedMode.Binding; -using Moq; using SonarLint.VisualStudio.ConnectedMode.Persistence; using SonarLint.VisualStudio.Core.Binding; using SonarQube.Client.Helpers; @@ -31,16 +29,16 @@ namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Persistence [TestClass] public class SolutionBindingCredentialsLoaderTests { - private Mock store; + private ICredentialStoreService store; private Uri mockUri; private SolutionBindingCredentialsLoader testSubject; [TestInitialize] public void Setup() { - store = new Mock(); + store = Substitute.For(); mockUri = new Uri("http://sonarsource.com"); - testSubject = new SolutionBindingCredentialsLoader(store.Object); + testSubject = new SolutionBindingCredentialsLoader(store); } [TestMethod] @@ -61,7 +59,7 @@ public void Load_ServerUriIsNull_Null() [TestMethod] public void Load_NoCredentials_Null() { - store.Setup(x => x.ReadCredentials(mockUri)).Returns(null as Credential); + store.ReadCredentials(mockUri).Returns(null as Credential); var actual = testSubject.Load(mockUri); actual.Should().Be(null); @@ -72,7 +70,7 @@ public void Load_CredentialsExist_CredentialsWithSecuredString() { var credentials = new Credential("user", "password"); store - .Setup(x => x.ReadCredentials(It.Is(t => t.ActualUri == mockUri))) + .ReadCredentials(Arg.Is(t => t.ActualUri == mockUri)) .Returns(credentials); var actual = testSubject.Load(mockUri); @@ -86,7 +84,7 @@ public void Save_ServerUriIsNull_CredentialsNotSaved() testSubject.Save(credentials, null); - store.Verify(x=> x.WriteCredentials(It.IsAny(), It.IsAny()), Times.Never); + store.DidNotReceive().WriteCredentials(Arg.Any(), Arg.Any()); } [TestMethod] @@ -94,7 +92,7 @@ public void Save_CredentialsAreNull_CredentialsNotSaved() { testSubject.Save(null, mockUri); - store.Verify(x => x.WriteCredentials(It.IsAny(), It.IsAny()), Times.Never); + store.DidNotReceive().WriteCredentials(Arg.Any(), Arg.Any()); } [TestMethod] @@ -103,7 +101,7 @@ public void Save_CredentialsAreNotBasicAuth_CredentialsNotSaved() var mockCredentials = new Mock(); testSubject.Save(mockCredentials.Object, mockUri); - store.Verify(x => x.WriteCredentials(It.IsAny(), It.IsAny()), Times.Never); + store.DidNotReceive().WriteCredentials(Arg.Any(), Arg.Any()); } [TestMethod] @@ -112,11 +110,26 @@ public void Save_CredentialsAreBasicAuth_CredentialsSavedWithUnsecuredString() var credentials = new BasicAuthCredentials("user", "password".ToSecureString()); testSubject.Save(credentials, mockUri); - store.Verify(x => - x.WriteCredentials( - It.Is(t => t.ActualUri == mockUri), - It.Is(c=> c.Username == "user" && c.Password == "password")), - Times.Once); + store.Received(1) + .WriteCredentials( + Arg.Is(t => t.ActualUri == mockUri), + Arg.Is(c=> c.Username == "user" && c.Password == "password")); + } + + [TestMethod] + public void DeleteCredentials_UriNull_DoesNotCallStoreDeleteCredentials() + { + testSubject.DeleteCredentials(null); + + store.DidNotReceive().DeleteCredentials(Arg.Any()); + } + + [TestMethod] + public void DeleteCredentials_UriProvided_CallsStoreDeleteCredentials() + { + testSubject.DeleteCredentials(mockUri); + + store.Received(1).DeleteCredentials(Arg.Any()); } } } diff --git a/src/ConnectedMode.UnitTests/Persistence/SolutionBindingFileLoaderTests.cs b/src/ConnectedMode.UnitTests/Persistence/SolutionBindingFileLoaderTests.cs index 61aa6ad45c..12625f989d 100644 --- a/src/ConnectedMode.UnitTests/Persistence/SolutionBindingFileLoaderTests.cs +++ b/src/ConnectedMode.UnitTests/Persistence/SolutionBindingFileLoaderTests.cs @@ -18,8 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Collections.Generic; using System.IO; using System.IO.Abstractions; using SonarLint.VisualStudio.ConnectedMode.Persistence; @@ -34,7 +32,7 @@ public class SolutionBindingFileLoaderTests private Mock logger; private Mock fileSystem; private SolutionBindingFileLoader testSubject; - private BoundSonarQubeProject boundProject; + private BindingDto bindingDto; private string serializedProject; private const string MockFilePath = "c:\\test.txt"; @@ -49,12 +47,14 @@ public void TestInitialize() testSubject = new SolutionBindingFileLoader(logger.Object, fileSystem.Object); fileSystem.Setup(x => x.Directory.Exists(MockDirectory)).Returns(true); - - boundProject = new BoundSonarQubeProject( - new Uri("http://xxx.www.zzz/yyy:9000"), - "MyProject Key", - "projectName") + + bindingDto = new BindingDto { + ServerUri = new Uri("http://xxx.www.zzz/yyy:9000"), + Organization = null, + ProjectKey = "MyProject Key", + ProjectName = "projectName", + ServerConnectionId = null, Profiles = new Dictionary { { @@ -69,7 +69,6 @@ public void TestInitialize() serializedProject = @"{ ""ServerUri"": ""http://xxx.www.zzz/yyy:9000"", - ""Organization"": null, ""ProjectKey"": ""MyProject Key"", ""ProjectName"": ""projectName"", ""Profiles"": { @@ -102,7 +101,7 @@ public void Save_DirectoryDoesNotExist_DirectoryIsCreated() { fileSystem.Setup(x => x.Directory.Exists(MockDirectory)).Returns(false); - testSubject.Save(MockFilePath, boundProject); + testSubject.Save(MockFilePath, bindingDto); fileSystem.Verify(x => x.Directory.CreateDirectory(MockDirectory), Times.Once); } @@ -112,7 +111,7 @@ public void Save_DirectoryExists_DirectoryNotCreated() { fileSystem.Setup(x => x.Directory.Exists(MockDirectory)).Returns(true); - testSubject.Save(MockFilePath, boundProject); + testSubject.Save(MockFilePath, bindingDto); fileSystem.Verify(x => x.Directory.CreateDirectory(It.IsAny()), Times.Never); } @@ -122,7 +121,7 @@ public void Save_ReturnsTrue() { fileSystem.Setup(x => x.File.WriteAllText(MockFilePath, serializedProject)); - var actual = testSubject.Save(MockFilePath, boundProject); + var actual = testSubject.Save(MockFilePath, bindingDto); actual.Should().BeTrue(); } @@ -131,7 +130,7 @@ public void Save_FileSerializedAndWritten() { fileSystem.Setup(x => x.File.WriteAllText(MockFilePath, serializedProject)); - testSubject.Save(MockFilePath, boundProject); + testSubject.Save(MockFilePath, bindingDto); fileSystem.Verify(x => x.File.WriteAllText(MockFilePath, serializedProject), Times.Once); } @@ -141,7 +140,7 @@ public void Save_NonCriticalException_False() { fileSystem.Setup(x => x.File.WriteAllText(MockFilePath, It.IsAny())).Throws(); - var actual = testSubject.Save(MockFilePath, boundProject); + var actual = testSubject.Save(MockFilePath, bindingDto); actual.Should().BeFalse(); } @@ -150,7 +149,7 @@ public void Save_CriticalException_Exception() { fileSystem.Setup(x => x.File.WriteAllText(MockFilePath, It.IsAny())).Throws(); - Action act = () => testSubject.Save(MockFilePath, boundProject); + Action act = () => testSubject.Save(MockFilePath, bindingDto); act.Should().ThrowExactly(); } @@ -211,7 +210,7 @@ public void Load_FileExists_DeserializedProject() fileSystem.Setup(x => x.File.ReadAllText(MockFilePath)).Returns(serializedProject); var actual = testSubject.Load(MockFilePath); - actual.Should().BeEquivalentTo(boundProject); + actual.Should().BeEquivalentTo(bindingDto); } [TestMethod] @@ -225,7 +224,7 @@ public void Load_FileExists_ProjectWithNonUtcTimestamp_DeserializedProjectWithCo fileSystem.Setup(x => x.File.ReadAllText(MockFilePath)).Returns(serializedProject); var actual = testSubject.Load(MockFilePath); - actual.Should().BeEquivalentTo(boundProject); + actual.Should().BeEquivalentTo(bindingDto); var deserializedTimestamp = actual.Profiles[Language.CSharp].ProfileTimestamp.Value.ToUniversalTime(); deserializedTimestamp.Should().Be(new DateTime(2020, 2, 25, 8, 57, 54)); diff --git a/src/ConnectedMode.UnitTests/Persistence/SolutionBindingRepositoryTests.cs b/src/ConnectedMode.UnitTests/Persistence/SolutionBindingRepositoryTests.cs index e0bce61b5b..0e4fc38918 100644 --- a/src/ConnectedMode.UnitTests/Persistence/SolutionBindingRepositoryTests.cs +++ b/src/ConnectedMode.UnitTests/Persistence/SolutionBindingRepositoryTests.cs @@ -18,246 +18,306 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Linq; 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.Helpers; -using SonarQube.Client.Models; -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Persistence +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Persistence; + +[TestClass] +public class SolutionBindingRepositoryTests { - [TestClass] - public class SolutionBindingRepositoryTests + private IUnintrusiveBindingPathProvider unintrusiveBindingPathProvider; + private IBindingDtoConverter bindingDtoConverter; + private IServerConnectionsRepository serverConnectionsRepository; + private ISolutionBindingCredentialsLoader credentialsLoader; + private ISolutionBindingFileLoader solutionBindingFileLoader; + private TestLogger logger; + + private BindingDto bindingDto; + private ServerConnection serverConnection; + private BoundServerProject boundServerProject; + private ISolutionBindingRepository testSubject; + + private BasicAuthCredentials mockCredentials; + private const string MockFilePath = "test file path"; + + [TestInitialize] + public void TestInitialize() { - private Mock unintrusiveBindingPathProvider; - private Mock credentialsLoader; - private Mock solutionBindingFileLoader; - - private BoundSonarQubeProject boundSonarQubeProject; - private ISolutionBindingRepository testSubject; - - private BasicAuthCredentials mockCredentials; - private const string MockFilePath = "test file path"; - - [TestInitialize] - public void TestInitialize() - { - unintrusiveBindingPathProvider = CreateUnintrusiveBindingPathProvider("C:\\Bindings\\Binding1\\binding.config", "C:\\Bindings\\Binding2\\binding.config"); - - credentialsLoader = new Mock(); - solutionBindingFileLoader = new Mock(); - - testSubject = new SolutionBindingRepository(unintrusiveBindingPathProvider.Object, solutionBindingFileLoader.Object, credentialsLoader.Object); - - mockCredentials = new BasicAuthCredentials("user", "pwd".ToSecureString()); + unintrusiveBindingPathProvider = Substitute.For(); + bindingDtoConverter = Substitute.For(); + serverConnectionsRepository = Substitute.For(); + credentialsLoader = Substitute.For(); + solutionBindingFileLoader = Substitute.For(); + logger = new TestLogger(); - boundSonarQubeProject = new BoundSonarQubeProject( - new Uri("http://xxx.www.zzz/yyy:9000"), - "MyProject Key", - "projectName", - mockCredentials); - } - - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void MefCtor_CheckIsSingleton() - { - MefTestHelpers.CheckIsSingletonMefComponent(); - } - - [TestMethod] - public void Read_ProjectIsNull_Null() - { - solutionBindingFileLoader.Setup(x => x.Load(MockFilePath)).Returns(null as BoundSonarQubeProject); - - var actual = testSubject.Read(MockFilePath); - actual.Should().Be(null); - } - - [TestMethod] - public void Read_ProjectIsNull_CredentialsNotRead() - { - solutionBindingFileLoader.Setup(x => x.Load(MockFilePath)).Returns(null as BoundSonarQubeProject); - - testSubject.Read(MockFilePath); - - credentialsLoader.Verify(x => x.Load(It.IsAny()), Times.Never); - } - - [TestMethod] - public void Read_ProjectIsNotNull_ReturnsProjectWithCredentials() - { - boundSonarQubeProject.ServerUri = new Uri("http://sonarsource.com"); - boundSonarQubeProject.Credentials = null; - - solutionBindingFileLoader.Setup(x => x.Load(MockFilePath)).Returns(boundSonarQubeProject); - credentialsLoader.Setup(x => x.Load(boundSonarQubeProject.ServerUri)).Returns(mockCredentials); - - var actual = testSubject.Read(MockFilePath); - actual.Credentials.Should().Be(mockCredentials); - } + testSubject = new SolutionBindingRepository(unintrusiveBindingPathProvider, bindingDtoConverter, serverConnectionsRepository, solutionBindingFileLoader, credentialsLoader, logger); - [DataTestMethod] - [DataRow(null)] - [DataRow("")] - public void Write_ConfigFilePathIsNull_ReturnsFalse(string filePath) - { - var actual = testSubject.Write(filePath, boundSonarQubeProject); - actual.Should().Be(false); - } + mockCredentials = new BasicAuthCredentials("user", "pwd".ToSecureString()); - [DataTestMethod] - [DataRow(null)] - [DataRow("")] - public void Write_ConfigFilePathIsNull_FileNotWritten(string filePath) + serverConnection = new ServerConnection.SonarCloud("org"); + boundServerProject = new BoundServerProject("solution.123", "project_123", serverConnection); + bindingDto = new BindingDto { - testSubject.Write(filePath, boundSonarQubeProject); - - solutionBindingFileLoader.Verify(x => x.Save(It.IsAny(), It.IsAny()), - Times.Never); - } + ServerConnectionId = serverConnection.Id + }; + } - [DataTestMethod] - [DataRow(null)] - [DataRow("")] - public void Write_ConfigFilePathIsNull_CredentialsNotWritten(string filePath) - { - testSubject.Write(filePath, boundSonarQubeProject); + [TestMethod] + public void MefCtor_CheckIsExported() + { + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + } - credentialsLoader.Verify(x => x.Save(It.IsAny(), It.IsAny()), Times.Never); - } + [TestMethod] + public void MefCtor_CheckIsSingleton() + { + MefTestHelpers.CheckIsSingletonMefComponent(); + } - [TestMethod] - public void Write_ProjectIsNull_Exception() - { - Assert.ThrowsException(() => testSubject.Write(MockFilePath, null)); - } + [TestMethod] + public void Read_ProjectIsNull_Null() + { + solutionBindingFileLoader.Load(MockFilePath).Returns(null as BindingDto); - [TestMethod] - public void Write_ProjectIsNull_FileNotWritten() - { - Assert.ThrowsException(() => testSubject.Write(MockFilePath, null)); + var actual = testSubject.Read(MockFilePath); + actual.Should().Be(null); + } - solutionBindingFileLoader.Verify(x => x.Save(It.IsAny(), It.IsAny()), Times.Never); - } + [TestMethod] + public void Read_ProjectIsNull_CredentialsNotRead() + { + solutionBindingFileLoader.Load(MockFilePath).Returns(null as BindingDto); - [TestMethod] - public void Write_ProjectIsNull_CredentialsNotWritten() - { - Assert.ThrowsException(() => testSubject.Write(MockFilePath, null)); + testSubject.Read(MockFilePath); - credentialsLoader.Verify(x => x.Save(It.IsAny(), It.IsAny()), Times.Never); - } + credentialsLoader.DidNotReceiveWithAnyArgs().Load(default); + } - [TestMethod] - public void Write_FileNotWritten_CredentialsNotWritten() + [TestMethod] + public void Read_ProjectIsNotNull_ReadsConnectionRepositoryForConnection() + { + serverConnectionsRepository.TryGet(bindingDto.ServerConnectionId, out Arg.Any()).Returns(call => { - solutionBindingFileLoader.Setup(x => x.Save(MockFilePath, boundSonarQubeProject)).Returns(false); + call[1] = serverConnection; + return true; + }); + solutionBindingFileLoader.Load(MockFilePath).Returns(bindingDto); + unintrusiveBindingPathProvider.GetBindingKeyFromPath(MockFilePath).Returns(boundServerProject.LocalBindingKey); + bindingDtoConverter.ConvertFromDto(bindingDto, serverConnection, boundServerProject.LocalBindingKey).Returns(boundServerProject); - testSubject.Write(MockFilePath, boundSonarQubeProject); + var actual = testSubject.Read(MockFilePath); - credentialsLoader.Verify(x => x.Save(It.IsAny(), It.IsAny()), Times.Never); - } + actual.Should().BeSameAs(boundServerProject); - [DataTestMethod] - [DataRow(true)] - [DataRow(false)] - public void Write_EventTriggered_DependingOnFileWriteStatus(bool triggered) - { - var eventTriggered = false; - testSubject.BindingUpdated += (_, _) => eventTriggered = true; - solutionBindingFileLoader.Setup(x => x.Save(MockFilePath, boundSonarQubeProject)).Returns(triggered); - - testSubject.Write(MockFilePath, boundSonarQubeProject); - - eventTriggered.Should().Be(triggered); - } - - [TestMethod] - public void Write_FileWritten_CredentialsWritten() - { - solutionBindingFileLoader.Setup(x => x.Save(MockFilePath, boundSonarQubeProject)).Returns(true); - - testSubject.Write(MockFilePath, boundSonarQubeProject); - - credentialsLoader.Verify(x => x.Save(boundSonarQubeProject.Credentials, boundSonarQubeProject.ServerUri), - Times.Once); - } - - [TestMethod] - public void Write_FileWritten_NoOnSaveCallback_NoException() - { - solutionBindingFileLoader.Setup(x => x.Save(MockFilePath, boundSonarQubeProject)).Returns(true); - - Action act = () => testSubject.Write(MockFilePath, boundSonarQubeProject); - act.Should().NotThrow(); - } - - [TestMethod] - public void List_FilesExist_Returns() - { - var binding1 = CreateBoundSonarQubeProject("https://sonarqube.somedomain.com", null, "projectKey1"); - var binding2 = CreateBoundSonarQubeProject("https://sonarcloud.io", "organisation", "projectKey2"); - - solutionBindingFileLoader.Setup(sbf => sbf.Load("C:\\Bindings\\Binding1\\binding.config")).Returns(binding1); - solutionBindingFileLoader.Setup(sbf => sbf.Load("C:\\Bindings\\Binding2\\binding.config")).Returns(binding2); - - var expected = new[] { binding1, binding2 }; - - var testSubject = new SolutionBindingRepository(unintrusiveBindingPathProvider.Object, solutionBindingFileLoader.Object, credentialsLoader.Object); - - var result = testSubject.List(); - - credentialsLoader.VerifyNoOtherCalls(); - - result.Should().HaveCount(2); - result.Should().BeEquivalentTo(expected); - } - - [TestMethod] - public void List_FilesMissing_Skips() + credentialsLoader.DidNotReceiveWithAnyArgs().Load(default); + } + + [TestMethod] + public void Read_ProjectIsNotNull_NoConnection_ReturnsNull() + { + serverConnectionsRepository.TryGet(bindingDto.ServerConnectionId, out Arg.Any()).Returns(call => { - var binding = CreateBoundSonarQubeProject("https://sonarqube.somedomain.com", null, "projectKey1"); + call[1] = null; + return false; + }); + solutionBindingFileLoader.Load(MockFilePath).Returns(bindingDto); - solutionBindingFileLoader.Setup(sbf => sbf.Load("C:\\Bindings\\Binding1\\binding.config")).Returns(binding); - solutionBindingFileLoader.Setup(sbf => sbf.Load("C:\\Bindings\\Binding2\\binding.config")).Returns((BoundSonarQubeProject)null); + var actual = testSubject.Read(MockFilePath); + + credentialsLoader.DidNotReceiveWithAnyArgs().Load(default); + actual.Should().BeNull(); + } - var testSubject = new SolutionBindingRepository(unintrusiveBindingPathProvider.Object, solutionBindingFileLoader.Object, credentialsLoader.Object); + [DataTestMethod] + [DataRow(null)] + [DataRow("")] + public void Write_ConfigFilePathIsNull_ReturnsFalse(string filePath) + { + var actual = testSubject.Write(filePath, boundServerProject); + actual.Should().Be(false); + } + + [DataTestMethod] + [DataRow(null)] + [DataRow("")] + public void Write_ConfigFilePathIsNull_FileNotWritten(string filePath) + { + testSubject.Write(filePath, boundServerProject); + + solutionBindingFileLoader.DidNotReceiveWithAnyArgs().Save(default, default); + } + + [TestMethod] + public void Write_ProjectIsNull_Exception() + { + Assert.ThrowsException(() => testSubject.Write(MockFilePath, null)); + } + + [TestMethod] + public void Write_ProjectIsNull_FileNotWritten() + { + Assert.ThrowsException(() => testSubject.Write(MockFilePath, null)); + + solutionBindingFileLoader.DidNotReceiveWithAnyArgs().Save(default, default); + } + + [DataTestMethod] + [DataRow(true)] + [DataRow(false)] + public void Write_EventTriggered_DependingOnFileWriteStatus(bool triggered) + { + var eventHandler = Substitute.For(); + testSubject.BindingUpdated += eventHandler; + bindingDtoConverter.ConvertToDto(boundServerProject).Returns(bindingDto); + solutionBindingFileLoader.Save(MockFilePath, bindingDto).Returns(triggered); + + testSubject.Write(MockFilePath, boundServerProject); + + eventHandler.ReceivedWithAnyArgs(triggered ? 1 : 0).Invoke(default, default); + } + + + [TestMethod] + public void Write_FileWritten_NoOnSaveCallback_NoException() + { + bindingDtoConverter.ConvertToDto(boundServerProject).Returns(bindingDto); + solutionBindingFileLoader.Save(MockFilePath, bindingDto).Returns(true); + + Action act = () => testSubject.Write(MockFilePath, boundServerProject); + act.Should().NotThrow(); + } + + [TestMethod] + public void List_FilesExist_Returns() + { + var connection1 = new ServerConnection.SonarCloud("org"); + var connection2 = new ServerConnection.SonarQube(new Uri("http://localhost/")); + var bindingConfig1 = "C:\\Bindings\\solution1\\binding.config"; + var solution1 = "solution1"; + var bindingConfig2 = "C:\\Bindings\\solution2\\binding.config"; + var solution2 = "solution2"; + SetUpUnintrusiveBindingPathProvider(bindingConfig1, bindingConfig2); + SetUpConnections(connection1, connection2); + var boundServerProject1 = SetUpBinding(solution1, connection1, bindingConfig1); + var boundServerProject2 = SetUpBinding(solution2, connection2, bindingConfig2); + + var result = testSubject.List(); + + result.Should().BeEquivalentTo(boundServerProject1, boundServerProject2); + } - var result = testSubject.List(); + [TestMethod] + public void List_SkipsBindingsWithoutConnections() + { + var connection1 = new ServerConnection.SonarCloud("org"); + var connection2 = new ServerConnection.SonarQube(new Uri("http://localhost/")); + var bindingConfig1 = "C:\\Bindings\\solution1\\binding.config"; + var solution1 = "solution1"; + var bindingConfig2 = "C:\\Bindings\\solution2\\binding.config"; + var solution2 = "solution2"; + SetUpUnintrusiveBindingPathProvider(bindingConfig1, bindingConfig2); + SetUpConnections(connection2); // only one connection + _ = SetUpBinding(solution1, null, bindingConfig1); + var boundServerProject2 = SetUpBinding(solution2, connection2, bindingConfig2); + + var result = testSubject.List(); + + result.Should().BeEquivalentTo(boundServerProject2); + } + + [TestMethod] + public void List_SkipsBindingsThatCannotBeRead() + { + var connection1 = new ServerConnection.SonarCloud("org"); + var connection2 = new ServerConnection.SonarQube(new Uri("http://localhost/")); + var bindingConfig1 = "C:\\Bindings\\solution1\\binding.config"; + var solution1 = "solution1"; + var bindingConfig2 = "C:\\Bindings\\solution2\\binding.config"; + SetUpUnintrusiveBindingPathProvider(bindingConfig1, bindingConfig2); + SetUpConnections(connection1, connection2); + var boundServerProject1 = SetUpBinding(solution1, connection1, bindingConfig1); + solutionBindingFileLoader.Load(bindingConfig2).Returns((BindingDto)null); + + var result = testSubject.List(); + + result.Should().BeEquivalentTo(boundServerProject1); + } + + [TestMethod] + public void List_CannotGetConnections_EmptyList() + { + serverConnectionsRepository.TryGetAll(out Arg.Any>()).Returns(false); + + var act = () => testSubject.List().ToList(); - result.Should().HaveCount(1); - result.ElementAt(0).Should().BeEquivalentTo(binding); - } + act.Should().Throw(); + } - private static Mock CreateUnintrusiveBindingPathProvider(params string[] bindigFolders) - { - var unintrusiveBindingPathProvider = new Mock(); - unintrusiveBindingPathProvider.Setup(u => u.GetBindingPaths()).Returns(bindigFolders); - return unintrusiveBindingPathProvider; - } + [TestMethod] + public void LegacyRead_NoFile_ReturnsNull() + { + solutionBindingFileLoader.Load(MockFilePath).Returns((BindingDto)null); + + ((ILegacySolutionBindingRepository)testSubject).Read(MockFilePath).Should().BeNull(); + credentialsLoader.DidNotReceiveWithAnyArgs().Load(default); + } + + [TestMethod] + public void LegacyRead_ValidBinding_LoadsCredentials() + { + var boundSonarQubeProject = new BoundSonarQubeProject(); + bindingDto.ServerUri = new Uri("http://localhost/"); + credentialsLoader.Load(bindingDto.ServerUri).Returns(mockCredentials); + solutionBindingFileLoader.Load(MockFilePath).Returns(bindingDto); + bindingDtoConverter.ConvertFromDtoToLegacy(bindingDto, mockCredentials).Returns(boundSonarQubeProject); + + ((ILegacySolutionBindingRepository)testSubject).Read(MockFilePath).Should().BeSameAs(boundSonarQubeProject); + credentialsLoader.Received().Load(bindingDto.ServerUri); + } - private static BoundSonarQubeProject CreateBoundSonarQubeProject(string uri, string organizationKey, string projectKey) + private BoundServerProject SetUpBinding(string solution, ServerConnection connection, string bindingConfig) + { + var dto = new BindingDto{ServerConnectionId = connection?.Id}; + solutionBindingFileLoader.Load(bindingConfig).Returns(dto); + if (connection == null) { - var organization = CreateOrganization(organizationKey); - - var serverUri = new Uri(uri); - - return new BoundSonarQubeProject(serverUri, projectKey, null, organization: organization); + return null; } + var bound = new BoundServerProject(solution, "any", connection); + unintrusiveBindingPathProvider.GetBindingKeyFromPath(bindingConfig).Returns(solution); + bindingDtoConverter.ConvertFromDto(dto, connection, solution).Returns(bound); + return bound; + } + + private void SetUpConnections(params ServerConnection[] connections) + { + serverConnectionsRepository + .TryGetAll(out Arg.Any>()) + .Returns(call => + { + call[0] = connections; + return true; + }); + } - private static SonarQubeOrganization CreateOrganization(string organizationKey) => organizationKey == null ? null : new SonarQubeOrganization(organizationKey, null); + private void SetUpUnintrusiveBindingPathProvider(params string[] bindigFolders) + { + unintrusiveBindingPathProvider.GetBindingPaths().Returns(bindigFolders); } } diff --git a/src/ConnectedMode.UnitTests/Persistence/ConfigurationPersisterTests.cs b/src/ConnectedMode.UnitTests/Persistence/UnintrusiveConfigurationPersisterTests.cs similarity index 63% rename from src/ConnectedMode.UnitTests/Persistence/ConfigurationPersisterTests.cs rename to src/ConnectedMode.UnitTests/Persistence/UnintrusiveConfigurationPersisterTests.cs index a28ef50fa6..00cad08389 100644 --- a/src/ConnectedMode.UnitTests/Persistence/ConfigurationPersisterTests.cs +++ b/src/ConnectedMode.UnitTests/Persistence/UnintrusiveConfigurationPersisterTests.cs @@ -18,36 +18,34 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; using SonarLint.VisualStudio.ConnectedMode.Binding; -using SonarLint.VisualStudio.ConnectedMode.Persistence; using SonarLint.VisualStudio.Core.Binding; using SonarLint.VisualStudio.TestInfrastructure; namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Persistence { [TestClass] - public class ConfigurationPersisterTests + public class UnintrusiveConfigurationPersisterTests { - private Mock configFilePathProvider; - private Mock solutionBindingRepository; - private ConfigurationPersister testSubject; + private IUnintrusiveBindingPathProvider configFilePathProvider; + private ISolutionBindingRepository solutionBindingRepository; + private UnintrusiveConfigurationPersister testSubject; [TestInitialize] public void TestInitialize() { - configFilePathProvider = new Mock(); - solutionBindingRepository = new Mock(); + configFilePathProvider = Substitute.For(); + solutionBindingRepository = Substitute.For(); - testSubject = new ConfigurationPersister( - configFilePathProvider.Object, - solutionBindingRepository.Object); + testSubject = new UnintrusiveConfigurationPersister( + configFilePathProvider, + solutionBindingRepository); } [TestMethod] public void MefCtor_CheckIsExported() { - MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CheckTypeCanBeImported( MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport()); } @@ -65,11 +63,11 @@ public void Persist_NullProject_Throws() [TestMethod] public void Persist_SaveNewConfig() { - var projectToWrite = new BoundSonarQubeProject(); - configFilePathProvider.Setup(x => x.GetCurrentBindingPath()).Returns("c:\\new.txt"); + var localBindingKey = "solution123"; + var projectToWrite = new BoundServerProject(localBindingKey, "project", new ServerConnection.SonarCloud("org")); + configFilePathProvider.GetBindingPath(localBindingKey).Returns("c:\\new.txt"); - solutionBindingRepository - .Setup(x => x.Write("c:\\new.txt", projectToWrite)) + solutionBindingRepository.Write("c:\\new.txt", projectToWrite) .Returns(true); // Act @@ -78,9 +76,7 @@ public void Persist_SaveNewConfig() // Assert actual.Should().NotBe(null); - solutionBindingRepository.Verify(x => - x.Write("c:\\new.txt", projectToWrite), - Times.Once); + solutionBindingRepository.Received().Write("c:\\new.txt", projectToWrite); } } } diff --git a/src/ConnectedMode.UnitTests/ProjectRootCalculatorTests.cs b/src/ConnectedMode.UnitTests/ProjectRootCalculatorTests.cs index 84426ee9a1..3a726c7648 100644 --- a/src/ConnectedMode.UnitTests/ProjectRootCalculatorTests.cs +++ b/src/ConnectedMode.UnitTests/ProjectRootCalculatorTests.cs @@ -66,7 +66,7 @@ public async Task CalculateBasedOnLocalPathAsync_ConnectedMode_ReturnsCorrectRoo configurationProviderMock .SetupGet(x => x.CurrentConfiguration) .Returns(BindingConfiguration.CreateBoundConfiguration( - new BoundSonarQubeProject(){ProjectKey = projectKey}, + new BoundServerProject("solution", projectKey, new ServerConnection.SonarQube(new Uri("http://localhost"))), SonarLintMode.Connected, "somedir")); branchProviderMock diff --git a/src/ConnectedMode.UnitTests/QualityProfiles/OutOfDateQualityProfileFinderTests.cs b/src/ConnectedMode.UnitTests/QualityProfiles/OutOfDateQualityProfileFinderTests.cs index 15ca1b3f30..38414541ec 100644 --- a/src/ConnectedMode.UnitTests/QualityProfiles/OutOfDateQualityProfileFinderTests.cs +++ b/src/ConnectedMode.UnitTests/QualityProfiles/OutOfDateQualityProfileFinderTests.cs @@ -202,14 +202,12 @@ public async Task GetAsync_MultipleQualityProfiles_ReturnsQP() sonarQubeServiceMock.Verify(x => x.GetAllQualityProfilesAsync(Project, Organization, CancellationToken.None), Times.Once); } - private static BoundSonarQubeProject CreateArgument(string project, + private static BoundServerProject CreateArgument(string project, string organization, Dictionary profiles) => - new(AnyUri, + new("solution", project, - null, - null, - organization == null ? null : new(organization, null)) + organization == null ? new ServerConnection.SonarQube(AnyUri) : new ServerConnection.SonarCloud(organization)) { Profiles = profiles }; diff --git a/src/ConnectedMode.UnitTests/QualityProfiles/QualityProfileDownloaderTests.cs b/src/ConnectedMode.UnitTests/QualityProfiles/QualityProfileDownloaderTests.cs index 94d48c767f..ad9bce56bc 100644 --- a/src/ConnectedMode.UnitTests/QualityProfiles/QualityProfileDownloaderTests.cs +++ b/src/ConnectedMode.UnitTests/QualityProfiles/QualityProfileDownloaderTests.cs @@ -257,7 +257,7 @@ public async Task UpdateAsync_SavesConfiguration() configPersister.SavedProject.Should().NotBeNull(); var savedProject = configPersister.SavedProject; - savedProject.ServerUri.Should().Be(boundProject.ServerUri); + savedProject.ServerConnection.Id.Should().Be(boundProject.ServerConnection.Id); savedProject.Profiles.Should().HaveCount(1); savedProject.Profiles[Language.VBNET].ProfileKey.Should().Be(myProfileKey); savedProject.Profiles[Language.VBNET].ProfileTimestamp.Should().Be(serverQpTimestamp); @@ -287,7 +287,7 @@ private static SonarQubeQualityProfile CreateQualityProfile(string key = "key", private static void SetupLanguagesToUpdate( out Mock outOfDateQualityProfileFinderMock, - BoundSonarQubeProject boundProject, + BoundServerProject boundProject, params Language[] languages) { SetupLanguagesToUpdate(out outOfDateQualityProfileFinderMock, @@ -297,7 +297,7 @@ private static void SetupLanguagesToUpdate( private static void SetupLanguagesToUpdate( out Mock outOfDateQualityProfileFinderMock, - BoundSonarQubeProject boundProject, + BoundServerProject boundProject, params (Language language, SonarQubeQualityProfile qualityProfile)[] qps) { outOfDateQualityProfileFinderMock = new Mock(); @@ -321,14 +321,12 @@ private static Mock SetupConfigProvider(Mock new BoundSonarQubeProject( - uri ?? new Uri("http://any"), + => new BoundServerProject( + "solution", projectKey, - projectName, - null, - null); + new ServerConnection.SonarQube(uri ?? new Uri("http://localhost/"))); private static void CheckRuleConfigSaved(Mock bindingConfig) => bindingConfig.Verify(x => x.Save(), Times.Once); @@ -338,12 +336,12 @@ private static void CheckRuleConfigNotSaved(Mock bindingConfig) private class DummyConfigPersister : IConfigurationPersister { - public BoundSonarQubeProject SavedProject { get; private set; } + public BoundServerProject SavedProject { get; private set; } - BindingConfiguration IConfigurationPersister.Persist(BoundSonarQubeProject project) + BindingConfiguration IConfigurationPersister.Persist(BoundServerProject project) { SavedProject = project; - return new BindingConfiguration(new BoundSonarQubeProject(), SonarLintMode.Connected, "c:\\any"); + return new BindingConfiguration(project, SonarLintMode.Connected, "c:\\any"); } } diff --git a/src/ConnectedMode.UnitTests/QualityProfiles/QualityProfileUpdaterTests.cs b/src/ConnectedMode.UnitTests/QualityProfiles/QualityProfileUpdaterTests.cs index 1c7de63ad2..4f3399a0d6 100644 --- a/src/ConnectedMode.UnitTests/QualityProfiles/QualityProfileUpdaterTests.cs +++ b/src/ConnectedMode.UnitTests/QualityProfiles/QualityProfileUpdaterTests.cs @@ -50,7 +50,7 @@ public void MefCtor_CheckIsSingleton() [DataRow(SonarLintMode.LegacyConnected)] public async Task UpdateBoundSolutionAsync_NotNewConnectedMode_DoesNotUpdateQP(SonarLintMode mode) { - var configProvider = CreateConfigProvider(mode); + var configProvider = CreateConfigProvider(mode, CreateDefaultProject()); var qpDownloader = new Mock(); var runner = CreatePassthroughRunner(); @@ -69,7 +69,7 @@ public async Task UpdateBoundSolutionAsync_NotNewConnectedMode_DoesNotUpdateQP(S public async Task UpdateBoundSolutionAsync_IsNewConnectedMode_UpdateIsDoneThroughRunner() { var cancellationToken = new CancellationToken(); - var boundProject = new BoundSonarQubeProject(); + var boundProject = CreateDefaultProject(); var configProvider = CreateConfigProvider(SonarLintMode.Connected, boundProject); var qpDownloader = new Mock(); SetUpDownloader(qpDownloader); @@ -90,7 +90,7 @@ public async Task UpdateBoundSolutionAsync_IsNewConnectedMode_UpdateIsDoneThroug [TestMethod] public async Task UpdateBoundSolutionAsync_IsNewConnectedMode_NoUpdates_EventIsNotRaised() { - var boundProject = new BoundSonarQubeProject(); + var boundProject = CreateDefaultProject(); var configProvider = CreateConfigProvider(SonarLintMode.Connected, boundProject); var qpDownloader = new Mock(); SetUpDownloader(qpDownloader, false); @@ -111,8 +111,7 @@ public async Task UpdateBoundSolutionAsync_IsNewConnectedMode_NoUpdates_EventIsN [TestMethod] public async Task UpdateBoundSolutionAsync_IsNewConnectedMode_UpdaterDoesNotCallDownloaderDirectly() { - var boundProject = new BoundSonarQubeProject(); - var configProvider = CreateConfigProvider(SonarLintMode.Connected, boundProject); + var configProvider = CreateConfigProvider(SonarLintMode.Connected, CreateDefaultProject()); var qpDownloader = new Mock(); // Here, we're not using a pass-through runner, so we're not expecting the // downloader to be invoked @@ -151,8 +150,7 @@ public async Task UpdateBoundSolutionAsync_JobIsCancelled_EventIsNotRaised() runner.Setup(x => x.RunAsync(It.IsAny>())) .Callback>(func => cts.Token.ThrowIfCancellationRequested()); - var boundProject = new BoundSonarQubeProject(); - var configProvider = CreateConfigProvider(SonarLintMode.Connected, boundProject); + var configProvider = CreateConfigProvider(SonarLintMode.Connected, CreateDefaultProject()); var qpDownloader = new Mock(); var testSubject = CreateTestSubject(configProvider.Object, qpDownloader.Object, runner.Object); @@ -174,8 +172,7 @@ public async Task UpdateBoundSolutionAsync_InvalidOperationException_EventIsNotR x.RunAsync(It.IsAny>())) .Throws(new InvalidOperationException()); - var boundProject = new BoundSonarQubeProject(); - var configProvider = CreateConfigProvider(SonarLintMode.Connected, boundProject); + var configProvider = CreateConfigProvider(SonarLintMode.Connected, CreateDefaultProject()); var testSubject = CreateTestSubject(configProvider.Object, runner: runner.Object); var eventListener = new QPUpdatedEventListener(testSubject); @@ -191,15 +188,20 @@ private static void SetUpDownloader(Mock qpDownloader { qpDownloader .Setup(x => - x.UpdateAsync(It.IsAny(), + x.UpdateAsync(It.IsAny(), It.IsAny>(), It.IsAny())) .ReturnsAsync(result); } - private Mock CreateConfigProvider(SonarLintMode mode, BoundSonarQubeProject boundProject = null) + private BoundServerProject CreateDefaultProject() + { + return new BoundServerProject("solution", "project", new ServerConnection.SonarCloud("org")); + } + + private Mock CreateConfigProvider(SonarLintMode mode, BoundServerProject boundProject) { - var config = new BindingConfiguration(boundProject ?? new BoundSonarQubeProject(), mode, "any directory"); + var config = new BindingConfiguration(boundProject, mode, "any directory"); var configProvider = new Mock(); configProvider.Setup(x => x.GetConfiguration()).Returns(config); diff --git a/src/ConnectedMode.UnitTests/ServerBranchProviderTests.cs b/src/ConnectedMode.UnitTests/ServerBranchProviderTests.cs index 892eda841f..a8c38bf938 100644 --- a/src/ConnectedMode.UnitTests/ServerBranchProviderTests.cs +++ b/src/ConnectedMode.UnitTests/ServerBranchProviderTests.cs @@ -184,7 +184,7 @@ private static ServerBranchProvider CreateTestSubject( } private static BindingConfiguration CreateBindingConfig(SonarLintMode mode = SonarLintMode.Connected, string projectKey = "any") - => new(new BoundSonarQubeProject { ProjectKey = projectKey }, mode, "any dir"); + => new(new BoundServerProject("solution", projectKey, new ServerConnection.SonarCloud("org")), mode, "any dir"); private static Mock CreateConfigProvider(BindingConfiguration config = null) { diff --git a/src/ConnectedMode.UnitTests/ServerConnectionsRepositoryAdapterTests.cs b/src/ConnectedMode.UnitTests/ServerConnectionsRepositoryAdapterTests.cs new file mode 100644 index 0000000000..e5c40eb39f --- /dev/null +++ b/src/ConnectedMode.UnitTests/ServerConnectionsRepositoryAdapterTests.cs @@ -0,0 +1,311 @@ +/* + * 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.ConnectedMode.Persistence; +using SonarLint.VisualStudio.ConnectedMode.UI.Credentials; +using SonarLint.VisualStudio.Core.Binding; +using SonarLint.VisualStudio.TestInfrastructure; +using SonarQube.Client.Helpers; +using static SonarLint.VisualStudio.Core.Binding.ServerConnection; + +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests; + +[TestClass] +public class ServerConnectionsRepositoryAdapterTests +{ + private IServerConnectionsRepository serverConnectionsRepository; + private ServerConnectionsRepositoryAdapter testSubject; + + [TestInitialize] + public void TestInitialize() + { + serverConnectionsRepository = Substitute.For(); + testSubject = new ServerConnectionsRepositoryAdapter(serverConnectionsRepository); + } + + [TestMethod] + public void MefCtor_CheckIsExported() + => MefTestHelpers.CheckTypeCanBeImported(MefTestHelpers.CreateExport()); + + [TestMethod] + public void TryGetServerConnectionById_CallServerConnectionsRepository() + { + var expectedServerConnection = new SonarCloud("myOrg"); + serverConnectionsRepository.TryGet("https://sonarcloud.io/organizations/myOrg", out _).Returns(callInfo => + { + callInfo[1] = expectedServerConnection; + return true; + }); + + testSubject.TryGet(new ConnectionInfo("myOrg", ConnectionServerType.SonarCloud), out var serverConnection); + + serverConnection.Should().Be(expectedServerConnection); + } + + [TestMethod] + public void TryGetAllConnections_CallServerConnectionsRepository() + { + MockServerConnections([]); + + testSubject.TryGetAllConnections(out var connections); + + serverConnectionsRepository.Received(1).TryGetAll(out Arg.Any>()); + connections.Should().BeEmpty(); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void TryGetAllConnections_HasOneSonarCloudConnection_ReturnsOneMappedConnection(bool isSmartNotificationsEnabled) + { + var sonarCloud = CreateSonarCloudServerConnection(isSmartNotificationsEnabled); + MockServerConnections([sonarCloud]); + + testSubject.TryGetAllConnections(out var connections); + + connections.Should().BeEquivalentTo([new Connection(new ConnectionInfo(sonarCloud.OrganizationKey, ConnectionServerType.SonarCloud), isSmartNotificationsEnabled)]); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void TryGetAllConnections_HasOneSonarQubeConnection_ReturnsOneMappedConnection(bool isSmartNotificationsEnabled) + { + var sonarQube = CreateSonarQubeServerConnection(isSmartNotificationsEnabled); + MockServerConnections([sonarQube]); + + testSubject.TryGetAllConnections(out var connections); + + connections.Should().BeEquivalentTo([new Connection(new ConnectionInfo(sonarQube.Id, ConnectionServerType.SonarQube), isSmartNotificationsEnabled)]); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void TryGetAllConnections_ReturnsStatusFromSlCore(bool expectedStatus) + { + var sonarCloud = CreateSonarCloudServerConnection(); + MockServerConnections([sonarCloud], succeeded:expectedStatus); + + var succeeded = testSubject.TryGetAllConnections(out _); + + succeeded.Should().Be(expectedStatus); + } + + [TestMethod] + public void TryGetAllConnectionsInfo_CallServerConnectionsRepository() + { + MockServerConnections([]); + + testSubject.TryGetAllConnectionsInfo(out var connections); + + serverConnectionsRepository.Received(1).TryGetAll(out Arg.Any>()); + connections.Should().BeEmpty(); + } + + [TestMethod] + public void TryGetAllConnectionsInfo_HasOneSonarCloudConnection_ReturnsOneMappedConnection() + { + var sonarCloud = CreateSonarCloudServerConnection(); + MockServerConnections([sonarCloud]); + + testSubject.TryGetAllConnectionsInfo(out var connections); + + connections.Should().BeEquivalentTo([new ConnectionInfo(sonarCloud.OrganizationKey, ConnectionServerType.SonarCloud)]); + } + + [TestMethod] + public void TryGetAllConnectionsInfo_HasOneSonarQubeConnection_ReturnsOneMappedConnection() + { + var sonarQube = CreateSonarQubeServerConnection(); + MockServerConnections([sonarQube]); + + testSubject.TryGetAllConnectionsInfo(out var connections); + + connections.Should().BeEquivalentTo([new ConnectionInfo(sonarQube.Id, ConnectionServerType.SonarQube)]); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void TryGetAllConnectionsInfo_ReturnsStatusFromSlCore(bool expectedStatus) + { + var sonarQube = CreateSonarQubeServerConnection(); + MockServerConnections([sonarQube], succeeded: expectedStatus); + + var succeeded = testSubject.TryGetAllConnectionsInfo(out _); + + succeeded.Should().Be(expectedStatus); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void TryAddConnection_ReturnsStatusFromSlCore(bool expectedStatus) + { + var sonarCloud = CreateSonarCloudConnection(); + serverConnectionsRepository.TryAdd(Arg.Any()).Returns(expectedStatus); + + var succeeded = testSubject.TryAddConnection(sonarCloud, Substitute.For()); + + succeeded.Should().Be(expectedStatus); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void TryAddConnection_AddsSonarCloudConnection_CallsSlCoreWithMappedConnection(bool enableSmartNotifications) + { + var sonarCloud = CreateSonarCloudConnection(enableSmartNotifications); + + testSubject.TryAddConnection(sonarCloud, Substitute.For()); + + serverConnectionsRepository.Received(1) + .TryAdd(Arg.Is(sc => + sc.Id == $"https://sonarcloud.io/organizations/{sonarCloud.Info.Id}" && + sc.OrganizationKey == sonarCloud.Info.Id && + sc.Settings.IsSmartNotificationsEnabled == sonarCloud.EnableSmartNotifications)); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void TryAddConnection_AddsSonarQubeConnection_CallsSlCoreWithMappedConnection(bool enableSmartNotifications) + { + var sonarQube = CreateSonarQubeConnection(enableSmartNotifications); + + testSubject.TryAddConnection(sonarQube, Substitute.For()); + + serverConnectionsRepository.Received(1) + .TryAdd(Arg.Is(sc => + sc.Id == new Uri(sonarQube.Info.Id).ToString() && + sc.ServerUri == new Uri(sonarQube.Info.Id) && + sc.Settings.IsSmartNotificationsEnabled == sonarQube.EnableSmartNotifications)); + } + + [TestMethod] + public void TryAddConnection_TokenCredentialsModel_MapsCredentials() + { + var sonarQube = CreateSonarQubeConnection(); + var token = "myToken"; + + testSubject.TryAddConnection(sonarQube, new TokenCredentialsModel(token.CreateSecureString())); + + serverConnectionsRepository.Received(1) + .TryAdd(Arg.Is(sc => IsExpectedCredentials(sc, token, string.Empty))); + } + + [TestMethod] + public void TryAddConnection_UsernamePasswordModel_MapsCredentials() + { + var sonarQube = CreateSonarQubeConnection(); + var username = "username"; + var password = "password"; + + testSubject.TryAddConnection(sonarQube, new UsernamePasswordModel(username, password.CreateSecureString())); + + serverConnectionsRepository.Received(1) + .TryAdd(Arg.Is(sc => IsExpectedCredentials(sc, username, password))); + } + + [TestMethod] + public void TryAddConnection_NullCredentials_TriesAddingAConnectionWithNoCredentials() + { + var sonarQube = CreateSonarQubeConnection(); + + testSubject.TryAddConnection(sonarQube, null); + + serverConnectionsRepository.Received(1).TryAdd(Arg.Is(sc => sc.Credentials == null)); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void TryDeleteConnection_ReturnsStatusFromSlCore(bool expectedStatus) + { + const string connectionInfoId = "http://localhost:9000/"; + var connectionInfo = new ConnectionInfo(connectionInfoId, ConnectionServerType.SonarQube); + serverConnectionsRepository.TryDelete(connectionInfoId).Returns(expectedStatus); + + var succeeded = testSubject.TryRemoveConnection(connectionInfo); + + succeeded.Should().Be(expectedStatus); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void TryGet_ReturnsStatusFromSlCore(bool expectedStatus) + { + const string connectionInfoId = "myOrg"; + var connectionInfo = new ConnectionInfo(connectionInfoId, ConnectionServerType.SonarCloud); + var expectedServerConnection = new SonarCloud("myOrg"); + MockTryGet("https://sonarcloud.io/organizations/myOrg", expectedStatus, expectedServerConnection); + + var succeeded = testSubject.TryGet(connectionInfo, out var receivedServerConnection); + + succeeded.Should().Be(expectedStatus); + receivedServerConnection.Should().Be(expectedServerConnection); + } + + private static SonarCloud CreateSonarCloudServerConnection(bool isSmartNotificationsEnabled = true) + { + return new SonarCloud("myOrg", new ServerConnectionSettings(isSmartNotificationsEnabled), Substitute.For()); + } + + private static ServerConnection.SonarQube CreateSonarQubeServerConnection(bool isSmartNotificationsEnabled = true) + { + var sonarQube = new ServerConnection.SonarQube(new Uri("http://localhost"), new ServerConnectionSettings(isSmartNotificationsEnabled), Substitute.For()); + return sonarQube; + } + + private void MockServerConnections(List connections, bool succeeded = true) + { + serverConnectionsRepository.TryGetAll(out Arg.Any>()).Returns(callInfo => + { + callInfo[0] = connections; + return succeeded; + }); + } + + private static Connection CreateSonarCloudConnection(bool enableSmartNotifications = true) + { + return new Connection(new ConnectionInfo("mySecondOrg", ConnectionServerType.SonarCloud), enableSmartNotifications); + } + + private static Connection CreateSonarQubeConnection(bool enableSmartNotifications = true) + { + return new Connection(new ConnectionInfo("http://localhost:9000", ConnectionServerType.SonarQube), enableSmartNotifications); + } + + private static bool IsExpectedCredentials(ServerConnection.SonarQube sc, string expectedUsername, string expectedPassword) + { + return sc.Credentials is BasicAuthCredentials basicAuthCredentials && basicAuthCredentials.UserName == expectedUsername && basicAuthCredentials.Password?.ToUnsecureString() == expectedPassword; + } + + private void MockTryGet(string connectionId, bool expectedResponse, ServerConnection expectedServerConnection) + { + serverConnectionsRepository.TryGet(connectionId, out _).Returns(callInfo => + { + callInfo[1] = expectedServerConnection; + return expectedResponse; + }); + } +} diff --git a/src/ConnectedMode.UnitTests/ServerIssueFinderTests.cs b/src/ConnectedMode.UnitTests/ServerIssueFinderTests.cs index 13b430d5a5..5d97ecc0ba 100644 --- a/src/ConnectedMode.UnitTests/ServerIssueFinderTests.cs +++ b/src/ConnectedMode.UnitTests/ServerIssueFinderTests.cs @@ -158,7 +158,7 @@ private void SetUpBinding(Mock activeSolutionBoundT activeSolutionBoundTrackerMock.SetupGet(x => x.CurrentConfiguration) .Returns(projectKey == null ? BindingConfiguration.Standalone - : new BindingConfiguration(new BoundSonarQubeProject { ProjectKey = projectKey }, SonarLintMode.Connected, default)); + : new BindingConfiguration(new BoundServerProject("solution", projectKey, new ServerConnection.SonarQube(new Uri("http://localhost"))), SonarLintMode.Connected, default)); } private ServerIssueFinder CreateTestSubject(out Mock projectRootCalculatorMock, diff --git a/src/ConnectedMode.UnitTests/ServerQueryInfoProviderTests.cs b/src/ConnectedMode.UnitTests/ServerQueryInfoProviderTests.cs index 2d038e0497..07cbdfd118 100644 --- a/src/ConnectedMode.UnitTests/ServerQueryInfoProviderTests.cs +++ b/src/ConnectedMode.UnitTests/ServerQueryInfoProviderTests.cs @@ -110,7 +110,7 @@ private static ServerQueryInfoProvider CreateTestSubject(IConfigurationProvider } private static BindingConfiguration CreateBindingConfig(SonarLintMode mode = SonarLintMode.Connected, string projectKey = "any") - => new(new BoundSonarQubeProject { ProjectKey = projectKey }, mode, "any dir"); + => new(new BoundServerProject("solution", projectKey, new ServerConnection.SonarCloud("org")), mode, "any dir"); private static Mock CreateConfigProvider(BindingConfiguration config = null) { diff --git a/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionManagerTests.cs b/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionManagerTests.cs index b69f071cc5..f37065b1b3 100644 --- a/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionManagerTests.cs +++ b/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionManagerTests.cs @@ -260,7 +260,7 @@ public static BindingConfiguration CreateConnectedModeBindingConfiguration(strin { var randomString = Guid.NewGuid().ToString(); var bindingConfiguration = new BindingConfiguration( - new BoundSonarQubeProject(new Uri("http://localhost"), projectKey, randomString), + new BoundServerProject(randomString, projectKey, new ServerConnection.SonarQube(new Uri("http://localhost"))), SonarLintMode.Connected, randomString); return bindingConfiguration; diff --git a/src/ConnectedMode.UnitTests/SlCoreConnectionAdapterTests.cs b/src/ConnectedMode.UnitTests/SlCoreConnectionAdapterTests.cs new file mode 100644 index 0000000000..f6eef10f5e --- /dev/null +++ b/src/ConnectedMode.UnitTests/SlCoreConnectionAdapterTests.cs @@ -0,0 +1,485 @@ +/* + * 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 System.Security; +using SonarLint.VisualStudio.ConnectedMode.Persistence; +using SonarLint.VisualStudio.ConnectedMode.UI.Credentials; +using SonarLint.VisualStudio.ConnectedMode.UI.OrganizationSelection; +using SonarLint.VisualStudio.ConnectedMode.UI.ProjectSelection; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Binding; +using SonarLint.VisualStudio.SLCore; +using SonarLint.VisualStudio.SLCore.Common.Models; +using SonarLint.VisualStudio.SLCore.Core; +using SonarLint.VisualStudio.SLCore.Protocol; +using SonarLint.VisualStudio.SLCore.Service.Connection; +using SonarLint.VisualStudio.SLCore.Service.Connection.Models; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests; + +[TestClass] +public class SlCoreConnectionAdapterTests +{ + private static readonly BasicAuthCredentials ValidToken = new ("I_AM_JUST_A_TOKEN", new SecureString()); + private readonly ServerConnection.SonarQube sonarQubeConnection = new(new Uri("http://localhost:9000/"), new ServerConnectionSettings(true), ValidToken); + private readonly ServerConnection.SonarCloud sonarCloudConnection = new("myOrg", new ServerConnectionSettings(true), ValidToken); + + private SlCoreConnectionAdapter testSubject; + private ISLCoreServiceProvider slCoreServiceProvider; + private IThreadHandling threadHandling; + private ILogger logger; + private IConnectionConfigurationSLCoreService connectionConfigurationSlCoreService; + private ConnectionInfo sonarCloudConnectionInfo; + private ConnectionInfo sonarQubeConnectionInfo; + + [TestInitialize] + public void TestInitialize() + { + slCoreServiceProvider = Substitute.For(); + threadHandling = new NoOpThreadHandler(); + logger = Substitute.For(); + connectionConfigurationSlCoreService = Substitute.For(); + testSubject = new SlCoreConnectionAdapter(slCoreServiceProvider, threadHandling, logger); + + SetupConnection(); + } + + [TestMethod] + public async Task ValidateConnectionAsync_SwitchesToBackgroundThread() + { + var threadHandlingMock = Substitute.For(); + var slCoreConnectionAdapter = new SlCoreConnectionAdapter(slCoreServiceProvider, threadHandlingMock, logger); + + await slCoreConnectionAdapter.ValidateConnectionAsync(sonarQubeConnectionInfo, new TokenCredentialsModel("myToken".CreateSecureString())); + + await threadHandlingMock.Received(1).RunOnBackgroundThread(Arg.Any>>()); + } + + [TestMethod] + public async Task ValidateConnectionAsync_GettingConnectionConfigurationSLCoreServiceFails_ReturnsUnsuccessfulResponseAndLogs() + { + slCoreServiceProvider.TryGetTransientService(out IConnectionConfigurationSLCoreService _).Returns(false); + + var response = await testSubject.ValidateConnectionAsync(sonarQubeConnectionInfo, new TokenCredentialsModel("myToken".CreateSecureString())); + + logger.Received(1).LogVerbose($"[{nameof(IConnectionConfigurationSLCoreService)}] {SLCoreStrings.ServiceProviderNotInitialized}"); + response.Success.Should().BeFalse(); + } + + [TestMethod] + public async Task ValidateConnectionAsync_ConnectionToSonarQubeWithToken_CallsValidateConnectionWithCorrectParams() + { + var token = "myToken"; + + await testSubject.ValidateConnectionAsync(sonarQubeConnectionInfo, new TokenCredentialsModel(token.CreateSecureString())); + + await connectionConfigurationSlCoreService.Received(1) + .ValidateConnectionAsync(Arg.Is(x => IsExpectedSonarQubeConnectionParams(x, token))); + } + + [TestMethod] + public async Task ValidateConnectionAsync_ConnectionToSonarQubeWithCredentials_CallsValidateConnectionWithCorrectParams() + { + var username = "username"; + var password = "password"; + + await testSubject.ValidateConnectionAsync(sonarQubeConnectionInfo, new UsernamePasswordModel(username, password.CreateSecureString())); + + await connectionConfigurationSlCoreService.Received(1) + .ValidateConnectionAsync(Arg.Is(x => IsExpectedSonarQubeConnectionParams(x, username, password))); + } + + [TestMethod] + public async Task ValidateConnectionAsync_ConnectionToSonarCloudWithToken_CallsValidateConnectionWithCorrectParams() + { + var token = "myToken"; + + await testSubject.ValidateConnectionAsync(sonarCloudConnectionInfo, new TokenCredentialsModel(token.CreateSecureString())); + + await connectionConfigurationSlCoreService.Received(1) + .ValidateConnectionAsync(Arg.Is(x => IsExpectedSonarCloudConnectionParams(x, token))); + } + + [TestMethod] + public async Task ValidateConnectionAsync_ConnectionToSonarCloudWithCredentials_CallsValidateConnectionWithCorrectParams() + { + var username = "username"; + var password = "password"; + + await testSubject.ValidateConnectionAsync(sonarCloudConnectionInfo, new UsernamePasswordModel(username, password.CreateSecureString())); + + await connectionConfigurationSlCoreService.Received(1) + .ValidateConnectionAsync(Arg.Is(x => IsExpectedSonarCloudConnectionParams(x, username, password))); + } + + [TestMethod] + [DataRow(true, "success")] + [DataRow(false, "failure")] + public async Task ValidateConnectionAsync_ReturnsResponseFromSlCore(bool success, string message) + { + var expectedResponse = new ValidateConnectionResponse(success, message); + connectionConfigurationSlCoreService.ValidateConnectionAsync(Arg.Any()).Returns(expectedResponse); + + var response = await testSubject.ValidateConnectionAsync(sonarCloudConnectionInfo, new TokenCredentialsModel("myToken".CreateSecureString())); + + response.Success.Should().Be(success); + } + + [TestMethod] + public async Task ValidateConnectionAsync_SlCoreValidationThrowsException_ReturnsUnsuccessfulResponse() + { + var exceptionMessage = "validation failed"; + connectionConfigurationSlCoreService.When(x => x.ValidateConnectionAsync(Arg.Any())) + .Do(_ => throw new Exception(exceptionMessage)); + + var response = await testSubject.ValidateConnectionAsync(sonarCloudConnectionInfo, new TokenCredentialsModel("token".CreateSecureString())); + + logger.Received(1).LogVerbose($"{Resources.ValidateCredentials_Fails}: {exceptionMessage}"); + response.Success.Should().BeFalse(); + } + + [TestMethod] + public async Task GetOrganizationsAsync_SwitchesToBackgroundThread() + { + var threadHandlingMock = Substitute.For(); + var slCoreConnectionAdapter = new SlCoreConnectionAdapter(slCoreServiceProvider, threadHandlingMock, logger); + + await slCoreConnectionAdapter.GetOrganizationsAsync(new TokenCredentialsModel("token".CreateSecureString())); + + await threadHandlingMock.Received(1).RunOnBackgroundThread(Arg.Any>>>>()); + } + + [TestMethod] + public async Task GetOrganizationsAsync_GettingConnectionConfigurationSLCoreServiceFails_ReturnsFailedResponseAndShouldLog() + { + slCoreServiceProvider.TryGetTransientService(out IConnectionConfigurationSLCoreService _).Returns(false); + + var response = await testSubject.GetOrganizationsAsync(new TokenCredentialsModel("token".CreateSecureString())); + + logger.Received(1).LogVerbose($"[{nameof(IConnectionConfigurationSLCoreService)}] {SLCoreStrings.ServiceProviderNotInitialized}"); + response.Success.Should().BeFalse(); + response.ResponseData.Should().BeEmpty(); + } + + [TestMethod] + public async Task GetOrganizationsAsync_SlCoreThrowsException_ReturnsFailedResponseAndShouldLog() + { + var exceptionMessage = "validation failed"; + connectionConfigurationSlCoreService.When(x => x.ListUserOrganizationsAsync(Arg.Any())) + .Do(_ => throw new Exception(exceptionMessage)); + + var response = await testSubject.GetOrganizationsAsync(new TokenCredentialsModel("token".CreateSecureString())); + + logger.Received(1).LogVerbose($"{Resources.ListUserOrganizations_Fails}: {exceptionMessage}"); + response.Success.Should().BeFalse(); + response.ResponseData.Should().BeEmpty(); + } + + [TestMethod] + public async Task GetOrganizationsAsync_TokenIsProvided_CallsSlCoreListUserOrganizationsWithToken() + { + var token = "token"; + + await testSubject.GetOrganizationsAsync(new TokenCredentialsModel(token.CreateSecureString())); + + await connectionConfigurationSlCoreService.Received(1).ListUserOrganizationsAsync(Arg.Is(x=> IsExpectedCredentials(x.credentials, token))); + } + + [TestMethod] + public async Task GetOrganizationsAsync_UsernameAndPasswordIsProvided_CallsSlCoreListUserOrganizationsWithUsernameAndPassword() + { + var username = "username"; + var password = "password"; + + await testSubject.GetOrganizationsAsync(new UsernamePasswordModel(username, password.CreateSecureString())); + + await connectionConfigurationSlCoreService.Received(1).ListUserOrganizationsAsync(Arg.Is(x => IsExpectedCredentials(x.credentials, username, password))); + } + + [TestMethod] + public async Task GetOrganizationsAsync_CredentialsIsNull_ReturnsFailedResponseAndShouldLog() + { + var response = await testSubject.GetOrganizationsAsync(null); + + logger.Received(1).LogVerbose($"{Resources.ListUserOrganizations_Fails}: Unexpected {nameof(ICredentialsModel)} argument"); + response.Success.Should().BeFalse(); + response.ResponseData.Should().BeEmpty(); + } + + [TestMethod] + public async Task GetOrganizationsAsync_NoOrganizationExists_ReturnsSuccessResponseAndEmptyOrganizations() + { + connectionConfigurationSlCoreService.ListUserOrganizationsAsync(Arg.Any()) + .Returns(new ListUserOrganizationsResponse([])); + + var response = await testSubject.GetOrganizationsAsync(new TokenCredentialsModel("token".CreateSecureString())); + + response.Success.Should().BeTrue(); + response.ResponseData.Should().BeEmpty(); + } + + [TestMethod] + public async Task GetOrganizationsAsync_OrganizationExists_ReturnsSuccessResponseAndMappedOrganizations() + { + List serverOrganizations = [new OrganizationDto("key", "name", "desc"), new OrganizationDto("key2", "name2", "desc2")]; + connectionConfigurationSlCoreService.ListUserOrganizationsAsync(Arg.Any()) + .Returns(new ListUserOrganizationsResponse(serverOrganizations)); + + var response = await testSubject.GetOrganizationsAsync(new TokenCredentialsModel("token".CreateSecureString())); + + response.Success.Should().BeTrue(); + response.ResponseData.Should().BeEquivalentTo([ + new OrganizationDisplay("key", "name"), + new OrganizationDisplay("key2", "name2") + ]); + } + + [TestMethod] + public async Task GetAllProjectsAsync_SwitchesToBackgroundThread() + { + var threadHandlingMock = Substitute.For(); + var slCoreConnectionAdapter = new SlCoreConnectionAdapter(slCoreServiceProvider, threadHandlingMock, logger); + + await slCoreConnectionAdapter.GetAllProjectsAsync(sonarQubeConnection); + + await threadHandlingMock.Received(1).RunOnBackgroundThread(Arg.Any>>>>()); + } + + [TestMethod] + public async Task GetAllProjectsAsync_GettingConnectionConfigurationSLCoreServiceFails_ReturnsUnsuccessfulResponseAndLogs() + { + slCoreServiceProvider.TryGetTransientService(out IConnectionConfigurationSLCoreService _).Returns(false); + + var response = await testSubject.GetAllProjectsAsync(sonarQubeConnection); + + logger.Received(1).LogVerbose($"[{nameof(IConnectionConfigurationSLCoreService)}] {SLCoreStrings.ServiceProviderNotInitialized}"); + response.Success.Should().BeFalse(); + } + + [TestMethod] + public async Task GetAllProjectsAsync_ConnectionToSonarQubeWithToken_CallsGetAllProjectsAsyncWithCorrectParams() + { + await testSubject.GetAllProjectsAsync(sonarQubeConnection); + + await connectionConfigurationSlCoreService.Received(1) + .GetAllProjectsAsync(Arg.Is(x => IsExpectedSonarQubeConnectionParams(x.transientConnection, ValidToken.UserName))); + } + + [TestMethod] + public async Task GetAllProjectsAsync_ConnectionToSonarQubeWithCredentials_CallsGetAllProjectsAsyncWithCorrectParams() + { + const string username = "username"; + const string password = "password"; + sonarQubeConnection.Credentials = new BasicAuthCredentials(username, password.CreateSecureString()); + + await testSubject.GetAllProjectsAsync(sonarQubeConnection); + + await connectionConfigurationSlCoreService.Received(1) + .GetAllProjectsAsync(Arg.Is(x => IsExpectedSonarQubeConnectionParams(x.transientConnection, username, password))); + } + + [TestMethod] + public async Task GetAllProjectsAsync_ConnectionToSonarCloudWithToken_CallsGetAllProjectsAsyncWithCorrectParams() + { + await testSubject.GetAllProjectsAsync(sonarCloudConnection); + + await connectionConfigurationSlCoreService.Received(1) + .GetAllProjectsAsync(Arg.Is(x => IsExpectedSonarCloudConnectionParams(x.transientConnection, ValidToken.UserName))); + } + + [TestMethod] + public async Task GetAllProjectsAsync_ConnectionToSonarCloudWithCredentials_CallsGetAllProjectsAsyncWithCorrectParams() + { + const string username = "username"; + const string password = "password"; + sonarCloudConnection.Credentials = new BasicAuthCredentials(username, password.CreateSecureString()); + + await testSubject.GetAllProjectsAsync(sonarCloudConnection); + + await connectionConfigurationSlCoreService.Received(1) + .GetAllProjectsAsync(Arg.Is(x => IsExpectedSonarCloudConnectionParams(x.transientConnection, username, password))); + } + + [TestMethod] + public async Task GetAllProjectsAsync_ReturnsResponseFromSlCore() + { + List expectedServerProjects = [CreateSonarProjectDto("projKey1", "projName1"), CreateSonarProjectDto("projKey2", "projName2")]; + connectionConfigurationSlCoreService.GetAllProjectsAsync(Arg.Any()).Returns(new GetAllProjectsResponse(expectedServerProjects)); + + var response = await testSubject.GetAllProjectsAsync(sonarCloudConnection); + + response.Success.Should().BeTrue(); + response.ResponseData.Count.Should().Be(expectedServerProjects.Count); + response.ResponseData.Should().BeEquivalentTo([ + new ServerProject("projKey1", "projName1"), + new ServerProject("projKey2", "projName2") + ]); + } + + [TestMethod] + public async Task GetServerProjectByKeyAsync_SwitchesToBackgroundThread() + { + var threadHandlingMock = Substitute.For(); + var slCoreConnectionAdapter = new SlCoreConnectionAdapter(slCoreServiceProvider, threadHandlingMock, logger); + + await slCoreConnectionAdapter.GetServerProjectByKeyAsync(sonarCloudConnection, "server-project-key"); + + await threadHandlingMock.Received(1).RunOnBackgroundThread(Arg.Any>>>()); + } + + [TestMethod] + public async Task GetServerProjectByKeyAsync_GettingConnectionConfigurationSLCoreServiceFails_ReturnsFailedResponseAndShouldLog() + { + slCoreServiceProvider.TryGetTransientService(out IConnectionConfigurationSLCoreService _).Returns(false); + + var response = await testSubject.GetServerProjectByKeyAsync(sonarCloudConnection, "server-project-key"); + + logger.Received(1).LogVerbose($"[{nameof(IConnectionConfigurationSLCoreService)}] {SLCoreStrings.ServiceProviderNotInitialized}"); + response.Success.Should().BeFalse(); + response.ResponseData.Should().BeNull(); + } + + [TestMethod] + public async Task GetServerProjectByKeyAsync_SlCoreThrowsException_ReturnsFailedResponseAndShouldLog() + { + const string exceptionMessage = "SLCore error"; + connectionConfigurationSlCoreService.When(x => x.GetProjectNamesByKeyAsync(Arg.Any())) + .Do(_ => throw new Exception(exceptionMessage)); + + var response = await testSubject.GetServerProjectByKeyAsync(sonarCloudConnection, "server-project-key"); + + logger.Received(1).LogVerbose($"{Resources.GetServerProjectByKey_Fails}: {exceptionMessage}"); + response.Success.Should().BeFalse(); + response.ResponseData.Should().BeNull(); + } + + [TestMethod] + public async Task GetServerProjectByKeyAsync_ProjectNotFound_ReturnsFailedResponse() + { + var slCoreResponse = new Dictionary { {"project-key", null} }; + connectionConfigurationSlCoreService.GetProjectNamesByKeyAsync(Arg.Any()) + .Returns(new GetProjectNamesByKeyResponse(slCoreResponse)); + + var response = await testSubject.GetServerProjectByKeyAsync(sonarCloudConnection, "project-key"); + + response.Success.Should().BeFalse(); + response.ResponseData.Should().BeNull(); + } + + [TestMethod] + public async Task GetServerProjectByKeyAsync_ProjectFound_ReturnsSuccessResponseAndMappedOrganizations() + { + var slCoreResponse = new Dictionary + { + {"project-key", "project-name"} + }; + connectionConfigurationSlCoreService.GetProjectNamesByKeyAsync(Arg.Any()) + .Returns(new GetProjectNamesByKeyResponse(slCoreResponse)); + var response = await testSubject.GetServerProjectByKeyAsync(sonarQubeConnection, "project-key"); + + response.Success.Should().BeTrue(); + response.ResponseData.Should().BeEquivalentTo(new ServerProject("project-key", "project-name")); + } + + + [TestMethod] + public async Task GetAllProjectsAsync_SlCoreValidationThrowsException_ReturnsUnsuccessfulResponse() + { + var exceptionMessage = "validation failed"; + connectionConfigurationSlCoreService.When(x => x.GetAllProjectsAsync(Arg.Any())) + .Do(_ => throw new Exception(exceptionMessage)); + + var response = await testSubject.GetAllProjectsAsync(sonarCloudConnection); + + logger.Received(1).LogVerbose($"{Resources.GetAllProjects_Fails}: {exceptionMessage}"); + response.Success.Should().BeFalse(); + } + + private bool IsExpectedSonarQubeConnectionParams(ValidateConnectionParams receivedParams, string token) + { + return IsExpectedSonarQubeConnectionParams(receivedParams.transientConnection, token); + } + + private bool IsExpectedSonarQubeConnectionParams(Either transientConnection, string token) + { + var transientSonarQubeDto = transientConnection.Left; + return transientSonarQubeDto.serverUrl == sonarQubeConnectionInfo.Id && IsExpectedCredentials(transientSonarQubeDto.credentials, token); + } + + private bool IsExpectedSonarQubeConnectionParams(ValidateConnectionParams receivedParams, string username, string password) + { + return IsExpectedSonarQubeConnectionParams(receivedParams.transientConnection, username, password); + } + + private bool IsExpectedSonarQubeConnectionParams(Either transientConnection, string username, string password) + { + var transientSonarQubeDto = transientConnection.Left; + return transientSonarQubeDto.serverUrl == sonarQubeConnectionInfo.Id && IsExpectedCredentials(transientSonarQubeDto.credentials, username, password); + } + + private static bool IsExpectedCredentials(Either credentials, string token) + { + return credentials.Left.token == token; + } + + private static bool IsExpectedCredentials(Either credentials, string username, string password) + { + return credentials.Right.username == username && credentials.Right.password == password; + } + + private bool IsExpectedSonarCloudConnectionParams(ValidateConnectionParams receivedParams, string token) + { + return IsExpectedSonarCloudConnectionParams(receivedParams.transientConnection, token); + } + + private bool IsExpectedSonarCloudConnectionParams(Either transientConnection, string token) + { + var transientSonarCloudDto = transientConnection.Right; + return transientSonarCloudDto.organization == sonarCloudConnectionInfo.Id && IsExpectedCredentials(transientSonarCloudDto.credentials, token); + } + + private bool IsExpectedSonarCloudConnectionParams(ValidateConnectionParams receivedParams, string username, string password) + { + return IsExpectedSonarCloudConnectionParams(receivedParams.transientConnection, username, password); + } + + private bool IsExpectedSonarCloudConnectionParams(Either transientConnection, string username, string password) + { + var transientSonarCloudDto = transientConnection.Right; + return transientSonarCloudDto.organization == sonarCloudConnectionInfo.Id && IsExpectedCredentials(transientSonarCloudDto.credentials, username, password); + } + + private void SetupConnection() + { + sonarCloudConnectionInfo = new ConnectionInfo("myOrg", ConnectionServerType.SonarCloud); + sonarQubeConnectionInfo = new ConnectionInfo("http://localhost:9000/", ConnectionServerType.SonarQube); + slCoreServiceProvider.TryGetTransientService(out IConnectionConfigurationSLCoreService _).Returns(x => + { + x[0] = connectionConfigurationSlCoreService; + return true; + }); + } + + private static SonarProjectDto CreateSonarProjectDto(string key, string name) + { + return new SonarProjectDto(key, name); + } +} diff --git a/src/ConnectedMode.UnitTests/StringExtensions.cs b/src/ConnectedMode.UnitTests/StringExtensions.cs new file mode 100644 index 0000000000..b215fc5323 --- /dev/null +++ b/src/ConnectedMode.UnitTests/StringExtensions.cs @@ -0,0 +1,41 @@ +/* + * 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 System.Security; + +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests; + +internal static class StringExtensions +{ + public static SecureString CreateSecureString(this string stringValue) + { + if(stringValue == null) + { + return null; + } + + SecureString secureString = new(); + foreach (char character in stringValue) + { + secureString.AppendChar(character); + } + return secureString; + } +} diff --git a/src/ConnectedMode.UnitTests/Suppressions/TimedUpdateHandlerTests.cs b/src/ConnectedMode.UnitTests/Suppressions/TimedUpdateHandlerTests.cs index 67f317f622..6df12d1386 100644 --- a/src/ConnectedMode.UnitTests/Suppressions/TimedUpdateHandlerTests.cs +++ b/src/ConnectedMode.UnitTests/Suppressions/TimedUpdateHandlerTests.cs @@ -170,7 +170,7 @@ private static TimedUpdateHandler CreateTestSubject(IActiveSolutionBoundTracker private BindingConfiguration CreateBindingConfiguration(SonarLintMode mode) { - return new BindingConfiguration(new BoundSonarQubeProject(new Uri("http://localhost"), "test", ""), mode, ""); + return new BindingConfiguration(new BoundServerProject("solution", "projectKey", new ServerConnection.SonarQube(new Uri("http://localhost"))), mode, ""); } private Mock CreateActiveSolutionBoundTrackerWihtBindingConfig(SonarLintMode mode) diff --git a/src/ConnectedMode.UnitTests/UI/ConnectedModeBindingServicesTests.cs b/src/ConnectedMode.UnitTests/UI/ConnectedModeBindingServicesTests.cs new file mode 100644 index 0000000000..7ebf0103e3 --- /dev/null +++ b/src/ConnectedMode.UnitTests/UI/ConnectedModeBindingServicesTests.cs @@ -0,0 +1,42 @@ +/* + * 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; +using SonarLint.VisualStudio.Core.Binding; + +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.UI; + +[TestClass] +public class ConnectedModeBindingServicesTests +{ + [TestMethod] + public void MefCtor_CheckIsExported() + { + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + } +} diff --git a/src/ConnectedMode.UnitTests/UI/ConnectedModeServicesTests.cs b/src/ConnectedMode.UnitTests/UI/ConnectedModeServicesTests.cs new file mode 100644 index 0000000000..333fd1d928 --- /dev/null +++ b/src/ConnectedMode.UnitTests/UI/ConnectedModeServicesTests.cs @@ -0,0 +1,43 @@ +/* + * 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.Core.Binding; + +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.UI; + +[TestClass] +public class ConnectedModeServicesTests +{ + [TestMethod] + public void MefCtor_CheckIsExported() + { + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + } +} diff --git a/src/ConnectedMode.UnitTests/UI/Credentials/CredentialsViewModelTests.cs b/src/ConnectedMode.UnitTests/UI/Credentials/CredentialsViewModelTests.cs new file mode 100644 index 0000000000..a2538dc292 --- /dev/null +++ b/src/ConnectedMode.UnitTests/UI/Credentials/CredentialsViewModelTests.cs @@ -0,0 +1,377 @@ +/* + * 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 System.ComponentModel; +using System.IO; +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.SLCore.Service.Connection; + +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.UI.Credentials +{ + [TestClass] + public class CredentialsViewModelTests + { + private CredentialsViewModel testSubject; + private ConnectionInfo sonarQubeConnectionInfo; + private ConnectionInfo sonarCloudConnectionInfo; + private ISlCoreConnectionAdapter slCoreConnectionAdapter; + private IProgressReporterViewModel progressReporterViewModel; + + [TestInitialize] + public void TestInitialize() + { + sonarQubeConnectionInfo = new ConnectionInfo("http://localhost:9000", ConnectionServerType.SonarQube); + sonarCloudConnectionInfo = new ConnectionInfo("myOrg", ConnectionServerType.SonarCloud); + slCoreConnectionAdapter = Substitute.For(); + progressReporterViewModel = Substitute.For(); + + testSubject = new CredentialsViewModel(sonarQubeConnectionInfo, slCoreConnectionAdapter, progressReporterViewModel); + } + + [TestMethod] + public void SelectedAuthenticationType_ShouldBeTokenByDefault() + { + testSubject.SelectedAuthenticationType.Should().Be(UiResources.AuthenticationTypeOptionToken); + testSubject.IsTokenAuthentication.Should().BeTrue(); + } + + [TestMethod] + public void SelectedAuthenticationType_ClearsWarning() + { + testSubject.ProgressReporterViewModel.Warning = "credentials warning"; + + testSubject.SelectedAuthenticationType = UiResources.AuthenticationTypeOptionCredentials; + + testSubject.ProgressReporterViewModel.Received(1).Warning = null; + } + + [TestMethod] + public void IsTokenAuthentication_TokenIsSelected_ReturnsTrue() + { + testSubject.SelectedAuthenticationType = UiResources.AuthenticationTypeOptionToken; + + testSubject.IsTokenAuthentication.Should().BeTrue(); + } + + [TestMethod] public void IsTokenAuthentication_CredentialsIsSelected_ReturnsFalse() + { + testSubject.SelectedAuthenticationType = UiResources.AuthenticationTypeOptionCredentials; + + testSubject.IsTokenAuthentication.Should().BeFalse(); + } + + [TestMethod] + public void IsCredentialsAuthentication_TokenIsSelected_ReturnsFalse() + { + testSubject.SelectedAuthenticationType = UiResources.AuthenticationTypeOptionToken; + + testSubject.IsCredentialsAuthentication.Should().BeFalse(); + } + + [TestMethod] + public void IsCredentialsAuthentication_CredentialsIsSelected_ReturnsTrue() + { + testSubject.SelectedAuthenticationType = UiResources.AuthenticationTypeOptionCredentials; + + testSubject.IsCredentialsAuthentication.Should().BeTrue(); + } + + [TestMethod] + public void IsConfirmationEnabled_TokenIsSelectedAndTokenIsFilled_ReturnsTrue() + { + testSubject.SelectedAuthenticationType = UiResources.AuthenticationTypeOptionToken; + + testSubject.Token = "dummy token".CreateSecureString(); + + testSubject.IsConfirmationEnabled.Should().BeTrue(); + } + + [TestMethod] + public void IsConfirmationEnabled_CorrectTokenIsProvidedAndValidationIsInProgress_ReturnsFalse() + { + testSubject.SelectedAuthenticationType = UiResources.AuthenticationTypeOptionToken; + testSubject.Token = "dummy token".CreateSecureString(); + + testSubject.ProgressReporterViewModel.IsOperationInProgress.Returns(true); + + testSubject.IsConfirmationEnabled.Should().BeFalse(); + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + public void IsConfirmationEnabled_TokenIsSelectedAndTokenIsNotFilled_ReturnsFalse(string token) + { + testSubject.SelectedAuthenticationType = UiResources.AuthenticationTypeOptionToken; + + testSubject.Token = token.CreateSecureString(); + + testSubject.IsConfirmationEnabled.Should().BeFalse(); + } + + [TestMethod] + public void IsConfirmationEnabled_CredentialsIsSelectedAndUsernameAndPasswordAreFilled_ReturnsTrue() + { + testSubject.SelectedAuthenticationType = UiResources.AuthenticationTypeOptionCredentials; + + testSubject.Username = "dummy username"; + testSubject.Password = "dummy password".CreateSecureString(); + + testSubject.IsConfirmationEnabled.Should().BeTrue(); + } + + [TestMethod] + public void IsConfirmationEnabled_CorrectCredentialsProvidedAndValidationIsInProgress_ReturnsFalse() + { + testSubject.SelectedAuthenticationType = UiResources.AuthenticationTypeOptionCredentials; + testSubject.Username = "dummy username"; + testSubject.Password = "dummy password".CreateSecureString(); + + testSubject.ProgressReporterViewModel.IsOperationInProgress.Returns(true); + + testSubject.IsConfirmationEnabled.Should().BeFalse(); + } + + [TestMethod] + [DataRow(null, "pwd")] + [DataRow("", "pwd")] + [DataRow(" ", "pwd")] + [DataRow("username", null)] + [DataRow("username", "")] + [DataRow("username", " ")] + public void IsConfirmationEnabled_CredentialsIsSelectedAndUsernameOrPasswordAreNotFilled_ReturnsFalse(string username, string password) + { + testSubject.SelectedAuthenticationType = UiResources.AuthenticationTypeOptionCredentials; + + testSubject.Username = username; + testSubject.Password = password.CreateSecureString(); + + testSubject.IsConfirmationEnabled.Should().BeFalse(); + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + public void ShouldTokenBeFilled_TokenAuthenticationIsSelectedAndTokenIsEmpty_ReturnsTrue(string token) + { + testSubject.SelectedAuthenticationType = UiResources.AuthenticationTypeOptionToken; + testSubject.Token = token.CreateSecureString(); + + testSubject.ShouldTokenBeFilled.Should().BeTrue(); + } + + [TestMethod] + public void ShouldTokenBeFilled_TokenAuthenticationIsSelectedAndTokenIsFilled_ReturnsFalse() + { + testSubject.SelectedAuthenticationType = UiResources.AuthenticationTypeOptionToken; + testSubject.Token = "dummy token".CreateSecureString(); + + testSubject.ShouldTokenBeFilled.Should().BeFalse(); + } + + [TestMethod] + public void ShouldTokenBeFilled_CredentialsAuthenticationIsSelected_ReturnsFalse() + { + testSubject.SelectedAuthenticationType = UiResources.AuthenticationTypeOptionCredentials; + + testSubject.ShouldTokenBeFilled.Should().BeFalse(); + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + public void ShouldUsernameBeFilled_CredentialsAuthenticationIsSelectedAndUsernameIsEmpty_ReturnsTrue(string username) + { + testSubject.SelectedAuthenticationType = UiResources.AuthenticationTypeOptionCredentials; + testSubject.Username = username; + + testSubject.ShouldUsernameBeFilled.Should().BeTrue(); + } + + [TestMethod] + public void ShouldUsernameBeFilled_CredentialsAuthenticationIsSelectedAndUsernameIsFilled_ReturnsFalse() + { + testSubject.SelectedAuthenticationType = UiResources.AuthenticationTypeOptionCredentials; + testSubject.Username = "dummy username"; + + testSubject.ShouldUsernameBeFilled.Should().BeFalse(); + } + + [TestMethod] + public void ShouldUsernameBeFilled_TokenAuthenticationIsSelected_ReturnsFalse() + { + testSubject.SelectedAuthenticationType = UiResources.AuthenticationTypeOptionToken; + + testSubject.ShouldUsernameBeFilled.Should().BeFalse(); + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + public void ShouldPasswordBeFilled_CredentialsAuthenticationIsSelectedAndPasswordIsEmpty_ReturnsTrue(string password) + { + testSubject.SelectedAuthenticationType = UiResources.AuthenticationTypeOptionCredentials; + testSubject.Password = password.CreateSecureString(); + + testSubject.ShouldPasswordBeFilled.Should().BeTrue(); + } + + [TestMethod] + public void ShouldPasswordBeFilled_CredentialsAuthenticationIsSelectedAndPasswordIsFilled_ReturnsFalse() + { + testSubject.SelectedAuthenticationType = UiResources.AuthenticationTypeOptionCredentials; + testSubject.Password = "dummy password".CreateSecureString(); + + testSubject.ShouldPasswordBeFilled.Should().BeFalse(); + } + + [TestMethod] + public void ShouldPasswordBeFilled_TokenAuthenticationIsSelected_ReturnsFalse() + { + testSubject.SelectedAuthenticationType = UiResources.AuthenticationTypeOptionToken; + + testSubject.ShouldPasswordBeFilled.Should().BeFalse(); + } + + [TestMethod] + public void AccountSecurityUrl_ConnectionIsSonarCloud_ReturnsSonarCloudUrl() + { + var viewModel = new CredentialsViewModel(sonarCloudConnectionInfo, slCoreConnectionAdapter, progressReporterViewModel); + + viewModel.AccountSecurityUrl.Should().Be(UiResources.SonarCloudAccountSecurityUrl); + } + + [TestMethod] + public void AccountSecurityUrl_ConnectionIsSonarQube_ReturnsSonarQubeUrl() + { + var qubeUrl = "http://localhost:9000/"; + var viewModel = new CredentialsViewModel(new ConnectionInfo(qubeUrl, ConnectionServerType.SonarQube), slCoreConnectionAdapter, progressReporterViewModel); + var expectedUrl = Path.Combine(qubeUrl, UiResources.SonarQubeAccountSecurityUrl); + + viewModel.AccountSecurityUrl.Should().Be(expectedUrl); + } + + [TestMethod] + public async Task AdapterValidateConnectionAsync_TokenIsProvided_ShouldValidateConnectionWithToken() + { + MockAdapterValidateConnectionAsync(); + testSubject.SelectedAuthenticationType = UiResources.AuthenticationTypeOptionToken; + testSubject.Token = "dummyToken".CreateSecureString(); + + await testSubject.AdapterValidateConnectionAsync(); + + await slCoreConnectionAdapter.Received(1).ValidateConnectionAsync(testSubject.ConnectionInfo, + Arg.Is(x => x.Token == testSubject.Token)); + } + + [TestMethod] + public async Task AdapterValidateConnectionAsync_CredentialsAreProvided_ShouldValidateConnectionWithToken() + { + MockAdapterValidateConnectionAsync(); + testSubject.SelectedAuthenticationType = UiResources.AuthenticationTypeOptionCredentials; + testSubject.Username = "username"; + testSubject.Password = "password".CreateSecureString(); + + await testSubject.AdapterValidateConnectionAsync(); + + await slCoreConnectionAdapter.Received(1).ValidateConnectionAsync(testSubject.ConnectionInfo, + Arg.Is(x => x.Username == testSubject.Username && x.Password == testSubject.Password)); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public async Task ValidateConnectionAsync_ReturnsResponseFromSlCore(bool success) + { + progressReporterViewModel.ExecuteTaskWithProgressAsync(Arg.Any>()).Returns(new AdapterResponse(success)); + + var response = await testSubject.ValidateConnectionAsync(); + + response.Should().Be(success); + } + + [TestMethod] + public async Task ValidateConnectionAsync_ReturnsResponseFromSlCore() + { + await testSubject.ValidateConnectionAsync(); + + await progressReporterViewModel.Received(1) + .ExecuteTaskWithProgressAsync(Arg.Is>(x => + x.TaskToPerform == testSubject.AdapterValidateConnectionAsync && + x.ProgressStatus == UiResources.ValidatingConnectionProgressText && + x.WarningText == UiResources.ValidatingConnectionFailedText && + x.AfterProgressUpdated == testSubject.AfterProgressStatusUpdated)); + } + + [TestMethod] + public void UpdateProgressStatus_RaisesEvents() + { + var eventHandler = Substitute.For(); + testSubject.PropertyChanged += eventHandler; + eventHandler.ReceivedCalls().Should().BeEmpty(); + + testSubject.AfterProgressStatusUpdated(); + + eventHandler.Received().Invoke(testSubject, Arg.Is(x => x.PropertyName == nameof(testSubject.IsConfirmationEnabled))); + } + + [TestMethod] + public void GetCredentialsModel_SelectedAuthenticationTypeIsToken_ReturnsModelWithToken() + { + testSubject.Token = "token".CreateSecureString(); + testSubject.Username = "username"; + testSubject.Password = "password".CreateSecureString(); + testSubject.SelectedAuthenticationType = UiResources.AuthenticationTypeOptionToken; + + var credentialsModel = testSubject.GetCredentialsModel(); + + credentialsModel.Should().BeOfType(); + ((TokenCredentialsModel)credentialsModel).Token.Should().Be(testSubject.Token); + } + + [TestMethod] + public void GetCredentialsModel_SelectedAuthenticationTypeIsCredentials_ReturnsModelWithUsernameAndPassword() + { + testSubject.Token = "token".CreateSecureString(); + testSubject.Username = "username"; + testSubject.Password = "password".CreateSecureString(); + testSubject.SelectedAuthenticationType = UiResources.AuthenticationTypeOptionCredentials; + + var credentialsModel = testSubject.GetCredentialsModel(); + + credentialsModel.Should().BeOfType(); + ((UsernamePasswordModel)credentialsModel).Username.Should().Be(testSubject.Username); + ((UsernamePasswordModel)credentialsModel).Password.Should().Be(testSubject.Password); + } + + private void MockAdapterValidateConnectionAsync(bool success = true) + { + slCoreConnectionAdapter.ValidateConnectionAsync(Arg.Any(), Arg.Any()) + .Returns(new AdapterResponse(success)); + } + } +} diff --git a/src/ConnectedMode.UnitTests/UI/DeleteConnection/DeleteConnectionDialogViewModelTests.cs b/src/ConnectedMode.UnitTests/UI/DeleteConnection/DeleteConnectionDialogViewModelTests.cs new file mode 100644 index 0000000000..887bfedd32 --- /dev/null +++ b/src/ConnectedMode.UnitTests/UI/DeleteConnection/DeleteConnectionDialogViewModelTests.cs @@ -0,0 +1,66 @@ +/* + * 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.ConnectedMode.UI.DeleteConnection; +using SonarLint.VisualStudio.ConnectedMode.UI.ManageBinding; +using SonarLint.VisualStudio.ConnectedMode.UI.ProjectSelection; + +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.UI.DeleteConnection; + +[TestClass] +public class DeleteConnectionDialogViewModelTests +{ + + [TestMethod] + public void Ctor_SetsProperties() + { + var projectsToUnbind = Substitute.For>(); + var connectionInfo = new ConnectionInfo(default, default); + var testSubject = new DeleteConnectionDialogViewModel(projectsToUnbind, connectionInfo); + + testSubject.ConnectionInfo.Should().BeSameAs(connectionInfo); + testSubject.ProjectsToUnbind.Should().BeSameAs(projectsToUnbind); + } + + [DataTestMethod] + public void DisplayProjectList_MultipleProjectsToUnbind_ReturnsTrue() + { + var projects = new[] { new ConnectedModeProject(new ServerProject("proj key", "proj name"), new SolutionInfoModel("my sol", SolutionType.Folder)) }; + var testSubject = new DeleteConnectionDialogViewModel(projects, new ConnectionInfo(default, default)); + + testSubject.DisplayProjectList.Should().BeTrue(); + } + + [DataTestMethod] + public void DisplayProjectList_ProjectsIsNull_ReturnsFalse() + { + var testSubject = new DeleteConnectionDialogViewModel(null, new ConnectionInfo(default, default)); + + testSubject.DisplayProjectList.Should().BeFalse(); + } + + [DataTestMethod] + public void DisplayProjectList_NoProjectsToUnbind_ReturnsFalse() + { + var testSubject = new DeleteConnectionDialogViewModel([], new ConnectionInfo(default, default)); + + testSubject.DisplayProjectList.Should().BeFalse(); + } +} diff --git a/src/ConnectedMode.UnitTests/UI/DeleteConnection/PreventDeleteConnectionViewModelTests.cs b/src/ConnectedMode.UnitTests/UI/DeleteConnection/PreventDeleteConnectionViewModelTests.cs new file mode 100644 index 0000000000..76e22efa62 --- /dev/null +++ b/src/ConnectedMode.UnitTests/UI/DeleteConnection/PreventDeleteConnectionViewModelTests.cs @@ -0,0 +1,65 @@ +/* + * 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.ConnectedMode.UI.DeleteConnection; + +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.UI.DeleteConnection; + +[TestClass] +public class PreventDeleteConnectionViewModelTests +{ + [TestMethod] + public void Ctor_SetsProperties() + { + var projectsToUnbind = Substitute.For>(); + var connectionInfo = new ConnectionInfo(default, default); + + var testSubject = new PreventDeleteConnectionViewModel(projectsToUnbind, connectionInfo); + + testSubject.ConnectionInfo.Should().BeSameAs(connectionInfo); + testSubject.ProjectsToUnbind.Should().BeSameAs(projectsToUnbind); + } + + [DataTestMethod] + public void DisplayProjectList_MultipleProjectsToUnbind_ReturnsTrue() + { + var projects = new[] { "binding1", "binding2"}; + + var testSubject = new PreventDeleteConnectionViewModel(projects, new ConnectionInfo(default, default)); + + testSubject.DisplayProjectList.Should().BeTrue(); + } + + [DataTestMethod] + public void DisplayProjectList_ProjectsIsNull_ReturnsFalse() + { + var testSubject = new PreventDeleteConnectionViewModel(null, new ConnectionInfo(default, default)); + + testSubject.DisplayProjectList.Should().BeFalse(); + } + + [DataTestMethod] + public void DisplayProjectList_NoProjectsToUnbind_ReturnsFalse() + { + var testSubject = new PreventDeleteConnectionViewModel([], new ConnectionInfo(default, default)); + + testSubject.DisplayProjectList.Should().BeFalse(); + } +} diff --git a/src/ConnectedMode.UnitTests/UI/ManageBinding/ManageBindingViewModelTests.cs b/src/ConnectedMode.UnitTests/UI/ManageBinding/ManageBindingViewModelTests.cs new file mode 100644 index 0000000000..5efc3af96d --- /dev/null +++ b/src/ConnectedMode.UnitTests/UI/ManageBinding/ManageBindingViewModelTests.cs @@ -0,0 +1,995 @@ +/* + * 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 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; +using SonarLint.VisualStudio.ConnectedMode.UI.Resources; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Binding; + +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.UI.ManageBinding; + +[TestClass] +public class ManageBindingViewModelTests +{ + private const string ALocalProjectKey = "local-project-key"; + + private readonly ServerProject serverProject = new ("a-project", "A Project"); + 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; + private IBindingController bindingController; + private ISolutionInfoProvider solutionInfoProvider; + 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(); + progressReporterViewModel = Substitute.For(); + connectedModeBindingServices = Substitute.For(); + + testSubject = new ManageBindingViewModel(connectedModeServices, connectedModeBindingServices, progressReporterViewModel); + + MockServices(); + } + + [TestMethod] + public void IsCurrentProjectBound_ProjectIsBound_ReturnsTrue() + { + testSubject.BoundProject = serverProject; + + testSubject.IsCurrentProjectBound.Should().BeTrue(); + } + + [TestMethod] + public void IsCurrentProjectBound_ProjectIsNotBound_ReturnsFalse() + { + testSubject.BoundProject = null; + + testSubject.IsCurrentProjectBound.Should().BeFalse(); + } + + [TestMethod] + public void BoundProject_Set_RaisesEvents() + { + var eventHandler = Substitute.For(); + testSubject.PropertyChanged += eventHandler; + eventHandler.ReceivedCalls().Should().BeEmpty(); + + testSubject.BoundProject = serverProject; + + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.BoundProject))); + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.IsCurrentProjectBound))); + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.IsConnectionSelectionEnabled))); + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.IsSelectProjectButtonEnabled))); + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.IsExportButtonEnabled))); + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.IsUseSharedBindingButtonVisible))); + } + + [TestMethod] + public void IsProjectSelected_ProjectIsSelected_ReturnsTrue() + { + testSubject.SelectedProject = serverProject; + + testSubject.IsProjectSelected.Should().BeTrue(); + } + + [TestMethod] + public void IsProjectSelected_ProjectIsNotSelected_ReturnsFalse() + { + testSubject.SelectedProject = null; + + testSubject.IsProjectSelected.Should().BeFalse(); + } + + [TestMethod] + public void SelectedProject_Set_RaisesEvents() + { + var eventHandler = Substitute.For(); + testSubject.PropertyChanged += eventHandler; + eventHandler.ReceivedCalls().Should().BeEmpty(); + + testSubject.SelectedProject = serverProject; + + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.SelectedProject))); + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.IsProjectSelected))); + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.IsBindButtonEnabled))); + } + + [TestMethod] + public void IsConnectionSelected_ProjectIsSelected_ReturnsTrue() + { + testSubject.SelectedConnectionInfo = sonarQubeConnectionInfo; + + testSubject.IsConnectionSelected.Should().BeTrue(); + } + + [TestMethod] + public void IsConnectionSelected_ProjectIsNotSelected_ReturnsFalse() + { + testSubject.SelectedConnectionInfo = null; + + testSubject.IsConnectionSelected.Should().BeFalse(); + } + + [TestMethod] + public void SelectedConnection_NewConnectionIsSet_ClearsSelectedProject() + { + testSubject.SelectedConnectionInfo = sonarQubeConnectionInfo; + testSubject.SelectedProject = serverProject; + + testSubject.SelectedConnectionInfo = sonarCloudConnectionInfo; + + testSubject.SelectedProject.Should().BeNull(); + } + + [TestMethod] + public void SelectedConnection_SameConnectionIsSet_DoesNotClearSelectedProject() + { + testSubject.SelectedConnectionInfo = sonarQubeConnectionInfo; + testSubject.SelectedProject = serverProject; + + testSubject.SelectedConnectionInfo = sonarQubeConnectionInfo; + + testSubject.SelectedProject.Should().Be(serverProject); + } + + [TestMethod] + public void SelectedConnection_Set_RaisesEvents() + { + var eventHandler = Substitute.For(); + testSubject.PropertyChanged += eventHandler; + eventHandler.ReceivedCalls().Should().BeEmpty(); + + testSubject.SelectedConnectionInfo = sonarQubeConnectionInfo; + + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.SelectedConnectionInfo))); + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.IsConnectionSelected))); + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.IsSelectProjectButtonEnabled))); + } + + [TestMethod] + public void Unbind_SetsBoundProjectToNull() + { + testSubject.BoundProject = serverProject; + + testSubject.Unbind(); + + testSubject.BoundProject.Should().BeNull(); + } + + [TestMethod] + public void Unbind_SetsConnectionInfoToNull() + { + testSubject.SelectedConnectionInfo = sonarQubeConnectionInfo; + testSubject.SelectedProject = serverProject; + + testSubject.Unbind(); + + testSubject.SelectedConnectionInfo.Should().BeNull(); + testSubject.SelectedProject.Should().BeNull(); + } + + [TestMethod] + public void IsBindButtonEnabled_ProjectIsSelectedAndBindingIsNotInProgress_ReturnsTrue() + { + testSubject.SelectedProject = serverProject; + progressReporterViewModel.IsOperationInProgress.Returns(false); + + testSubject.IsBindButtonEnabled.Should().BeTrue(); + } + + [TestMethod] + public void IsBindButtonEnabled_ProjectIsSelectedAndBindingIsInProgress_ReturnsFalse() + { + testSubject.SelectedProject = serverProject; + progressReporterViewModel.IsOperationInProgress.Returns(true); + + testSubject.IsBindButtonEnabled.Should().BeFalse(); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void IsBindButtonEnabled_ProjectIsNotSelected_ReturnsFalse(bool isBindingInProgress) + { + testSubject.SelectedProject = null; + progressReporterViewModel.IsOperationInProgress.Returns(isBindingInProgress); + + testSubject.IsBindButtonEnabled.Should().BeFalse(); + } + + [TestMethod] + [DataRow(true, false)] + [DataRow(false, true)] + public void IsManageConnectionsButtonEnabled_ReturnsTrueOnlyWhenNoBindingIsInProgress(bool isBindingInProgress, bool expectedResult) + { + progressReporterViewModel.IsOperationInProgress.Returns(isBindingInProgress); + + testSubject.IsManageConnectionsButtonEnabled.Should().Be(expectedResult); + } + + [TestMethod] + [DataRow(true, false)] + [DataRow(false, true)] + public void IsUseSharedBindingButtonEnabled_ReturnsTrueOnlyWhenNoBindingIsInProgress(bool isBindingInProgress, bool expectedResult) + { + progressReporterViewModel.IsOperationInProgress.Returns(isBindingInProgress); + + testSubject.IsUseSharedBindingButtonEnabled.Should().Be(expectedResult); + } + + [TestMethod] + public void SharedBindingConfigModel_Set_RaisesEvents() + { + var eventHandler = Substitute.For(); + testSubject.PropertyChanged += eventHandler; + eventHandler.ReceivedCalls().Should().BeEmpty(); + + testSubject.SharedBindingConfigModel = new SharedBindingConfigModel(); + + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.IsUseSharedBindingButtonVisible))); + } + + [TestMethod] + [DataRow(true, false)] + [DataRow(false, true)] + public void IsUnbindButtonEnabled_ReturnsTrueOnlyWhenNoBindingIsInProgress(bool isBindingInProgress, bool expectedResult) + { + progressReporterViewModel.IsOperationInProgress.Returns(isBindingInProgress); + + testSubject.IsUnbindButtonEnabled.Should().Be(expectedResult); + } + + [TestMethod] + public void IsSelectProjectButtonEnabled_ConnectionIsSelectedAndNoBindingIsInProgressAndProjectIsNotBound_ReturnsTrue() + { + testSubject.BoundProject = null; + progressReporterViewModel.IsOperationInProgress.Returns(false); + testSubject.SelectedConnectionInfo = sonarQubeConnectionInfo; + + testSubject.IsSelectProjectButtonEnabled.Should().BeTrue(); + } + + [TestMethod] + public void IsSelectProjectButtonEnabled_BindingIsInProgress_ReturnsFalse() + { + testSubject.BoundProject = null; + progressReporterViewModel.IsOperationInProgress.Returns(true); + testSubject.SelectedConnectionInfo = sonarQubeConnectionInfo; + + testSubject.IsSelectProjectButtonEnabled.Should().BeFalse(); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void IsSelectProjectButtonEnabled_ConnectionIsNotSelected_ReturnsFalse(bool isBindingInProgress) + { + progressReporterViewModel.IsOperationInProgress.Returns(isBindingInProgress); + testSubject.SelectedConnectionInfo = null; + + testSubject.IsSelectProjectButtonEnabled.Should().BeFalse(); + } + + [TestMethod] + public void IsSelectProjectButtonEnabled_ProjectIsAlreadyBound_ReturnsFalse() + { + testSubject.BoundProject = serverProject; + progressReporterViewModel.IsOperationInProgress.Returns(false); + testSubject.SelectedConnectionInfo = sonarQubeConnectionInfo; + + testSubject.IsSelectProjectButtonEnabled.Should().BeFalse(); + } + + [TestMethod] + public void IsConnectionSelectionEnabled_BindingIsInProgress_ReturnsFalse() + { + testSubject.BoundProject = null; + progressReporterViewModel.IsOperationInProgress.Returns(true); + + testSubject.IsConnectionSelectionEnabled.Should().BeFalse(); + } + + [TestMethod] + [DataRow(false)] + [DataRow(true)] + public void IsConnectionSelectionEnabled_ProjectIsBound_ReturnsFalse(bool isBindingInProgress) + { + testSubject.BoundProject = serverProject; + progressReporterViewModel.IsOperationInProgress.Returns(isBindingInProgress); + + testSubject.IsConnectionSelectionEnabled.Should().BeFalse(); + } + + [TestMethod] + public void IsConnectionSelectionEnabled_NoConnectionsExist_ReturnsFalse() + { + testSubject.BoundProject = null; + progressReporterViewModel.IsOperationInProgress.Returns(false); + + testSubject.IsConnectionSelectionEnabled.Should().BeFalse(); + } + + [TestMethod] + public void IsConnectionSelectionEnabled_ProjectIsNotBoundAndBindingIsNotInProgressAndConnectionsExist_ReturnsTrue() + { + testSubject.BoundProject = null; + progressReporterViewModel.IsOperationInProgress.Returns(false); + MockTryGetAllConnectionsInfo([sonarCloudConnectionInfo]); + testSubject.LoadConnections(); + + testSubject.IsConnectionSelectionEnabled.Should().BeTrue(); + } + + [TestMethod] + public void UpdateProgress_RaisesEvents() + { + var eventHandler = Substitute.For(); + testSubject.PropertyChanged += eventHandler; + eventHandler.ReceivedCalls().Should().BeEmpty(); + + testSubject.UpdateProgress("In progress..."); + + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.IsUseSharedBindingButtonEnabled))); + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.IsBindButtonEnabled))); + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.IsUnbindButtonEnabled))); + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.IsManageConnectionsButtonEnabled))); + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.IsSelectProjectButtonEnabled))); + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.IsConnectionSelectionEnabled))); + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.IsExportButtonEnabled))); + } + + [TestMethod] + public void IsExportButtonEnabled_BindingIsInProgress_ReturnsFalse() + { + testSubject.BoundProject = serverProject; + progressReporterViewModel.IsOperationInProgress.Returns(true); + + testSubject.IsExportButtonEnabled.Should().BeFalse(); + } + + [TestMethod] + [DataRow(false)] + [DataRow(true)] + public void IsExportButtonEnabled_ProjectIsNotBound_ReturnsFalse(bool isBindingInProgress) + { + testSubject.BoundProject = null; + progressReporterViewModel.IsOperationInProgress.Returns(isBindingInProgress); + + testSubject.IsExportButtonEnabled.Should().BeFalse(); + } + + [TestMethod] + public void IsExportButtonEnabled_ProjectIsBoundAndBindingIsNotInProgress_ReturnsTrue() + { + testSubject.BoundProject = serverProject; + progressReporterViewModel.IsOperationInProgress.Returns(false); + + testSubject.IsExportButtonEnabled.Should().BeTrue(); + } + + [TestMethod] + public void LoadConnections_FillsConnections() + { + List existingConnections = [sonarQubeConnectionInfo, sonarCloudConnectionInfo]; + MockTryGetAllConnectionsInfo(existingConnections); + + testSubject.LoadConnections(); + + testSubject.Connections.Should().BeEquivalentTo(existingConnections); + } + + [TestMethod] + public void LoadConnections_ClearsPreviousConnections() + { + MockTryGetAllConnectionsInfo([sonarQubeConnectionInfo]); + testSubject.Connections.Add(sonarCloudConnectionInfo); + + testSubject.LoadConnections(); + + testSubject.Connections.Should().BeEquivalentTo([sonarQubeConnectionInfo]); + } + + [TestMethod] + public void LoadConnections_RaisesEvents() + { + var eventHandler = Substitute.For(); + testSubject.PropertyChanged += eventHandler; + eventHandler.ReceivedCalls().Should().BeEmpty(); + + testSubject.LoadConnections(); + + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.IsConnectionSelectionEnabled))); + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.ConnectionSelectionCaptionText))); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void LoadConnectionsAsync_ReturnsResponseFromAdapter(bool expectedStatus) + { + serverConnectionsRepositoryAdapter.TryGetAllConnectionsInfo(out Arg.Any>()).Returns(expectedStatus); + + var succeeded = testSubject.LoadConnections(); + + succeeded.Should().Be(expectedStatus); + } + + [TestMethod] + public async Task InitializeDataAsync_InitializesDataAndReportsProgress() + { + await testSubject.InitializeDataAsync(); + + await progressReporterViewModel.Received(1) + .ExecuteTaskWithProgressAsync( + Arg.Is>(x => + x.TaskToPerform == testSubject.LoadDataAsync && + x.ProgressStatus == UiResources.LoadingConnectionsText && + x.WarningText == UiResources.LoadingConnectionsFailedText && + x.AfterProgressUpdated == testSubject.OnProgressUpdated)); + } + + [TestMethod] + public async Task InitializeDataAsync_DisplaysBindStatusAndReportsProgress() + { + await testSubject.InitializeDataAsync(); + + await progressReporterViewModel.Received(1) + .ExecuteTaskWithProgressAsync( + Arg.Is>(x => + x.TaskToPerform == testSubject.DisplayBindStatusAsync && + x.ProgressStatus == UiResources.FetchingBindingStatusText && + x.WarningText == UiResources.FetchingBindingStatusFailedText && + x.AfterProgressUpdated == testSubject.OnProgressUpdated)); + } + + [TestMethod] + public async Task DisplayBindStatusAsync_WhenProjectIsNotBound_Succeeds() + { + SetupUnboundProject(); + + var response = await testSubject.DisplayBindStatusAsync(); + + response.Should().BeEquivalentTo(new AdapterResponse(true)); + } + + [TestMethod] + public async Task DisplayBindStatusAsync_WhenProjectIsBoundAndBindingStatusIsFetched_Succeeds() + { + var sonarCloudConnection = new ServerConnection.SonarCloud("organization", credentials: validCredentials); + SetupBoundProject(sonarCloudConnection, serverProject); + + var response = await testSubject.DisplayBindStatusAsync(); + + testSubject.BoundProject.Should().NotBeNull(); + response.Should().BeEquivalentTo(new AdapterResponse(true)); + } + + [TestMethod] + public async Task DisplayBindStatusAsync_WhenProjectIsBoundButBindingStatusIsNotFetched_Fails() + { + var sonarCloudConnection = new ServerConnection.SonarCloud("organization", credentials: validCredentials); + SetupBoundProjectThatDoesNotExistOnServer(sonarCloudConnection); + + var response = await testSubject.DisplayBindStatusAsync(); + + testSubject.BoundProject.Should().BeNull(); + response.Should().BeEquivalentTo(new AdapterResponse(false)); + } + + [TestMethod] + public async Task DisplayBindStatusAsync_WhenSolutionIsOpen_FetchesSolutionInfo() + { + solutionInfoProvider.GetSolutionNameAsync().Returns("Local solution name"); + solutionInfoProvider.IsFolderWorkspaceAsync().Returns(false); + + await testSubject.DisplayBindStatusAsync(); + + testSubject.SolutionInfo.Should().BeEquivalentTo(new SolutionInfoModel("Local solution name", SolutionType.Solution)); + } + + [TestMethod] + public async Task DisplayBindStatusAsync_WhenFolderIsOpen_FetchesSolutionInfo() + { + solutionInfoProvider.GetSolutionNameAsync().Returns("Local folder name"); + solutionInfoProvider.IsFolderWorkspaceAsync().Returns(true); + + await testSubject.DisplayBindStatusAsync(); + + testSubject.SolutionInfo.Should().BeEquivalentTo(new SolutionInfoModel("Local folder name", SolutionType.Folder)); + } + + [TestMethod] + public async Task DisplayBindStatusAsync_WhenProjectIsBoundToSonarCloud_SelectsBoundSonarCloudConnection() + { + var sonarCloudConnection = new ServerConnection.SonarCloud("organization", credentials: validCredentials); + SetupBoundProject(sonarCloudConnection); + + await testSubject.DisplayBindStatusAsync(); + + testSubject.SelectedConnectionInfo.Should().BeEquivalentTo(new ConnectionInfo("organization", ConnectionServerType.SonarCloud)); + } + + [TestMethod] + public async Task DisplayBindStatusAsync_WhenProjectIsBoundToSonarQube_SelectsBoundSonarQubeConnection() + { + var sonarQubeConnection = new ServerConnection.SonarQube(new Uri("http://localhost:9000/"), credentials: validCredentials); + SetupBoundProject(sonarQubeConnection); + + await testSubject.DisplayBindStatusAsync(); + + testSubject.SelectedConnectionInfo.Should().BeEquivalentTo(new ConnectionInfo("http://localhost:9000/", ConnectionServerType.SonarQube)); + } + + [TestMethod] + public async Task DisplayBindStatusAsync_WhenProjectIsNotBound_SelectedConnectionShouldBeEmpty() + { + SetupUnboundProject(); + + await testSubject.DisplayBindStatusAsync(); + + testSubject.SelectedConnectionInfo.Should().BeNull(); + } + + [TestMethod] + public async Task DisplayBindStatusAsync_WhenProjectIsBound_SelectsServerProject() + { + var expectedServerProject = new ServerProject("server-project-key", "server-project-name"); + var sonarCloudConnection = new ServerConnection.SonarCloud("organization", credentials: validCredentials); + SetupBoundProject(sonarCloudConnection, expectedServerProject); + + await testSubject.DisplayBindStatusAsync(); + + testSubject.SelectedProject.Should().BeEquivalentTo(expectedServerProject); + testSubject.BoundProject.Should().BeEquivalentTo(testSubject.SelectedProject); + } + + [TestMethod] + public async Task DisplayBindStatusAsync_WhenProjectIsBoundButProjectNotFoundOnServer_SelectedProjectShouldBeEmpty() + { + var sonarCloudConnection = new ServerConnection.SonarCloud("organization", credentials: validCredentials); + SetupBoundProjectThatDoesNotExistOnServer(sonarCloudConnection); + + await testSubject.DisplayBindStatusAsync(); + + testSubject.SelectedProject.Should().BeNull(); + testSubject.BoundProject.Should().BeNull(); + } + + [TestMethod] + public async Task LoadDataAsync_LoadsConnectionsOnUIThread() + { + await testSubject.LoadDataAsync(); + + await threadHandling.Received(1).RunOnUIThreadAsync(Arg.Any()); + } + + [TestMethod] + public async Task LoadDataAsync_LoadingConnectionsThrows_ReturnsFalse() + { + var exceptionMsg = "Failed to load connections"; + var mockedThreadHandling = Substitute.For(); + connectedModeServices.ThreadHandling.Returns(mockedThreadHandling); + mockedThreadHandling.When(x => x.RunOnUIThreadAsync(Arg.Any())).Do(callInfo=> throw new Exception(exceptionMsg)); + + var adapterResponse = await testSubject.LoadDataAsync(); + + adapterResponse.Success.Should().BeFalse(); + logger.Received(1).WriteLine(exceptionMsg); + } + + [TestMethod] + public void ConnectionSelectionCaptionText_ConnectionsExists_ReturnsSelectConnectionToBindDescription() + { + testSubject.Connections.Add(sonarCloudConnectionInfo); + + testSubject.ConnectionSelectionCaptionText.Should().Be(UiResources.SelectConnectionToBindDescription); + } + + [TestMethod] + public void ConnectionSelectionCaptionText_NoConnectionExists_ReturnsNoConnectionExistsLabel() + { + testSubject.SelectedConnectionInfo = null; + + testSubject.ConnectionSelectionCaptionText.Should().Be(UiResources.NoConnectionExistsLabel); + } + + [TestMethod] + public async Task BindWithProgressAsync_BindsProjectAndReportsProgress() + { + await testSubject.BindWithProgressAsync(); + + await progressReporterViewModel.Received(1) + .ExecuteTaskWithProgressAsync( + Arg.Is>(x => + x.TaskToPerform == testSubject.BindAsync && + x.ProgressStatus == UiResources.BindingInProgressText && + x.WarningText == UiResources.BindingFailedText && + x.AfterProgressUpdated == testSubject.OnProgressUpdated)); + } + + [TestMethod] + public async Task BindAsync_WhenConnectionNotFound_Fails() + { + var connectionInfo = new ConnectionInfo("organization", ConnectionServerType.SonarCloud); + testSubject.SelectedConnectionInfo = connectionInfo; + serverConnectionsRepositoryAdapter.TryGet(connectionInfo, out _).Returns(callInfo => + { + callInfo[1] = null; + return false; + }); + + var response = await testSubject.BindAsync(); + + response.Success.Should().BeFalse(); + } + + [TestMethod] + public async Task BindAsync_WhenBindingFailsUnexpectedly_FailsAndLogs() + { + var sonarCloudConnection = new ServerConnection.SonarCloud("organization", credentials: validCredentials); + SetupConnectionAndProjectToBind(sonarCloudConnection, serverProject); + bindingController.BindAsync(Arg.Any(), Arg.Any()).ThrowsAsync(new Exception("Failed unexpectedly")); + + var response = await testSubject.BindAsync(); + + response.Success.Should().BeFalse(); + logger.Received(1).WriteLine(Resources.Binding_Fails, "Failed unexpectedly"); + } + + [TestMethod] + public async Task BindAsync_WhenBindingCompletesSuccessfully_Succeeds() + { + var sonarCloudConnection = new ServerConnection.SonarCloud("organization", credentials: validCredentials); + SetupConnectionAndProjectToBind(sonarCloudConnection, serverProject); + + var response = await testSubject.BindAsync(); + + response.Success.Should().BeTrue(); + } + + [TestMethod] + public async Task BindAsync_WhenBindingCompletesSuccessfully_SetsBoundProjectToSelectedProject() + { + var sonarCloudConnection = new ServerConnection.SonarCloud("organization", credentials: validCredentials); + SetupConnectionAndProjectToBind(sonarCloudConnection, serverProject); + + await testSubject.BindAsync(); + + 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>(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(proj => + proj.ServerProjectKey == testSubject.SharedBindingConfigModel.ProjectKey), Arg.Any()); + } + + [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(proj => + proj.ServerProjectKey == testSubject.SharedBindingConfigModel.ProjectKey), Arg.Any()); + } + + [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).TryGet(new ConnectionInfo(testSubject.SharedBindingConfigModel.Uri.ToString(), ConnectionServerType.SonarQube), out _); + await bindingController.Received(1) + .BindAsync(Arg.Is(proj => proj.ServerConnection == expectedServerConnection), Arg.Any()); + } + + [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).TryGet(new ConnectionInfo(testSubject.SharedBindingConfigModel.Organization, ConnectionServerType.SonarCloud), out _); + await bindingController.Received(1) + .BindAsync(Arg.Is(proj => proj.ServerConnection == expectedServerConnection), Arg.Any()); + } + + [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(proj => + proj.ServerProjectKey == testSubject.SharedBindingConfigModel.ProjectKey), Arg.Any()); + } + + [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(proj => + proj.ServerProjectKey == testSubject.SharedBindingConfigModel.ProjectKey), Arg.Any()); + } + + [TestMethod] + public async Task UseSharedBindingAsync_SharedBindingSonarCloudConnectionWithMissingCredentials_ReturnsFalseAndLogsAndInformsUser() + { + testSubject.SharedBindingConfigModel = sonarCloudSharedBindingConfigModel; + var expectedServerConnection = new ServerConnection.SonarCloud(testSubject.SharedBindingConfigModel.Organization); + SetupBoundProject(expectedServerConnection); + expectedServerConnection.Credentials = null; + + var response = await testSubject.UseSharedBindingAsync(); + + response.Success.Should().BeFalse(); + logger.WriteLine(Resources.UseSharedBinding_CredentiasNotFound, testSubject.SharedBindingConfigModel.Organization); + messageBox.Received(1).Show(UiResources.NotFoundCredentialsForSharedBindingMessageBoxText, UiResources.NotFoundCredentialsForSharedBindingMessageBoxCaption, MessageBoxButton.OK, MessageBoxImage.Warning); + await bindingController.DidNotReceive() + .BindAsync(Arg.Is(proj => + proj.ServerProjectKey == testSubject.SharedBindingConfigModel.ProjectKey), Arg.Any()); + } + + + [TestMethod] + public async Task UseSharedBindingAsync_SharedBindingSonarQubeConnectionWithMissingCredentials_ReturnsFalseAndLogsAndInformsUser() + { + testSubject.SharedBindingConfigModel = sonarQubeSharedBindingConfigModel; + var expectedServerConnection = new ServerConnection.SonarQube(testSubject.SharedBindingConfigModel.Uri); + SetupBoundProject(expectedServerConnection); + expectedServerConnection.Credentials = null; + + var response = await testSubject.UseSharedBindingAsync(); + + response.Success.Should().BeFalse(); + logger.WriteLine(Resources.UseSharedBinding_CredentiasNotFound, testSubject.SharedBindingConfigModel.Uri); + messageBox.Received(1).Show(UiResources.NotFoundCredentialsForSharedBindingMessageBoxText, UiResources.NotFoundCredentialsForSharedBindingMessageBoxCaption, MessageBoxButton.OK, MessageBoxImage.Warning); + await bindingController.DidNotReceive() + .BindAsync(Arg.Is(proj => + proj.ServerProjectKey == testSubject.SharedBindingConfigModel.ProjectKey), Arg.Any()); + } + + [TestMethod] + public async Task UseSharedBindingAsync_BindingFails_ReturnsFalse() + { + var sonarCloudConnection = new ServerConnection.SonarCloud("organization", credentials: validCredentials); + MockTryGetServerConnection(sonarCloudConnection); + bindingController.When(x => x.BindAsync(Arg.Any(), Arg.Any())) + .Do(_ => throw new Exception()); + testSubject.SharedBindingConfigModel = sonarCloudSharedBindingConfigModel; + + var response = await testSubject.UseSharedBindingAsync(); + + response.Success.Should().BeFalse(); + } + + + private void MockServices() + { + serverConnectionsRepositoryAdapter = Substitute.For(); + threadHandling = Substitute.For(); + logger = Substitute.For(); + messageBox = Substitute.For(); + + connectedModeServices.ServerConnectionsRepositoryAdapter.Returns(serverConnectionsRepositoryAdapter); + connectedModeServices.ThreadHandling.Returns(threadHandling); + connectedModeServices.Logger.Returns(logger); + connectedModeServices.MessageBox.Returns(messageBox); + + bindingController = Substitute.For(); + solutionInfoProvider = Substitute.For(); + sharedBindingConfigProvider = Substitute.For(); + connectedModeBindingServices.BindingController.Returns(bindingController); + connectedModeBindingServices.SolutionInfoProvider.Returns(solutionInfoProvider); + connectedModeBindingServices.SharedBindingConfigProvider.Returns(sharedBindingConfigProvider); + + MockTryGetAllConnectionsInfo([]); + } + + private void MockTryGetAllConnectionsInfo(List connectionInfos) + { + connectedModeServices.ServerConnectionsRepositoryAdapter.TryGetAllConnectionsInfo(out _).Returns(callInfo => + { + callInfo[0] = connectionInfos; + return true; + }); + } + + private void SetupConnectionAndProjectToBind(ServerConnection selectedServerConnection, ServerProject selectedServerProject) + { + SetupBoundProject(selectedServerConnection, selectedServerProject); + testSubject.SelectedConnectionInfo = sonarCloudConnectionInfo; + testSubject.SelectedProject = selectedServerProject; + } + + private void SetupBoundProject(ServerConnection serverConnection, ServerProject expectedServerProject = null) + { + expectedServerProject ??= serverProject; + + serverConnection.Credentials = validCredentials; + var boundServerProject = new BoundServerProject(ALocalProjectKey, expectedServerProject.Key, serverConnection); + var configurationProvider = Substitute.For(); + configurationProvider.GetConfiguration().Returns(new BindingConfiguration(boundServerProject, SonarLintMode.Connected, "binding-dir")); + connectedModeServices.ConfigurationProvider.Returns(configurationProvider); + MockTryGetServerConnection(serverConnection); + solutionInfoProvider.GetSolutionNameAsync().Returns(ALocalProjectKey); + + MockGetServerProjectByKey(true, expectedServerProject); + } + + private void MockTryGetServerConnection(ServerConnection expectedServerConnection = null) + { + serverConnectionsRepositoryAdapter.TryGet(Arg.Any(), out _).Returns(callInfo => + { + callInfo[1] = expectedServerConnection; + return true; + }); + } + + private void SetupUnboundProject() + { + var configurationProvider = Substitute.For(); + configurationProvider.GetConfiguration().Returns(new BindingConfiguration(null, SonarLintMode.Standalone, null)); + connectedModeServices.ConfigurationProvider.Returns(configurationProvider); + + MockGetServerProjectByKey(false, null); + } + + private void SetupBoundProjectThatDoesNotExistOnServer(ServerConnection serverConnection) + { + var boundServerProject = new BoundServerProject(ALocalProjectKey, "a-server-project", serverConnection); + var configurationProvider = Substitute.For(); + configurationProvider.GetConfiguration().Returns(new BindingConfiguration(boundServerProject, SonarLintMode.Connected, "binding-dir")); + connectedModeServices.ConfigurationProvider.Returns(configurationProvider); + + MockGetServerProjectByKey(false, null); + } + + private void MockGetServerProjectByKey(bool success, ServerProject responseData) + { + var slCoreConnectionAdapter = Substitute.For(); + slCoreConnectionAdapter.GetServerProjectByKeyAsync(Arg.Any(),Arg.Any()) + .Returns(Task.FromResult(new AdapterResponseWithData(success, responseData))); + connectedModeServices.SlCoreConnectionAdapter.Returns(slCoreConnectionAdapter); + } +} diff --git a/src/ConnectedMode.UnitTests/UI/ManageConnections/ManageConnectionsViewModelTest.cs b/src/ConnectedMode.UnitTests/UI/ManageConnections/ManageConnectionsViewModelTest.cs new file mode 100644 index 0000000000..815b6bd1f1 --- /dev/null +++ b/src/ConnectedMode.UnitTests/UI/ManageConnections/ManageConnectionsViewModelTest.cs @@ -0,0 +1,463 @@ +/* + * 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 System.ComponentModel; +using SonarLint.VisualStudio.ConnectedMode.UI; +using SonarLint.VisualStudio.ConnectedMode.UI.Credentials; +using SonarLint.VisualStudio.ConnectedMode.UI.ManageConnections; +using SonarLint.VisualStudio.ConnectedMode.UI.Resources; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Binding; + +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.UI.ManageConnections; + +[TestClass] +public class ManageConnectionsViewModelTest +{ + private ManageConnectionsViewModel testSubject; + private List twoConnections; + private IProgressReporterViewModel progressReporterViewModel; + private IConnectedModeServices connectedModeServices; + private IServerConnectionsRepositoryAdapter serverConnectionsRepositoryAdapter; + private IThreadHandling threadHandling; + private ILogger logger; + private IConnectedModeBindingServices connectedModeBindingServices; + private ISolutionBindingRepository solutionBindingRepository; + + [TestInitialize] + public void TestInitialize() + { + twoConnections = + [ + new Connection(new ConnectionInfo(new Uri("http://localhost:9000").ToString(), ConnectionServerType.SonarQube), true), + new Connection(new ConnectionInfo("myOrg", ConnectionServerType.SonarCloud), false) + ]; + progressReporterViewModel = Substitute.For(); + connectedModeServices = Substitute.For(); + connectedModeBindingServices = Substitute.For(); + + testSubject = new ManageConnectionsViewModel(connectedModeServices, connectedModeBindingServices, progressReporterViewModel); + + MockServices(); + } + + [TestMethod] + public void ConnectionViewModels_NoInitialization_HasEmptyList() + { + testSubject.ConnectionViewModels.Should().NotBeNull(); + testSubject.ConnectionViewModels.Count.Should().Be(0); + } + + [TestMethod] + public void InitializeConnectionViewModels_InitializesConnectionsCorrectly() + { + MockTryGetConnections(twoConnections); + + testSubject.InitializeConnectionViewModels(); + + HasExpectedConnections(twoConnections); + } + + [TestMethod] + public async Task RemoveConnectionWithProgressAsync_InitializesDataAndReportsProgress() + { + await testSubject.RemoveConnectionWithProgressAsync(new ConnectionViewModel(new Connection(new ConnectionInfo("myOrg", ConnectionServerType.SonarCloud)))); + + await progressReporterViewModel.Received(1) + .ExecuteTaskWithProgressAsync( + Arg.Is>(x => + x.ProgressStatus == UiResources.RemovingConnectionText && + x.WarningText == UiResources.RemovingConnectionFailedText)); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void RemoveConnectionViewModel_ReturnsStatusFromSlCore(bool expectedStatus) + { + InitializeTwoConnections(); + var connectionToRemove = testSubject.ConnectionViewModels[0]; + serverConnectionsRepositoryAdapter.TryRemoveConnection(connectionToRemove.Connection.Info).Returns(expectedStatus); + + var succeeded = testSubject.RemoveConnectionViewModel(connectionToRemove); + + succeeded.Should().Be(expectedStatus); + serverConnectionsRepositoryAdapter.Received(1).TryRemoveConnection(connectionToRemove.Connection.Info); + } + + [TestMethod] + public void RemoveConnection_ConnectionWasRemoved_RemovesProvidedConnectionViewModel() + { + InitializeTwoConnections(); + var connectionToRemove = testSubject.ConnectionViewModels[0]; + serverConnectionsRepositoryAdapter.TryRemoveConnection(connectionToRemove.Connection.Info).Returns(true); + + testSubject.RemoveConnectionViewModel(connectionToRemove); + + testSubject.ConnectionViewModels.Count.Should().Be(twoConnections.Count - 1); + testSubject.ConnectionViewModels.Should().NotContain(connectionToRemove); + } + + [TestMethod] + public void RemoveConnectionViewModel_ConnectionWasNotRemoved_DoesNotRemoveProvidedConnectionViewModel() + { + InitializeTwoConnections(); + var connectionToRemove = testSubject.ConnectionViewModels[0]; + serverConnectionsRepositoryAdapter.TryRemoveConnection(connectionToRemove.Connection.Info).Returns(false); + + testSubject.RemoveConnectionViewModel(connectionToRemove); + + testSubject.ConnectionViewModels.Count.Should().Be(twoConnections.Count); + testSubject.ConnectionViewModels.Should().Contain(connectionToRemove); + } + + [TestMethod] + public void RemoveConnection_ConnectionWasRemoved_RaisesEvents() + { + InitializeTwoConnections(); + serverConnectionsRepositoryAdapter.TryRemoveConnection(Arg.Any()).Returns(true); + var eventHandler = Substitute.For(); + testSubject.PropertyChanged += eventHandler; + + testSubject.RemoveConnectionViewModel(testSubject.ConnectionViewModels[0]); + + eventHandler.Received().Invoke(testSubject, Arg.Is(x => x.PropertyName == nameof(testSubject.NoConnectionExists))); + } + + [TestMethod] + public void RemoveConnectionViewModel_ConnectionWasNotRemoved_DoesNotRaiseEvents() + { + InitializeTwoConnections(); + serverConnectionsRepositoryAdapter.TryRemoveConnection(Arg.Any()).Returns(false); + var eventHandler = Substitute.For(); + testSubject.PropertyChanged += eventHandler; + + testSubject.RemoveConnectionViewModel(testSubject.ConnectionViewModels[0]); + + eventHandler.DidNotReceive().Invoke(testSubject, Arg.Any()); + } + + [TestMethod] + public async Task SafeExecuteActionAsync_LoadsConnectionsOnUIThread() + { + await testSubject.SafeExecuteActionAsync(() => true); + + await threadHandling.Received(1).RunOnUIThreadAsync(Arg.Any()); + } + + [TestMethod] + public async Task SafeExecuteActionAsync_LoadingConnectionsThrows_ReturnsFalse() + { + var exceptionMsg = "Failed to load connections"; + var mockedThreadHandling = Substitute.For(); + connectedModeServices.ThreadHandling.Returns(mockedThreadHandling); + mockedThreadHandling.When(x => x.RunOnUIThreadAsync(Arg.Any())).Do(callInfo => throw new Exception(exceptionMsg)); + + var adapterResponse = await testSubject.SafeExecuteActionAsync(() => true); + + adapterResponse.Success.Should().BeFalse(); + logger.Received(1).WriteLine(exceptionMsg); + } + + [TestMethod] + public void AddConnectionViewModel_AddsProvidedConnection() + { + var connectionToAdd = new Connection(new ConnectionInfo("mySecondOrg", ConnectionServerType.SonarCloud), false); + + testSubject.AddConnectionViewModel(connectionToAdd); + + testSubject.ConnectionViewModels.Count.Should().Be( 1); + testSubject.ConnectionViewModels[0].Connection.Should().Be(connectionToAdd); + } + + [TestMethod] + public void AddConnectionViewModel_RaisesEvents() + { + var eventHandler = Substitute.For(); + testSubject.PropertyChanged += eventHandler; + + testSubject.AddConnectionViewModel(new Connection(new ConnectionInfo("mySecondOrg", ConnectionServerType.SonarCloud), false)); + + eventHandler.Received().Invoke(testSubject, Arg.Is(x => x.PropertyName == nameof(testSubject.NoConnectionExists))); + } + + [TestMethod] + public void NoConnectionExists_NoConnections_ReturnsTrue() + { + MockTryGetConnections([]); + + testSubject.InitializeConnectionViewModels(); + + testSubject.NoConnectionExists.Should().BeTrue(); + } + + [TestMethod] + public void NoConnectionExists_HasConnections_ReturnsFalse() + { + InitializeTwoConnections(); + + testSubject.NoConnectionExists.Should().BeFalse(); + } + + [TestMethod] + public async Task LoadConnectionsWithProgressAsync_InitializesDataAndReportsProgress() + { + await testSubject.LoadConnectionsWithProgressAsync(); + + await progressReporterViewModel.Received(1) + .ExecuteTaskWithProgressAsync( + Arg.Is>(x => + x.ProgressStatus == UiResources.LoadingConnectionsText && + x.WarningText == UiResources.LoadingConnectionsFailedText)); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void InitializeConnectionViewModels_ReturnsResponseFromAdapter(bool expectedStatus) + { + serverConnectionsRepositoryAdapter.TryGetAllConnections(out _).Returns(expectedStatus); + + var adapterResponse = testSubject.InitializeConnectionViewModels(); + + adapterResponse.Should().Be(expectedStatus); + } + + [TestMethod] + public async Task CreateConnectionsWithProgressAsync_InitializesDataAndReportsProgress() + { + var connectionToAdd = CreateSonarCloudConnection(); + + await testSubject.CreateConnectionsWithProgressAsync(connectionToAdd, Substitute.For()); + + await progressReporterViewModel.Received(1) + .ExecuteTaskWithProgressAsync( + Arg.Is>(x => + x.ProgressStatus == UiResources.CreatingConnectionProgressText && + x.WarningText == UiResources.CreatingConnectionFailedText)); + } + + [TestMethod] + public void CreateNewConnection_ConnectionWasAddedToRepository_AddsProvidedConnection() + { + var connectionToAdd = new Connection(new ConnectionInfo("mySecondOrg", ConnectionServerType.SonarCloud), false); + serverConnectionsRepositoryAdapter.TryAddConnection(connectionToAdd, Arg.Any()).Returns(true); + + var succeeded = testSubject.CreateNewConnection(connectionToAdd, Substitute.For()); + + succeeded.Should().BeTrue(); + testSubject.ConnectionViewModels.Count.Should().Be(1); + testSubject.ConnectionViewModels[0].Connection.Should().Be(connectionToAdd); + serverConnectionsRepositoryAdapter.Received(1).TryAddConnection(connectionToAdd, Arg.Any()); + } + + [TestMethod] + public void CreateNewConnection_ConnectionWasNotAddedToRepository_DoesNotAddConnection() + { + var connectionToAdd = new Connection(new ConnectionInfo("mySecondOrg", ConnectionServerType.SonarCloud), false); + serverConnectionsRepositoryAdapter.TryAddConnection(connectionToAdd, Arg.Any()).Returns(false); + + var succeeded = testSubject.CreateNewConnection(connectionToAdd, Substitute.For()); + + succeeded.Should().BeFalse(); + testSubject.ConnectionViewModels.Should().BeEmpty(); + serverConnectionsRepositoryAdapter.Received(1).TryAddConnection(connectionToAdd, Arg.Any()); + } + + [TestMethod] + public async Task GetConnectionReferencesWithProgressAsync_CalculatesReferencesAndReportsProgress() + { + progressReporterViewModel.ExecuteTaskWithProgressAsync(Arg.Any>>>()).Returns(new AdapterResponseWithData>(true, [])); + + await testSubject.GetConnectionReferencesWithProgressAsync(new ConnectionViewModel(new Connection(new ConnectionInfo("myOrg", ConnectionServerType.SonarCloud)))); + + await progressReporterViewModel.Received(1) + .ExecuteTaskWithProgressAsync( + Arg.Is>>>(x => + x.ProgressStatus == UiResources.CalculatingConnectionReferencesText && + x.WarningText == UiResources.CalculatingConnectionReferencesFailedText)); + } + + [TestMethod] + public async Task GetConnectionReferencesOnBackgroundThreadAsync_RunsOnBackgroundThread() + { + threadHandling.RunOnBackgroundThread(Arg.Any>>>>()).Returns(new AdapterResponseWithData>(true, [])); + + await testSubject.GetConnectionReferencesOnBackgroundThreadAsync(new ConnectionViewModel(new Connection(new ConnectionInfo("myOrg", ConnectionServerType.SonarCloud)))); + + await threadHandling.Received(1).RunOnBackgroundThread(Arg.Any>>>>()); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public async Task GetConnectionReferencesOnBackgroundThreadAsync_ReturnsCalculatedReferences(bool expectedResponse) + { + var bindingKey = "localBindingKey"; + threadHandling.RunOnBackgroundThread(Arg.Any>>>>()).Returns(new AdapterResponseWithData>(expectedResponse, [bindingKey])); + + var responses = await testSubject.GetConnectionReferencesOnBackgroundThreadAsync(new ConnectionViewModel(new Connection(new ConnectionInfo("myOrg", ConnectionServerType.SonarCloud)))); + + responses.Success.Should().Be(expectedResponse); + responses.ResponseData.Should().Contain(bindingKey); + } + + [TestMethod] + public void GetConnectionReferences_NoBindingReferencesConnection_ReturnsEmptyList() + { + var response = testSubject.GetConnectionReferences(new ConnectionViewModel(new Connection(new ConnectionInfo("myOrg", ConnectionServerType.SonarCloud)))); + + response.Success.Should().BeTrue(); + response.ResponseData.Should().BeEmpty(); + } + + [TestMethod] + public void GetConnectionReferences_OneBindingReferencesSonarCloudConnection_ReturnsOneBinding() + { + var bindingKey = "localBindingKey"; + var sonarCloud = twoConnections.First(conn => conn.Info.ServerType == ConnectionServerType.SonarCloud); + solutionBindingRepository.List().Returns([new BoundServerProject(bindingKey, "myProject", CreateSonarCloudServerConnection(sonarCloud))]); + + var response = testSubject.GetConnectionReferences(new ConnectionViewModel(sonarCloud)); + + response.Success.Should().BeTrue(); + response.ResponseData.Should().Contain(bindingKey); + } + + [TestMethod] + public void GetConnectionReferences_OneBindingReferencesSonarQubeConnection_ReturnsOneBinding() + { + var bindingKey = "localBindingKey"; + var sonarQube = twoConnections.First(conn => conn.Info.ServerType == ConnectionServerType.SonarQube); + solutionBindingRepository.List().Returns([new BoundServerProject(bindingKey, "myProject", CreateSonarQubeServerConnection(sonarQube))]); + + var response = testSubject.GetConnectionReferences(new ConnectionViewModel(sonarQube)); + + response.Success.Should().BeTrue(); + response.ResponseData.Should().Contain(bindingKey); + } + + [TestMethod] + public void GetConnectionReferences_TwoBindingsReferencesSonarQubeConnection_ReturnsTwoBindings() + { + var sonarQube = twoConnections.First(conn => conn.Info.ServerType == ConnectionServerType.SonarQube); + var serverConnectionToBeRemoved = CreateSonarQubeServerConnection(sonarQube); + solutionBindingRepository.List().Returns([ + new BoundServerProject("binding1", "myProject", serverConnectionToBeRemoved), + new BoundServerProject("binding2", "myProject2", serverConnectionToBeRemoved) + ]); + + var response = testSubject.GetConnectionReferences(new ConnectionViewModel(sonarQube)); + + response.Success.Should().BeTrue(); + response.ResponseData.Should().BeEquivalentTo(["binding1", "binding2"]); + } + + [TestMethod] + public void GetConnectionReferences_TwoBindingsReferencesSonarCloudConnection_ReturnsTwoBindings() + { + var sonarCloud = twoConnections.First(conn => conn.Info.ServerType == ConnectionServerType.SonarQube); + var serverConnectionToBeRemoved = CreateSonarCloudServerConnection(sonarCloud); + solutionBindingRepository.List().Returns([ + new BoundServerProject("binding1", "myProject", serverConnectionToBeRemoved), + new BoundServerProject("binding2", "myProject2", serverConnectionToBeRemoved) + ]); + + var response = testSubject.GetConnectionReferences(new ConnectionViewModel(sonarCloud)); + + response.Success.Should().BeTrue(); + response.ResponseData.Should().BeEquivalentTo(["binding1", "binding2"]); + } + + [TestMethod] + public void GetConnectionReferences_BindingRepositoryThrowsException_ReturnsEmptyList() + { + var exceptionMsg = "Failed to retrieve bindings"; + solutionBindingRepository.When(repo => repo.List()).Do(_ => throw new Exception(exceptionMsg)); + + var response = testSubject.GetConnectionReferences(new ConnectionViewModel(twoConnections.First())); + + response.Success.Should().BeFalse(); + response.ResponseData.Should().BeEmpty(); + logger.Received(1).WriteLine(nameof(testSubject.GetConnectionReferences), exceptionMsg); + } + + private void HasExpectedConnections(IEnumerable expectedConnections) + { + testSubject.ConnectionViewModels.Should().NotBeNull(); + testSubject.ConnectionViewModels.Count.Should().Be(twoConnections.Count); + foreach (var connection in expectedConnections) + { + var connectionViewModel = testSubject.ConnectionViewModels.SingleOrDefault(c => c.Name == connection.Info.Id); + connectionViewModel.Should().NotBeNull(); + connectionViewModel.ServerType.Should().Be(connection.Info.ServerType.ToString()); + connectionViewModel.EnableSmartNotifications.Should().Be(connection.EnableSmartNotifications); + } + } + + private void InitializeTwoConnections() + { + serverConnectionsRepositoryAdapter.TryGetAllConnections(out _).Returns(callInfo => + { + callInfo[0] = twoConnections; + return true; + }); + testSubject.InitializeConnectionViewModels(); + } + + private void MockServices() + { + serverConnectionsRepositoryAdapter = Substitute.For(); + threadHandling = Substitute.For(); + logger = Substitute.For(); + + connectedModeServices.ServerConnectionsRepositoryAdapter.Returns(serverConnectionsRepositoryAdapter); + connectedModeServices.ThreadHandling.Returns(threadHandling); + connectedModeServices.Logger.Returns(logger); + MockTryGetConnections(twoConnections); + + solutionBindingRepository = Substitute.For(); + connectedModeBindingServices.SolutionBindingRepository.Returns(solutionBindingRepository); + } + + private void MockTryGetConnections(List connections) + { + serverConnectionsRepositoryAdapter.TryGetAllConnections(out _).Returns(callInfo => + { + callInfo[0] = connections; + return true; + }); + } + + private static Connection CreateSonarCloudConnection() + { + return new Connection(new ConnectionInfo("mySecondOrg", ConnectionServerType.SonarCloud), false); + } + + private static ServerConnection.SonarCloud CreateSonarCloudServerConnection(Connection sonarCloud) + { + return new ServerConnection.SonarCloud(sonarCloud.Info.Id); + } + + private static ServerConnection.SonarQube CreateSonarQubeServerConnection(Connection sonarQube) + { + return new ServerConnection.SonarQube(new Uri(sonarQube.Info.Id)); + } +} diff --git a/src/ConnectedMode.UnitTests/UI/OrganizationSelection/ManualOrganizationSelectionViewModelTests.cs b/src/ConnectedMode.UnitTests/UI/OrganizationSelection/ManualOrganizationSelectionViewModelTests.cs new file mode 100644 index 0000000000..9658f35ad4 --- /dev/null +++ b/src/ConnectedMode.UnitTests/UI/OrganizationSelection/ManualOrganizationSelectionViewModelTests.cs @@ -0,0 +1,68 @@ +/* + * 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 System.ComponentModel; +using SonarLint.VisualStudio.ConnectedMode.UI.OrganizationSelection; + +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.UI.OrganizationSelection; + +[TestClass] +public class ManualOrganizationSelectionViewModelTests +{ + private ManualOrganizationSelectionViewModel testSubject; + + [TestInitialize] + public void TestInitialize() + { + testSubject = new(); + } + + [TestMethod] + public void OrganizationKey_NotSet_DefaultsToNull() + { + testSubject.OrganizationKey.Should().BeNull(); + testSubject.IsValidOrganizationKey.Should().BeFalse(); + } + + [TestMethod] + public void OrganizationKey_Set_EventsRaised() + { + var eventHandler = Substitute.For(); + testSubject.PropertyChanged += eventHandler; + + testSubject.OrganizationKey = "key"; + + eventHandler.Received().Invoke(testSubject, Arg.Is(x => x.PropertyName == nameof(testSubject.OrganizationKey))); + eventHandler.Received().Invoke(testSubject, Arg.Is(x => x.PropertyName == nameof(testSubject.IsValidOrganizationKey))); + } + + [DataTestMethod] + [DataRow(null, false)] + [DataRow("", false)] + [DataRow(" ", false)] + [DataRow("my key", true)] + [DataRow("key", true)] + public void IsValidOrganizationKey_Validates(string key, bool expectedResult) + { + testSubject.OrganizationKey = key; + + testSubject.IsValidOrganizationKey.Should().Be(expectedResult); + } +} diff --git a/src/ConnectedMode.UnitTests/UI/OrganizationSelection/OrganizationSelectionViewModelTests.cs b/src/ConnectedMode.UnitTests/UI/OrganizationSelection/OrganizationSelectionViewModelTests.cs new file mode 100644 index 0000000000..d5ae228fee --- /dev/null +++ b/src/ConnectedMode.UnitTests/UI/OrganizationSelection/OrganizationSelectionViewModelTests.cs @@ -0,0 +1,268 @@ +/* + * 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 System.ComponentModel; +using Microsoft.VisualStudio.Threading; +using SonarLint.VisualStudio.ConnectedMode.UI; +using SonarLint.VisualStudio.ConnectedMode.UI.Credentials; +using SonarLint.VisualStudio.ConnectedMode.UI.OrganizationSelection; +using SonarLint.VisualStudio.ConnectedMode.UI.Resources; + +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.UI.OrganizationSelection; + +[TestClass] +public class OrganizationSelectionViewModelTests +{ + private OrganizationSelectionViewModel testSubject; + private ISlCoreConnectionAdapter slCoreConnectionAdapter; + private ICredentialsModel credentialsModel; + private IProgressReporterViewModel progressReporterViewModel; + + [TestInitialize] + 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); + } + + [TestMethod] + public void Ctor_Organizations_SetsEmptyAsDefault() + { + new OrganizationSelectionViewModel(credentialsModel, slCoreConnectionAdapter, progressReporterViewModel).Organizations.Should().BeEmpty(); + } + + [TestMethod] + public void Ctor_OrganizationList_SetsPropertyValue() + { + var organization = new OrganizationDisplay("key", "name"); + + testSubject.AddOrganization(organization); + + testSubject.Organizations.Count.Should().Be(1); + testSubject.Organizations[0].Should().Be(organization); + } + + [TestMethod] + public void FinalConnectionInfo_SetByDefaultToNull() + { + testSubject.FinalConnectionInfo.Should().BeNull(); + } + + [TestMethod] + public void SelectedOrganization_NotSet_ValueIsNull() + { + testSubject.SelectedOrganization.Should().BeNull(); + testSubject.IsValidSelectedOrganization.Should().BeFalse(); + } + + [TestMethod] + public void SelectedOrganization_Set_RaisesEvents() + { + var eventHandler = Substitute.For(); + testSubject.PropertyChanged += eventHandler; + + eventHandler.ReceivedCalls().Should().BeEmpty(); + + testSubject.SelectedOrganization = new OrganizationDisplay("key", "name"); + + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.SelectedOrganization))); + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.IsValidSelectedOrganization))); + } + + [TestMethod] + public void IsValidSelectedOrganization_NullOrganization_ReturnsFalse() + { + testSubject.SelectedOrganization = null; + + testSubject.IsValidSelectedOrganization.Should().BeFalse(); + } + + [DataTestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + public void IsValidSelectedOrganization_OrganizationWithInvalidKey_ReturnsFalse(string key) + { + testSubject.SelectedOrganization = new OrganizationDisplay(key, "value"); + + testSubject.IsValidSelectedOrganization.Should().BeFalse(); + } + + [DataTestMethod] + [DataRow("mykey")] + [DataRow("my key")] + public void IsValidSelectedOrganization_OrganizationWithValidKey_ReturnsTrue(string key) + { + testSubject.SelectedOrganization = new OrganizationDisplay(key, "value"); + + testSubject.IsValidSelectedOrganization.Should().BeTrue(); + } + + [DataTestMethod] + [DataRow("key", null, true)] + [DataRow("key", "name", true)] + [DataRow(null, null, false)] + [DataRow(null, "name", false)] + public void IsValidSelectedOrganization_IgnoresName(string key, string name, bool expectedResult) + { + testSubject.SelectedOrganization = new OrganizationDisplay(key, name); + + testSubject.IsValidSelectedOrganization.Should().Be(expectedResult); + } + + [TestMethod] + public void NoOrganizationExists_NoOrganizations_ReturnsTrue() + { + testSubject.NoOrganizationExists.Should().BeTrue(); + } + + [TestMethod] + public void NoOrganizationExists_HasOrganizations_ReturnsFalse() + { + testSubject.AddOrganization(new OrganizationDisplay("key", "name")); + + testSubject.NoOrganizationExists.Should().BeFalse(); + } + + [TestMethod] + public void AddOrganization_RaisesEvent() + { + var eventHandler = Substitute.For(); + testSubject.PropertyChanged += eventHandler; + eventHandler.ReceivedCalls().Should().BeEmpty(); + + testSubject.AddOrganization(new OrganizationDisplay("key", "name")); + + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.NoOrganizationExists))); + } + + [TestMethod] + public void AddOrganization_AddsToList() + { + testSubject.Organizations.Should().BeEmpty(); + var newOrganization = new OrganizationDisplay("key", "name"); + + testSubject.AddOrganization(newOrganization); + + testSubject.Organizations.Should().BeEquivalentTo(newOrganization); + } + + [TestMethod] + public async Task LoadOrganizationsAsync_AddsOrganization() + { + await testSubject.LoadOrganizationsAsync(); + + await progressReporterViewModel.Received(1) + .ExecuteTaskWithProgressAsync( + Arg.Is>>>(x => + x.TaskToPerform == testSubject.AdapterLoadOrganizationsAsync && + x.ProgressStatus == UiResources.LoadingOrganizationsProgressText && + x.WarningText == UiResources.LoadingOrganizationsFailedText && + x.AfterSuccess == testSubject.UpdateOrganizations)); + } + + [TestMethod] + public void UpdateOrganizations_AddsOrganization() + { + var loadedOrganizations = new List { new("key", "name") }; + var response = new AdapterResponseWithData>(true, loadedOrganizations); + + testSubject.UpdateOrganizations(response); + + testSubject.Organizations.Should().BeEquivalentTo(loadedOrganizations); + } + + [TestMethod] + public void UpdateOrganizations_ClearsPreviousOrganizations() + { + testSubject.Organizations.Add(new("key", "name")); + var loadedOrganizations = new List { new("new_key", "new_name") }; + var response = new AdapterResponseWithData>(true, loadedOrganizations); + + testSubject.UpdateOrganizations(response); + + testSubject.Organizations.Should().BeEquivalentTo(loadedOrganizations); + } + + [TestMethod] + public void UpdateOrganizations_RaisesEvents() + { + var eventHandler = Substitute.For(); + testSubject.PropertyChanged += eventHandler; + eventHandler.ReceivedCalls().Should().BeEmpty(); + var response = new AdapterResponseWithData>(true, []); + + testSubject.UpdateOrganizations(response); + + 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.UnitTests/UI/ProgressReporterViewModelTests.cs b/src/ConnectedMode.UnitTests/UI/ProgressReporterViewModelTests.cs new file mode 100644 index 0000000000..e5a2471ffc --- /dev/null +++ b/src/ConnectedMode.UnitTests/UI/ProgressReporterViewModelTests.cs @@ -0,0 +1,203 @@ +/* + * 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 System.ComponentModel; +using SonarLint.VisualStudio.ConnectedMode.UI; + +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.UI; + +[TestClass] +public class ProgressReporterViewModelTests +{ + private ProgressReporterViewModel testSubject; + + [TestInitialize] + public void TestInitialize() + { + testSubject = new ProgressReporterViewModel(); + } + + [TestMethod] + public void ProgressStatus_Set_RaisesEvents() + { + var eventHandler = Substitute.For(); + testSubject.PropertyChanged += eventHandler; + eventHandler.ReceivedCalls().Should().BeEmpty(); + + testSubject.ProgressStatus = "In progress..."; + + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.ProgressStatus))); + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.IsOperationInProgress))); + } + + [TestMethod] + public void IsOperationInProgress_ProgressStatusIsSet_ReturnsTrue() + { + testSubject.ProgressStatus = "In progress..."; + + testSubject.IsOperationInProgress.Should().BeTrue(); + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + public void IsOperationInProgress_ProgressStatusIsNull_ReturnsFalse(string status) + { + testSubject.ProgressStatus = status; + + testSubject.IsOperationInProgress.Should().BeFalse(); + } + + [TestMethod] + public void Warning_Set_RaisesEvents() + { + var eventHandler = Substitute.For(); + testSubject.PropertyChanged += eventHandler; + eventHandler.ReceivedCalls().Should().BeEmpty(); + + testSubject.Warning = "Process failed"; + + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.Warning))); + eventHandler.Received().Invoke(testSubject, + Arg.Is(x => x.PropertyName == nameof(testSubject.HasWarning))); + } + + [TestMethod] + public void HasWarning_WarningIsSet_ReturnsTrue() + { + testSubject.Warning = "Process failed"; + + testSubject.HasWarning.Should().BeTrue(); + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + public void HasWarning_WarningsIsNull_ReturnsFalse(string warning) + { + testSubject.Warning = warning; + + testSubject.HasWarning.Should().BeFalse(); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public async Task ExecuteTaskWithProgressAsync_ReturnsReceivedResponse(bool success) + { + var parameters = GetTaskWithResponse(success); + var taskResponse = new AdapterResponseWithData(true, null); + parameters.TaskToPerform().Returns(taskResponse); + + var response = await testSubject.ExecuteTaskWithProgressAsync(parameters); + + response.Should().Be(taskResponse); + } + + [TestMethod] + public async Task ExecuteTaskWithProgressAsync_TaskWithSuccessResponse_WorkflowIsCorrect() + { + var parameters = GetTaskWithResponse(true); + + await testSubject.ExecuteTaskWithProgressAsync(parameters); + + Received.InOrder(() => + { + _ = parameters.ProgressStatus; + parameters.AfterProgressUpdated(); + parameters.TaskToPerform(); + parameters.AfterSuccess(Arg.Any()); + parameters.AfterProgressUpdated(); + }); + testSubject.ProgressStatus.Should().BeNull(); + _ = parameters.DidNotReceive().WarningText; + } + + [TestMethod] + public async Task ExecuteTaskWithProgressAsync_TaskWithSuccessResponse_ClearsPreviousWarning() + { + var parameters = GetTaskWithResponse(true); + testSubject.Warning = "warning"; + + await testSubject.ExecuteTaskWithProgressAsync(parameters); + + testSubject.Warning.Should().BeNull(); + } + + [TestMethod] + public async Task ExecuteTaskWithProgressAsync_TaskWithFailureResponse_WorkflowIsCorrect() + { + var warningText = "warning"; + var parameters = GetTaskWithResponse(false); + parameters.WarningText.Returns(warningText); + + await testSubject.ExecuteTaskWithProgressAsync(parameters); + + Received.InOrder(() => + { + _ = parameters.ProgressStatus; + parameters.AfterProgressUpdated(); + parameters.TaskToPerform(); + _ = parameters.WarningText; + parameters.AfterFailure(Arg.Any()); + parameters.AfterProgressUpdated(); + }); + testSubject.Warning.Should().Be(warningText); + testSubject.ProgressStatus.Should().BeNull(); + } + + [TestMethod] + public async Task ExecuteTaskWithProgressAsync_TaskThrowsException_SetsProgressToNull() + { + testSubject.ProgressStatus = "In progress..."; + + await ExecuteTaskThatThrows(); + + testSubject.ProgressStatus.Should().BeNull(); + } + + private static ITaskToPerformParams GetTaskWithResponse(bool success) + { + var parameters = Substitute.For>(); + var taskResponse = Substitute.For(); + parameters.TaskToPerform().Returns(taskResponse); + taskResponse.Success.Returns(success); + + return parameters; + } + + private async Task ExecuteTaskThatThrows() + { + var parameters = Substitute.For>(); + parameters.TaskToPerform.Returns(x => throw new Exception("test")); + + try + { + await testSubject.ExecuteTaskWithProgressAsync(parameters); + } + catch (Exception) + { + // this is for testing only + } + } +} diff --git a/src/ConnectedMode.UnitTests/UI/ProjectSelection/ProjectSelectionViewModelTests.cs b/src/ConnectedMode.UnitTests/UI/ProjectSelection/ProjectSelectionViewModelTests.cs new file mode 100644 index 0000000000..85e49cb67c --- /dev/null +++ b/src/ConnectedMode.UnitTests/UI/ProjectSelection/ProjectSelectionViewModelTests.cs @@ -0,0 +1,234 @@ +/* + * 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 System.ComponentModel; +using SonarLint.VisualStudio.ConnectedMode.UI; +using SonarLint.VisualStudio.ConnectedMode.UI.ProjectSelection; +using SonarLint.VisualStudio.ConnectedMode.UI.Resources; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Binding; + +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.UI.ProjectSelection; + +[TestClass] +public class ProjectSelectionViewModelTests +{ + private static readonly List AnInitialListOfProjects = + [ + new ServerProject("a-project", "A Project"), + new ServerProject("another-project", "Another Project") + ]; + + private static readonly ConnectionInfo AConnectionInfo = new("http://localhost:9000", ConnectionServerType.SonarQube); + + private ProjectSelectionViewModel testSubject; + private ISlCoreConnectionAdapter slCoreConnectionAdapter; + private IProgressReporterViewModel progressReporterViewModel; + private IConnectedModeServices connectedModeServices; + private IServerConnectionsRepositoryAdapter serverConnectionsRepositoryAdapter; + private ILogger logger; + + [TestInitialize] + public void TestInitialize() + { + slCoreConnectionAdapter = Substitute.For(); + progressReporterViewModel = Substitute.For(); + serverConnectionsRepositoryAdapter = Substitute.For(); + connectedModeServices = Substitute.For(); + logger = Substitute.For(); + connectedModeServices.SlCoreConnectionAdapter.Returns(slCoreConnectionAdapter); + connectedModeServices.ServerConnectionsRepositoryAdapter.Returns(serverConnectionsRepositoryAdapter); + connectedModeServices.Logger.Returns(logger); + + testSubject = new ProjectSelectionViewModel(AConnectionInfo, connectedModeServices, progressReporterViewModel); + } + + [TestMethod] + public void IsProjectSelected_NoProjectSelected_ReturnsFalse() + { + testSubject.IsProjectSelected.Should().BeFalse(); + } + + [TestMethod] + public void IsProjectSelected_ProjectSelected_ReturnsTrue() + { + testSubject.SelectedProject = new ServerProject("a-project", "A Project"); + + testSubject.IsProjectSelected.Should().BeTrue(); + } + + [TestMethod] + public void InitProjects_ResetsTheProjectResults() + { + MockInitializedProjects(AnInitialListOfProjects); + testSubject.ProjectResults.Should().BeEquivalentTo(AnInitialListOfProjects); + + var updatedListOfProjects = new List + { + new("new-project", "New Project") + }; + MockInitializedProjects(updatedListOfProjects); + testSubject.ProjectResults.Should().BeEquivalentTo(updatedListOfProjects); + } + + [TestMethod] + public void InitProjects_SortsTheProjectResultsByName() + { + var unsortedListOfProjects = new List + { + new("a-project", "Y Project"), + new("b-project", "X Project"), + new("c-project", "Z Project") + }; + + MockInitializedProjects(unsortedListOfProjects); + + testSubject.ProjectResults[0].Name.Should().Be("X Project"); + testSubject.ProjectResults[1].Name.Should().Be("Y Project"); + testSubject.ProjectResults[2].Name.Should().Be("Z Project"); + } + + [TestMethod] + public void InitProjects_RaisesEvents() + { + var eventHandler = Substitute.For(); + testSubject.PropertyChanged += eventHandler; + + MockInitializedProjects(AnInitialListOfProjects); + + eventHandler.Received().Invoke(testSubject, Arg.Is(x => x.PropertyName == nameof(testSubject.NoProjectExists))); + } + + [TestMethod] + public void ProjectSearchTerm_WithEmptyTerm_ShouldNotUpdateSearchResult() + { + MockInitializedProjects(AnInitialListOfProjects); + + testSubject.ProjectSearchTerm = ""; + + testSubject.ProjectResults.Should().BeEquivalentTo(AnInitialListOfProjects); + } + + [TestMethod] + public void ProjectSearchTerm_WithTerm_ShouldUpdateSearchResult() + { + MockInitializedProjects(AnInitialListOfProjects); + + testSubject.ProjectSearchTerm = "My Project"; + + testSubject.ProjectResults.Should().NotContain(AnInitialListOfProjects); + } + + [TestMethod] + public void SearchForProject_RaisesEvents() + { + var eventHandler = Substitute.For(); + testSubject.PropertyChanged += eventHandler; + + testSubject.ProjectSearchTerm = "proj1"; + + eventHandler.Received().Invoke(testSubject, Arg.Is(x => x.PropertyName == nameof(testSubject.NoProjectExists))); + } + + [TestMethod] + public void NoProjectExists_NoProjects_ReturnsTrue() + { + MockInitializedProjects([]); + + testSubject.NoProjectExists.Should().BeTrue(); + } + + [TestMethod] + public void NoProjectExists_HasProjects_ReturnsFalse() + { + MockInitializedProjects(AnInitialListOfProjects); + + testSubject.NoProjectExists.Should().BeFalse(); + } + + [TestMethod] + public async Task InitializeProjectWithProgressAsync_ExecutesInitializationWithProgress() + { + await testSubject.InitializeProjectWithProgressAsync(); + + await progressReporterViewModel.Received(1) + .ExecuteTaskWithProgressAsync( + Arg.Is>>>(x => + x.TaskToPerform == testSubject.AdapterGetAllProjectsAsync && + x.ProgressStatus == UiResources.LoadingProjectsProgressText && + x.WarningText == UiResources.LoadingProjectsFailedText && + x.AfterSuccess == testSubject.InitProjects)); + } + + [TestMethod] + public async Task AdapterGetAllProjectsAsync_GettingServerConnectionSucceeded_CallsAdapterWithCredentialsForServerConnection() + { + var expectedCredentials = Substitute.For(); + MockTrySonarQubeConnection(AConnectionInfo, success:true, expectedCredentials); + + await testSubject.AdapterGetAllProjectsAsync(); + + serverConnectionsRepositoryAdapter.Received(1).TryGet(AConnectionInfo, out Arg.Any()); + await slCoreConnectionAdapter.Received(1).GetAllProjectsAsync(Arg.Is(x => x.Credentials == expectedCredentials)); + } + + [TestMethod] + public async Task AdapterGetAllProjectsAsync_GettingServerConnectionFailed_ReturnsFailure() + { + MockTrySonarQubeConnection(AConnectionInfo, success:false); + + var response = await testSubject.AdapterGetAllProjectsAsync(); + + response.Success.Should().BeFalse(); + response.ResponseData.Should().BeNull(); + logger.Received(1).WriteLine(Arg.Any()); + await slCoreConnectionAdapter.DidNotReceive().GetAllProjectsAsync(Arg.Any()); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public async Task AdapterGetAllProjectsAsync_ReturnsResponseFromAdapter(bool expectedResponse) + { + MockTrySonarQubeConnection(AConnectionInfo, success: true); + var expectedServerProjects = new List{new("proj1", "name1"), new("proj2", "name2") }; + slCoreConnectionAdapter.GetAllProjectsAsync(Arg.Any()) + .Returns(new AdapterResponseWithData>(expectedResponse, expectedServerProjects)); + + var response = await testSubject.AdapterGetAllProjectsAsync(); + + response.Success.Should().Be(expectedResponse); + response.ResponseData.Should().BeEquivalentTo(expectedServerProjects); + } + + private void MockInitializedProjects(List serverProjects) + { + testSubject.InitProjects(new AdapterResponseWithData>(true, serverProjects)); + } + + private void MockTrySonarQubeConnection(ConnectionInfo connectionInfo, bool success = true, ICredentials expectedCredentials = null) + { + serverConnectionsRepositoryAdapter.TryGet(connectionInfo, out _).Returns(callInfo => + { + callInfo[1] = new ServerConnection.SonarQube(new Uri(connectionInfo.Id), credentials: expectedCredentials); + return success; + }); + } +} diff --git a/src/ConnectedMode.UnitTests/UI/ServerSelection/ServerSelectionViewModelTests.cs b/src/ConnectedMode.UnitTests/UI/ServerSelection/ServerSelectionViewModelTests.cs new file mode 100644 index 0000000000..6552eb0dc1 --- /dev/null +++ b/src/ConnectedMode.UnitTests/UI/ServerSelection/ServerSelectionViewModelTests.cs @@ -0,0 +1,183 @@ +/* + * 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.ConnectedMode.UI.Resources; +using SonarLint.VisualStudio.ConnectedMode.UI.ServerSelection; + +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.UI.ServerSelection +{ + [TestClass] + public class ServerSelectionViewModelTests + { + private ServerSelectionViewModel testSubject; + + [TestInitialize] + public void TestInitialize() + { + testSubject = new ServerSelectionViewModel(); + } + + [TestMethod] + public void IsSonarCloudSelected_ShouldBeTrueByDefault() + { + testSubject.IsSonarCloudSelected.Should().BeTrue(); + testSubject.IsSonarQubeSelected.Should().BeFalse(); + } + + [TestMethod] + public void IsNextButtonEnabled_NoServerIsSelected_ReturnsFalse() + { + testSubject.IsSonarCloudSelected = false; + testSubject.IsSonarQubeSelected = false; + + testSubject.IsNextButtonEnabled.Should().BeFalse(); + } + + [TestMethod] + public void IsNextButtonEnabled_SonarCloudIsSelected_ReturnsTrue() + { + testSubject.IsSonarCloudSelected = true; + testSubject.IsSonarQubeSelected = false; + + testSubject.IsNextButtonEnabled.Should().BeTrue(); + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + public void IsNextButtonEnabled_SonarQubeIsSelectedAndNoUrlProvided_ReturnsFalse(string url) + { + testSubject.IsSonarCloudSelected = false; + testSubject.IsSonarQubeSelected = true; + + testSubject.SonarQubeUrl = url; + + testSubject.IsNextButtonEnabled.Should().BeFalse(); + } + + [TestMethod] + public void IsNextButtonEnabled_SonarQubeIsSelectedAndUrlIsProvided_ReturnsTrue() + { + testSubject.IsSonarCloudSelected = false; + testSubject.IsSonarQubeSelected = true; + + testSubject.SonarQubeUrl = "dummy URL"; + + testSubject.IsNextButtonEnabled.Should().BeTrue(); + } + + [TestMethod] + public void ShouldSonarQubeUrlBeFilled_SonarCloudIsSelected_ReturnsFalse() + { + testSubject.IsSonarCloudSelected = true; + testSubject.IsSonarQubeSelected = false; + + testSubject.ShouldSonarQubeUrlBeFilled.Should().BeFalse(); + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + public void ShouldSonarQubeUrlBeFilled_SonarQubeIsSelectedAndUrlIsEmpty_ReturnsTrue(string url) + { + testSubject.IsSonarCloudSelected = false; + testSubject.IsSonarQubeSelected = true; + + testSubject.SonarQubeUrl = url; + + testSubject.ShouldSonarQubeUrlBeFilled.Should().BeTrue(); + } + + [TestMethod] + public void ShouldSonarQubeUrlBeFilled_SonarQubeIsSelectedAndUrlIsNotEmpty_ReturnsFalse() + { + testSubject.IsSonarCloudSelected = false; + testSubject.IsSonarQubeSelected = true; + + testSubject.SonarQubeUrl = "dummy url"; + + testSubject.ShouldSonarQubeUrlBeFilled.Should().BeFalse(); + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + [DataRow(" asab")] + public void ShowSecurityWarning_UrlInvalid_ReturnsFalse(string url) + { + testSubject.IsSonarCloudSelected = false; + testSubject.IsSonarQubeSelected = true; + + testSubject.SonarQubeUrl = url; + + testSubject.ShowSecurityWarning.Should().BeFalse(); + } + + [TestMethod] + public void ShowSecurityWarning_UrlSecureProtocol_ReturnsFalse() + { + testSubject.IsSonarCloudSelected = false; + testSubject.IsSonarQubeSelected = true; + + testSubject.SonarQubeUrl = "https://localhost:9000"; + + testSubject.ShowSecurityWarning.Should().BeFalse(); + } + + [TestMethod] + public void ShowSecurityWarning_UrlInsecureProtocol_ReturnsTrue() + { + testSubject.IsSonarCloudSelected = false; + testSubject.IsSonarQubeSelected = true; + + testSubject.SonarQubeUrl = "http://localhost:9000"; + + testSubject.ShowSecurityWarning.Should().BeTrue(); + } + + [TestMethod] + public void CreateTransientConnectionInfo_SonarQubeIsSelected_ReturnsConnectionWithSmartNotificationsEnabled() + { + testSubject.IsSonarCloudSelected = false; + testSubject.IsSonarQubeSelected = true; + testSubject.SonarQubeUrl = "http://localhost:90"; + + var createdConnection = testSubject.CreateTransientConnectionInfo(); + + createdConnection.Id.Should().Be(testSubject.SonarQubeUrl); + createdConnection.ServerType.Should().Be(ConnectionServerType.SonarQube); + } + + [TestMethod] + public void CreateTransientConnectionInfo_SonarCloudIsSelected_ReturnsConnectionWithSmartNotificationsEnabled() + { + testSubject.IsSonarCloudSelected = true; + testSubject.IsSonarQubeSelected = false; + + var createdConnection = testSubject.CreateTransientConnectionInfo(); + + createdConnection.Id.Should().BeNull(); + createdConnection.ServerType.Should().Be(ConnectionServerType.SonarCloud); + } + } +} diff --git a/src/ConnectedMode/Binding/BindCommandArgs.cs b/src/ConnectedMode/Binding/BindCommandArgs.cs index fa90ad0791..69f509b9f9 100644 --- a/src/ConnectedMode/Binding/BindCommandArgs.cs +++ b/src/ConnectedMode/Binding/BindCommandArgs.cs @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using SonarLint.VisualStudio.Core.Binding; using SonarQube.Client.Models; namespace SonarLint.VisualStudio.ConnectedMode.Binding @@ -27,17 +28,11 @@ namespace SonarLint.VisualStudio.ConnectedMode.Binding /// public class BindCommandArgs { - public BindCommandArgs(string projectKey, string projectName, ConnectionInformation connection) + public BoundServerProject ProjectToBind { get; } + + public BindCommandArgs(BoundServerProject projectToBind) { - this.ProjectKey = projectKey; - this.ProjectName = projectName; - this.Connection = connection; + ProjectToBind = projectToBind; } - - public string ProjectKey { get; } - - public string ProjectName { get; } - - public ConnectionInformation Connection { get; } } } diff --git a/src/ConnectedMode/Binding/BindingProcessImpl.cs b/src/ConnectedMode/Binding/BindingProcessImpl.cs index 703078926c..d5b29c4e55 100644 --- a/src/ConnectedMode/Binding/BindingProcessImpl.cs +++ b/src/ConnectedMode/Binding/BindingProcessImpl.cs @@ -54,21 +54,15 @@ public BindingProcessImpl( this.sonarQubeService = sonarQubeService ?? throw new ArgumentNullException(nameof(sonarQubeService)); this.qualityProfileDownloader = qualityProfileDownloader ?? throw new ArgumentNullException(nameof(qualityProfileDownloader)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - - Debug.Assert(bindingArgs.ProjectKey != null); - Debug.Assert(bindingArgs.ProjectName != null); - Debug.Assert(bindingArgs.Connection != null); } #region IBindingTemplate methods public async Task DownloadQualityProfileAsync(IProgress progress, CancellationToken cancellationToken) { - var boundProject = CreateNewBindingConfig(); - try { - await qualityProfileDownloader.UpdateAsync(boundProject, progress, cancellationToken); + await qualityProfileDownloader.UpdateAsync(bindingArgs.ProjectToBind, progress, cancellationToken); // ignore the UpdateAsync result, as the return value of false indicates error, rather than lack of changes return true; } @@ -80,24 +74,11 @@ public async Task DownloadQualityProfileAsync(IProgress SaveServerExclusionsAsync(CancellationToken cancellationToken) { try { - var exclusions = await sonarQubeService.GetServerExclusions(bindingArgs.ProjectKey, cancellationToken); + var exclusions = await sonarQubeService.GetServerExclusions(bindingArgs.ProjectToBind.ServerProjectKey, cancellationToken); exclusionSettingsStorage.SaveSettings(exclusions); } catch(Exception ex) when (!ErrorHandler.IsCriticalException(ex)) diff --git a/src/ConnectedMode/Binding/BindingStrings.Designer.cs b/src/ConnectedMode/Binding/BindingStrings.Designer.cs index 036269b70c..93b400fb15 100644 --- a/src/ConnectedMode/Binding/BindingStrings.Designer.cs +++ b/src/ConnectedMode/Binding/BindingStrings.Designer.cs @@ -1,6 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. +// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -113,5 +114,14 @@ internal static string SubTextPaddingFormat { return ResourceManager.GetString("SubTextPaddingFormat", resourceCulture); } } + + /// + /// Looks up a localized string similar to Could not convert connection information for the binding. + /// + internal static string UnintrusiveController_InvalidConnection { + get { + return ResourceManager.GetString("UnintrusiveController_InvalidConnection", resourceCulture); + } + } } } diff --git a/src/ConnectedMode/Binding/BindingStrings.resx b/src/ConnectedMode/Binding/BindingStrings.resx index 4aedeb5d2c..c3cd2aee38 100644 --- a/src/ConnectedMode/Binding/BindingStrings.resx +++ b/src/ConnectedMode/Binding/BindingStrings.resx @@ -138,4 +138,7 @@ Learn more + + Could not convert connection information for the binding + \ No newline at end of file diff --git a/src/ConnectedMode/Binding/CSharpVBBindingConfigProvider.cs b/src/ConnectedMode/Binding/CSharpVBBindingConfigProvider.cs index a616ee767e..8e98d48b79 100644 --- a/src/ConnectedMode/Binding/CSharpVBBindingConfigProvider.cs +++ b/src/ConnectedMode/Binding/CSharpVBBindingConfigProvider.cs @@ -65,7 +65,8 @@ public bool IsLanguageSupported(Language language) return Language.CSharp.Equals(language) || Language.VBNET.Equals(language); } - public Task GetConfigurationAsync(SonarQubeQualityProfile qualityProfile, Language language, BindingConfiguration bindingConfiguration, CancellationToken cancellationToken) + public Task GetConfigurationAsync(SonarQubeQualityProfile qualityProfile, Language language, + BindingConfiguration bindingConfiguration, CancellationToken cancellationToken) { if (!IsLanguageSupported(language)) { @@ -93,11 +94,11 @@ private async Task DoGetConfigurationAsync(SonarQubeQualityProfi } // Now fetch the data required for the NuGet configuration - var sonarProperties = await FetchPropertiesAsync(bindingConfiguration.Project.ProjectKey, cancellationToken); + var sonarProperties = await FetchPropertiesAsync(bindingConfiguration.Project.ServerProjectKey, cancellationToken); // Finally, fetch the remaining data needed to build the globalconfig var inactiveRules = await FetchSupportedRulesAsync(false, qualityProfile.Key, cancellationToken); - var exclusions = await FetchInclusionsExclusionsAsync(bindingConfiguration.Project.ProjectKey, cancellationToken); + var exclusions = await FetchInclusionsExclusionsAsync(bindingConfiguration.Project.ServerProjectKey, cancellationToken); var globalConfig = GetGlobalConfig(language, bindingConfiguration, activeRules, inactiveRules); var additionalFile = GetAdditionalFile(language, bindingConfiguration, activeRules, sonarProperties, exclusions); diff --git a/src/ConnectedMode/Binding/CompositeBindingConfigProvider.cs b/src/ConnectedMode/Binding/CompositeBindingConfigProvider.cs index 750e72154f..654f74b873 100644 --- a/src/ConnectedMode/Binding/CompositeBindingConfigProvider.cs +++ b/src/ConnectedMode/Binding/CompositeBindingConfigProvider.cs @@ -59,7 +59,8 @@ public CompositeBindingConfigProvider(ISonarQubeService sonarQubeService, ILogge #region IBindingConfigProvider methods - public Task GetConfigurationAsync(SonarQubeQualityProfile qualityProfile, Language language, BindingConfiguration bindingConfiguration, CancellationToken cancellationToken) + public Task GetConfigurationAsync(SonarQubeQualityProfile qualityProfile, Language language, + BindingConfiguration bindingConfiguration, CancellationToken cancellationToken) { var provider = Providers.FirstOrDefault(p => p.IsLanguageSupported(language)); diff --git a/src/ConnectedMode/Binding/IBindingConfigProvider.cs b/src/ConnectedMode/Binding/IBindingConfigProvider.cs index 78f18eeae6..c4e08ab1de 100644 --- a/src/ConnectedMode/Binding/IBindingConfigProvider.cs +++ b/src/ConnectedMode/Binding/IBindingConfigProvider.cs @@ -36,7 +36,8 @@ public interface IBindingConfigProvider /// /// Returns a configuration file for the specified language /// - Task GetConfigurationAsync(SonarQubeQualityProfile qualityProfile, Language language, BindingConfiguration bindingConfiguration, CancellationToken cancellationToken); + Task GetConfigurationAsync(SonarQubeQualityProfile qualityProfile, Language language, + BindingConfiguration bindingConfiguration, CancellationToken cancellationToken); } /// diff --git a/src/ConnectedMode/Binding/IConfigurationPersister.cs b/src/ConnectedMode/Binding/IConfigurationPersister.cs index 714638238f..70622a9647 100644 --- a/src/ConnectedMode/Binding/IConfigurationPersister.cs +++ b/src/ConnectedMode/Binding/IConfigurationPersister.cs @@ -18,28 +18,27 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; using System.ComponentModel.Composition; using System.IO; -using SonarLint.VisualStudio.ConnectedMode.Persistence; +using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; namespace SonarLint.VisualStudio.ConnectedMode.Binding { public interface IConfigurationPersister { - BindingConfiguration Persist(BoundSonarQubeProject project); + BindingConfiguration Persist(BoundServerProject project); } [Export(typeof(IConfigurationPersister))] [PartCreationPolicy(CreationPolicy.Shared)] - internal class ConfigurationPersister : IConfigurationPersister + internal class UnintrusiveConfigurationPersister : IConfigurationPersister { private readonly IUnintrusiveBindingPathProvider configFilePathProvider; private readonly ISolutionBindingRepository solutionBindingRepository; [ImportingConstructor] - public ConfigurationPersister( + public UnintrusiveConfigurationPersister( IUnintrusiveBindingPathProvider configFilePathProvider, ISolutionBindingRepository solutionBindingRepository) { @@ -48,22 +47,22 @@ public ConfigurationPersister( this.solutionBindingRepository = solutionBindingRepository; } - public BindingConfiguration Persist(BoundSonarQubeProject project) + public BindingConfiguration Persist(BoundServerProject project) { if (project == null) { throw new ArgumentNullException(nameof(project)); } - var configFilePath = configFilePathProvider.GetCurrentBindingPath(); + var configFilePath = configFilePathProvider.GetBindingPath(project.LocalBindingKey); - var success = configFilePath != null && - solutionBindingRepository.Write(configFilePath, project); + var success = configFilePath != null && solutionBindingRepository.Write(configFilePath, project); // The binding directory is the folder containing the binding config file var bindingConfigDirectory = Path.GetDirectoryName(configFilePath); - return success ? - BindingConfiguration.CreateBoundConfiguration(project, SonarLintMode.Connected, bindingConfigDirectory) : null; + return success + ? BindingConfiguration.CreateBoundConfiguration(project, SonarLintMode.Connected, bindingConfigDirectory) + : null; } } } diff --git a/src/ConnectedMode/Binding/IUnintrusiveBindingController.cs b/src/ConnectedMode/Binding/IUnintrusiveBindingController.cs index 3c1b6ed6d6..ef1b2b5752 100644 --- a/src/ConnectedMode/Binding/IUnintrusiveBindingController.cs +++ b/src/ConnectedMode/Binding/IUnintrusiveBindingController.cs @@ -18,43 +18,58 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; using System.ComponentModel.Composition; -using System.Threading; using SonarLint.VisualStudio.Core.Binding; +using SonarQube.Client; using Task = System.Threading.Tasks.Task; namespace SonarLint.VisualStudio.ConnectedMode.Binding { + public interface IBindingController + { + Task BindAsync(BoundServerProject project, CancellationToken cancellationToken); + } + internal interface IUnintrusiveBindingController { - Task BindAsync(BoundSonarQubeProject project, IProgress progress, CancellationToken token); + Task BindAsync(BoundServerProject project, IProgress progress, CancellationToken token); } + [Export(typeof(IBindingController))] [Export(typeof(IUnintrusiveBindingController))] [PartCreationPolicy(CreationPolicy.NonShared)] - internal class UnintrusiveBindingController : IUnintrusiveBindingController + internal class UnintrusiveBindingController : IUnintrusiveBindingController, IBindingController { private readonly IBindingProcessFactory bindingProcessFactory; + private readonly ISonarQubeService sonarQubeService; + private readonly IActiveSolutionChangedHandler activeSolutionChangedHandler; [ImportingConstructor] - public UnintrusiveBindingController(IBindingProcessFactory bindingProcessFactory) + public UnintrusiveBindingController(IBindingProcessFactory bindingProcessFactory, ISonarQubeService sonarQubeService, IActiveSolutionChangedHandler activeSolutionChangedHandler) { this.bindingProcessFactory = bindingProcessFactory; + this.sonarQubeService = sonarQubeService; + this.activeSolutionChangedHandler = activeSolutionChangedHandler; } - public async Task BindAsync(BoundSonarQubeProject project, IProgress progress, CancellationToken token) + public async Task BindAsync(BoundServerProject project, CancellationToken cancellationToken) { - var bindingProcess = CreateBindingProcess(project); + var connectionInformation = project.ServerConnection.Credentials.CreateConnectionInformation(project.ServerConnection.ServerUri); + await sonarQubeService.ConnectAsync(connectionInformation, cancellationToken); + await BindAsync(project, null, cancellationToken); + activeSolutionChangedHandler.HandleBindingChange(false); + } + public async Task BindAsync(BoundServerProject project, IProgress progress, CancellationToken token) + { + var bindingProcess = CreateBindingProcess(project); await bindingProcess.DownloadQualityProfileAsync(progress, token); await bindingProcess.SaveServerExclusionsAsync(token); } - private IBindingProcess CreateBindingProcess(BoundSonarQubeProject project) + private IBindingProcess CreateBindingProcess(BoundServerProject project) { - var commandArgs = new BindCommandArgs(project.ProjectKey, project.ProjectName, project.CreateConnectionInformation()); - var bindingProcess = bindingProcessFactory.Create(commandArgs); + var bindingProcess = bindingProcessFactory.Create(new BindCommandArgs(project)); return bindingProcess; } diff --git a/src/ConnectedMode/Binding/NonRoslynBindingConfigProvider.cs b/src/ConnectedMode/Binding/NonRoslynBindingConfigProvider.cs index c3385f2eba..611c520bcc 100644 --- a/src/ConnectedMode/Binding/NonRoslynBindingConfigProvider.cs +++ b/src/ConnectedMode/Binding/NonRoslynBindingConfigProvider.cs @@ -63,7 +63,8 @@ public NonRoslynBindingConfigProvider(ISonarQubeService sonarQubeService, ILogge public bool IsLanguageSupported(Language language) => supportedLanguages.Contains(language); - public async Task GetConfigurationAsync(SonarQubeQualityProfile qualityProfile, Language language, BindingConfiguration bindingConfiguration, CancellationToken cancellationToken) + public async Task GetConfigurationAsync(SonarQubeQualityProfile qualityProfile, Language language, + BindingConfiguration bindingConfiguration, CancellationToken cancellationToken) { if (!IsLanguageSupported(language)) { diff --git a/src/ConnectedMode/Binding/UnintrusiveBindingPathProvider.cs b/src/ConnectedMode/Binding/UnintrusiveBindingPathProvider.cs index a973db95b1..986ef84ff8 100644 --- a/src/ConnectedMode/Binding/UnintrusiveBindingPathProvider.cs +++ b/src/ConnectedMode/Binding/UnintrusiveBindingPathProvider.cs @@ -31,7 +31,9 @@ namespace SonarLint.VisualStudio.ConnectedMode.Binding /// internal interface IUnintrusiveBindingPathProvider { - string GetCurrentBindingPath(); + string GetBindingPath(string localBindingKey); + + string GetBindingKeyFromPath(string path); IEnumerable GetBindingPaths(); } @@ -41,32 +43,32 @@ internal class UnintrusiveBindingPathProvider : IUnintrusiveBindingPathProvider { private const string configFile = "binding.config"; - private readonly ISolutionInfoProvider solutionInfoProvider; - private readonly string SLVSRootBindingFolder; private readonly IFileSystem fileSystem; [ImportingConstructor] - public UnintrusiveBindingPathProvider(ISolutionInfoProvider solutionInfoProvider) - : this(solutionInfoProvider, EnvironmentVariableProvider.Instance, new FileSystem()) + public UnintrusiveBindingPathProvider() + : this(EnvironmentVariableProvider.Instance, new FileSystem()) { } - internal /* for testing */ UnintrusiveBindingPathProvider(ISolutionInfoProvider solutionInfoProvider, - IEnvironmentVariableProvider environmentVariables, IFileSystem fileSystem) + internal /* for testing */ UnintrusiveBindingPathProvider(IEnvironmentVariableProvider environmentVariables, IFileSystem fileSystem) { SLVSRootBindingFolder = Path.Combine(environmentVariables.GetSLVSAppDataRootPath(), "Bindings"); - this.solutionInfoProvider = solutionInfoProvider; this.fileSystem = fileSystem; } - public string GetCurrentBindingPath() + public string GetBindingPath(string localBindingKey) { // The path must match the one in the SonarLintTargets.xml file that is dropped in // the MSBuild ImportBefore folder i.e. - // $(APPDATA)\SonarLint for Visual Studio\\Bindings\\$(SolutionName)\binding.config - var solutionName = solutionInfoProvider.GetSolutionName(); - return solutionName != null ? Path.Combine(SLVSRootBindingFolder, $"{solutionName}", configFile) : null; + // $(APPDATA)\SonarLint for Visual Studio\\Bindings\\$(localBindingKey)\binding.config + return localBindingKey != null ? Path.Combine(SLVSRootBindingFolder, localBindingKey, configFile) : null; + } + + public string GetBindingKeyFromPath(string path) + { + return Path.GetFileName(Path.GetDirectoryName(path)); } public IEnumerable GetBindingPaths() diff --git a/src/ConnectedMode/Binding/UnintrusiveConfigurationProvider.cs b/src/ConnectedMode/Binding/UnintrusiveConfigurationProvider.cs index 59d971ea5d..ec922f7a5d 100644 --- a/src/ConnectedMode/Binding/UnintrusiveConfigurationProvider.cs +++ b/src/ConnectedMode/Binding/UnintrusiveConfigurationProvider.cs @@ -20,51 +20,51 @@ using System.ComponentModel.Composition; using System.IO; -using SonarLint.VisualStudio.ConnectedMode.Persistence; +using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; -namespace SonarLint.VisualStudio.ConnectedMode.Binding +namespace SonarLint.VisualStudio.ConnectedMode.Binding; + +[Export(typeof(IConfigurationProvider))] +[PartCreationPolicy(CreationPolicy.Shared)] +internal class UnintrusiveConfigurationProvider : IConfigurationProvider { - [Export(typeof(IConfigurationProvider))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal class UnintrusiveConfigurationProvider : IConfigurationProvider + private readonly IUnintrusiveBindingPathProvider pathProvider; + private readonly ISolutionBindingRepository solutionBindingRepository; + private readonly ISolutionInfoProvider solutionInfoProvider; + + [ImportingConstructor] + public UnintrusiveConfigurationProvider(IUnintrusiveBindingPathProvider pathProvider, ISolutionInfoProvider solutionInfoProvider, + ISolutionBindingRepository solutionBindingRepository) { - private readonly IUnintrusiveBindingPathProvider pathProvider; - private readonly ISolutionBindingRepository solutionBindingRepository; + this.pathProvider = pathProvider; + this.solutionBindingRepository = solutionBindingRepository; + this.solutionInfoProvider = solutionInfoProvider; + } - [ImportingConstructor] - public UnintrusiveConfigurationProvider( - IUnintrusiveBindingPathProvider pathProvider, - ISolutionBindingRepository solutionBindingRepository) - { - this.pathProvider = pathProvider; - this.solutionBindingRepository = solutionBindingRepository; - } + public BindingConfiguration GetConfiguration() => + TryGetBindingConfiguration() ?? BindingConfiguration.Standalone; - public BindingConfiguration GetConfiguration() + private BindingConfiguration TryGetBindingConfiguration() + { + if (solutionInfoProvider.GetSolutionName() is not { } localBindingKey + || pathProvider.GetBindingPath(localBindingKey) is not { } bindingPath) { - var bindingConfiguration = TryGetBindingConfiguration(pathProvider.GetCurrentBindingPath()); - - return bindingConfiguration ?? BindingConfiguration.Standalone; + return null; } - private BindingConfiguration TryGetBindingConfiguration(string bindingPath) - { - if (bindingPath == null) - { - return null; - } + var boundProject = solutionBindingRepository.Read(bindingPath); - var boundProject = solutionBindingRepository.Read(bindingPath); + if (boundProject == null) + { + return null; + } - if (boundProject == null) - { - return null; - } + var bindingConfigDirectory = Path.GetDirectoryName(bindingPath); - var bindingConfigDirectory = Path.GetDirectoryName(bindingPath); + return BindingConfiguration.CreateBoundConfiguration(boundProject, + SonarLintMode.Connected, + bindingConfigDirectory); - return BindingConfiguration.CreateBoundConfiguration(boundProject, SonarLintMode.Connected, bindingConfigDirectory); - } } } diff --git a/src/ConnectedMode/ConnectedMode.csproj b/src/ConnectedMode/ConnectedMode.csproj index d0085597eb..173cf67edb 100644 --- a/src/ConnectedMode/ConnectedMode.csproj +++ b/src/ConnectedMode/ConnectedMode.csproj @@ -37,11 +37,52 @@ + + + + + + + + + + + + + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + + + MSBuild:Compile + + + MSBuild:Compile + + + + + MSBuild:Compile + + + + + + + @@ -85,6 +126,11 @@ True Resources.resx + + True + True + UiResources.resx + @@ -109,6 +155,10 @@ ResXFileCodeGenerator Resources.Designer.cs + + PublicResXFileCodeGenerator + UiResources.Designer.cs + diff --git a/src/ConnectedMode/ConnectionInfo.cs b/src/ConnectedMode/ConnectionInfo.cs new file mode 100644 index 0000000000..2e4bca6628 --- /dev/null +++ b/src/ConnectedMode/ConnectionInfo.cs @@ -0,0 +1,67 @@ +/* + * 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.Core.Binding; + +namespace SonarLint.VisualStudio.ConnectedMode; + +public enum ConnectionServerType +{ + SonarQube, + SonarCloud +} + +/// +/// Model containing connection information, intended to be used by UI components +/// +/// The organization key for SonarCloud or the server uri for SonarQube +/// The type of server (SonarCloud, SonarQube) +public record ConnectionInfo(string Id, ConnectionServerType ServerType) +{ + public static ConnectionInfo From(ServerConnection serverConnection) + { + return serverConnection switch + { + ServerConnection.SonarQube sonarQubeConnection => new ConnectionInfo(sonarQubeConnection.Id, ConnectionServerType.SonarQube), + ServerConnection.SonarCloud sonarCloudConnection => new ConnectionInfo(sonarCloudConnection.OrganizationKey, ConnectionServerType.SonarCloud), + _ => throw new ArgumentException(Resources.UnexpectedConnectionType) + }; + } +} + +public class Connection(ConnectionInfo info, bool enableSmartNotifications = true) +{ + public ConnectionInfo Info { get; } = info; + public bool EnableSmartNotifications { get; set; } = enableSmartNotifications; +} + +public static class ConnectionInfoExtensions +{ + public static string GetIdForTransientConnection(this ConnectionInfo connection) + { + if (connection.Id == null && connection.ServerType == ConnectionServerType.SonarCloud) + { + return CoreStrings.SonarCloudUrl; + } + return connection.Id; + } + +} diff --git a/src/ConnectedMode/Migration/BindingToConnectionMigration.cs b/src/ConnectedMode/Migration/BindingToConnectionMigration.cs new file mode 100644 index 0000000000..0591663af7 --- /dev/null +++ b/src/ConnectedMode/Migration/BindingToConnectionMigration.cs @@ -0,0 +1,147 @@ +/* + * 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 System.ComponentModel.Composition; +using System.IO.Abstractions; +using SonarLint.VisualStudio.Core.Binding; +using SonarLint.VisualStudio.ConnectedMode.Binding; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Infrastructure.VS; + +namespace SonarLint.VisualStudio.ConnectedMode.Migration; + +public interface IBindingToConnectionMigration +{ + Task MigrateAllBindingsToServerConnectionsIfNeededAsync(); +} + +/// +/// Migrates the information about the server connection that, in the past, was stored into the binding.config file to the new connections.json file. +/// The binding.config file is also updated to contain a ServerConnectionId that references the new connection. +/// +[Export(typeof(IBindingToConnectionMigration))] +[PartCreationPolicy(CreationPolicy.Shared)] +internal class BindingToConnectionMigration : IBindingToConnectionMigration +{ + private readonly IServerConnectionsRepository serverConnectionsRepository; + private readonly ILegacySolutionBindingRepository legacyBindingRepository; + private readonly ISolutionBindingRepository solutionBindingRepository; + private readonly IUnintrusiveBindingPathProvider unintrusiveBindingPathProvider; + private readonly IThreadHandling threadHandling; + private readonly ILogger logger; + + [ImportingConstructor] + public BindingToConnectionMigration( + IServerConnectionsRepository serverConnectionsRepository, + ILegacySolutionBindingRepository legacyBindingRepository, + ISolutionBindingRepository solutionBindingRepository, + IUnintrusiveBindingPathProvider unintrusiveBindingPathProvider, + ILogger logger) : + this( + serverConnectionsRepository, + legacyBindingRepository, + solutionBindingRepository, + unintrusiveBindingPathProvider, + ThreadHandling.Instance, + logger) + { } + + internal /* for testing */ BindingToConnectionMigration( + IServerConnectionsRepository serverConnectionsRepository, + ILegacySolutionBindingRepository legacyBindingRepository, + ISolutionBindingRepository solutionBindingRepository, + IUnintrusiveBindingPathProvider unintrusiveBindingPathProvider, + IThreadHandling threadHandling, + ILogger logger) + { + this.serverConnectionsRepository = serverConnectionsRepository; + this.legacyBindingRepository = legacyBindingRepository; + this.solutionBindingRepository = solutionBindingRepository; + this.unintrusiveBindingPathProvider = unintrusiveBindingPathProvider; + this.threadHandling = threadHandling; + this.logger = logger; + } + + public Task MigrateAllBindingsToServerConnectionsIfNeededAsync() + { + return threadHandling.RunOnBackgroundThread(MigrateBindingToServerConnectionIfNeeded); + } + + private void MigrateBindingToServerConnectionIfNeeded() + { + if (serverConnectionsRepository.ConnectionsFileExists()) + { + return; + } + + logger.WriteLine(MigrationStrings.ConnectionMigration_StartMigration); + foreach (var bindingPath in unintrusiveBindingPathProvider.GetBindingPaths()) + { + MigrateBindingToServerConnection(bindingPath); + } + } + + private void MigrateBindingToServerConnection(string bindingFilePath) + { + try + { + if (legacyBindingRepository.Read(bindingFilePath) is not {} legacyBoundProject) + { + logger.WriteLine(string.Format(MigrationStrings.ConnectionMigration_BindingNotMigrated, bindingFilePath, $"{nameof(legacyBoundProject)} was not found")); + return; + } + + if (MigrateServerConnection(legacyBoundProject) is {} serverConnection) + { + MigrateBinding(bindingFilePath, legacyBoundProject, serverConnection); + } + } + catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) + { + logger.WriteLine(string.Format(MigrationStrings.ConnectionMigration_BindingNotMigrated, bindingFilePath, ex.Message)); + } + } + + private ServerConnection MigrateServerConnection(BoundSonarQubeProject legacyBoundProject) + { + var serverConnection = ServerConnection.FromBoundSonarQubeProject(legacyBoundProject); + serverConnection.Credentials = legacyBoundProject.Credentials; + + if (serverConnectionsRepository.TryGet(serverConnection.Id, out _)) + { + logger.WriteLine(string.Format(MigrationStrings.ConnectionMigration_ExistingServerConnectionNotMigrated, serverConnection.Id)); + return serverConnection; + } + + if (serverConnectionsRepository.TryAdd(serverConnection)) + { + return serverConnection; + } + + logger.WriteLine(string.Format(MigrationStrings.ConnectionMigration_ServerConnectionNotMigrated, serverConnection.Id)); + return null; + } + + private void MigrateBinding(string bindingPath, BoundSonarQubeProject legacyBoundProject, ServerConnection serverConnection) + { + var boundServerProject = BoundServerProject.FromBoundSonarQubeProject(legacyBoundProject, bindingPath, serverConnection); + solutionBindingRepository.Write(bindingPath, boundServerProject); + } +} diff --git a/src/ConnectedMode/Migration/ConnectedModeMigration.cs b/src/ConnectedMode/Migration/ConnectedModeMigration.cs index 2970afbd50..270fca0a93 100644 --- a/src/ConnectedMode/Migration/ConnectedModeMigration.cs +++ b/src/ConnectedMode/Migration/ConnectedModeMigration.cs @@ -19,11 +19,9 @@ */ using System; -using System.Collections.Generic; using System.ComponentModel.Composition; -using System.Diagnostics; -using System.Linq; -using System.Threading; +using Microsoft.Alm.Authentication; +using Microsoft.VisualStudio.LanguageServer.Client; using SonarLint.VisualStudio.ConnectedMode.Binding; using SonarLint.VisualStudio.ConnectedMode.Shared; using SonarLint.VisualStudio.ConnectedMode.Suppressions; @@ -51,6 +49,9 @@ private sealed class ChangedFiles : List> { } private readonly ISharedBindingConfigProvider sharedBindingConfigProvider; private readonly ILogger logger; private readonly IThreadHandling threadHandling; + private readonly ISolutionInfoProvider solutionInfoProvider; + private readonly IServerConnectionsRepository serverConnectionsRepository; + private readonly IUnintrusiveBindingPathProvider unintrusiveBindingPathProvider; // The user can have both the legacy and new connected mode files. In that case, we expect the SonarQubeService to already be connected. private bool isAlreadyConnectedToServer; @@ -65,7 +66,10 @@ public ConnectedModeMigration(IMigrationSettingsProvider settingsProvider, ISuppressionIssueStoreUpdater suppressionIssueStoreUpdater, ISharedBindingConfigProvider sharedBindingConfigProvider, ILogger logger, - IThreadHandling threadHandling) + IThreadHandling threadHandling, + ISolutionInfoProvider solutionInfoProvider, + IServerConnectionsRepository serverConnectionsRepository, + IUnintrusiveBindingPathProvider unintrusiveBindingPathProvider) { this.settingsProvider = settingsProvider; this.fileProvider = fileProvider; @@ -78,6 +82,9 @@ public ConnectedModeMigration(IMigrationSettingsProvider settingsProvider, this.logger = logger; this.threadHandling = threadHandling; + this.solutionInfoProvider = solutionInfoProvider; + this.serverConnectionsRepository = serverConnectionsRepository; + this.unintrusiveBindingPathProvider = unintrusiveBindingPathProvider; } public async Task MigrateAsync(BoundSonarQubeProject oldBinding, IProgress progress, bool shareBinding, CancellationToken token) @@ -136,7 +143,8 @@ private async Task MigrateImplAsync(BoundSonarQubeProject oldBinding, IProgress< logger.WriteLine(MigrationStrings.Process_ProcessingNewBinding); var progressAdapter = new FixedStepsProgressToMigrationProgressAdapter(progress); - await unintrusiveBindingController.BindAsync(oldBinding, progressAdapter, token); + var serverConnection = GetServerConnectionWithMigration(oldBinding); + await unintrusiveBindingController.BindAsync(BoundServerProject.FromBoundSonarQubeProject(oldBinding, await solutionInfoProvider.GetSolutionNameAsync(), serverConnection), progressAdapter, token); // Now make all of the files changes required to remove the legacy settings // i.e. update project files and delete .sonarlint folder @@ -263,5 +271,30 @@ private async Task SaveChangedFilesAsync(ChangedFiles changedFiles, IProgress - public interface IObsoleteConfigurationProvider : IConfigurationProvider + public interface IObsoleteConfigurationProvider { + /// + /// Returns the binding configuration for the current solution + /// + LegacyBindingConfiguration GetConfiguration(); } } diff --git a/src/ConnectedMode/Migration/MigrationStrings.Designer.cs b/src/ConnectedMode/Migration/MigrationStrings.Designer.cs index 59b53c559f..c0979f7d76 100644 --- a/src/ConnectedMode/Migration/MigrationStrings.Designer.cs +++ b/src/ConnectedMode/Migration/MigrationStrings.Designer.cs @@ -105,6 +105,51 @@ internal static string Cleaner_Unchanged { } } + /// + /// Looks up a localized string similar to [Connection Migration] The binding {0} could not be migrated due to: {1}. + /// + internal static string ConnectionMigration_BindingNotMigrated { + get { + return ResourceManager.GetString("ConnectionMigration_BindingNotMigrated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [Connection Migration] The server connection with the id {0} was not migrated as it already exists.. + /// + internal static string ConnectionMigration_ExistingServerConnectionNotMigrated { + get { + return ResourceManager.GetString("ConnectionMigration_ExistingServerConnectionNotMigrated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [Connection Migration] The binding with the id {0} could not be migrated. + /// + internal static string ConnectionMigration_ServerConnectionNotMigrated { + get { + return ResourceManager.GetString("ConnectionMigration_ServerConnectionNotMigrated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [Connection Migration] Start migrating connections from existing bindings. + /// + internal static string ConnectionMigration_StartMigration { + get { + return ResourceManager.GetString("ConnectionMigration_StartMigration", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [Migration] Connections.json file does not exist. The new migration has to be performed first.. + /// + internal static string ConnectionsJson_DoesNotExist { + get { + return ResourceManager.GetString("ConnectionsJson_DoesNotExist", resourceCulture); + } + } + /// /// Looks up a localized string similar to [Migration] Error during migration: {0} /// Run migration again with verbose logging enabled for more information.. diff --git a/src/ConnectedMode/Migration/MigrationStrings.resx b/src/ConnectedMode/Migration/MigrationStrings.resx index 2f6dbb4da2..c37f8f0b69 100644 --- a/src/ConnectedMode/Migration/MigrationStrings.resx +++ b/src/ConnectedMode/Migration/MigrationStrings.resx @@ -215,4 +215,19 @@ [Migration] No settings to remove from the file + + [Connection Migration] The binding {0} could not be migrated due to: {1} + + + [Connection Migration] The binding with the id {0} could not be migrated + + + [Connection Migration] Start migrating connections from existing bindings + + + [Connection Migration] The server connection with the id {0} was not migrated as it already exists. + + + [Migration] Connections.json file does not exist. The new migration has to be performed first. + \ No newline at end of file diff --git a/src/ConnectedMode/Migration/ObsoleteConfigurationProvider.cs b/src/ConnectedMode/Migration/ObsoleteConfigurationProvider.cs index cb69f34b13..20677bad06 100644 --- a/src/ConnectedMode/Migration/ObsoleteConfigurationProvider.cs +++ b/src/ConnectedMode/Migration/ObsoleteConfigurationProvider.cs @@ -33,12 +33,12 @@ internal class ObsoleteConfigurationProvider : IObsoleteConfigurationProvider { private readonly ISolutionBindingPathProvider legacyPathProvider; private readonly ISolutionBindingPathProvider connectedModePathProvider; - private readonly ISolutionBindingRepository solutionBindingRepository; + private readonly ILegacySolutionBindingRepository solutionBindingRepository; [ImportingConstructor] public ObsoleteConfigurationProvider( ISolutionInfoProvider solutionInfoProvider, - ISolutionBindingRepository solutionBindingRepository) + ILegacySolutionBindingRepository solutionBindingRepository) : this( new LegacySolutionBindingPathProvider(solutionInfoProvider), new ObsoleteConnectedModeSolutionBindingPathProvider(solutionInfoProvider), @@ -48,23 +48,23 @@ public ObsoleteConfigurationProvider( internal /* for testing */ ObsoleteConfigurationProvider(ISolutionBindingPathProvider legacyPathProvider, ISolutionBindingPathProvider connectedModePathProvider, - ISolutionBindingRepository solutionBindingRepository) + ILegacySolutionBindingRepository solutionBindingRepository) { this.legacyPathProvider = legacyPathProvider ?? throw new ArgumentNullException(nameof(legacyPathProvider)); this.connectedModePathProvider = connectedModePathProvider ?? throw new ArgumentNullException(nameof(connectedModePathProvider)); this.solutionBindingRepository = solutionBindingRepository ?? throw new ArgumentNullException(nameof(solutionBindingRepository)); } - public BindingConfiguration GetConfiguration() + public LegacyBindingConfiguration GetConfiguration() { var bindingConfiguration = TryGetBindingConfiguration(legacyPathProvider.Get(), SonarLintMode.LegacyConnected) ?? TryGetBindingConfiguration(connectedModePathProvider.Get(), SonarLintMode.Connected); - return bindingConfiguration ?? BindingConfiguration.Standalone; + return bindingConfiguration ?? LegacyBindingConfiguration.Standalone; } - private BindingConfiguration TryGetBindingConfiguration(string bindingPath, SonarLintMode sonarLintMode) + private LegacyBindingConfiguration TryGetBindingConfiguration(string bindingPath, SonarLintMode sonarLintMode) { if (bindingPath == null) { @@ -80,7 +80,7 @@ private BindingConfiguration TryGetBindingConfiguration(string bindingPath, Sona var bindingConfigDirectory = Path.GetDirectoryName(bindingPath); - return BindingConfiguration.CreateBoundConfiguration(boundProject, sonarLintMode, bindingConfigDirectory); + return LegacyBindingConfiguration.CreateBoundConfiguration(boundProject, sonarLintMode, bindingConfigDirectory); } } } diff --git a/src/ConnectedMode/Persistence/BindingDto.cs b/src/ConnectedMode/Persistence/BindingDto.cs new file mode 100644 index 0000000000..579eaf43c0 --- /dev/null +++ b/src/ConnectedMode/Persistence/BindingDto.cs @@ -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 Newtonsoft.Json; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Binding; +using SonarQube.Client.Models; + +namespace SonarLint.VisualStudio.ConnectedMode.Persistence; + +/// +/// The information for the connection related properties are now stored in a separate file, but they are needed here for backward compatibility with previous binding formats +/// +[JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)] +internal class BindingDto +{ + public string ServerConnectionId { get; set; } + public Uri ServerUri { get; set; } // left here for backward compatibility reasons + public SonarQubeOrganization Organization { get; set; } // left here for backward compatibility reasons + public string ProjectKey { get; set; } + public string ProjectName { get; set; } // left here for backward compatibility reasons + public Dictionary Profiles { get; set; } +} diff --git a/src/ConnectedMode/Persistence/BindingDtoConverter.cs b/src/ConnectedMode/Persistence/BindingDtoConverter.cs new file mode 100644 index 0000000000..5f50f32961 --- /dev/null +++ b/src/ConnectedMode/Persistence/BindingDtoConverter.cs @@ -0,0 +1,66 @@ +/* + * 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 System.ComponentModel.Composition; +using SonarLint.VisualStudio.Core.Binding; +using SonarQube.Client.Models; + +namespace SonarLint.VisualStudio.ConnectedMode.Persistence; + +internal interface IBindingDtoConverter +{ + BoundServerProject ConvertFromDto(BindingDto bindingDto, ServerConnection connection, string localBindingKey); + BindingDto ConvertToDto(BoundServerProject binding); + BoundSonarQubeProject ConvertFromDtoToLegacy(BindingDto bindingDto, ICredentials credentials); +} + +[Export(typeof(IBindingDtoConverter))] +[PartCreationPolicy(CreationPolicy.Shared)] +internal class BindingDtoConverter : IBindingDtoConverter +{ + public BoundServerProject ConvertFromDto(BindingDto bindingDto, ServerConnection connection, string localBindingKey) => + new(localBindingKey, bindingDto.ProjectKey, connection) + { + Profiles = bindingDto.Profiles + }; + + public BindingDto ConvertToDto(BoundServerProject binding) => + new() + { + ProjectKey = binding.ServerProjectKey, + ServerConnectionId = binding.ServerConnection.Id, + Profiles = binding.Profiles, + // for compatibility reasons: + ServerUri = binding.ServerConnection.ServerUri, + Organization = binding.ServerConnection is ServerConnection.SonarCloud sonarCloudConnection + ? new SonarQubeOrganization(sonarCloudConnection.OrganizationKey, null) + : null + }; + + public BoundSonarQubeProject ConvertFromDtoToLegacy(BindingDto bindingDto, ICredentials credentials) => + new(bindingDto.ServerUri, + bindingDto.ProjectKey, + bindingDto.ProjectName, + credentials, + bindingDto.Organization) + { + Profiles = bindingDto.Profiles + }; +} diff --git a/src/ConnectedMode/Persistence/ConnectionInfoConverter.cs b/src/ConnectedMode/Persistence/ConnectionInfoConverter.cs new file mode 100644 index 0000000000..8162240004 --- /dev/null +++ b/src/ConnectedMode/Persistence/ConnectionInfoConverter.cs @@ -0,0 +1,38 @@ +/* + * 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 System.Diagnostics.CodeAnalysis; +using SonarLint.VisualStudio.Core.Binding; +using SonarQube.Client.Models; + +namespace SonarLint.VisualStudio.ConnectedMode.Persistence; + +[ExcludeFromCodeCoverage] // todo remove https://sonarsource.atlassian.net/browse/SLVS-1408 +public static class ConnectionInfoConverter +{ + public static ServerConnection ToServerConnection(this ConnectionInformation connectionInformation) => + connectionInformation switch + { + { Organization.Key: { } organization } => new ServerConnection.SonarCloud(organization, + credentials: new BasicAuthCredentials(connectionInformation.UserName, connectionInformation.Password)), + _ => new ServerConnection.SonarQube(connectionInformation.ServerUri, + credentials: new BasicAuthCredentials(connectionInformation.UserName, connectionInformation.Password)) + }; +} diff --git a/src/ConnectedMode/Persistence/ISolutionBindingCredentialsLoader.cs b/src/ConnectedMode/Persistence/ISolutionBindingCredentialsLoader.cs index bbbdc79d0c..d3bfb0c31b 100644 --- a/src/ConnectedMode/Persistence/ISolutionBindingCredentialsLoader.cs +++ b/src/ConnectedMode/Persistence/ISolutionBindingCredentialsLoader.cs @@ -25,6 +25,7 @@ namespace SonarLint.VisualStudio.ConnectedMode.Persistence { interface ISolutionBindingCredentialsLoader { + void DeleteCredentials(Uri boundServerUri); ICredentials Load(Uri boundServerUri); void Save(ICredentials credentials, Uri boundServerUri); } diff --git a/src/ConnectedMode/Persistence/ISolutionBindingFileLoader.cs b/src/ConnectedMode/Persistence/ISolutionBindingFileLoader.cs index a898c41872..a465c678f8 100644 --- a/src/ConnectedMode/Persistence/ISolutionBindingFileLoader.cs +++ b/src/ConnectedMode/Persistence/ISolutionBindingFileLoader.cs @@ -18,13 +18,11 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using SonarLint.VisualStudio.Core.Binding; - namespace SonarLint.VisualStudio.ConnectedMode.Persistence { internal interface ISolutionBindingFileLoader { - BoundSonarQubeProject Load(string filePath); - bool Save(string filePath, BoundSonarQubeProject project); + BindingDto Load(string filePath); + bool Save(string filePath, BindingDto project); } } diff --git a/src/ConnectedMode/Persistence/ServerConnectionJsonModel.cs b/src/ConnectedMode/Persistence/ServerConnectionJsonModel.cs new file mode 100644 index 0000000000..ed8cd8ca64 --- /dev/null +++ b/src/ConnectedMode/Persistence/ServerConnectionJsonModel.cs @@ -0,0 +1,46 @@ +/* + * 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 Newtonsoft.Json.Converters; +using Newtonsoft.Json; +using SonarLint.VisualStudio.Core.Binding; + +namespace SonarLint.VisualStudio.ConnectedMode.Persistence; + +public class ServerConnectionsListJsonModel +{ + [JsonProperty("serverConnections")] + public List ServerConnections { get; set; } = new(); +} + +public record ServerConnectionJsonModel +{ + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("settings")] + public ServerConnectionSettings Settings { get; set; } + + [JsonProperty("organizationKey")] + public string OrganizationKey { get; set; } + + [JsonProperty("serverUri")] + public string ServerUri { get; set; } +} diff --git a/src/ConnectedMode/Persistence/ServerConnectionModelMapper.cs b/src/ConnectedMode/Persistence/ServerConnectionModelMapper.cs new file mode 100644 index 0000000000..bf7a79d6ef --- /dev/null +++ b/src/ConnectedMode/Persistence/ServerConnectionModelMapper.cs @@ -0,0 +1,123 @@ +/* + * 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 System.ComponentModel.Composition; +using SonarLint.VisualStudio.Core.Binding; + +namespace SonarLint.VisualStudio.ConnectedMode.Persistence; + +public interface IServerConnectionModelMapper +{ + ServerConnection GetServerConnection(ServerConnectionJsonModel jsonModel); + ServerConnectionsListJsonModel GetServerConnectionsListJsonModel(IEnumerable serverConnections); +} + +[Export(typeof(IServerConnectionModelMapper))] +[PartCreationPolicy(CreationPolicy.Shared)] +public class ServerConnectionModelMapper : IServerConnectionModelMapper +{ + [ImportingConstructor] + public ServerConnectionModelMapper() { } + + public ServerConnection GetServerConnection(ServerConnectionJsonModel jsonModel) + { + if (IsServerConnectionForSonarCloud(jsonModel)) + { + return new ServerConnection.SonarCloud(jsonModel.OrganizationKey, jsonModel.Settings); + } + if (IsServerConnectionForSonarQube(jsonModel)) + { + return new ServerConnection.SonarQube(new Uri(jsonModel.ServerUri), jsonModel.Settings); + } + + throw new InvalidOperationException($"Invalid {nameof(ServerConnectionJsonModel)}. {nameof(ServerConnection)} could not be created"); + } + + public ServerConnectionsListJsonModel GetServerConnectionsListJsonModel(IEnumerable serverConnections) + { + var model = new ServerConnectionsListJsonModel + { + ServerConnections = serverConnections.Select(GetServerConnectionJsonModel).ToList() + }; + + return model; + } + + internal static bool IsServerConnectionForSonarCloud(ServerConnectionJsonModel jsonModel) + { + return IsOrganizationKeyFilled(jsonModel) && !IsServerUriFilled(jsonModel); + } + + internal static bool IsServerConnectionForSonarQube(ServerConnectionJsonModel jsonModel) + { + return IsServerUriFilled(jsonModel) && !IsOrganizationKeyFilled(jsonModel); + } + + private static ServerConnectionJsonModel GetServerConnectionJsonModel(ServerConnection serverConnection) + { + return new ServerConnectionJsonModel + { + Id = serverConnection.Id, + Settings = serverConnection.Settings ?? throw new InvalidOperationException($"{nameof(ServerConnection.Settings)} can not be null"), + OrganizationKey = GetOrganizationKey(serverConnection), + ServerUri = GetServerUri(serverConnection) + }; + } + + private static string GetOrganizationKey(ServerConnection serverConnection) + { + if (serverConnection is not ServerConnection.SonarCloud sonarCloud) + { + return null; + } + + if(string.IsNullOrWhiteSpace(sonarCloud.OrganizationKey)) + { + throw new InvalidOperationException($"{nameof(ServerConnection.SonarCloud.OrganizationKey)} can not be null"); + } + + return sonarCloud.OrganizationKey; + } + + private static string GetServerUri(ServerConnection serverConnection) + { + if (serverConnection is not ServerConnection.SonarQube sonarQube) + { + return null; + } + + if (sonarQube.ServerUri == null) + { + throw new InvalidOperationException($"{nameof(ServerConnection.SonarQube.ServerUri)} can not be null"); + } + + return sonarQube.ServerUri.ToString(); + } + + private static bool IsServerUriFilled(ServerConnectionJsonModel jsonModel) + { + return !string.IsNullOrWhiteSpace(jsonModel.ServerUri); + } + + private static bool IsOrganizationKeyFilled(ServerConnectionJsonModel jsonModel) + { + return !string.IsNullOrWhiteSpace(jsonModel.OrganizationKey); + } +} diff --git a/src/ConnectedMode/Persistence/ServerConnectionsRepository.cs b/src/ConnectedMode/Persistence/ServerConnectionsRepository.cs new file mode 100644 index 0000000000..3578bc3750 --- /dev/null +++ b/src/ConnectedMode/Persistence/ServerConnectionsRepository.cs @@ -0,0 +1,243 @@ +/* + * 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.Core.Binding; +using System.ComponentModel.Composition; +using System.IO; +using System.IO.Abstractions; +using SonarLint.VisualStudio.Core.Persistence; +using SonarLint.VisualStudio.ConnectedMode.Binding; + +namespace SonarLint.VisualStudio.ConnectedMode.Persistence; + + +[Export(typeof(IServerConnectionsRepository))] +[PartCreationPolicy(CreationPolicy.Shared)] +internal class ServerConnectionsRepository : IServerConnectionsRepository +{ + internal const string ConnectionsFileName = "connections.json"; + + private readonly ISolutionBindingCredentialsLoader credentialsLoader; + private readonly IFileSystem fileSystem; + private readonly ILogger logger; + private readonly IJsonFileHandler jsonFileHandle; + private readonly IServerConnectionModelMapper serverConnectionModelMapper; + private readonly string connectionsStorageFilePath; + private static readonly object LockObject = new(); + + [ImportingConstructor] + public ServerConnectionsRepository( + IJsonFileHandler jsonFileHandle, + IServerConnectionModelMapper serverConnectionModelMapper, + ICredentialStoreService credentialStoreService, + ILogger logger) : this(jsonFileHandle, + serverConnectionModelMapper, + new SolutionBindingCredentialsLoader(credentialStoreService), + EnvironmentVariableProvider.Instance, + new FileSystem(), + logger) { } + + internal /* for testing */ ServerConnectionsRepository( + IJsonFileHandler jsonFileHandle, + IServerConnectionModelMapper serverConnectionModelMapper, + ISolutionBindingCredentialsLoader credentialsLoader, + IEnvironmentVariableProvider environmentVariables, + IFileSystem fileSystem, + ILogger logger) + { + this.jsonFileHandle = jsonFileHandle; + this.serverConnectionModelMapper = serverConnectionModelMapper; + this.credentialsLoader = credentialsLoader; + this.fileSystem = fileSystem; + this.logger = logger; + connectionsStorageFilePath = GetStorageFilePath(environmentVariables); + } + + public bool TryGet(string connectionId, out ServerConnection serverConnection) + { + serverConnection = ReadServerConnectionsFromFile()?.Find(c => c.Id == connectionId); + if (serverConnection is null) + { + return false; + } + + serverConnection.Credentials = credentialsLoader.Load(serverConnection.CredentialsUri); + return true; + + } + + public bool TryGetAll(out IReadOnlyList serverConnections) + { + serverConnections = ReadServerConnectionsFromFile(); + + return serverConnections != null; + } + + public bool TryAdd(ServerConnection connectionToAdd) + { + return SafeUpdateConnectionsFile(connections => TryAddConnection(connections, connectionToAdd)); + } + + public bool TryDelete(string connectionId) + { + ServerConnection removedConnection = null; + var wasDeleted = SafeUpdateConnectionsFile(connections => + { + removedConnection = connections?.Find(c => c.Id == connectionId); + connections?.Remove(removedConnection); + + return removedConnection != null; + }); + if (wasDeleted) + { + TryDeleteCredentials(removedConnection); + } + + return wasDeleted; + } + + public bool TryUpdateSettingsById(string connectionId, ServerConnectionSettings connectionSettings) + { + return SafeUpdateConnectionsFile(connections => TryUpdateConnectionSettings(connections, connectionId, connectionSettings)); + } + + public bool TryUpdateCredentialsById(string connectionId, ICredentials credentials) + { + try + { + var wasFound = TryGet(connectionId, out ServerConnection serverConnection); + if (wasFound) + { + credentialsLoader.Save(credentials, serverConnection.CredentialsUri); + return true; + } + } + catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) + { + logger.WriteLine($"Failed updating credentials: {ex.Message}"); + } + return false; + } + + public bool ConnectionsFileExists() => fileSystem.File.Exists(connectionsStorageFilePath); + + private bool TryAddConnection(List connections, ServerConnection connection) + { + if (connection.Credentials is null) + { + logger.LogVerbose($"Connection was not added.{nameof(ServerConnection.Credentials)} is not filled"); + return false; + } + + if (connections.Find(x => x.Id == connection.Id) is not null) + { + logger.LogVerbose($"Connection was not added.{nameof(ServerConnection.Id)} already exist"); + return false; + } + + try + { + connections.Add(connection); + credentialsLoader.Save(connection.Credentials, connection.CredentialsUri); + return true; + } + catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) + { + logger.WriteLine($"Failed adding server connection: {ex.Message}"); + } + + return false; + } + + + private void TryDeleteCredentials(ServerConnection removedConnection) + { + try + { + credentialsLoader.DeleteCredentials(removedConnection.CredentialsUri); + } + catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) + { + logger.WriteLine($"Failed deleting credentials: {ex.Message}"); + } + } + + private static bool TryUpdateConnectionSettings(List connections, string connectionId, ServerConnectionSettings connectionSettings) + { + var serverConnectionToUpdate = connections?.Find(c => c.Id == connectionId); + if (serverConnectionToUpdate == null) + { + return false; + } + + serverConnectionToUpdate.Settings = connectionSettings; + return true; + } + + private static string GetStorageFilePath(IEnvironmentVariableProvider environmentVariables) + { + var appDataFolder = environmentVariables.GetSLVSAppDataRootPath(); + return Path.Combine(appDataFolder, ConnectionsFileName); + } + + private List ReadServerConnectionsFromFile() + { + try + { + var model = jsonFileHandle.ReadFile(connectionsStorageFilePath); + return model.ServerConnections.Select(serverConnectionModelMapper.GetServerConnection).ToList(); + } + catch (FileNotFoundException) + { + // file not existing should not be treated as an error, as it will be created at the first write + return []; + } + catch(Exception ex) when (!ErrorHandler.IsCriticalException(ex)) + { + logger.WriteLine($"Failed reading the {ConnectionsFileName}: {ex.Message}"); + } + + return null; + } + + private bool SafeUpdateConnectionsFile(Func, bool> tryUpdateConnectionModels) + { + lock (LockObject) + { + try + { + var serverConnections = ReadServerConnectionsFromFile(); + + if (tryUpdateConnectionModels(serverConnections)) + { + var model = serverConnectionModelMapper.GetServerConnectionsListJsonModel(serverConnections); + return jsonFileHandle.TryWriteToFile(connectionsStorageFilePath, model); + } + } + catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) + { + logger.WriteLine($"Failed updating the {ConnectionsFileName}: {ex.Message}"); + } + + return false; + } + } +} diff --git a/src/ConnectedMode/Persistence/SolutionBindingCredentialsLoader.cs b/src/ConnectedMode/Persistence/SolutionBindingCredentialsLoader.cs index 74899b32f6..6232873efd 100644 --- a/src/ConnectedMode/Persistence/SolutionBindingCredentialsLoader.cs +++ b/src/ConnectedMode/Persistence/SolutionBindingCredentialsLoader.cs @@ -18,8 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Diagnostics; using Microsoft.Alm.Authentication; using SonarLint.VisualStudio.ConnectedMode.Binding; using SonarLint.VisualStudio.Core.Binding; @@ -36,6 +34,15 @@ public SolutionBindingCredentialsLoader(ICredentialStoreService store) this.store = store ?? throw new ArgumentNullException(nameof(store)); } + public void DeleteCredentials(Uri boundServerUri) + { + if(boundServerUri == null) + { + return; + } + store.DeleteCredentials(boundServerUri); + } + public ICredentials Load(Uri boundServerUri) { if (boundServerUri == null) diff --git a/src/ConnectedMode/Persistence/SolutionBindingFileLoader.cs b/src/ConnectedMode/Persistence/SolutionBindingFileLoader.cs index 6e61efbe5c..40859a7be6 100644 --- a/src/ConnectedMode/Persistence/SolutionBindingFileLoader.cs +++ b/src/ConnectedMode/Persistence/SolutionBindingFileLoader.cs @@ -44,7 +44,7 @@ internal SolutionBindingFileLoader(ILogger logger, IFileSystem fileSystem) this.fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); } - public bool Save(string filePath, BoundSonarQubeProject project) + public bool Save(string filePath, BindingDto project) { var serializedProject = Serialize(project); @@ -65,7 +65,7 @@ private void WriteConfig(string configFile, string serializedProject) fileSystem.File.WriteAllText(configFile, serializedProject); } - public BoundSonarQubeProject Load(string filePath) + public BindingDto Load(string filePath) { if (string.IsNullOrEmpty(filePath) || !fileSystem.File.Exists(filePath)) { @@ -110,9 +110,9 @@ private bool SafePerformFileSystemOperation(Action operation) } } - private BoundSonarQubeProject Deserialize(string projectJson) + private BindingDto Deserialize(string projectJson) { - return JsonConvert.DeserializeObject(projectJson, new JsonSerializerSettings + return JsonConvert.DeserializeObject(projectJson, new JsonSerializerSettings { DateFormatHandling = DateFormatHandling.IsoDateFormat, DateTimeZoneHandling = DateTimeZoneHandling.Local, @@ -120,7 +120,7 @@ private BoundSonarQubeProject Deserialize(string projectJson) }); } - private string Serialize(BoundSonarQubeProject project) + private string Serialize(BindingDto project) { return JsonConvert.SerializeObject(project, Formatting.Indented, new JsonSerializerSettings { diff --git a/src/ConnectedMode/Persistence/SolutionBindingRepository.cs b/src/ConnectedMode/Persistence/SolutionBindingRepository.cs index 5e535316cc..008272c407 100644 --- a/src/ConnectedMode/Persistence/SolutionBindingRepository.cs +++ b/src/ConnectedMode/Persistence/SolutionBindingRepository.cs @@ -18,97 +18,144 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Collections.Generic; using System.ComponentModel.Composition; -using System.Diagnostics; using SonarLint.VisualStudio.ConnectedMode.Binding; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; +using SonarLint.VisualStudio.SLCore.Service.Project.Models; -namespace SonarLint.VisualStudio.ConnectedMode.Persistence +namespace SonarLint.VisualStudio.ConnectedMode.Persistence; + +[Export(typeof(ISolutionBindingRepository))] +[Export(typeof(ILegacySolutionBindingRepository))] +[PartCreationPolicy(CreationPolicy.Shared)] +internal class SolutionBindingRepository : ISolutionBindingRepository, ILegacySolutionBindingRepository { - [Export(typeof(ISolutionBindingRepository))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal class SolutionBindingRepository : ISolutionBindingRepository + private readonly ISolutionBindingFileLoader solutionBindingFileLoader; + private readonly ISolutionBindingCredentialsLoader credentialsLoader; + private readonly IUnintrusiveBindingPathProvider unintrusiveBindingPathProvider; + private readonly IServerConnectionsRepository serverConnectionsRepository; + private readonly IBindingDtoConverter bindingDtoConverter; + private readonly ILogger logger; + + [ImportingConstructor] + public SolutionBindingRepository(IUnintrusiveBindingPathProvider unintrusiveBindingPathProvider, + IBindingDtoConverter bindingDtoConverter, + IServerConnectionsRepository serverConnectionsRepository, + ICredentialStoreService credentialStoreService, + ILogger logger) + : this(unintrusiveBindingPathProvider, + bindingDtoConverter, + serverConnectionsRepository, + new SolutionBindingFileLoader(logger), + new SolutionBindingCredentialsLoader(credentialStoreService), + logger) + { + } + + internal /* for testing */ SolutionBindingRepository(IUnintrusiveBindingPathProvider unintrusiveBindingPathProvider, + IBindingDtoConverter bindingDtoConverter, + IServerConnectionsRepository serverConnectionsRepository, + ISolutionBindingFileLoader solutionBindingFileLoader, + ISolutionBindingCredentialsLoader credentialsLoader, + ILogger logger) + { + this.unintrusiveBindingPathProvider = unintrusiveBindingPathProvider; + this.serverConnectionsRepository = serverConnectionsRepository; + this.bindingDtoConverter = bindingDtoConverter; + this.solutionBindingFileLoader = solutionBindingFileLoader ?? throw new ArgumentNullException(nameof(solutionBindingFileLoader)); + this.credentialsLoader = credentialsLoader ?? throw new ArgumentNullException(nameof(credentialsLoader)); + this.logger = logger; + } + + BoundSonarQubeProject ILegacySolutionBindingRepository.Read(string configFilePath) + { + var bindingDto = ReadBindingFile(configFilePath); + return bindingDto switch + { + null => null, + not null => bindingDtoConverter.ConvertFromDtoToLegacy(bindingDto, credentialsLoader.Load(bindingDto.ServerUri)) + }; + } + + public BoundServerProject Read(string configFilePath) + { + return Convert(ReadBindingFile(configFilePath), configFilePath); + } + + public bool Write(string configFilePath, BoundServerProject binding) { - private readonly ISolutionBindingFileLoader solutionBindingFileLoader; - private readonly ISolutionBindingCredentialsLoader credentialsLoader; - private readonly IUnintrusiveBindingPathProvider unintrusiveBindingPathProvider; + _ = binding ?? throw new ArgumentNullException(nameof(binding)); - [ImportingConstructor] - public SolutionBindingRepository(IUnintrusiveBindingPathProvider unintrusiveBindingPathProvider, ICredentialStoreService credentialStoreService, ILogger logger) - : this(unintrusiveBindingPathProvider, new SolutionBindingFileLoader(logger), new SolutionBindingCredentialsLoader(credentialStoreService)) + if (string.IsNullOrEmpty(configFilePath)) { + return false; } - internal /* for testing */ SolutionBindingRepository(IUnintrusiveBindingPathProvider unintrusiveBindingPathProvider, ISolutionBindingFileLoader solutionBindingFileLoader, ISolutionBindingCredentialsLoader credentialsLoader) + if (!solutionBindingFileLoader.Save(configFilePath, bindingDtoConverter.ConvertToDto(binding))) { - this.solutionBindingFileLoader = solutionBindingFileLoader ?? throw new ArgumentNullException(nameof(solutionBindingFileLoader)); - this.credentialsLoader = credentialsLoader ?? throw new ArgumentNullException(nameof(credentialsLoader)); - this.unintrusiveBindingPathProvider = unintrusiveBindingPathProvider; + return false; } - public BoundSonarQubeProject Read(string configFilePath) + BindingUpdated?.Invoke(this, EventArgs.Empty); + + return true; + } + + public event EventHandler BindingUpdated; + + public IEnumerable List() + { + if (!serverConnectionsRepository.TryGetAll(out var connections)) { - return Read(configFilePath, true); + throw new InvalidOperationException("Could not retrieve all connections."); } + + var bindingConfigPaths = unintrusiveBindingPathProvider.GetBindingPaths(); - public bool Write(string configFilePath, BoundSonarQubeProject binding) + foreach (var bindingConfigPath in bindingConfigPaths) { - _ = binding ?? throw new ArgumentNullException(nameof(binding)); + var bindingDto = ReadBindingFile(bindingConfigPath); - if (string.IsNullOrEmpty(configFilePath)) + if (bindingDto == null) { - return false; + logger.LogVerbose($"Skipped {bindingConfigPath} because it could not be read"); + continue; } - if (!solutionBindingFileLoader.Save(configFilePath, binding)) + if (connections.FirstOrDefault(c => c.Id == bindingDto.ServerConnectionId) is not {} serverConnection) { - return false; + logger.LogVerbose($"Skipped {bindingConfigPath} because connection {bindingDto.ServerConnectionId} doesn't exist"); + continue; } - credentialsLoader.Save(binding.Credentials, binding.ServerUri); + var boundServerProject = Convert(bindingDto, serverConnection, bindingConfigPath); - BindingUpdated?.Invoke(this, EventArgs.Empty); - - return true; + yield return boundServerProject; } + } + + private BindingDto ReadBindingFile(string configFilePath) + { + var bound = solutionBindingFileLoader.Load(configFilePath); - public event EventHandler BindingUpdated; - - public IEnumerable List() + if (bound is null) { - var bindingConfigPaths = unintrusiveBindingPathProvider.GetBindingPaths(); - - foreach (var bindingConfigPath in bindingConfigPaths) - { - var boundSonarQubeProject = Read(bindingConfigPath, false); - - if (boundSonarQubeProject == null) { continue; } - - yield return boundSonarQubeProject; - } + return null; } - private BoundSonarQubeProject Read(string configFilePath, bool loadCredentials) - { - var bound = solutionBindingFileLoader.Load(configFilePath); + Debug.Assert(!bound.Profiles?.ContainsKey(Core.Language.Unknown) ?? true, + "Not expecting the deserialized binding config to contain the profile for an unknown language"); - if (bound is null) - { - return null; - } + return bound; - if (loadCredentials) - { - bound.Credentials = credentialsLoader.Load(bound.ServerUri); - } + } - Debug.Assert(!bound.Profiles?.ContainsKey(Core.Language.Unknown) ?? true, - "Not expecting the deserialized binding config to contain the profile for an unknown language"); + private BoundServerProject Convert(BindingDto bindingDto, string configFilePath) => + bindingDto is not null && serverConnectionsRepository.TryGet(bindingDto.ServerConnectionId, out var connection) + ? Convert(bindingDto, connection, configFilePath) + : null; - return bound; - } - } + private BoundServerProject Convert(BindingDto bindingDto, ServerConnection connection, string configFilePath) => + bindingDtoConverter.ConvertFromDto(bindingDto, connection, unintrusiveBindingPathProvider.GetBindingKeyFromPath(configFilePath)); } diff --git a/src/ConnectedMode/ProjectRootCalculator.cs b/src/ConnectedMode/ProjectRootCalculator.cs index fd41fbe561..1d652c8f7a 100644 --- a/src/ConnectedMode/ProjectRootCalculator.cs +++ b/src/ConnectedMode/ProjectRootCalculator.cs @@ -60,7 +60,7 @@ public async Task CalculateBasedOnLocalPathAsync(string localPath, Cance } return PathHelper.CalculateServerRoot(localPath, - await sonarQubeService.SearchFilesByNameAsync(bindingConfiguration.Project.ProjectKey, + await sonarQubeService.SearchFilesByNameAsync(bindingConfiguration.Project.ServerProjectKey, await branchProvider.GetServerBranchNameAsync(token), Path.GetFileName(localPath), token)); diff --git a/src/ConnectedMode/QualityProfiles/OutOfDateQualityProfileFinder.cs b/src/ConnectedMode/QualityProfiles/OutOfDateQualityProfileFinder.cs index 25db17efeb..b47501edf2 100644 --- a/src/ConnectedMode/QualityProfiles/OutOfDateQualityProfileFinder.cs +++ b/src/ConnectedMode/QualityProfiles/OutOfDateQualityProfileFinder.cs @@ -37,8 +37,7 @@ internal interface IOutOfDateQualityProfileFinder /// /// Gives the list of outdated quality profiles based on the existing ones from /// - Task> GetAsync( - BoundSonarQubeProject sonarQubeProject, + Task> GetAsync(BoundServerProject sonarQubeProject, CancellationToken cancellationToken); } @@ -55,12 +54,12 @@ public OutOfDateQualityProfileFinder(ISonarQubeService sonarQubeService) } public async Task> GetAsync( - BoundSonarQubeProject sonarQubeProject, + BoundServerProject sonarQubeProject, CancellationToken cancellationToken) { var sonarQubeQualityProfiles = - await sonarQubeService.GetAllQualityProfilesAsync(sonarQubeProject.ProjectKey, - sonarQubeProject.Organization?.Key, + await sonarQubeService.GetAllQualityProfilesAsync(sonarQubeProject.ServerProjectKey, + (sonarQubeProject.ServerConnection as ServerConnection.SonarCloud)?.OrganizationKey, cancellationToken); return sonarQubeQualityProfiles @@ -72,7 +71,7 @@ await sonarQubeService.GetAllQualityProfilesAsync(sonarQubeProject.ProjectKey, .ToArray(); } - private static bool IsLocalQPOutOfDate(BoundSonarQubeProject sonarQubeProject, Language language, + private static bool IsLocalQPOutOfDate(BoundServerProject sonarQubeProject, Language language, SonarQubeQualityProfile serverQualityProfile) { if (language == default) diff --git a/src/ConnectedMode/QualityProfiles/QualityProfileDownloader.cs b/src/ConnectedMode/QualityProfiles/QualityProfileDownloader.cs index 1193747c6f..631d9cd1a7 100644 --- a/src/ConnectedMode/QualityProfiles/QualityProfileDownloader.cs +++ b/src/ConnectedMode/QualityProfiles/QualityProfileDownloader.cs @@ -37,7 +37,7 @@ internal interface IQualityProfileDownloader /// /// true if there were changes updated, false if everything is up to date /// If binding failed for one of the languages - Task UpdateAsync(BoundSonarQubeProject boundProject, IProgress progress, CancellationToken cancellationToken); + Task UpdateAsync(BoundServerProject boundProject, IProgress progress, CancellationToken cancellationToken); } [Export(typeof(IQualityProfileDownloader))] @@ -80,7 +80,8 @@ public QualityProfileDownloader( this.outOfDateQualityProfileFinder = outOfDateQualityProfileFinder; } - public async Task UpdateAsync(BoundSonarQubeProject boundProject, IProgress progress, CancellationToken cancellationToken) + public async Task UpdateAsync(BoundServerProject boundProject, IProgress progress, + CancellationToken cancellationToken) { var isChanged = false; @@ -140,7 +141,7 @@ public async Task UpdateAsync(BoundSonarQubeProject boundProject, IProgres /// /// If we add support for new language in the future, this method will make sure it's /// Quality Profile is fetched next time an update is triggered - private void EnsureProfilesExistForAllSupportedLanguages(BoundSonarQubeProject boundProject) + private void EnsureProfilesExistForAllSupportedLanguages(BoundServerProject boundProject) { if (boundProject.Profiles == null) { @@ -160,7 +161,7 @@ private void EnsureProfilesExistForAllSupportedLanguages(BoundSonarQubeProject b } } - private static void UpdateProfile(BoundSonarQubeProject boundSonarQubeProject, Language language, SonarQubeQualityProfile serverProfile) + private static void UpdateProfile(BoundServerProject boundSonarQubeProject, Language language, SonarQubeQualityProfile serverProfile) { boundSonarQubeProject.Profiles[language] = new ApplicableQualityProfile { diff --git a/src/ConnectedMode/Resources.Designer.cs b/src/ConnectedMode/Resources.Designer.cs index 38b01fc13b..897fc6f430 100644 --- a/src/ConnectedMode/Resources.Designer.cs +++ b/src/ConnectedMode/Resources.Designer.cs @@ -69,6 +69,15 @@ internal static string ActionRunner_CancellingCurrentOperation { } } + /// + /// Looks up a localized string similar to [ConnectedMode] Binding failed due to: {0}. + /// + internal static string Binding_Fails { + get { + return ResourceManager.GetString("Binding_Fails", resourceCulture); + } + } + /// /// Looks up a localized string similar to [ConnectedMode/BranchMapping] Finished calculating closest Sonar server branch. Result: {0}. /// @@ -168,6 +177,33 @@ internal static string BranchProvider_NotInConnectedMode { } } + /// + /// Looks up a localized string similar to [ConnectedMode] Getting all projects failed. + /// + internal static string GetAllProjects_Fails { + get { + return ResourceManager.GetString("GetAllProjects_Fails", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [ConnectedMode] Failed to get project name by key. + /// + internal static string GetServerProjectByKey_Fails { + get { + return ResourceManager.GetString("GetServerProjectByKey_Fails", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [ConnectedMode] Server did not return a project name for the specified project key {0}. + /// + internal static string GetServerProjectByKey_ProjectNotFound { + get { + return ResourceManager.GetString("GetServerProjectByKey_ProjectNotFound", resourceCulture); + } + } + /// /// Looks up a localized string similar to [ConnectedMode] Error handling repo change notification: {0}. /// @@ -321,6 +357,15 @@ internal static string ImportBeforeFileGenerator_WritingTargetFileToDisk { } } + /// + /// Looks up a localized string similar to [ConnectedMode] Listing user organizations failed. + /// + internal static string ListUserOrganizations_Fails { + get { + return ResourceManager.GetString("ListUserOrganizations_Fails", resourceCulture); + } + } + /// /// Looks up a localized string similar to Error. /// @@ -590,5 +635,41 @@ internal static string TimedUpdateTriggered { return ResourceManager.GetString("TimedUpdateTriggered", resourceCulture); } } + + /// + /// Looks up a localized string similar to Unexpected server connection type. + /// + internal static string UnexpectedConnectionType { + get { + return ResourceManager.GetString("UnexpectedConnectionType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [ConnectedMode/UseSharedBinding] Connection to server {0} could not be found. + /// + internal static string UseSharedBinding_ConnectionNotFound { + get { + return ResourceManager.GetString("UseSharedBinding_ConnectionNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [ConnectedMode/UseSharedBinding] The credentials for the connection {0} could not be found. + /// + internal static string UseSharedBinding_CredentiasNotFound { + get { + return ResourceManager.GetString("UseSharedBinding_CredentiasNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [ConnectedMode] Validating credentials failed. + /// + internal static string ValidateCredentials_Fails { + get { + return ResourceManager.GetString("ValidateCredentials_Fails", resourceCulture); + } + } } } diff --git a/src/ConnectedMode/Resources.resx b/src/ConnectedMode/Resources.resx index b4afb73048..93a17b553e 100644 --- a/src/ConnectedMode/Resources.resx +++ b/src/ConnectedMode/Resources.resx @@ -301,4 +301,31 @@ Credentials you have provided do not have enough permission to resolve issues. It requires the permission 'Administer Issues'. + + [ConnectedMode] Validating credentials failed + + + [ConnectedMode] Listing user organizations failed + + + [ConnectedMode] Failed to get project name by key + + + [ConnectedMode] Server did not return a project name for the specified project key {0} + + + [ConnectedMode] Getting all projects failed + + + [ConnectedMode] Binding failed due to: {0} + + + [ConnectedMode/UseSharedBinding] Connection to server {0} could not be found + + + Unexpected server connection type + + + [ConnectedMode/UseSharedBinding] The credentials for the connection {0} could not be found + \ No newline at end of file diff --git a/src/ConnectedMode/ServerBranchProvider.cs b/src/ConnectedMode/ServerBranchProvider.cs index 871f71d581..4c9a76cd4f 100644 --- a/src/ConnectedMode/ServerBranchProvider.cs +++ b/src/ConnectedMode/ServerBranchProvider.cs @@ -87,7 +87,7 @@ public async Task GetServerBranchNameAsync(CancellationToken token) { logger.LogVerbose(Resources.BranchProvider_FailedToCalculateMatchingBranch); - var remoteBranches = await sonarQubeService.GetProjectBranchesAsync(config.Project.ProjectKey, token); + var remoteBranches = await sonarQubeService.GetProjectBranchesAsync(config.Project.ServerProjectKey, token); matchingBranchName = remoteBranches.First(rb => rb.IsMain).Name; } @@ -108,7 +108,7 @@ private async Task CalculateMatchingBranchAsync(BindingConfiguration con } var repo = createRepo(gitRepoRoot); - var branchName = await branchMatcher.GetMatchingBranch(config.Project.ProjectKey, repo, token); + var branchName = await branchMatcher.GetMatchingBranch(config.Project.ServerProjectKey, repo, token); return branchName; } diff --git a/src/ConnectedMode/ServerConnectionsRepositoryAdapter.cs b/src/ConnectedMode/ServerConnectionsRepositoryAdapter.cs new file mode 100644 index 0000000000..3935514fd1 --- /dev/null +++ b/src/ConnectedMode/ServerConnectionsRepositoryAdapter.cs @@ -0,0 +1,115 @@ +/* + * 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 System.ComponentModel.Composition; +using System.Security; +using SonarLint.VisualStudio.ConnectedMode.Persistence; +using SonarLint.VisualStudio.ConnectedMode.UI.Credentials; +using SonarLint.VisualStudio.Core.Binding; +using SonarQube.Client.Helpers; + +namespace SonarLint.VisualStudio.ConnectedMode; + +public interface IServerConnectionsRepositoryAdapter +{ + bool TryGetAllConnections(out List connections); + bool TryGetAllConnectionsInfo(out List connectionInfos); + bool TryRemoveConnection(ConnectionInfo connectionInfo); + bool TryAddConnection(Connection connection, ICredentialsModel credentialsModel); + bool TryGet(ConnectionInfo connectionInfo, out ServerConnection serverConnection); +} + +[Export(typeof(IServerConnectionsRepositoryAdapter))] +[method: ImportingConstructor] +internal class ServerConnectionsRepositoryAdapter(IServerConnectionsRepository serverConnectionsRepository) : IServerConnectionsRepositoryAdapter +{ + public bool TryGetAllConnections(out List connections) + { + var succeeded = serverConnectionsRepository.TryGetAll(out var serverConnections); + connections = serverConnections?.Select(MapServerConnectionModel).ToList(); + return succeeded; + } + + public bool TryGetAllConnectionsInfo(out List connectionInfos) + { + var succeeded = TryGetAllConnections(out var connections); + connectionInfos = connections?.Select(conn => conn.Info).ToList(); + return succeeded; + } + + public bool TryAddConnection(Connection connection, ICredentialsModel credentialsModel) + { + var serverConnection = MapConnection(connection); + serverConnection.Credentials = MapCredentials(credentialsModel); + return serverConnectionsRepository.TryAdd(serverConnection); + } + + public bool TryGet(ConnectionInfo connectionInfo, out ServerConnection serverConnection) + { + var connectionId = GetServerIdFromConnectionInfo(connectionInfo); + return serverConnectionsRepository.TryGet(connectionId, out serverConnection); + } + + public bool TryRemoveConnection(ConnectionInfo connectionInfo) + { + var connectionId = GetServerIdFromConnectionInfo(connectionInfo); + return serverConnectionsRepository.TryDelete(connectionId); + } + + private static Connection MapServerConnectionModel(ServerConnection serverConnection) + { + var connectionInfo = ConnectionInfo.From(serverConnection); + return new Connection(connectionInfo, serverConnection.Settings.IsSmartNotificationsEnabled); + } + + private static ServerConnection MapConnection(Connection connection) + { + if (connection.Info.ServerType == ConnectionServerType.SonarCloud) + { + return new ServerConnection.SonarCloud(connection.Info.Id, new ServerConnectionSettings(connection.EnableSmartNotifications)); + } + + return new ServerConnection.SonarQube(new Uri(connection.Info.Id), new ServerConnectionSettings(connection.EnableSmartNotifications)); + } + + private static ICredentials MapCredentials(ICredentialsModel credentialsModel) + { + switch (credentialsModel) + { + case TokenCredentialsModel tokenCredentialsModel: + return new BasicAuthCredentials(tokenCredentialsModel.Token.ToUnsecureString(), new SecureString()); + case UsernamePasswordModel usernameCredentialsModel: + { + return new BasicAuthCredentials(usernameCredentialsModel.Username, usernameCredentialsModel.Password); + } + default: + return null; + } + } + + private static string GetServerIdFromConnectionInfo(ConnectionInfo connectionInfo) + { + ServerConnection partialServerConnection = connectionInfo.ServerType == ConnectionServerType.SonarCloud + ? new ServerConnection.SonarCloud(connectionInfo.Id) + : new ServerConnection.SonarQube(new Uri(connectionInfo.Id)); + + return partialServerConnection.Id; + } +} diff --git a/src/ConnectedMode/ServerIssueFinder.cs b/src/ConnectedMode/ServerIssueFinder.cs index 34fa6f5902..b4c5b2a002 100644 --- a/src/ConnectedMode/ServerIssueFinder.cs +++ b/src/ConnectedMode/ServerIssueFinder.cs @@ -80,10 +80,10 @@ public async Task FindServerIssueAsync(IFilterableIssue localIss var componentKey = ComponentKeyGenerator.GetComponentKey(localIssue.FilePath, projectRoot, - bindingConfiguration.Project.ProjectKey); + bindingConfiguration.Project.ServerProjectKey); var serverIssues = await sonarQubeService.GetIssuesForComponentAsync( - bindingConfiguration.Project.ProjectKey, + bindingConfiguration.Project.ServerProjectKey, await serverBranchProvider.GetServerBranchNameAsync(token), componentKey, localIssue.RuleId, diff --git a/src/ConnectedMode/ServerQueryInfoProvider.cs b/src/ConnectedMode/ServerQueryInfoProvider.cs index b0bdbc08a4..0414d1466c 100644 --- a/src/ConnectedMode/ServerQueryInfoProvider.cs +++ b/src/ConnectedMode/ServerQueryInfoProvider.cs @@ -64,7 +64,7 @@ public ServerQueryInfoProvider(IConfigurationProvider configurationProvider, ISt } var branchName = await serverBranchProvider.GetServerBranchNameAsync(token); - return (config.Project.ProjectKey, branchName); + return (config.Project.ServerProjectKey, branchName); } } } diff --git a/src/ConnectedMode/ServerSentEvents/SSESessionManager.cs b/src/ConnectedMode/ServerSentEvents/SSESessionManager.cs index d27437e99b..2d0c8bc020 100644 --- a/src/ConnectedMode/ServerSentEvents/SSESessionManager.cs +++ b/src/ConnectedMode/ServerSentEvents/SSESessionManager.cs @@ -94,7 +94,7 @@ public void CreateSessionIfInConnectedMode(BindingConfiguration bindingConfigura logger.LogVerbose("[SSESessionManager] In connected mode, creating session..."); - currentSession = sseSessionFactory.Create(bindingConfiguration.Project.ProjectKey, OnSessionFailedAsync); + currentSession = sseSessionFactory.Create(bindingConfiguration.Project.ServerProjectKey, OnSessionFailedAsync); logger.LogVerbose("[SSESessionManager] Created session: {0}", currentSession.GetHashCode()); diff --git a/src/ConnectedMode/SlCoreConnectionAdapter.cs b/src/ConnectedMode/SlCoreConnectionAdapter.cs new file mode 100644 index 0000000000..1ed36eaea2 --- /dev/null +++ b/src/ConnectedMode/SlCoreConnectionAdapter.cs @@ -0,0 +1,255 @@ +/* + * 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 System.ComponentModel.Composition; +using SonarLint.VisualStudio.ConnectedMode.Persistence; +using SonarLint.VisualStudio.ConnectedMode.UI; +using SonarLint.VisualStudio.ConnectedMode.UI.Credentials; +using SonarLint.VisualStudio.ConnectedMode.UI.OrganizationSelection; +using SonarLint.VisualStudio.ConnectedMode.UI.ProjectSelection; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Binding; +using SonarLint.VisualStudio.SLCore; +using SonarLint.VisualStudio.SLCore.Common.Models; +using SonarLint.VisualStudio.SLCore.Core; +using SonarLint.VisualStudio.SLCore.Protocol; +using SonarLint.VisualStudio.SLCore.Service.Connection; +using SonarLint.VisualStudio.SLCore.Service.Connection.Models; +using SonarQube.Client.Helpers; + +namespace SonarLint.VisualStudio.ConnectedMode; + +public interface ISlCoreConnectionAdapter +{ + Task ValidateConnectionAsync(ConnectionInfo connectionInfo, ICredentialsModel credentialsModel); + Task>> GetOrganizationsAsync(ICredentialsModel credentialsModel); + Task> GetServerProjectByKeyAsync(ServerConnection serverConnection, string serverProjectKey); + Task>> GetAllProjectsAsync(ServerConnection serverConnection); +} + +public class AdapterResponseWithData(bool success, T responseData) : IResponseStatus +{ + public bool Success { get; init; } = success; + public T ResponseData { get; } = responseData; +} + +public class AdapterResponse(bool success) : IResponseStatus +{ + public bool Success { get; } = success; +} + +[Export(typeof(ISlCoreConnectionAdapter))] +public class SlCoreConnectionAdapter : ISlCoreConnectionAdapter +{ + private readonly ISLCoreServiceProvider serviceProvider; + private readonly IThreadHandling threadHandling; + private readonly ILogger logger; + private static readonly AdapterResponseWithData> FailedResponseWithData = new(false, []); + private static readonly AdapterResponse FailedResponse = new(false); + + [ImportingConstructor] + public SlCoreConnectionAdapter(ISLCoreServiceProvider serviceProvider, IThreadHandling threadHandling, ILogger logger) + { + this.serviceProvider = serviceProvider; + this.threadHandling = threadHandling; + this.logger = logger; + } + + public async Task ValidateConnectionAsync(ConnectionInfo connectionInfo, ICredentialsModel credentialsModel) + { + var credentials = credentialsModel.ToICredentials(); + + var validateConnectionParams = new ValidateConnectionParams(GetTransientConnectionDto(connectionInfo, credentials)); + return await ValidateConnectionAsync(validateConnectionParams); + } + + public Task>> GetOrganizationsAsync(ICredentialsModel credentialsModel) + { + return threadHandling.RunOnBackgroundThread(async () => + { + if (!TryGetConnectionConfigurationSlCoreService(out var connectionConfigurationSlCoreService)) + { + return FailedResponseWithData; + } + + try + { + var credentials = MapCredentials(credentialsModel?.ToICredentials()); + var response = await connectionConfigurationSlCoreService.ListUserOrganizationsAsync(new ListUserOrganizationsParams(credentials)); + var organizationDisplays = response.userOrganizations.Select(o => new OrganizationDisplay(o.key, o.name)).ToList(); + + return new AdapterResponseWithData>(true, organizationDisplays); + } + catch (Exception ex) + { + logger.LogVerbose($"{Resources.ListUserOrganizations_Fails}: {ex.Message}"); + return FailedResponseWithData; + } + }); + } + + public Task> GetServerProjectByKeyAsync(ServerConnection serverConnection, string serverProjectKey) + { + var failedResponse = new AdapterResponseWithData(false, null); + + return threadHandling.RunOnBackgroundThread(async () => + { + if (!TryGetConnectionConfigurationSlCoreService(out var connectionConfigurationSlCoreService)) + { + return failedResponse; + } + + try + { + var transientConnection = GetTransientConnectionDto(serverConnection); + var response = await connectionConfigurationSlCoreService.GetProjectNamesByKeyAsync(new GetProjectNamesByKeyParams(transientConnection, [serverProjectKey])); + + if (response.projectNamesByKey.TryGetValue(serverProjectKey, out var projectName) && projectName == null) + { + logger.LogVerbose(Resources.GetServerProjectByKey_ProjectNotFound, serverProjectKey); + return failedResponse; + } + + return new AdapterResponseWithData(true, new ServerProject(serverProjectKey, response.projectNamesByKey[serverProjectKey])); + } + catch (Exception ex) + { + logger.LogVerbose($"{Resources.GetServerProjectByKey_Fails}: {ex.Message}"); + return failedResponse; + } + }); + } + + public async Task>> GetAllProjectsAsync(ServerConnection serverConnection) + { + var validateConnectionParams = new GetAllProjectsParams(GetTransientConnectionDto(serverConnection)); + return await GetAllProjectsAsync(validateConnectionParams); + } + + private async Task ValidateConnectionAsync(ValidateConnectionParams validateConnectionParams) + { + return await threadHandling.RunOnBackgroundThread(async () => + { + if (!TryGetConnectionConfigurationSlCoreService(out var connectionConfigurationSlCoreService)) + { + return FailedResponse; + } + + try + { + var slCoreResponse = await connectionConfigurationSlCoreService.ValidateConnectionAsync(validateConnectionParams); + return new AdapterResponse(slCoreResponse.success); + } + catch (Exception ex) + { + logger.LogVerbose($"{Resources.ValidateCredentials_Fails}: {ex.Message}"); + return FailedResponse; + } + }); + } + + private async Task>> GetAllProjectsAsync(GetAllProjectsParams getAllProjectsParams) + { + var failedResponse = new AdapterResponseWithData>(false, []); + return await threadHandling.RunOnBackgroundThread(async () => + { + if (!TryGetConnectionConfigurationSlCoreService(out var connectionConfigurationSlCoreService)) + { + return failedResponse; + } + + try + { + var slCoreResponse = await connectionConfigurationSlCoreService.GetAllProjectsAsync(getAllProjectsParams); + var serverProjects = slCoreResponse.sonarProjects.Select(proj => new ServerProject(proj.key, proj.name)).ToList(); + return new AdapterResponseWithData>(true, serverProjects); + } + catch (Exception ex) + { + logger.LogVerbose($"{Resources.GetAllProjects_Fails}: {ex.Message}"); + return failedResponse; + } + }); + } + + private bool TryGetConnectionConfigurationSlCoreService(out IConnectionConfigurationSLCoreService connectionConfigurationSlCoreService) + { + if (serviceProvider.TryGetTransientService(out IConnectionConfigurationSLCoreService slCoreService)) + { + connectionConfigurationSlCoreService = slCoreService; + return true; + } + + connectionConfigurationSlCoreService = null; + logger.LogVerbose($"[{nameof(IConnectionConfigurationSLCoreService)}] {SLCoreStrings.ServiceProviderNotInitialized}"); + return false; + } + + private static Either GetTransientConnectionDto(ConnectionInfo connectionInfo, ICredentials credentials) + { + var credentialsDto = MapCredentials(credentials); + + return connectionInfo.ServerType switch + { + ConnectionServerType.SonarQube => Either.CreateLeft( + new TransientSonarQubeConnectionDto(connectionInfo.Id, credentialsDto)), + ConnectionServerType.SonarCloud => Either.CreateRight( + new TransientSonarCloudConnectionDto(connectionInfo.Id, credentialsDto)), + _ => throw new ArgumentException(Resources.UnexpectedConnectionType) + }; + } + + private static Either GetTransientConnectionDto(ServerConnection serverConnection) + { + var credentials = MapCredentials(serverConnection.Credentials); + + return serverConnection switch + { + ServerConnection.SonarQube sonarQubeConnection => Either.CreateLeft( + new TransientSonarQubeConnectionDto(sonarQubeConnection.Id, credentials)), + ServerConnection.SonarCloud sonarCloudConnection => Either.CreateRight( + new TransientSonarCloudConnectionDto(sonarCloudConnection.OrganizationKey, credentials)), + _ => throw new ArgumentException(Resources.UnexpectedConnectionType) + }; + } + + private static Either GetEitherForToken(string token) + { + return Either.CreateLeft(new TokenDto(token)); + } + + private static Either GetEitherForUsernamePassword(string username, string password) + { + return Either.CreateRight(new UsernamePasswordDto(username, password)); + } + + private static Either MapCredentials(ICredentials credentials) + { + if (credentials == null) + { + throw new ArgumentException($"Unexpected {nameof(ICredentialsModel)} argument"); + } + + var basicAuthCredentials = (BasicAuthCredentials) credentials; + return basicAuthCredentials.Password?.Length > 0 + ? GetEitherForUsernamePassword(basicAuthCredentials.UserName, basicAuthCredentials.Password.ToUnsecureString()) + : GetEitherForToken(basicAuthCredentials.UserName); + } +} diff --git a/src/ConnectedMode/UI/ConnectedModeBindingServices.cs b/src/ConnectedMode/UI/ConnectedModeBindingServices.cs new file mode 100644 index 0000000000..0ac2bd1511 --- /dev/null +++ b/src/ConnectedMode/UI/ConnectedModeBindingServices.cs @@ -0,0 +1,51 @@ +/* + * 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 System.ComponentModel.Composition; +using SonarLint.VisualStudio.ConnectedMode.Binding; +using SonarLint.VisualStudio.ConnectedMode.Shared; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Binding; + +namespace SonarLint.VisualStudio.ConnectedMode.UI; + +public interface IConnectedModeBindingServices +{ + public IBindingController BindingController { get; } + public ISolutionInfoProvider SolutionInfoProvider { get; } + public ISharedBindingConfigProvider SharedBindingConfigProvider { get; } + public ISolutionBindingRepository SolutionBindingRepository { get; } +} + +[Export(typeof(IConnectedModeBindingServices))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +public class ConnectedModeBindingServices( + IBindingController bindingController, + ISolutionInfoProvider solutionInfoProvider, + ISharedBindingConfigProvider sharedBindingConfigProvider, + ISolutionBindingRepository solutionBindingRepository) + : IConnectedModeBindingServices +{ + public IBindingController BindingController { get; } = bindingController; + public ISolutionInfoProvider SolutionInfoProvider { get; } = solutionInfoProvider; + public ISharedBindingConfigProvider SharedBindingConfigProvider { get; } = sharedBindingConfigProvider; + public ISolutionBindingRepository SolutionBindingRepository { get; } = solutionBindingRepository; +} diff --git a/src/ConnectedMode/UI/ConnectedModeServices.cs b/src/ConnectedMode/UI/ConnectedModeServices.cs new file mode 100644 index 0000000000..0bdd1ee902 --- /dev/null +++ b/src/ConnectedMode/UI/ConnectedModeServices.cs @@ -0,0 +1,59 @@ +/* + * 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 System.ComponentModel.Composition; +using SonarLint.VisualStudio.ConnectedMode.Shared; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Binding; + +namespace SonarLint.VisualStudio.ConnectedMode.UI; + +public interface IConnectedModeServices +{ + public IBrowserService BrowserService { get; } + public IThreadHandling ThreadHandling { get; } + public ILogger Logger { get; } + public ISlCoreConnectionAdapter SlCoreConnectionAdapter { get; } + public IConfigurationProvider ConfigurationProvider { get; } + public IServerConnectionsRepositoryAdapter ServerConnectionsRepositoryAdapter { get; } + public IMessageBox MessageBox { get; } +} + +[Export(typeof(IConnectedModeServices))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +public class ConnectedModeServices( + IBrowserService browserService, + IThreadHandling threadHandling, + ISlCoreConnectionAdapter slCoreConnectionAdapter, + IConfigurationProvider configurationProvider, + IServerConnectionsRepositoryAdapter serverConnectionsRepositoryAdapter, + IMessageBox messageBox, + ILogger logger) + : IConnectedModeServices +{ + public IServerConnectionsRepositoryAdapter ServerConnectionsRepositoryAdapter { get; } = serverConnectionsRepositoryAdapter; + public IMessageBox MessageBox { get; } = messageBox; + public IBrowserService BrowserService { get; } = browserService; + public IThreadHandling ThreadHandling { get; } = threadHandling; + public ILogger Logger { get; } = logger; + public ISlCoreConnectionAdapter SlCoreConnectionAdapter { get; } = slCoreConnectionAdapter; + public IConfigurationProvider ConfigurationProvider { get; } = configurationProvider; +} diff --git a/src/ConnectedMode/UI/ConnectionInfoComponent.xaml b/src/ConnectedMode/UI/ConnectionInfoComponent.xaml new file mode 100644 index 0000000000..33580275b5 --- /dev/null +++ b/src/ConnectedMode/UI/ConnectionInfoComponent.xaml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/ConnectedMode/UI/ConnectionInfoComponent.xaml.cs b/src/ConnectedMode/UI/ConnectionInfoComponent.xaml.cs new file mode 100644 index 0000000000..db274fdcc8 --- /dev/null +++ b/src/ConnectedMode/UI/ConnectionInfoComponent.xaml.cs @@ -0,0 +1,72 @@ +/* + * 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 System.Diagnostics.CodeAnalysis; +using System.Windows; +using System.Windows.Controls; + +namespace SonarLint.VisualStudio.ConnectedMode.UI; + +[ExcludeFromCodeCoverage] // UI, not really unit-testable +public sealed partial class ConnectionInfoComponent : UserControl +{ + public static readonly DependencyProperty ConnectionInfoProp = DependencyProperty.Register(nameof(ConnectionInfo), typeof(ConnectionInfo), typeof(ConnectionInfoComponent), new PropertyMetadata(OnConnectionInfoSet)); + public static readonly DependencyProperty FontWeightProp = DependencyProperty.Register(nameof(TextFontWeight), typeof(FontWeight), typeof(ConnectionInfoComponent), new PropertyMetadata(FontWeights.DemiBold)); + public static readonly DependencyProperty TextAndIconVerticalAlignmentProp = DependencyProperty.Register(nameof(TextAndIconVerticalAlignment), typeof(VerticalAlignment), typeof(ConnectionInfoComponent), new PropertyMetadata(VerticalAlignment.Bottom)); + public static readonly DependencyProperty ImageMarginProp = DependencyProperty.Register(nameof(ImageMargin), typeof(Thickness), typeof(ConnectionInfoComponent), new PropertyMetadata(new Thickness(-5,-5,0,-5))); + + public ConnectionInfoComponent() + { + InitializeComponent(); + } + + public ConnectionInfo ConnectionInfo + { + get => (ConnectionInfo)GetValue(ConnectionInfoProp); + set => SetValue(ConnectionInfoProp, value); + } + + public FontWeight TextFontWeight + { + get => (FontWeight)GetValue(FontWeightProp); + set => SetValue(FontWeightProp, value); + } + + public VerticalAlignment TextAndIconVerticalAlignment + { + get => (VerticalAlignment)GetValue(TextAndIconVerticalAlignmentProp); + set => SetValue(TextAndIconVerticalAlignmentProp, value); + } + + public Thickness ImageMargin + { + get => (Thickness)GetValue(ImageMarginProp); + set => SetValue(ImageMarginProp, value); + } + + + private static void OnConnectionInfoSet(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is ConnectionInfoComponent component && e.NewValue is ConnectionInfo connectionInfo) + { + component.IdTextBlock.Text = connectionInfo.GetIdForTransientConnection(); + } + } +} diff --git a/src/ConnectedMode/UI/Credentials/CredentialsDialog.xaml b/src/ConnectedMode/UI/Credentials/CredentialsDialog.xaml new file mode 100644 index 0000000000..456f8109c0 --- /dev/null +++ b/src/ConnectedMode/UI/Credentials/CredentialsDialog.xaml @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Generate + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +