diff --git a/src/ConnectedMode.UnitTests/Binding/UnintrusiveBindingControllerTests.cs b/src/ConnectedMode.UnitTests/Binding/UnintrusiveBindingControllerTests.cs index 7a19eba585..9d0c447696 100644 --- a/src/ConnectedMode.UnitTests/Binding/UnintrusiveBindingControllerTests.cs +++ b/src/ConnectedMode.UnitTests/Binding/UnintrusiveBindingControllerTests.cs @@ -34,7 +34,7 @@ namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Binding; public class UnintrusiveBindingControllerTests { private static readonly CancellationToken ACancellationToken = CancellationToken.None; - private static readonly BasicAuthCredentials ValidToken = new("TOKEN", new SecureString()); + private static readonly UsernameAndPasswordCredentials ValidToken = new("TOKEN", new SecureString()); private static readonly BoundServerProject AnyBoundProject = new("any", "any", new ServerConnection.SonarCloud("any", credentials: ValidToken)); private IActiveSolutionChangedHandler activeSolutionChangedHandler; private IBindingProcess bindingProcess; @@ -86,8 +86,8 @@ await sonarQubeService .Received() .ConnectAsync( Arg.Is(x => x.ServerUri.Equals("https://sonarcloud.io/") - && x.UserName.Equals(ValidToken.UserName) - && string.IsNullOrEmpty(x.Password.ToUnsecureString())), + && ((UsernameAndPasswordCredentials)x.Credentials).UserName.Equals(ValidToken.UserName) + && string.IsNullOrEmpty(((UsernameAndPasswordCredentials)x.Credentials).Password.ToUnsecureString())), ACancellationToken); } diff --git a/src/ConnectedMode.UnitTests/Migration/BindingToConnectionMigrationTests.cs b/src/ConnectedMode.UnitTests/Migration/BindingToConnectionMigrationTests.cs index 1ff381840a..f2dfe1158f 100644 --- a/src/ConnectedMode.UnitTests/Migration/BindingToConnectionMigrationTests.cs +++ b/src/ConnectedMode.UnitTests/Migration/BindingToConnectionMigrationTests.cs @@ -234,7 +234,7 @@ private void MockValidBinding(string bindingPath, BoundSonarQubeProject sonarQub private static BoundSonarQubeProject CreateBoundProject(string url, string projectKey) { - return new BoundSonarQubeProject(new Uri(url), projectKey, "projectName", credentials: new BasicAuthCredentials("admin", "admin".ToSecureString())); + return new BoundSonarQubeProject(new Uri(url), projectKey, "projectName", credentials: new UsernameAndPasswordCredentials("admin", "admin".ToSecureString())); } private static bool IsExpectedServerConnection(ServerConnection serverConnection, BoundSonarQubeProject boundProject) diff --git a/src/ConnectedMode.UnitTests/Persistence/BindingJsonModelConverterTests.cs b/src/ConnectedMode.UnitTests/Persistence/BindingJsonModelConverterTests.cs index c329050866..7c62b2842a 100644 --- a/src/ConnectedMode.UnitTests/Persistence/BindingJsonModelConverterTests.cs +++ b/src/ConnectedMode.UnitTests/Persistence/BindingJsonModelConverterTests.cs @@ -111,7 +111,7 @@ public void ConvertToModel_SonarQubeConnection_ConvertsCorrectly() [TestMethod] public void ConvertFromModelToLegacy_ConvertsCorrectly() { - var credentials = Substitute.For(); + var credentials = Substitute.For(); var bindingModel = new BindingJsonModel { Organization = new SonarQubeOrganization("org", "my org"), diff --git a/src/ConnectedMode.UnitTests/Persistence/BoundSonarQubeProjectExtensionsTests.cs b/src/ConnectedMode.UnitTests/Persistence/BoundSonarQubeProjectExtensionsTests.cs index 00dcacdc8c..a37394851e 100644 --- a/src/ConnectedMode.UnitTests/Persistence/BoundSonarQubeProjectExtensionsTests.cs +++ b/src/ConnectedMode.UnitTests/Persistence/BoundSonarQubeProjectExtensionsTests.cs @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using Microsoft.VisualStudio.LanguageServices.Progression; using SonarLint.VisualStudio.ConnectedMode.Persistence; using SonarLint.VisualStudio.Core.Binding; using SonarLint.VisualStudio.TestInfrastructure; @@ -47,8 +48,7 @@ public void BoundSonarQubeProject_CreateConnectionInformation_NoCredentials() // Assert conn.ServerUri.Should().Be(input.ServerUri); - conn.UserName.Should().BeNull(); - conn.Password.Should().BeNull(); + conn.Credentials.Should().BeAssignableTo(); conn.Organization.Key.Should().Be("org_key"); conn.Organization.Name.Should().Be("org_name"); } @@ -57,7 +57,7 @@ public void BoundSonarQubeProject_CreateConnectionInformation_NoCredentials() public void BoundSonarQubeProject_CreateConnectionInformation_BasicAuthCredentials() { // Arrange - var creds = new BasicAuthCredentials("UserName", "password".ToSecureString()); + var creds = new UsernameAndPasswordCredentials("UserName", "password".ToSecureString()); var input = new BoundSonarQubeProject(new Uri("http://server"), "ProjectKey", "projectName", creds, new SonarQubeOrganization("org_key", "org_name")); @@ -66,8 +66,10 @@ public void BoundSonarQubeProject_CreateConnectionInformation_BasicAuthCredentia // Assert conn.ServerUri.Should().Be(input.ServerUri); - conn.UserName.Should().Be(creds.UserName); - conn.Password.ToUnsecureString().Should().Be(creds.Password.ToUnsecureString()); + var basicAuth = conn.Credentials as UsernameAndPasswordCredentials; + basicAuth.Should().NotBeNull(); + basicAuth.UserName.Should().Be(creds.UserName); + basicAuth.Password.ToUnsecureString().Should().Be(creds.Password.ToUnsecureString()); conn.Organization.Key.Should().Be("org_key"); conn.Organization.Name.Should().Be("org_name"); } @@ -83,11 +85,10 @@ public void BoundSonarQubeProject_CreateConnectionInformation_NoOrganizationNoAu // Assert conn.ServerUri.Should().Be(input.ServerUri); - conn.UserName.Should().BeNull(); - conn.Password.Should().BeNull(); + conn.Credentials.Should().BeAssignableTo(); conn.Organization.Should().BeNull(); } - + [TestMethod] public void BoundServerProject_CreateConnectionInformation_ArgCheck() { @@ -105,17 +106,15 @@ public void BoundServerProject_CreateConnectionInformation_NoCredentials() // Assert conn.ServerUri.Should().Be(input.ServerConnection.ServerUri); - conn.UserName.Should().BeNull(); - conn.Password.Should().BeNull(); + conn.Credentials.Should().BeAssignableTo(); conn.Organization.Key.Should().Be("org_key"); } - [TestMethod] public void BoundServerProject_CreateConnectionInformation_BasicAuthCredentials() { // Arrange - var creds = new BasicAuthCredentials("UserName", "password".ToSecureString()); + var creds = new UsernameAndPasswordCredentials("UserName", "password".ToSecureString()); var input = new BoundServerProject("solution", "ProjectKey", new ServerConnection.SonarCloud("org_key", credentials: creds)); // Act @@ -123,8 +122,10 @@ public void BoundServerProject_CreateConnectionInformation_BasicAuthCredentials( // Assert conn.ServerUri.Should().Be(input.ServerConnection.ServerUri); - conn.UserName.Should().Be(creds.UserName); - conn.Password.ToUnsecureString().Should().Be(creds.Password.ToUnsecureString()); + var basicAuth = conn.Credentials as UsernameAndPasswordCredentials; + basicAuth.Should().NotBeNull(); + basicAuth.UserName.Should().Be(creds.UserName); + basicAuth.Password.ToUnsecureString().Should().Be(creds.Password.ToUnsecureString()); conn.Organization.Key.Should().Be("org_key"); } @@ -139,8 +140,7 @@ public void BoundServerProject_CreateConnectionInformation_NoOrganizationNoAuth( // Assert conn.ServerUri.Should().Be(input.ServerConnection.ServerUri); - conn.UserName.Should().BeNull(); - conn.Password.Should().BeNull(); + conn.Credentials.Should().BeAssignableTo(); conn.Organization.Should().BeNull(); } } diff --git a/src/ConnectedMode.UnitTests/Persistence/BoundSonarQubeProjectTests.cs b/src/ConnectedMode.UnitTests/Persistence/BoundSonarQubeProjectTests.cs index 1318f7cdd8..94c81ba94c 100644 --- a/src/ConnectedMode.UnitTests/Persistence/BoundSonarQubeProjectTests.cs +++ b/src/ConnectedMode.UnitTests/Persistence/BoundSonarQubeProjectTests.cs @@ -36,7 +36,7 @@ public void BoundProject_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())); + var testSubject = new BoundSonarQubeProject(serverUri, projectKey, "projectName", new UsernameAndPasswordCredentials("used", "pwd".ToSecureString())); // Act (serialize + de-serialize) string data = JsonHelper.Serialize(testSubject); @@ -55,7 +55,7 @@ public void BoundProject_BindingJsonModel_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())); + var testSubject = new BoundSonarQubeProject(serverUri, projectKey, "projectName", new UsernameAndPasswordCredentials("used", "pwd".ToSecureString())); // Act (serialize + de-serialize) string data = JsonHelper.Serialize(testSubject); diff --git a/src/ConnectedMode.UnitTests/Persistence/ServerConnectionsRepositoryTests.cs b/src/ConnectedMode.UnitTests/Persistence/ServerConnectionsRepositoryTests.cs index 03e2e2a209..4be7d15872 100644 --- a/src/ConnectedMode.UnitTests/Persistence/ServerConnectionsRepositoryTests.cs +++ b/src/ConnectedMode.UnitTests/Persistence/ServerConnectionsRepositoryTests.cs @@ -27,6 +27,7 @@ using SonarLint.VisualStudio.Core.Binding; using SonarLint.VisualStudio.Core.Persistence; using SonarLint.VisualStudio.TestInfrastructure; +using SonarQube.Client.Models; using static SonarLint.VisualStudio.Core.Binding.ServerConnection; namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Persistence; @@ -40,8 +41,8 @@ public class ServerConnectionsRepositoryTests 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 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] @@ -128,7 +129,7 @@ public void TryGet_FileExistsAndConnectionIsSonarCloud_ReturnsSonarCloudConnecti public void TryGet_FileExistsAndConnectionIsSonarCloud_FillsCredentials() { var expectedConnection = MockFileWithOneSonarCloudConnection(); - var credentials = Substitute.For(); + var credentials = Substitute.For(); credentialsLoader.Load(expectedConnection.CredentialsUri).Returns(credentials); var succeeded = testSubject.TryGet(expectedConnection.Id, out ServerConnection serverConnection); @@ -157,7 +158,7 @@ public void TryGet_FileExistsAndConnectionIsSonarQube_ReturnsSonarQubeConnection public void TryGet_FileExistsAndConnectionIsSonarQube_FillsCredentials() { var expectedConnection = MockFileWithOneSonarQubeConnection(); - var credentials = Substitute.For(); + var credentials = Substitute.For(); credentialsLoader.Load(expectedConnection.CredentialsUri).Returns(credentials); var succeeded = testSubject.TryGet(expectedConnection.Id, out ServerConnection serverConnection); @@ -289,7 +290,7 @@ public void TryAdd_ConnectionIsAddedAndCredentialsAreNull_ReturnsFalse() var succeeded = testSubject.TryAdd(sonarCloudServerConnection); succeeded.Should().BeFalse(); - credentialsLoader.DidNotReceive().Save(Arg.Any(), Arg.Any()); + credentialsLoader.DidNotReceive().Save(Arg.Any(), Arg.Any()); } [TestMethod] @@ -300,7 +301,7 @@ public void TryAdd_ConnectionIsNotAdded_DoesNotSaveCredentials() var succeeded = testSubject.TryAdd(sonarCloud); succeeded.Should().BeFalse(); - credentialsLoader.DidNotReceive().Save(Arg.Any(), Arg.Any()); + credentialsLoader.DidNotReceive().Save(Arg.Any(), Arg.Any()); } [TestMethod] @@ -589,17 +590,17 @@ public void TryUpdateCredentialsById_ConnectionDoesNotExist_DoesNotUpdateCredent { MockReadingFile(new ServerConnectionsListJsonModel()); - var succeeded = testSubject.TryUpdateCredentialsById("myConn", Substitute.For()); + var succeeded = testSubject.TryUpdateCredentialsById("myConn", Substitute.For()); succeeded.Should().BeFalse(); - credentialsLoader.DidNotReceive().Save(Arg.Any(), Arg.Any()); + credentialsLoader.DidNotReceive().Save(Arg.Any(), Arg.Any()); } [TestMethod] public void TryUpdateCredentialsById_SonarCloudConnectionExists_UpdatesCredentials() { var sonarCloud = MockFileWithOneSonarCloudConnection(); - var newCredentials = Substitute.For(); + var newCredentials = Substitute.For(); var succeeded = testSubject.TryUpdateCredentialsById(sonarCloud.Id, newCredentials); @@ -611,7 +612,7 @@ public void TryUpdateCredentialsById_SonarCloudConnectionExists_UpdatesCredentia public void TryUpdateCredentialsById_SonarQubeConnectionExists_UpdatesCredentials() { var sonarQube = MockFileWithOneSonarQubeConnection(); - var newCredentials = Substitute.For(); + var newCredentials = Substitute.For(); var succeeded = testSubject.TryUpdateCredentialsById(sonarQube.Id, newCredentials); @@ -626,7 +627,7 @@ public void TryUpdateCredentialsById_DoesNotUpdateCredentials_DoesNotInvokeConne var eventHandler = Substitute.For>(); testSubject.CredentialsChanged += eventHandler; - testSubject.TryUpdateCredentialsById("non-existingConn", Substitute.For()); + testSubject.TryUpdateCredentialsById("non-existingConn", Substitute.For()); eventHandler.DidNotReceive().Invoke(testSubject, Arg.Any()); } @@ -638,7 +639,7 @@ public void TryUpdateCredentialsById_UpdatesCredentials_InvokesConnectionChanged var eventHandler = Substitute.For>(); testSubject.CredentialsChanged += eventHandler; - testSubject.TryUpdateCredentialsById(sonarQube.Id, Substitute.For()); + testSubject.TryUpdateCredentialsById(sonarQube.Id, Substitute.For()); eventHandler.Received(1).Invoke(testSubject, Arg.Is(args => args.ServerConnection == sonarQube)); } @@ -660,9 +661,9 @@ 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)); + credentialsLoader.When(x => x.Save(Arg.Any(), Arg.Any())).Do(x => throw new Exception(exceptionMsg)); - var succeeded = testSubject.TryUpdateCredentialsById(connection.Id, Substitute.For()); + var succeeded = testSubject.TryUpdateCredentialsById(connection.Id, Substitute.For()); succeeded.Should().BeFalse(); logger.Received(1).WriteLine($"Failed updating credentials: {exceptionMsg}"); @@ -671,7 +672,7 @@ public void TryUpdateCredentialsById_SavingCredentialsThrows_ReturnsFalseAndLogs private SonarCloud MockFileWithOneSonarCloudConnection(bool isSmartNotificationsEnabled = true) { var sonarCloudModel = GetSonarCloudJsonModel(isSmartNotificationsEnabled); - var sonarCloud = new SonarCloud(sonarCloudModel.OrganizationKey, sonarCloudModel.Settings, Substitute.For()); + var sonarCloud = new SonarCloud(sonarCloudModel.OrganizationKey, sonarCloudModel.Settings, Substitute.For()); MockReadingFile(new ServerConnectionsListJsonModel { ServerConnections = [sonarCloudModel] }); serverConnectionModelMapper.GetServerConnection(sonarCloudModel).Returns(sonarCloud); @@ -681,7 +682,7 @@ private SonarCloud MockFileWithOneSonarCloudConnection(bool isSmartNotifications 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()); + var sonarQube = new ServerConnection.SonarQube(new Uri(sonarQubeModel.ServerUri), sonarQubeModel.Settings, Substitute.For()); MockReadingFile(new ServerConnectionsListJsonModel { ServerConnections = [sonarQubeModel] }); serverConnectionModelMapper.GetServerConnection(sonarQubeModel).Returns(sonarQube); diff --git a/src/ConnectedMode.UnitTests/Persistence/SolutionBindingCredentialsLoaderTests.cs b/src/ConnectedMode.UnitTests/Persistence/SolutionBindingCredentialsLoaderTests.cs index d92a4bd945..9301207c9b 100644 --- a/src/ConnectedMode.UnitTests/Persistence/SolutionBindingCredentialsLoaderTests.cs +++ b/src/ConnectedMode.UnitTests/Persistence/SolutionBindingCredentialsLoaderTests.cs @@ -23,6 +23,7 @@ using SonarLint.VisualStudio.ConnectedMode.Persistence; using SonarLint.VisualStudio.Core.Binding; using SonarQube.Client.Helpers; +using SonarQube.Client.Models; namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Persistence { @@ -74,13 +75,13 @@ public void Load_CredentialsExist_CredentialsWithSecuredString() .Returns(credentials); var actual = testSubject.Load(mockUri); - actual.Should().BeEquivalentTo(new BasicAuthCredentials("user", "password".ToSecureString())); + actual.Should().BeEquivalentTo(new UsernameAndPasswordCredentials("user", "password".ToSecureString())); } [TestMethod] public void Save_ServerUriIsNull_CredentialsNotSaved() { - var credentials = new BasicAuthCredentials("user", "password".ToSecureString()); + var credentials = new UsernameAndPasswordCredentials("user", "password".ToSecureString()); testSubject.Save(credentials, null); @@ -98,7 +99,7 @@ public void Save_CredentialsAreNull_CredentialsNotSaved() [TestMethod] public void Save_CredentialsAreNotBasicAuth_CredentialsNotSaved() { - var mockCredentials = new Mock(); + var mockCredentials = new Mock(); testSubject.Save(mockCredentials.Object, mockUri); store.DidNotReceive().WriteCredentials(Arg.Any(), Arg.Any()); @@ -107,7 +108,7 @@ public void Save_CredentialsAreNotBasicAuth_CredentialsNotSaved() [TestMethod] public void Save_CredentialsAreBasicAuth_CredentialsSavedWithUnsecuredString() { - var credentials = new BasicAuthCredentials("user", "password".ToSecureString()); + var credentials = new UsernameAndPasswordCredentials("user", "password".ToSecureString()); testSubject.Save(credentials, mockUri); store.Received(1) diff --git a/src/ConnectedMode.UnitTests/Persistence/SolutionBindingRepositoryTests.cs b/src/ConnectedMode.UnitTests/Persistence/SolutionBindingRepositoryTests.cs index 7ad191e611..7fd950779e 100644 --- a/src/ConnectedMode.UnitTests/Persistence/SolutionBindingRepositoryTests.cs +++ b/src/ConnectedMode.UnitTests/Persistence/SolutionBindingRepositoryTests.cs @@ -40,7 +40,7 @@ public class SolutionBindingRepositoryTests private ISolutionBindingCredentialsLoader credentialsLoader; private TestLogger logger; - private BasicAuthCredentials mockCredentials; + private UsernameAndPasswordCredentials mockCredentials; private ServerConnection serverConnection; private IServerConnectionsRepository serverConnectionsRepository; private ISolutionBindingFileLoader solutionBindingFileLoader; @@ -59,7 +59,7 @@ public void TestInitialize() testSubject = new SolutionBindingRepository(unintrusiveBindingPathProvider, bindingJsonModelConverter, serverConnectionsRepository, solutionBindingFileLoader, credentialsLoader, logger); - mockCredentials = new BasicAuthCredentials("user", "pwd".ToSecureString()); + mockCredentials = new UsernameAndPasswordCredentials("user", "pwd".ToSecureString()); serverConnection = new ServerConnection.SonarCloud("org"); boundServerProject = new BoundServerProject("solution.123", "project_123", serverConnection); diff --git a/src/ConnectedMode.UnitTests/Persistence/UsernameAndPasswordCredentialsTests.cs b/src/ConnectedMode.UnitTests/Persistence/UsernameAndPasswordCredentialsTests.cs new file mode 100644 index 0000000000..e573bf78d9 --- /dev/null +++ b/src/ConnectedMode.UnitTests/Persistence/UsernameAndPasswordCredentialsTests.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 SonarLint.VisualStudio.ConnectedMode.Persistence; +using SonarLint.VisualStudio.TestInfrastructure; +using SonarQube.Client.Helpers; + +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Persistence; + +[TestClass] +public class UsernameAndPasswordCredentialsTests +{ + private const string Username = "username"; + private const string Password = "pwd"; + + [TestMethod] + public void Ctor_WhenUsernameIsNull_ThrowsArgumentNullException() + { + Action act = () => new UsernameAndPasswordCredentials(null, Password.ToSecureString()); + + act.Should().Throw(); + } + + [TestMethod] + public void Ctor_WhenPasswordIsNull_ThrowsArgumentNullException() + { + Action act = () => new UsernameAndPasswordCredentials(Username, null); + + act.Should().Throw(); + } + + [TestMethod] + public void Dispose_DisposesPassword() + { + var testSubject = new UsernameAndPasswordCredentials(Username, Password.ToSecureString()); + + testSubject.Dispose(); + + Exceptions.Expect(() => testSubject.Password.ToUnsecureString()); + } + + [TestMethod] + public void Clone_ClonesPassword() + { + var password = "pwd"; + var testSubject = new UsernameAndPasswordCredentials(Username, password.ToSecureString()); + + var clone = (UsernameAndPasswordCredentials)testSubject.Clone(); + + clone.Should().NotBeSameAs(testSubject); + clone.Password.Should().NotBeSameAs(testSubject.Password); + clone.Password.ToUnsecureString().Should().Be(testSubject.Password.ToUnsecureString()); + clone.UserName.Should().Be(testSubject.UserName); + } +} diff --git a/src/ConnectedMode.UnitTests/ServerConnectionsRepositoryAdapterTests.cs b/src/ConnectedMode.UnitTests/ServerConnectionsRepositoryAdapterTests.cs index 363a1798af..1d51f86340 100644 --- a/src/ConnectedMode.UnitTests/ServerConnectionsRepositoryAdapterTests.cs +++ b/src/ConnectedMode.UnitTests/ServerConnectionsRepositoryAdapterTests.cs @@ -23,6 +23,7 @@ using SonarLint.VisualStudio.Core.Binding; using SonarLint.VisualStudio.TestInfrastructure; using SonarQube.Client.Helpers; +using SonarQube.Client.Models; using static SonarLint.VisualStudio.Core.Binding.ServerConnection; namespace SonarLint.VisualStudio.ConnectedMode.UnitTests; @@ -241,7 +242,7 @@ public void TryAddConnection_NullCredentials_TriesAddingAConnectionWithNoCredent public void TryUpdateCredentials_ReturnsStatusFromSlCore(bool slCoreResponse) { var sonarCloud = CreateSonarCloudConnection(); - serverConnectionsRepository.TryUpdateCredentialsById(Arg.Any(), Arg.Any()).Returns(slCoreResponse); + serverConnectionsRepository.TryUpdateCredentialsById(Arg.Any(), Arg.Any()).Returns(slCoreResponse); var succeeded = testSubject.TryUpdateCredentials(sonarCloud, Substitute.For()); @@ -257,7 +258,7 @@ public void TryUpdateCredentials_TokenCredentialsModel_MapsCredentials() testSubject.TryUpdateCredentials(sonarQube, new TokenCredentialsModel(token.CreateSecureString())); serverConnectionsRepository.Received(1) - .TryUpdateCredentialsById(Arg.Any(), Arg.Is(x => IsExpectedCredentials(x, token, string.Empty))); + .TryUpdateCredentialsById(Arg.Any(), Arg.Is(x => IsExpectedCredentials(x, token, string.Empty))); } [TestMethod] @@ -270,7 +271,7 @@ public void TryUpdateCredentials_UserPasswordModel_MapsCredentials() testSubject.TryUpdateCredentials(sonarQube, new UsernamePasswordModel(username, password.CreateSecureString())); serverConnectionsRepository.Received(1) - .TryUpdateCredentialsById(Arg.Any(), Arg.Is(x => IsExpectedCredentials(x, username, password))); + .TryUpdateCredentialsById(Arg.Any(), Arg.Is(x => IsExpectedCredentials(x, username, password))); } [TestMethod] @@ -281,7 +282,7 @@ public void TryUpdateCredentials_SonarQube_MapsConnection() testSubject.TryUpdateCredentials(sonarQube, Substitute.For()); serverConnectionsRepository.Received(1) - .TryUpdateCredentialsById(Arg.Is(x => x.Equals(sonarQube.Info.Id)), Arg.Any()); + .TryUpdateCredentialsById(Arg.Is(x => x.Equals(sonarQube.Info.Id)), Arg.Any()); } [TestMethod] @@ -292,7 +293,7 @@ public void TryUpdateCredentials_SonarCloud_MapsConnection() testSubject.TryUpdateCredentials(sonarCloud, Substitute.For()); serverConnectionsRepository.Received(1) - .TryUpdateCredentialsById(Arg.Is(x => x.EndsWith(sonarCloud.Info.Id)), Arg.Any()); + .TryUpdateCredentialsById(Arg.Is(x => x.EndsWith(sonarCloud.Info.Id)), Arg.Any()); } [TestMethod] @@ -302,7 +303,7 @@ public void TryUpdateCredentials_NullCredentials_TriesUpdatingConnectionWithNoCr testSubject.TryUpdateCredentials(sonarQube, null); - serverConnectionsRepository.Received(1).TryUpdateCredentialsById(Arg.Any(), Arg.Is(x => x == null)); + serverConnectionsRepository.Received(1).TryUpdateCredentialsById(Arg.Any(), Arg.Is(x => x == null)); } [TestMethod] @@ -337,12 +338,12 @@ public void TryGet_ReturnsStatusFromSlCore(bool expectedStatus) private static SonarCloud CreateSonarCloudServerConnection(bool isSmartNotificationsEnabled = true) { - return new SonarCloud("myOrg", new ServerConnectionSettings(isSmartNotificationsEnabled), Substitute.For()); + 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()); + var sonarQube = new ServerConnection.SonarQube(new Uri("http://localhost"), new ServerConnectionSettings(isSmartNotificationsEnabled), Substitute.For()); return sonarQube; } @@ -365,9 +366,9 @@ private static Connection CreateSonarQubeConnection(bool enableSmartNotification return new Connection(new ConnectionInfo("http://localhost:9000/", ConnectionServerType.SonarQube), enableSmartNotifications); } - private static bool IsExpectedCredentials(ICredentials credentials, string expectedUsername, string expectedPassword) + private static bool IsExpectedCredentials(IConnectionCredentials credentials, string expectedUsername, string expectedPassword) { - return credentials is BasicAuthCredentials basicAuthCredentials && basicAuthCredentials.UserName == expectedUsername && basicAuthCredentials.Password?.ToUnsecureString() == expectedPassword; + return credentials is UsernameAndPasswordCredentials basicAuthCredentials && basicAuthCredentials.UserName == expectedUsername && basicAuthCredentials.Password?.ToUnsecureString() == expectedPassword; } private void MockTryGet(string connectionId, bool expectedResponse, ServerConnection expectedServerConnection) diff --git a/src/ConnectedMode.UnitTests/SlCoreConnectionAdapterTests.cs b/src/ConnectedMode.UnitTests/SlCoreConnectionAdapterTests.cs index d6c2e72896..c3f659fbb6 100644 --- a/src/ConnectedMode.UnitTests/SlCoreConnectionAdapterTests.cs +++ b/src/ConnectedMode.UnitTests/SlCoreConnectionAdapterTests.cs @@ -38,7 +38,7 @@ namespace SonarLint.VisualStudio.ConnectedMode.UnitTests; [TestClass] public class SlCoreConnectionAdapterTests { - private static readonly BasicAuthCredentials ValidToken = new ("I_AM_JUST_A_TOKEN", new SecureString()); + private static readonly UsernameAndPasswordCredentials 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); @@ -288,7 +288,7 @@ public async Task GetAllProjectsAsync_ConnectionToSonarQubeWithCredentials_Calls { const string username = "username"; const string password = "password"; - sonarQubeConnection.Credentials = new BasicAuthCredentials(username, password.CreateSecureString()); + sonarQubeConnection.Credentials = new UsernameAndPasswordCredentials(username, password.CreateSecureString()); await testSubject.GetAllProjectsAsync(sonarQubeConnection); @@ -310,7 +310,7 @@ public async Task GetAllProjectsAsync_ConnectionToSonarCloudWithCredentials_Call { const string username = "username"; const string password = "password"; - sonarCloudConnection.Credentials = new BasicAuthCredentials(username, password.CreateSecureString()); + sonarCloudConnection.Credentials = new UsernameAndPasswordCredentials(username, password.CreateSecureString()); await testSubject.GetAllProjectsAsync(sonarCloudConnection); diff --git a/src/ConnectedMode.UnitTests/UI/ManageBinding/ManageBindingViewModelTests.cs b/src/ConnectedMode.UnitTests/UI/ManageBinding/ManageBindingViewModelTests.cs index a3ece5229c..91e676ba95 100644 --- a/src/ConnectedMode.UnitTests/UI/ManageBinding/ManageBindingViewModelTests.cs +++ b/src/ConnectedMode.UnitTests/UI/ManageBinding/ManageBindingViewModelTests.cs @@ -44,7 +44,7 @@ public class ManageBindingViewModelTests 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 UsernameAndPasswordCredentials 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" }; diff --git a/src/ConnectedMode.UnitTests/UI/ProjectSelection/ProjectSelectionViewModelTests.cs b/src/ConnectedMode.UnitTests/UI/ProjectSelection/ProjectSelectionViewModelTests.cs index 304743c649..1822cfe8e9 100644 --- a/src/ConnectedMode.UnitTests/UI/ProjectSelection/ProjectSelectionViewModelTests.cs +++ b/src/ConnectedMode.UnitTests/UI/ProjectSelection/ProjectSelectionViewModelTests.cs @@ -24,6 +24,7 @@ using SonarLint.VisualStudio.ConnectedMode.UI.Resources; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; +using SonarQube.Client.Models; namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.UI.ProjectSelection; @@ -229,7 +230,7 @@ public async Task InitializeProjectWithProgressAsync_OnFailure_InitialServerProj [TestMethod] public async Task AdapterGetAllProjectsAsync_GettingServerConnectionSucceeded_CallsAdapterWithCredentialsForServerConnection() { - var expectedCredentials = Substitute.For(); + var expectedCredentials = Substitute.For(); MockTrySonarQubeConnection(AConnectionInfo, success:true, expectedCredentials); await testSubject.AdapterGetAllProjectsAsync(); @@ -241,7 +242,7 @@ public async Task AdapterGetAllProjectsAsync_GettingServerConnectionSucceeded_Ca [TestMethod] public async Task AdapterGetAllProjectsAsync_GettingServerConnectionSucceeded_StoresServerConnection() { - MockTrySonarQubeConnection(AConnectionInfo, success: true, Substitute.For()); + MockTrySonarQubeConnection(AConnectionInfo, success: true, Substitute.For()); await testSubject.AdapterGetAllProjectsAsync(); @@ -304,7 +305,7 @@ private void MockInitializedProjects(List serverProjects) testSubject.InitProjects(new AdapterResponseWithData>(true, serverProjects)); } - private void MockTrySonarQubeConnection(ConnectionInfo connectionInfo, bool success = true, ICredentials expectedCredentials = null) + private void MockTrySonarQubeConnection(ConnectionInfo connectionInfo, bool success = true, IConnectionCredentials expectedCredentials = null) { serverConnectionsRepositoryAdapter.TryGet(connectionInfo, out _).Returns(callInfo => { diff --git a/src/ConnectedMode/Binding/IUnintrusiveBindingController.cs b/src/ConnectedMode/Binding/IUnintrusiveBindingController.cs index d7d139d094..d638d0d73f 100644 --- a/src/ConnectedMode/Binding/IUnintrusiveBindingController.cs +++ b/src/ConnectedMode/Binding/IUnintrusiveBindingController.cs @@ -21,6 +21,7 @@ using System.ComponentModel.Composition; using SonarLint.VisualStudio.Core.Binding; using SonarQube.Client; +using SonarQube.Client.Models; using Task = System.Threading.Tasks.Task; namespace SonarLint.VisualStudio.ConnectedMode.Binding @@ -28,6 +29,7 @@ namespace SonarLint.VisualStudio.ConnectedMode.Binding public interface IBindingController { Task BindAsync(BoundServerProject project, CancellationToken cancellationToken); + bool Unbind(string localBindingKey); } @@ -47,7 +49,11 @@ internal class UnintrusiveBindingController : IUnintrusiveBindingController, IBi private readonly ISolutionBindingRepository solutionBindingRepository; [ImportingConstructor] - public UnintrusiveBindingController(IBindingProcessFactory bindingProcessFactory, ISonarQubeService sonarQubeService, IActiveSolutionChangedHandler activeSolutionChangedHandler, ISolutionBindingRepository solutionBindingRepository) + public UnintrusiveBindingController( + IBindingProcessFactory bindingProcessFactory, + ISonarQubeService sonarQubeService, + IActiveSolutionChangedHandler activeSolutionChangedHandler, + ISolutionBindingRepository solutionBindingRepository) { this.bindingProcessFactory = bindingProcessFactory; this.sonarQubeService = sonarQubeService; @@ -57,7 +63,7 @@ public UnintrusiveBindingController(IBindingProcessFactory bindingProcessFactory public async Task BindAsync(BoundServerProject project, CancellationToken cancellationToken) { - var connectionInformation = project.ServerConnection.Credentials.CreateConnectionInformation(project.ServerConnection.ServerUri); + var connectionInformation = new ConnectionInformation(project.ServerConnection.ServerUri, project.ServerConnection.Credentials); await sonarQubeService.ConnectAsync(connectionInformation, cancellationToken); await BindAsync(project, null, cancellationToken); activeSolutionChangedHandler.HandleBindingChange(false); diff --git a/src/ConnectedMode/Persistence/BindingJsonModelConverter.cs b/src/ConnectedMode/Persistence/BindingJsonModelConverter.cs index 763724a04b..63b243b54a 100644 --- a/src/ConnectedMode/Persistence/BindingJsonModelConverter.cs +++ b/src/ConnectedMode/Persistence/BindingJsonModelConverter.cs @@ -28,7 +28,7 @@ internal interface IBindingJsonModelConverter { BoundServerProject ConvertFromModel(BindingJsonModel bindingJsonModel, ServerConnection connection, string localBindingKey); BindingJsonModel ConvertToModel(BoundServerProject binding); - BoundSonarQubeProject ConvertFromModelToLegacy(BindingJsonModel bindingJsonModel, ICredentials credentials); + BoundSonarQubeProject ConvertFromModelToLegacy(BindingJsonModel bindingJsonModel, IConnectionCredentials credentials); } [Export(typeof(IBindingJsonModelConverter))] @@ -54,7 +54,7 @@ public BindingJsonModel ConvertToModel(BoundServerProject binding) => : null }; - public BoundSonarQubeProject ConvertFromModelToLegacy(BindingJsonModel bindingJsonModel, ICredentials credentials) => + public BoundSonarQubeProject ConvertFromModelToLegacy(BindingJsonModel bindingJsonModel, IConnectionCredentials credentials) => new(bindingJsonModel.ServerUri, bindingJsonModel.ProjectKey, bindingJsonModel.ProjectName, diff --git a/src/ConnectedMode/Persistence/ConnectionInfoConverter.cs b/src/ConnectedMode/Persistence/ConnectionInfoConverter.cs deleted file mode 100644 index 8162240004..0000000000 --- a/src/ConnectedMode/Persistence/ConnectionInfoConverter.cs +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 d3bfb0c31b..a26366d85b 100644 --- a/src/ConnectedMode/Persistence/ISolutionBindingCredentialsLoader.cs +++ b/src/ConnectedMode/Persistence/ISolutionBindingCredentialsLoader.cs @@ -18,15 +18,14 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using SonarLint.VisualStudio.Core.Binding; +using SonarQube.Client.Models; namespace SonarLint.VisualStudio.ConnectedMode.Persistence { interface ISolutionBindingCredentialsLoader { void DeleteCredentials(Uri boundServerUri); - ICredentials Load(Uri boundServerUri); - void Save(ICredentials credentials, Uri boundServerUri); + IConnectionCredentials Load(Uri boundServerUri); + void Save(IConnectionCredentials credentials, Uri boundServerUri); } } diff --git a/src/ConnectedMode/Persistence/ServerConnectionsRepository.cs b/src/ConnectedMode/Persistence/ServerConnectionsRepository.cs index 7554307cfc..333f9da328 100644 --- a/src/ConnectedMode/Persistence/ServerConnectionsRepository.cs +++ b/src/ConnectedMode/Persistence/ServerConnectionsRepository.cs @@ -25,6 +25,7 @@ using System.IO.Abstractions; using SonarLint.VisualStudio.Core.Persistence; using SonarLint.VisualStudio.ConnectedMode.Binding; +using SonarQube.Client.Models; namespace SonarLint.VisualStudio.ConnectedMode.Persistence; @@ -122,7 +123,7 @@ public bool TryUpdateSettingsById(string connectionId, ServerConnectionSettings return SafeUpdateConnectionsFile(connections => TryUpdateConnectionSettings(connections, connectionId, connectionSettings)); } - public bool TryUpdateCredentialsById(string connectionId, ICredentials credentials) + public bool TryUpdateCredentialsById(string connectionId, IConnectionCredentials credentials) { try { diff --git a/src/ConnectedMode/Persistence/SolutionBindingCredentialsLoader.cs b/src/ConnectedMode/Persistence/SolutionBindingCredentialsLoader.cs index 6232873efd..4eebb3ead9 100644 --- a/src/ConnectedMode/Persistence/SolutionBindingCredentialsLoader.cs +++ b/src/ConnectedMode/Persistence/SolutionBindingCredentialsLoader.cs @@ -20,8 +20,8 @@ using Microsoft.Alm.Authentication; using SonarLint.VisualStudio.ConnectedMode.Binding; -using SonarLint.VisualStudio.Core.Binding; using SonarQube.Client.Helpers; +using SonarQube.Client.Models; namespace SonarLint.VisualStudio.ConnectedMode.Persistence { @@ -43,7 +43,7 @@ public void DeleteCredentials(Uri boundServerUri) store.DeleteCredentials(boundServerUri); } - public ICredentials Load(Uri boundServerUri) + public IConnectionCredentials Load(Uri boundServerUri) { if (boundServerUri == null) { @@ -53,7 +53,7 @@ public ICredentials Load(Uri boundServerUri) return credentials == null ? null - : new BasicAuthCredentials(credentials.Username, credentials.Password.ToSecureString()); + : new UsernameAndPasswordCredentials(credentials.Username, credentials.Password.ToSecureString()); } [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", @@ -61,9 +61,9 @@ public ICredentials Load(Uri boundServerUri) Justification = "Casting as BasicAuthCredentials is because it's the only credential type we support. Once we add more we need to think again on how to refactor the code to avoid this", Scope = "member", Target = "~M:SonarLint.VisualStudio.Integration.Persistence.FileBindingSerializer.WriteBindingInformation(System.String,SonarLint.VisualStudio.Integration.Persistence.BoundProject)~System.Boolean")] - public void Save(ICredentials credentials, Uri boundServerUri) + public void Save(IConnectionCredentials credentials, Uri boundServerUri) { - if (boundServerUri == null || !(credentials is BasicAuthCredentials basicCredentials)) + if (boundServerUri == null || !(credentials is UsernameAndPasswordCredentials basicCredentials)) { return; } diff --git a/src/ConnectedMode/Persistence/BasicAuthCredentials.cs b/src/ConnectedMode/Persistence/UsernameAndPasswordCredentials.cs similarity index 59% rename from src/ConnectedMode/Persistence/BasicAuthCredentials.cs rename to src/ConnectedMode/Persistence/UsernameAndPasswordCredentials.cs index d13d6255dc..7cc50a2581 100644 --- a/src/ConnectedMode/Persistence/BasicAuthCredentials.cs +++ b/src/ConnectedMode/Persistence/UsernameAndPasswordCredentials.cs @@ -18,28 +18,19 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; using System.Security; -using SonarLint.VisualStudio.Core.Binding; +using SonarQube.Client.Helpers; using SonarQube.Client.Models; -namespace SonarLint.VisualStudio.ConnectedMode.Persistence +namespace SonarLint.VisualStudio.ConnectedMode.Persistence; + +internal sealed class UsernameAndPasswordCredentials(string userName, SecureString password) : IUsernameAndPasswordCredentials { - internal class BasicAuthCredentials : ICredentials - { - public BasicAuthCredentials(string userName, SecureString password) - { - this.UserName = userName; - this.Password = password; - } + public string UserName { get; } = userName ?? throw new ArgumentNullException(nameof(userName)); - public string UserName { get; } + public SecureString Password { get; } = password ?? throw new ArgumentNullException(nameof(password)); - public SecureString Password { get; } + public void Dispose() => Password?.Dispose(); - ConnectionInformation ICredentials.CreateConnectionInformation(Uri serverUri) - { - return new ConnectionInformation(serverUri, this.UserName, this.Password); - } - } + public object Clone() => new UsernameAndPasswordCredentials(UserName, Password.CopyAsReadOnly()); } diff --git a/src/ConnectedMode/ServerConnectionsRepositoryAdapter.cs b/src/ConnectedMode/ServerConnectionsRepositoryAdapter.cs index 5e684787ea..5338a7f4c2 100644 --- a/src/ConnectedMode/ServerConnectionsRepositoryAdapter.cs +++ b/src/ConnectedMode/ServerConnectionsRepositoryAdapter.cs @@ -24,6 +24,7 @@ using SonarLint.VisualStudio.ConnectedMode.UI.Credentials; using SonarLint.VisualStudio.Core.Binding; using SonarQube.Client.Helpers; +using SonarQube.Client.Models; namespace SonarLint.VisualStudio.ConnectedMode; @@ -97,14 +98,14 @@ private static ServerConnection MapConnection(Connection connection) return new ServerConnection.SonarQube(new Uri(connection.Info.Id), new ServerConnectionSettings(connection.EnableSmartNotifications)); } - private static ICredentials MapCredentials(ICredentialsModel credentialsModel) + private static IConnectionCredentials MapCredentials(ICredentialsModel credentialsModel) { switch (credentialsModel) { case TokenCredentialsModel tokenCredentialsModel: - return new BasicAuthCredentials(tokenCredentialsModel.Token.ToUnsecureString(), new SecureString()); + return new UsernameAndPasswordCredentials(tokenCredentialsModel.Token.ToUnsecureString(), new SecureString()); case UsernamePasswordModel usernameCredentialsModel: - return new BasicAuthCredentials(usernameCredentialsModel.Username, usernameCredentialsModel.Password); + return new UsernameAndPasswordCredentials(usernameCredentialsModel.Username, usernameCredentialsModel.Password); default: return null; } diff --git a/src/ConnectedMode/SlCoreConnectionAdapter.cs b/src/ConnectedMode/SlCoreConnectionAdapter.cs index 169433bd83..523f794e2b 100644 --- a/src/ConnectedMode/SlCoreConnectionAdapter.cs +++ b/src/ConnectedMode/SlCoreConnectionAdapter.cs @@ -33,6 +33,7 @@ using SonarLint.VisualStudio.SLCore.Service.Connection; using SonarLint.VisualStudio.SLCore.Service.Connection.Models; using SonarQube.Client.Helpers; +using SonarQube.Client.Models; namespace SonarLint.VisualStudio.ConnectedMode; @@ -230,7 +231,7 @@ private bool TryGetConnectionConfigurationSlCoreService(out IConnectionConfigura return false; } - private static Either GetTransientConnectionDto(ConnectionInfo connectionInfo, ICredentials credentials) + private static Either GetTransientConnectionDto(ConnectionInfo connectionInfo, IConnectionCredentials credentials) { var credentialsDto = MapCredentials(credentials); @@ -268,14 +269,14 @@ private static Either GetEitherForUsernamePasswor return Either.CreateRight(new UsernamePasswordDto(username, password)); } - private static Either MapCredentials(ICredentials credentials) + private static Either MapCredentials(IConnectionCredentials credentials) { if (credentials == null) { throw new ArgumentException($"Unexpected {nameof(ICredentialsModel)} argument"); } - var basicAuthCredentials = (BasicAuthCredentials) credentials; + var basicAuthCredentials = (UsernameAndPasswordCredentials) credentials; return basicAuthCredentials.Password?.Length > 0 ? GetEitherForUsernamePassword(basicAuthCredentials.UserName, basicAuthCredentials.Password.ToUnsecureString()) : GetEitherForToken(basicAuthCredentials.UserName); diff --git a/src/ConnectedMode/UI/Credentials/ICredentialsModel.cs b/src/ConnectedMode/UI/Credentials/ICredentialsModel.cs index 2e14786ec5..faaa880eb4 100644 --- a/src/ConnectedMode/UI/Credentials/ICredentialsModel.cs +++ b/src/ConnectedMode/UI/Credentials/ICredentialsModel.cs @@ -20,23 +20,23 @@ using System.Security; using SonarLint.VisualStudio.ConnectedMode.Persistence; -using SonarLint.VisualStudio.Core.Binding; using SonarQube.Client.Helpers; +using SonarQube.Client.Models; namespace SonarLint.VisualStudio.ConnectedMode.UI.Credentials; public interface ICredentialsModel { - ICredentials ToICredentials(); + IConnectionCredentials ToICredentials(); } public class TokenCredentialsModel(SecureString token) : ICredentialsModel { public SecureString Token { get; } = token; - public ICredentials ToICredentials() + public IConnectionCredentials ToICredentials() { - return new BasicAuthCredentials(Token.ToUnsecureString(), new SecureString()); + return new UsernameAndPasswordCredentials(Token.ToUnsecureString(), new SecureString()); } } @@ -45,8 +45,8 @@ public class UsernamePasswordModel(string username, SecureString password) : ICr public string Username { get; } = username; public SecureString Password { get; } = password; - public ICredentials ToICredentials() + public IConnectionCredentials ToICredentials() { - return new BasicAuthCredentials(Username, Password); + return new UsernameAndPasswordCredentials(Username, Password); } } diff --git a/src/Core.UnitTests/Binding/ServerConnectionTests.cs b/src/Core.UnitTests/Binding/ServerConnectionTests.cs index af266ddcec..39842c4323 100644 --- a/src/Core.UnitTests/Binding/ServerConnectionTests.cs +++ b/src/Core.UnitTests/Binding/ServerConnectionTests.cs @@ -20,7 +20,6 @@ using SonarLint.VisualStudio.Core.Binding; using SonarQube.Client.Models; -using ICredentials = SonarLint.VisualStudio.Core.Binding.ICredentials; namespace SonarLint.VisualStudio.Core.UnitTests.Binding; @@ -58,7 +57,7 @@ public void Ctor_SonarCloud_NullCredentials_SetsNull() public void Ctor_SonarCloud_SetsProperties() { var serverConnectionSettings = new ServerConnectionSettings(false); - var credentials = Substitute.For(); + var credentials = Substitute.For(); var sonarCloud = new ServerConnection.SonarCloud(Org, serverConnectionSettings, credentials); sonarCloud.Id.Should().Be($"https://sonarcloud.io/organizations/{Org}"); @@ -97,7 +96,7 @@ public void Ctor_SonarQube_NullCredentials_SetsNull() public void Ctor_SonarQube_SetsProperties() { var serverConnectionSettings = new ServerConnectionSettings(false); - var credentials = Substitute.For(); + var credentials = Substitute.For(); var sonarQube = new ServerConnection.SonarQube(Localhost, serverConnectionSettings, credentials); sonarQube.Id.Should().Be(Localhost.ToString()); @@ -110,7 +109,7 @@ public void Ctor_SonarQube_SetsProperties() [TestMethod] public void FromBoundSonarQubeProject_SonarQubeConnection_ConvertedCorrectly() { - var credentials = Substitute.For(); + var credentials = Substitute.For(); var expectedConnection = new ServerConnection.SonarQube(Localhost, credentials: credentials); var connection = ServerConnection.FromBoundSonarQubeProject(new BoundSonarQubeProject(Localhost, "any", "any", credentials)); @@ -123,7 +122,7 @@ public void FromBoundSonarQubeProject_SonarCloudConnection_ConvertedCorrectly() { var uri = new Uri("https://sonarcloud.io"); var organization = "org"; - var credentials = Substitute.For(); + var credentials = Substitute.For(); var expectedConnection = new ServerConnection.SonarCloud(organization, credentials: credentials); var connection = ServerConnection.FromBoundSonarQubeProject(new BoundSonarQubeProject(uri, "any", "any", credentials, new SonarQubeOrganization(organization, null))); diff --git a/src/Core/Binding/BoundSonarQubeProject.cs b/src/Core/Binding/BoundSonarQubeProject.cs index e66ea8434d..fdfbae1b42 100644 --- a/src/Core/Binding/BoundSonarQubeProject.cs +++ b/src/Core/Binding/BoundSonarQubeProject.cs @@ -32,7 +32,7 @@ public BoundSonarQubeProject() } public BoundSonarQubeProject(Uri serverUri, string projectKey, string projectName, - ICredentials credentials = null, SonarQubeOrganization organization = null) + IConnectionCredentials credentials = null, SonarQubeOrganization organization = null) : this() { if (serverUri == null) @@ -60,6 +60,6 @@ public BoundSonarQubeProject(Uri serverUri, string projectKey, string projectNam public Dictionary Profiles { get; set; } [JsonIgnore] - public ICredentials Credentials { get; set; } + public IConnectionCredentials Credentials { get; set; } } } diff --git a/src/Core/Binding/BoundSonarQubeProjectExtensions.cs b/src/Core/Binding/BoundSonarQubeProjectExtensions.cs index abbcc63204..29af790919 100644 --- a/src/Core/Binding/BoundSonarQubeProjectExtensions.cs +++ b/src/Core/Binding/BoundSonarQubeProjectExtensions.cs @@ -32,14 +32,12 @@ public static ConnectionInformation CreateConnectionInformation(this BoundSonarQ throw new ArgumentNullException(nameof(binding)); } - var connection = binding.Credentials == null ? - new ConnectionInformation(binding.ServerUri) - : binding.Credentials.CreateConnectionInformation(binding.ServerUri); + var connection = new ConnectionInformation(binding.ServerUri, binding.Credentials); connection.Organization = binding.Organization; return connection; } - + public static ConnectionInformation CreateConnectionInformation(this BoundServerProject binding) { if (binding == null) @@ -47,9 +45,7 @@ public static ConnectionInformation CreateConnectionInformation(this BoundServer throw new ArgumentNullException(nameof(binding)); } - var connection = binding.ServerConnection.Credentials == null ? - new ConnectionInformation(binding.ServerConnection.ServerUri) - : binding.ServerConnection.Credentials.CreateConnectionInformation(binding.ServerConnection.ServerUri); + var connection = new ConnectionInformation(binding.ServerConnection.ServerUri, binding.ServerConnection.Credentials); connection.Organization = binding.ServerConnection is ServerConnection.SonarCloud sc ? new SonarQubeOrganization(sc.OrganizationKey, null) : null; return connection; diff --git a/src/Core/Binding/IServerConnectionsRepository.cs b/src/Core/Binding/IServerConnectionsRepository.cs index 22e988257a..21ab89bed8 100644 --- a/src/Core/Binding/IServerConnectionsRepository.cs +++ b/src/Core/Binding/IServerConnectionsRepository.cs @@ -18,6 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using SonarQube.Client.Models; + namespace SonarLint.VisualStudio.Core.Binding; public interface IServerConnectionsRepository @@ -27,7 +29,7 @@ public interface IServerConnectionsRepository bool TryAdd(ServerConnection connectionToAdd); bool TryDelete(string connectionId); bool TryUpdateSettingsById(string connectionId, ServerConnectionSettings connectionSettings); - bool TryUpdateCredentialsById(string connectionId, ICredentials credentials); + bool TryUpdateCredentialsById(string connectionId, IConnectionCredentials credentials); bool ConnectionsFileExists(); event EventHandler ConnectionChanged; event EventHandler CredentialsChanged; diff --git a/src/Core/Binding/ServerConnection.cs b/src/Core/Binding/ServerConnection.cs index f4d2f5c2ee..d1e51272d5 100644 --- a/src/Core/Binding/ServerConnection.cs +++ b/src/Core/Binding/ServerConnection.cs @@ -19,6 +19,7 @@ */ using System.IO; +using SonarQube.Client.Models; namespace SonarLint.VisualStudio.Core.Binding; @@ -28,7 +29,7 @@ public abstract class ServerConnection public string Id { get; } public ServerConnectionSettings Settings { get; set; } - public ICredentials Credentials { get; set; } + public IConnectionCredentials Credentials { get; set; } public abstract Uri ServerUri { get; } public abstract Uri CredentialsUri { get; } @@ -41,7 +42,7 @@ public static ServerConnection FromBoundSonarQubeProject(BoundSonarQubeProject b _ => null }; - private ServerConnection(string id, ServerConnectionSettings settings = null, ICredentials credentials = null) + private ServerConnection(string id, ServerConnectionSettings settings = null, IConnectionCredentials credentials = null) { Id = id ?? throw new ArgumentNullException(nameof(id)); Settings = settings ?? DefaultSettings; @@ -52,7 +53,7 @@ public sealed class SonarCloud : ServerConnection { private static readonly string SonarCloudUrl = CoreStrings.SonarCloudUrl; - public SonarCloud(string organizationKey, ServerConnectionSettings settings = null, ICredentials credentials = null) + public SonarCloud(string organizationKey, ServerConnectionSettings settings = null, IConnectionCredentials credentials = null) : base(OrganizationKeyToId(organizationKey), settings, credentials) { OrganizationKey = organizationKey; @@ -75,7 +76,7 @@ private static string OrganizationKeyToId(string organizationKey) } } - public sealed class SonarQube(Uri serverUri, ServerConnectionSettings settings = null, ICredentials credentials = null) + public sealed class SonarQube(Uri serverUri, ServerConnectionSettings settings = null, IConnectionCredentials credentials = null) : ServerConnection(serverUri?.ToString(), settings, credentials) { public override Uri ServerUri { get; } = serverUri; diff --git a/src/Integration.UnitTests/Service/ConnectionInformationTests.cs b/src/Integration.UnitTests/Service/ConnectionInformationTests.cs index c4ccb8625f..1730e22203 100644 --- a/src/Integration.UnitTests/Service/ConnectionInformationTests.cs +++ b/src/Integration.UnitTests/Service/ConnectionInformationTests.cs @@ -21,6 +21,7 @@ using System; using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; +using SonarLint.VisualStudio.ConnectedMode.Persistence; using SonarQube.Client.Helpers; using SonarQube.Client.Models; using SonarLint.VisualStudio.TestInfrastructure; @@ -38,14 +39,15 @@ public void ConnectionInformation_WithLoginInformation() var passwordUnsecure = "admin"; var password = passwordUnsecure.ToSecureString(); var serverUri = new Uri("http://localhost/"); - var testSubject = new ConnectionInformation(serverUri, userName, password); + var credentials = new UsernameAndPasswordCredentials(userName, password); + var testSubject = new ConnectionInformation(serverUri, credentials); // Act password.Dispose(); // Connection information should maintain it's own copy of the password // Assert - testSubject.Password.ToUnsecureString().Should().Be(passwordUnsecure, "Password doesn't match"); - testSubject.UserName.Should().Be(userName, "UserName doesn't match"); + ((UsernameAndPasswordCredentials)testSubject.Credentials).Password.ToUnsecureString().Should().Be(passwordUnsecure, "Password doesn't match"); + ((UsernameAndPasswordCredentials)testSubject.Credentials).UserName.Should().Be(userName, "UserName doesn't match"); testSubject.ServerUri.Should().Be(serverUri, "ServerUri doesn't match"); // Act clone @@ -55,11 +57,11 @@ public void ConnectionInformation_WithLoginInformation() testSubject.Dispose(); // Assert testSubject - Exceptions.Expect(() => testSubject.Password.ToUnsecureString()); + Exceptions.Expect(() => ((UsernameAndPasswordCredentials)testSubject.Credentials).Password.ToUnsecureString()); // Assert testSubject2 - testSubject2.Password.ToUnsecureString().Should().Be(passwordUnsecure, "Password doesn't match"); - testSubject2.UserName.Should().Be(userName, "UserName doesn't match"); + ((UsernameAndPasswordCredentials)testSubject2.Credentials).Password.ToUnsecureString().Should().Be(passwordUnsecure, "Password doesn't match"); + ((UsernameAndPasswordCredentials)testSubject.Credentials).UserName.Should().Be(userName, "UserName doesn't match"); testSubject2.ServerUri.Should().Be(serverUri, "ServerUri doesn't match"); } @@ -70,19 +72,17 @@ public void ConnectionInformation_WithoutLoginInformation() var serverUri = new Uri("http://localhost/"); // Act - var testSubject = new ConnectionInformation(serverUri); + var testSubject = new ConnectionInformation(serverUri, null); // Assert - testSubject.Password.Should().BeNull("Password wasn't provided"); - testSubject.UserName.Should().BeNull("UserName wasn't provided"); + testSubject.Credentials.Should().BeAssignableTo(); testSubject.ServerUri.Should().Be(serverUri, "ServerUri doesn't match"); // Act clone var testSubject2 = (ConnectionInformation)((ICloneable)testSubject).Clone(); // Assert testSubject2 - testSubject2.Password.Should().BeNull("Password wasn't provided"); - testSubject2.UserName.Should().BeNull("UserName wasn't provided"); + testSubject2.Credentials.Should().BeAssignableTo(); testSubject2.ServerUri.Should().Be(serverUri, "ServerUri doesn't match"); } @@ -110,7 +110,7 @@ public void ConnectionInformation_Ctor_FixesSonarCloudUri() public void ConnectionInformation_Ctor_ArgChecks() { Exceptions.Expect(() => new ConnectionInformation(null)); - Exceptions.Expect(() => new ConnectionInformation(null, "user", "pwd".ToSecureString())); + Exceptions.Expect(() => new ConnectionInformation(null, new UsernameAndPasswordCredentials("user", "pwd".ToSecureString()))); } } } diff --git a/src/SonarQube.Client.Tests/CallRealServerTestHarness.cs b/src/SonarQube.Client.Tests/CallRealServerTestHarness.cs index a3e6c116a5..0640c69b7b 100644 --- a/src/SonarQube.Client.Tests/CallRealServerTestHarness.cs +++ b/src/SonarQube.Client.Tests/CallRealServerTestHarness.cs @@ -18,13 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; using System.Net.Http; using System.Security; -using System.Threading; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; using SonarQube.Client.Models; using SonarQube.Client.Tests.Infra; @@ -53,7 +48,7 @@ public async Task Call_Real_SonarQube() password.AppendChar('i'); password.AppendChar('n'); - var connInfo = new ConnectionInformation(url, userName, password); + var connInfo = new ConnectionInformation(url, MockBasicAuthCredentials(userName, password)); var service = new SonarQubeService(new HttpClientHandler(), "agent", new TestLogger()); try @@ -79,7 +74,7 @@ public async Task Call_Real_SonarCloud() var url = ConnectionInformation.FixedSonarCloudUri; var password = new SecureString(); - var connInfo = new ConnectionInformation(url, validSonarCloudToken, password); + var connInfo = new ConnectionInformation(url, MockBasicAuthCredentials(validSonarCloudToken, password)); var service = new SonarQubeService(new HttpClientHandler(), "agent", new TestLogger()); try @@ -96,5 +91,13 @@ public async Task Call_Real_SonarCloud() service.Disconnect(); } } + + private static IUsernameAndPasswordCredentials MockBasicAuthCredentials(string userName, SecureString password) + { + var mock = Substitute.For(); + mock.UserName.Returns(userName); + mock.Password.Returns(password); + return mock; + } } } diff --git a/src/SonarQube.Client.Tests/Helpers/AuthenticationHeaderFactoryTests.cs b/src/SonarQube.Client.Tests/Helpers/AuthenticationHeaderFactoryTests.cs index 723c6d23af..704a7fde30 100644 --- a/src/SonarQube.Client.Tests/Helpers/AuthenticationHeaderFactoryTests.cs +++ b/src/SonarQube.Client.Tests/Helpers/AuthenticationHeaderFactoryTests.cs @@ -18,18 +18,20 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Security; using SonarQube.Client.Helpers; +using SonarQube.Client.Models; namespace SonarQube.Client.Tests.Helpers { [TestClass] public class AuthenticationHeaderFactoryTests { + private const string Password = "password"; + private const string Username = "username"; + [TestMethod] - public void AuthenticationHeaderHelper_GetAuthToken() + public void GetAuthToken_ReturnsExpectedString() { // Invalid input string user = "hello:"; @@ -59,8 +61,50 @@ public void AuthenticationHeaderHelper_GetAuthToken() AuthenticationHeaderFactory.GetBasicAuthToken(user, password.ToSecureString())); } - private void AssertAreEqualUserNameAndPassword(string expectedUser, string expectedPassword, + [TestMethod] + public void Create_NoCredentials_ReturnsNull() + { + var authenticationHeaderValue = AuthenticationHeaderFactory.Create(new NoCredentials()); + + authenticationHeaderValue.Should().BeNull(); + } + + [TestMethod] + public void Create_UnsupportedAuthentication_ReturnsNull() + { + using var scope = new AssertIgnoreScope(); + + var authenticationHeaderValue = AuthenticationHeaderFactory.Create(null); + + authenticationHeaderValue.Should().BeNull(); + } + + [TestMethod] + public void Create_BasicAuth_UsernameIsNull_Throws() + { + var credentials = MockBasicAuthCredentials(null, Password.ToSecureString()); + + var act = () => AuthenticationHeaderFactory.Create(credentials); + + act.Should().Throw(); + } + + [TestMethod] + public void Create_BasicAuth_CredentialsProvided_ReturnsBasicScheme() + { + var credentials = MockBasicAuthCredentials(Username, Password.ToSecureString()); + + var authenticationHeaderValue = AuthenticationHeaderFactory.Create(credentials); + + authenticationHeaderValue.Scheme.Should().Be("Basic"); + AssertAreEqualUserNameAndPassword(Username, Password, authenticationHeaderValue.Parameter); + } + + private void AssertAreEqualUserNameAndPassword( + string expectedUser, + string expectedPassword, string userAndPasswordBase64String) + { string userNameAndPassword = AuthenticationHeaderFactory.BasicAuthEncoding.GetString(Convert.FromBase64String(userAndPasswordBase64String)); @@ -76,5 +120,13 @@ private void AssertAreEqualUserNameAndPassword(string expectedUser, string expec userNameAndPasswordTokens.Should().HaveElementAt(0, expectedUser); userNameAndPasswordTokens.Should().HaveElementAt(1, expectedPassword); } + + private static IUsernameAndPasswordCredentials MockBasicAuthCredentials(string userName, SecureString password) + { + var mock = Substitute.For(); + mock.UserName.Returns(userName); + mock.Password.Returns(password); + return mock; + } } } diff --git a/src/SonarQube.Client.Tests/Models/ConnectionInformationTests.cs b/src/SonarQube.Client.Tests/Models/ConnectionInformationTests.cs index 07df37d347..7a55c077b4 100644 --- a/src/SonarQube.Client.Tests/Models/ConnectionInformationTests.cs +++ b/src/SonarQube.Client.Tests/Models/ConnectionInformationTests.cs @@ -18,10 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; using System.Security; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; using SonarQube.Client.Helpers; using SonarQube.Client.Models; @@ -54,14 +51,14 @@ public void Ctor_SonarQubeUrl_IsProcessedCorrectly(string inputUrl, string expec } [TestMethod] - [DataRow("http://sonarcloud.io") ] - [DataRow("http://sonarcloud.io/") ] - [DataRow("https://sonarcloud.io") ] - [DataRow("https://sonarcloud.io/") ] - [DataRow("http://SONARCLOUD.IO") ] - [DataRow("http://www.sonarcloud.io") ] - [DataRow("https://www.sonarcloud.io/") ] - [DataRow("http://sonarcloud.io:9999") ] + [DataRow("http://sonarcloud.io")] + [DataRow("http://sonarcloud.io/")] + [DataRow("https://sonarcloud.io")] + [DataRow("https://sonarcloud.io/")] + [DataRow("http://SONARCLOUD.IO")] + [DataRow("http://www.sonarcloud.io")] + [DataRow("https://www.sonarcloud.io/")] + [DataRow("http://sonarcloud.io:9999")] public void Ctor_SonarCloudUrl_IsProcessedCorrectly(string inputUrl) { var testSubject = new ConnectionInformation(new Uri(inputUrl)); @@ -76,61 +73,73 @@ public void Ctor_SonarCloudUrl_IsProcessedCorrectly(string inputUrl) [DataRow("http://localhost", "user1", "secret", null)] [DataRow("http://sonarcloud.io", null, null, "myorg")] [DataRow("http://sonarcloud.io", "a token", null, "myorg")] - public void Clone_PropertiesAreCopiedCorrectly(string serverUrl, string userName, string password, string orgKey) + public void Clone_PropertiesAreCopiedCorrectly( + string serverUrl, + string userName, + string password, + string orgKey) { var securePwd = InitializeSecureString(password); var org = InitializeOrganization(orgKey); + var credentials = MockBasicAuthCredentials(userName, securePwd); - var testSubject = new ConnectionInformation(new Uri(serverUrl), userName, securePwd) - { - Organization = org - }; + var testSubject = new ConnectionInformation(new Uri(serverUrl), credentials) { Organization = org }; var cloneObj = ((ICloneable)testSubject).Clone(); cloneObj.Should().BeOfType(); CheckPropertiesMatch(testSubject, (ConnectionInformation)cloneObj); + _ = credentials.Received().Clone(); } [TestMethod] public void Dispose_PasswordIsDisposed() { var pwd = "secret".ToSecureString(); - var testSubject = new ConnectionInformation(new Uri("http://any"), "any", pwd); + var credentials = MockBasicAuthCredentials("any", pwd); + var testSubject = new ConnectionInformation(new Uri("http://any"), credentials); testSubject.Dispose(); testSubject.IsDisposed.Should().BeTrue(); - - Action accessPassword = () => _ = testSubject.Password.Length; - accessPassword.Should().ThrowExactly(); + credentials.Received(1).Dispose(); } private static SecureString InitializeSecureString(string password) => // The "ToSecureString" doesn't expect nulls, which we want to use in the tests password?.ToSecureString(); - private static SonarQubeOrganization InitializeOrganization(string orgKey) => - orgKey == null ? null : new SonarQubeOrganization(orgKey, Guid.NewGuid().ToString()); + private static SonarQubeOrganization InitializeOrganization(string orgKey) => orgKey == null ? null : new SonarQubeOrganization(orgKey, Guid.NewGuid().ToString()); private static void CheckPropertiesMatch(ConnectionInformation item1, ConnectionInformation item2) { item1.ServerUri.Should().Be(item2.ServerUri); - item1.UserName.Should().Be(item2.UserName); - item1.Organization.Should().Be(item2.Organization); + var credentials1 = (IUsernameAndPasswordCredentials)item1.Credentials; + var credentials2 = (IUsernameAndPasswordCredentials)item2.Credentials; + + credentials1.UserName.Should().Be(credentials2.UserName); + item1.Organization.Should().Be(item2.Organization); - if (item1.Password == null) + if (credentials1.Password == null) { - item2.Password.Should().BeNull(); + credentials2.Password.Should().BeNull(); } else { - item1.Password.ToUnsecureString().Should().Be(item2.Password.ToUnsecureString()); + credentials1.Password.ToUnsecureString().Should().Be(credentials2.Password.ToUnsecureString()); } - item1.Authentication.Should().Be(item2.Authentication); item1.IsSonarCloud.Should().Be(item2.IsSonarCloud); } + + private static IUsernameAndPasswordCredentials MockBasicAuthCredentials(string userName, SecureString password) + { + var mock = Substitute.For(); + mock.UserName.Returns(userName); + mock.Password.Returns(password); + mock.Clone().Returns(mock); + return mock; + } } } diff --git a/src/SonarQube.Client.Tests/SonarQubeService_Lifecycle.cs b/src/SonarQube.Client.Tests/SonarQubeService_Lifecycle.cs index 187663f918..5045b3bdc5 100644 --- a/src/SonarQube.Client.Tests/SonarQubeService_Lifecycle.cs +++ b/src/SonarQube.Client.Tests/SonarQubeService_Lifecycle.cs @@ -18,13 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Threading; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; using Moq.Protected; -using SonarQube.Client.Helpers; using SonarQube.Client.Models; namespace SonarQube.Client.Tests @@ -43,7 +38,7 @@ public async Task Connect_To_SonarQube_Valid_Credentials() service.GetServerInfo().Should().BeNull(); await service.ConnectAsync( - new ConnectionInformation(new Uri("http://localhost"), "user", "pass".ToSecureString()), + new ConnectionInformation(new Uri("http://localhost"), Mock.Of()), CancellationToken.None); service.IsConnected.Should().BeTrue(); @@ -61,7 +56,7 @@ public async Task Connect_To_SonarQube_Invalid_Credentials() service.GetServerInfo().Should().BeNull(); Func act = () => service.ConnectAsync( - new ConnectionInformation(new Uri("http://localhost"), "user", "pass".ToSecureString()), + new ConnectionInformation(new Uri("http://localhost"), Mock.Of()), CancellationToken.None); var ex = await act.Should().ThrowAsync(); @@ -83,7 +78,7 @@ public async Task Connect_ServerIsNotReachable_IsConnectedIsFalse() service.GetServerInfo().Should().BeNull(); Func act = () => service.ConnectAsync( - new ConnectionInformation(new Uri("http://localhost"), "user", "pass".ToSecureString()), + new ConnectionInformation(new Uri("http://localhost"), Mock.Of()), CancellationToken.None); var ex = await act.Should().ThrowAsync(); @@ -106,7 +101,7 @@ public async Task Connect_SonarQube_IsSonarCloud_SonarQubeUrl_ReturnsFalse(strin SetupRequest("api/authentication/validate", "{ \"valid\": true }", serverUrl: canonicalUrl); await service.ConnectAsync( - new ConnectionInformation(new Uri(inputUrl), "user", "pass".ToSecureString()), + new ConnectionInformation(new Uri(inputUrl), Mock.Of()), CancellationToken.None); service.GetServerInfo().ServerType.Should().Be(ServerType.SonarQube); @@ -126,7 +121,7 @@ public async Task Connect_SonarQube_IsSonarCloud_SonarCloud_ReturnTrue(string in SetupRequest("api/authentication/validate", "{ \"valid\": true }", serverUrl: fixedSonarCloudUrl); await service.ConnectAsync( - new ConnectionInformation(new Uri(inputUrl), "user", "pass".ToSecureString()), + new ConnectionInformation(new Uri(inputUrl), Mock.Of()), CancellationToken.None); service.GetServerInfo().ServerType.Should().Be(ServerType.SonarCloud); diff --git a/src/SonarQube.Client.Tests/SonarQubeService_TestBase.cs b/src/SonarQube.Client.Tests/SonarQubeService_TestBase.cs index d23b649a73..9b8034b51f 100644 --- a/src/SonarQube.Client.Tests/SonarQubeService_TestBase.cs +++ b/src/SonarQube.Client.Tests/SonarQubeService_TestBase.cs @@ -18,16 +18,11 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; using System.Globalization; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Security; -using System.Threading; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Moq.Protected; using SonarQube.Client.Helpers; @@ -71,7 +66,11 @@ public void TestInitialize() ResetService(); } - protected void SetupRequest(string relativePath, string response, HttpStatusCode statusCode = HttpStatusCode.OK, string serverUrl = DefaultBasePath) => + protected void SetupRequest( + string relativePath, + string response, + HttpStatusCode statusCode = HttpStatusCode.OK, + string serverUrl = DefaultBasePath) => MocksHelper.SetupHttpRequest(messageHandler, relativePath, response, statusCode, serverUrl); protected void SetupRequest(string relativePath, HttpResponseMessage response, params MediaTypeHeaderValue[] expectedHeaderValues) => @@ -94,7 +93,7 @@ protected async Task ConnectToSonarQube(string version = "5.6.0.0", string serve SetupRequest("api/authentication/validate", "{ \"valid\": true}", serverUrl: serverUrl); await service.ConnectAsync( - new ConnectionInformation(new Uri(serverUrl), "valeri", new SecureString()), + new ConnectionInformation(new Uri(serverUrl), MockBasicAuthCredentials("valeri", new SecureString())), CancellationToken.None); // Sanity checks @@ -120,5 +119,14 @@ protected internal virtual SonarQubeService CreateTestSubject() { return new SonarQubeService(messageHandler.Object, UserAgent, logger, requestFactorySelector, secondaryIssueHashUpdater.Object, sseStreamFactory.Object); } + + private static IUsernameAndPasswordCredentials MockBasicAuthCredentials(string userName, SecureString password) + { + var mock = new Mock(); + mock.SetupGet(x => x.UserName).Returns(userName); + mock.SetupGet(x => x.Password).Returns(password); + + return mock.Object; + } } } diff --git a/src/SonarQube.Client/Helpers/AuthenticationHeaderFactory.cs b/src/SonarQube.Client/Helpers/AuthenticationHeaderFactory.cs index 5f92668a9a..91b61f06cb 100644 --- a/src/SonarQube.Client/Helpers/AuthenticationHeaderFactory.cs +++ b/src/SonarQube.Client/Helpers/AuthenticationHeaderFactory.cs @@ -18,8 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Diagnostics; using System.Net.Http.Headers; using System.Security; using System.Text; @@ -36,20 +34,20 @@ public static class AuthenticationHeaderFactory /// internal static readonly Encoding BasicAuthEncoding = Encoding.UTF8; - public static AuthenticationHeaderValue Create(string userName, SecureString password, AuthenticationType authentication) + public static AuthenticationHeaderValue Create(IConnectionCredentials credentials) { - if (authentication == AuthenticationType.Basic) + if (credentials is IUsernameAndPasswordCredentials basicAuthCredentials) { - return string.IsNullOrWhiteSpace(userName) - ? null - : new AuthenticationHeaderValue("Basic", GetBasicAuthToken(userName, password)); + ValidateCredentials(basicAuthCredentials); + return new AuthenticationHeaderValue("Basic", GetBasicAuthToken(basicAuthCredentials.UserName, basicAuthCredentials.Password)); // See more info: https://www.visualstudio.com/en-us/integrate/get-started/auth/overview } - else + if (credentials is INoCredentials) { - Debug.Fail("Unsupported Authentication: " + authentication); return null; } + Debug.Fail("Unsupported Authentication: " + credentials?.GetType()); + return null; } internal static string GetBasicAuthToken(string user, SecureString password) @@ -64,5 +62,13 @@ internal static string GetBasicAuthToken(string user, SecureString password) return Convert.ToBase64String(BasicAuthEncoding.GetBytes(string.Join(BasicAuthCredentialSeparator, user, password.ToUnsecureString()))); } + + private static void ValidateCredentials(IUsernameAndPasswordCredentials basicAuthCredentials) + { + if (string.IsNullOrEmpty(basicAuthCredentials.UserName)) + { + throw new ArgumentException(nameof(basicAuthCredentials.UserName)); + } + } } } diff --git a/src/SonarQube.Client/Models/ConnectionInformation.cs b/src/SonarQube.Client/Models/ConnectionInformation.cs index 9eb320a09c..2dafca49e8 100644 --- a/src/SonarQube.Client/Models/ConnectionInformation.cs +++ b/src/SonarQube.Client/Models/ConnectionInformation.cs @@ -18,8 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Security; using SonarQube.Client.Helpers; namespace SonarQube.Client.Models @@ -35,7 +33,7 @@ public sealed class ConnectionInformation : ICloneable, IDisposable private bool isDisposed; - public ConnectionInformation(Uri serverUri, string userName, SecureString password) + public ConnectionInformation(Uri serverUri, IConnectionCredentials credentials) { if (serverUri == null) { @@ -43,14 +41,12 @@ public ConnectionInformation(Uri serverUri, string userName, SecureString passwo } ServerUri = FixSonarCloudUri(serverUri).EnsureTrailingSlash(); - UserName = userName; - Password = password?.CopyAsReadOnly(); - Authentication = AuthenticationType.Basic; // Only one supported at this point + Credentials = (IConnectionCredentials)credentials?.Clone() ?? new NoCredentials(); IsSonarCloud = ServerUri == FixedSonarCloudUri; } public ConnectionInformation(Uri serverUri) - : this(serverUri, null, null) + : this(serverUri, null) { } @@ -58,11 +54,7 @@ public ConnectionInformation(Uri serverUri) public bool IsSonarCloud { get; } - public string UserName { get; } - - public SecureString Password { get; } - - public AuthenticationType Authentication { get; } + public IConnectionCredentials Credentials { get; } public bool IsDisposed => isDisposed; @@ -70,7 +62,7 @@ public ConnectionInformation(Uri serverUri) public ConnectionInformation Clone() { - return new ConnectionInformation(ServerUri, UserName, Password?.CopyAsReadOnly()) { Organization = Organization }; + return new ConnectionInformation(ServerUri, (IConnectionCredentials)Credentials?.Clone()) { Organization = Organization }; } object ICloneable.Clone() @@ -96,7 +88,7 @@ public void Dispose() { if (!isDisposed) { - Password?.Dispose(); + Credentials?.Dispose(); isDisposed = true; } } diff --git a/src/Core/Binding/ICredentials.cs b/src/SonarQube.Client/Models/IConnectionCredentials.cs similarity index 71% rename from src/Core/Binding/ICredentials.cs rename to src/SonarQube.Client/Models/IConnectionCredentials.cs index 4a488970cd..f544a056f3 100644 --- a/src/Core/Binding/ICredentials.cs +++ b/src/SonarQube.Client/Models/IConnectionCredentials.cs @@ -18,13 +18,20 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using SonarQube.Client.Models; +using System.Security; -namespace SonarLint.VisualStudio.Core.Binding +namespace SonarQube.Client.Models; + +public interface IConnectionCredentials : IDisposable, ICloneable +{ +} + +public interface IUsernameAndPasswordCredentials : IConnectionCredentials +{ + public string UserName { get; } + public SecureString Password { get; } +} + +public interface INoCredentials : IConnectionCredentials { - public interface ICredentials - { - ConnectionInformation CreateConnectionInformation(Uri serverUri); - } } diff --git a/src/SonarQube.Client/Models/AuthenticationType.cs b/src/SonarQube.Client/Models/NoCredentials.cs similarity index 83% rename from src/SonarQube.Client/Models/AuthenticationType.cs rename to src/SonarQube.Client/Models/NoCredentials.cs index acc6c4a0a3..fdae5285d4 100644 --- a/src/SonarQube.Client/Models/AuthenticationType.cs +++ b/src/SonarQube.Client/Models/NoCredentials.cs @@ -18,7 +18,11 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -namespace SonarQube.Client.Models +namespace SonarQube.Client.Models; + +internal sealed class NoCredentials : INoCredentials { - public enum AuthenticationType { Basic } + public void Dispose() { } + + public object Clone() => new NoCredentials(); } diff --git a/src/SonarQube.Client/SonarQubeService.cs b/src/SonarQube.Client/SonarQubeService.cs index 35617bf6fd..eeb48a4d43 100644 --- a/src/SonarQube.Client/SonarQubeService.cs +++ b/src/SonarQube.Client/SonarQubeService.cs @@ -67,7 +67,10 @@ public SonarQubeService(HttpMessageHandler messageHandler, string userAgent, ILo { } - internal /* for testing */ SonarQubeService(HttpMessageHandler messageHandler, string userAgent, ILogger logger, + internal /* for testing */ SonarQubeService( + HttpMessageHandler messageHandler, + string userAgent, + ILogger logger, IRequestFactorySelector requestFactorySelector, ISecondaryIssueHashUpdater secondaryIssueHashUpdater, ISSEStreamReaderFactory sseStreamReaderFactory) @@ -110,7 +113,8 @@ private Task InvokeCheckedRequestAsync(Cancellat /// Action that configures a type instance that implements TRequest. /// Cancellation token. /// Returns the result of the request invocation. - private Task InvokeCheckedRequestAsync(Action configure, + private Task InvokeCheckedRequestAsync( + Action configure, CancellationToken token) where TRequest : IRequest { @@ -141,12 +145,7 @@ public async Task ConnectAsync(ConnectionInformation connection, CancellationTok httpClient = new HttpClient(messageHandler) { - BaseAddress = connection.ServerUri, - DefaultRequestHeaders = - { - Authorization = AuthenticationHeaderFactory.Create( - connection.UserName, connection.Password, connection.Authentication), - }, + BaseAddress = connection.ServerUri, DefaultRequestHeaders = { Authorization = AuthenticationHeaderFactory.Create(connection.Credentials), }, }; httpClient.DefaultRequestHeaders.Add("User-Agent", userAgent); @@ -215,8 +214,7 @@ await InvokeCheckedRequestAsync> GetAllLanguagesAsync(CancellationToken token) => - await InvokeCheckedRequestAsync(token); + public async Task> GetAllLanguagesAsync(CancellationToken token) => await InvokeCheckedRequestAsync(token); public async Task DownloadStaticFileAsync(string pluginKey, string fileName, CancellationToken token) => await InvokeCheckedRequestAsync( @@ -227,8 +225,7 @@ await InvokeCheckedRequestAsync( }, token); - public async Task> GetAllPluginsAsync(CancellationToken token) => - await InvokeCheckedRequestAsync(token); + public async Task> GetAllPluginsAsync(CancellationToken token) => await InvokeCheckedRequestAsync(token); public async Task> GetAllProjectsAsync(string organizationKey, CancellationToken token) => await InvokeCheckedRequestAsync( @@ -269,7 +266,11 @@ public async Task> GetAllQualityProfilesAsync(str token); } - public async Task GetQualityProfileAsync(string projectKey, string organizationKey, SonarQubeLanguage language, CancellationToken token) + public async Task GetQualityProfileAsync( + string projectKey, + string organizationKey, + SonarQubeLanguage language, + CancellationToken token) { var qualityProfiles = await InvokeCheckedRequestAsync( request => @@ -310,8 +311,11 @@ public async Task GetQualityProfileAsync(string project qualityProfile.IsDefault, updatedDate); } - public async Task GetRoslynExportProfileAsync(string qualityProfileName, - string organizationKey, SonarQubeLanguage language, CancellationToken token) => + public async Task GetRoslynExportProfileAsync( + string qualityProfileName, + string organizationKey, + SonarQubeLanguage language, + CancellationToken token) => await InvokeCheckedRequestAsync( request => { @@ -321,8 +325,11 @@ await InvokeCheckedRequestAsync> GetSuppressedIssuesAsync(string projectKey, string branch, - string[] issueKeys, CancellationToken token) => + public async Task> GetSuppressedIssuesAsync( + string projectKey, + string branch, + string[] issueKeys, + CancellationToken token) => await InvokeCheckedRequestAsync( request => { @@ -333,7 +340,11 @@ await InvokeCheckedRequestAsync( }, token); - public async Task> GetIssuesForComponentAsync(string projectKey, string branch, string componentKey, string ruleId, + public async Task> GetIssuesForComponentAsync( + string projectKey, + string branch, + string componentKey, + string ruleId, CancellationToken token) { return await InvokeCheckedRequestAsync( @@ -347,7 +358,9 @@ public async Task> GetIssuesForComponentAsync(string proje token); } - public async Task> GetNotificationEventsAsync(string projectKey, DateTimeOffset eventsSince, + public async Task> GetNotificationEventsAsync( + string projectKey, + DateTimeOffset eventsSince, CancellationToken token) => await InvokeCheckedRequestAsync( request => @@ -366,17 +379,21 @@ await InvokeCheckedRequestAsync( }, token); - public async Task> SearchFilesByNameAsync(string projectKey, string branch, string fileName, CancellationToken token) + public async Task> SearchFilesByNameAsync( + string projectKey, + string branch, + string fileName, + CancellationToken token) { return await InvokeCheckedRequestAsync( - request => - { - request.ProjectKey = projectKey; - request.BranchName = branch; - request.FileName = fileName; - }, - token - ); + request => + { + request.ProjectKey = projectKey; + request.BranchName = branch; + request.FileName = fileName; + }, + token + ); } public async Task> GetRulesAsync(bool isActive, string qualityProfileKey, CancellationToken token) => @@ -386,7 +403,7 @@ await InvokeCheckedRequestAsync( request.IsActive = isActive; request.QualityProfileKey = qualityProfileKey; }, - token); + token); public async Task GetRuleByKeyAsync(string ruleKey, string qualityProfileKey, CancellationToken token) { @@ -396,7 +413,7 @@ public async Task GetRuleByKeyAsync(string ruleKey, string qualit request.RuleKey = ruleKey; request.QualityProfileKey = qualityProfileKey; }, - token); + token); Debug.Assert(rules.Length <= 1); return rules.FirstOrDefault(); @@ -410,7 +427,8 @@ await InvokeCheckedRequestAsync( }, token); - public async Task> SearchHotspotsAsync(string projectKey, string branch, CancellationToken token) => await InvokeCheckedRequestAsync( + public async Task> SearchHotspotsAsync(string projectKey, string branch, CancellationToken token) => + await InvokeCheckedRequestAsync( request => { request.BranchKey = branch; @@ -418,7 +436,11 @@ public async Task> SearchHotspotsAsync(string proj }, token); - public async Task TransitionIssueAsync(string issueKey, SonarQubeIssueTransition transition, string optionalComment, CancellationToken token) + public async Task TransitionIssueAsync( + string issueKey, + SonarQubeIssueTransition transition, + string optionalComment, + CancellationToken token) { var transitionResult = await InvokeCheckedRequestAsync( request =>