From 3fe9eba46f33dd190b1395e444c5c40d717f75ac Mon Sep 17 00:00:00 2001 From: Georgii Borovinskikh Date: Wed, 7 Feb 2024 15:18:38 +0100 Subject: [PATCH 1/4] Add SLCore credentials provider implementation --- ...egration.Vsix_Baseline_WithStrongNames.txt | 9 +- ...ation.Vsix_Baseline_WithoutStrongNames.txt | 9 +- .../CredentialsListenerTests.cs | 162 ++++++++++++++++++ .../Implementation/ICredentialsListener.cs | 92 ++++++++++ src/SLCore.Listeners/InternalsVisibleTo.cs | 32 ++++ .../Credentials/CredentialsListenerTests.cs | 58 ------- src/SLCore/Common/ConnectionIdPrefix.cs | 28 +++ src/SLCore/InternalsVisibleTo.cs | 4 + ...alsListener.cs => ICredentialsListener.cs} | 14 +- 9 files changed, 334 insertions(+), 74 deletions(-) create mode 100644 src/SLCore.Listeners.UnitTests/CredentialsListenerTests.cs create mode 100644 src/SLCore.Listeners/Implementation/ICredentialsListener.cs create mode 100644 src/SLCore.Listeners/InternalsVisibleTo.cs delete mode 100644 src/SLCore.UnitTests/Listener/Credentials/CredentialsListenerTests.cs create mode 100644 src/SLCore/Common/ConnectionIdPrefix.cs rename src/SLCore/Listener/Credentials/{CredentialsListener.cs => ICredentialsListener.cs} (67%) diff --git a/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithStrongNames.txt b/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithStrongNames.txt index d6d0c3593..f07c377d5 100644 --- a/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithStrongNames.txt +++ b/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithStrongNames.txt @@ -1,7 +1,7 @@ --- ################################ # Assembly references report -# Report date/time: 2024-02-07T10:15:27.0203827Z +# Report date/time: 2024-02-07T13:54:19.6745665Z ################################ # # Generated by Devtility CheckAsmRefs v0.11.0.223 @@ -458,8 +458,13 @@ Assembly: 'SonarLint.VisualStudio.SLCore.Listeners, Version=7.7.0.0, Culture=neu Relative path: 'SonarLint.VisualStudio.SLCore.Listeners.dll' Referenced assemblies: +- 'Microsoft.Alm.Authentication, Version=4.0.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' - 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' -# Number of references: 1 +- 'SonarLint.VisualStudio.ConnectedMode, Version=7.7.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' +- 'SonarLint.VisualStudio.SLCore, Version=7.7.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' +- 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' +- 'System.ComponentModel.Composition, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' +# Number of references: 6 --- Assembly: 'SonarLint.VisualStudio.TypeScript, Version=7.7.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' diff --git a/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithoutStrongNames.txt b/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithoutStrongNames.txt index 0800db0fd..d52045d9e 100644 --- a/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithoutStrongNames.txt +++ b/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithoutStrongNames.txt @@ -1,7 +1,7 @@ --- ################################ # Assembly references report -# Report date/time: 2024-02-07T10:15:27.0203827Z +# Report date/time: 2024-02-07T13:54:19.6745665Z ################################ # # Generated by Devtility CheckAsmRefs v0.11.0.223 @@ -458,8 +458,13 @@ Assembly: 'SonarLint.VisualStudio.SLCore.Listeners, Version=7.7.0.0, Culture=neu Relative path: 'SonarLint.VisualStudio.SLCore.Listeners.dll' Referenced assemblies: +- 'Microsoft.Alm.Authentication, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null' - 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' -# Number of references: 1 +- 'SonarLint.VisualStudio.ConnectedMode, Version=7.7.0.0, Culture=neutral, PublicKeyToken=null' +- 'SonarLint.VisualStudio.SLCore, Version=7.7.0.0, Culture=neutral, PublicKeyToken=null' +- 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' +- 'System.ComponentModel.Composition, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' +# Number of references: 6 --- Assembly: 'SonarLint.VisualStudio.TypeScript, Version=7.7.0.0, Culture=neutral, PublicKeyToken=null' diff --git a/src/SLCore.Listeners.UnitTests/CredentialsListenerTests.cs b/src/SLCore.Listeners.UnitTests/CredentialsListenerTests.cs new file mode 100644 index 000000000..fc468f7af --- /dev/null +++ b/src/SLCore.Listeners.UnitTests/CredentialsListenerTests.cs @@ -0,0 +1,162 @@ +/* + * 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; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Alm.Authentication; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using SonarLint.VisualStudio.ConnectedMode.Binding; +using SonarLint.VisualStudio.SLCore.Common; +using SonarLint.VisualStudio.SLCore.Common.Models; +using SonarLint.VisualStudio.SLCore.Core; +using SonarLint.VisualStudio.SLCore.Listener.Credentials; +using SonarLint.VisualStudio.SLCore.Listeners.Implementation; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.SLCore.Listeners.UnitTests; + +[TestClass] +public class CredentialsListenerTests +{ + private static readonly Uri SonarQubeUri = new("https://next.sonarqube.com"); + private static readonly Uri SonarCloudUri = new("https://sonarcloud.io"); + private static readonly string SonarQubeConnectionId = ConnectionIdPrefix.SonarQubePrefix + SonarQubeUri; + private const string SonarCloudConnectionId = ConnectionIdPrefix.SonarCloudPrefix + "myorganization"; + + [TestMethod] + public void MefCtor_CheckIsExported() + { + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport()); + } + + [TestMethod] + public void MefCtor_CheckIsSingleton() + { + MefTestHelpers.CheckIsSingletonMefComponent(); + } + + [TestMethod] + public async Task GetCredentialsAsync_NullConnectionId_ReturnsNoCredentials() + { + var testSubject = CreateTestSubject(out _); + + var response = await testSubject.GetCredentialsAsync(new GetCredentialsParams(null)); + + response.Should().BeSameAs(GetCredentialsResponse.NoCredentials); + } + + [TestMethod] + public async Task GetCredentialsAsync_NullParams_ReturnsNoCredentials() + { + var testSubject = CreateTestSubject(out _); + + var response = await testSubject.GetCredentialsAsync(null); + + response.Should().BeSameAs(GetCredentialsResponse.NoCredentials); + } + + [TestMethod] + public async Task GetCredentialsAsync_SonarQubeCredentialsNotFound_ReturnsNoCredentials() + { + var testSubject = CreateTestSubject(out var credentialStoreMock); + credentialStoreMock.Setup(x => x.ReadCredentials(It.Is((TargetUri uri) => UriEquals(uri, SonarQubeUri)))).Returns((Credential)null); + + var response = await testSubject.GetCredentialsAsync(new GetCredentialsParams(SonarQubeConnectionId)); + + response.Should().BeSameAs(GetCredentialsResponse.NoCredentials); + } + + [TestMethod] + public async Task GetCredentialsAsync_SonarCloudCredentialsNotFound_ReturnsNoCredentials() + { + var testSubject = CreateTestSubject(out var credentialStoreMock); + credentialStoreMock.Setup(x => x.ReadCredentials(It.Is((TargetUri uri) => UriEquals(uri, SonarCloudUri)))).Returns((Credential)null); + + var response = await testSubject.GetCredentialsAsync(new GetCredentialsParams(SonarQubeConnectionId)); + + response.Should().BeSameAs(GetCredentialsResponse.NoCredentials); + } + + [TestMethod] + public async Task GetCredentialsAsync_SonarQubeUsernameAndPasswordFound_ReturnsUsernameAndPassword() + { + const string username = "user1"; + const string password = "password123"; + + var testSubject = CreateTestSubject(out var credentialStoreMock); + credentialStoreMock.Setup(x => x.ReadCredentials(It.Is((TargetUri uri) => UriEquals(uri, SonarQubeUri)))).Returns(new Credential(username, password)); + + var response = await testSubject.GetCredentialsAsync(new GetCredentialsParams(SonarQubeConnectionId)); + + response.Should().BeEquivalentTo(new GetCredentialsResponse(new UsernamePasswordDto(username, password))); + } + + [TestMethod] + public async Task GetCredentialsAsync_SonarQubeTokenFound_ReturnsToken() + { + const string token = "token123"; + + var testSubject = CreateTestSubject(out var credentialStoreMock); + credentialStoreMock.Setup(x => x.ReadCredentials(It.Is((TargetUri uri) => UriEquals(uri, SonarQubeUri)))).Returns(new Credential(token)); + + var response = await testSubject.GetCredentialsAsync(new GetCredentialsParams(SonarQubeConnectionId)); + + response.Should().BeEquivalentTo(new GetCredentialsResponse(new TokenDto(token))); + } + + [TestMethod] + public async Task GetCredentialsAsync_SonarCloudUsernameAndPasswordFound_ReturnsUsernameAndPassword() + { + const string username = "user1"; + const string password = "password123"; + + var testSubject = CreateTestSubject(out var credentialStoreMock); + credentialStoreMock.Setup(x => x.ReadCredentials(It.Is((TargetUri uri) => UriEquals(uri, SonarCloudUri)))).Returns(new Credential(username, password)); + + var response = await testSubject.GetCredentialsAsync(new GetCredentialsParams(SonarCloudConnectionId)); + + response.Should().BeEquivalentTo(new GetCredentialsResponse(new UsernamePasswordDto(username, password))); + } + + [TestMethod] + public async Task GetCredentialsAsync_SonarCloudTokenFound_ReturnsToken() + { + const string token = "token123"; + + var testSubject = CreateTestSubject(out var credentialStoreMock); + credentialStoreMock.Setup(x => x.ReadCredentials(It.Is((TargetUri uri) => UriEquals(uri, SonarCloudUri)))).Returns(new Credential(token)); + + var response = await testSubject.GetCredentialsAsync(new GetCredentialsParams(SonarCloudConnectionId)); + + response.Should().BeEquivalentTo(new GetCredentialsResponse(new TokenDto(token))); + } + + private CredentialsListener CreateTestSubject(out Mock credentialStoreMock) + { + credentialStoreMock = new Mock(); + + return new CredentialsListener(credentialStoreMock.Object); + } + + private bool UriEquals(TargetUri uri, Uri serverUri) => serverUri.Equals((Uri)uri); +} diff --git a/src/SLCore.Listeners/Implementation/ICredentialsListener.cs b/src/SLCore.Listeners/Implementation/ICredentialsListener.cs new file mode 100644 index 000000000..e48a839e3 --- /dev/null +++ b/src/SLCore.Listeners/Implementation/ICredentialsListener.cs @@ -0,0 +1,92 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2023 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; +using System.ComponentModel.Composition; +using System.Threading.Tasks; +using SonarLint.VisualStudio.ConnectedMode.Binding; +using SonarLint.VisualStudio.SLCore.Common; +using SonarLint.VisualStudio.SLCore.Common.Models; +using SonarLint.VisualStudio.SLCore.Core; +using SonarLint.VisualStudio.SLCore.Listener.Credentials; + +namespace SonarLint.VisualStudio.SLCore.Listeners.Implementation +{ + /// + /// Credentials provider for SLCore + /// + [Export(typeof(ISLCoreListener))] + [PartCreationPolicy(CreationPolicy.Shared)] + internal class CredentialsListener : ICredentialsListener + { + private static readonly Uri SonarCloudUri = new Uri("https://sonarcloud.io"); + private readonly ICredentialStoreService credentialStore; + + [ImportingConstructor] + public CredentialsListener(ICredentialStoreService credentialStore) + { + this.credentialStore = credentialStore; + } + + public Task GetCredentialsAsync(GetCredentialsParams parameters) + { + var serverUri = GetServerUriFromConnectionId(parameters?.connectionId); + + if (serverUri == null) + { + return Task.FromResult(GetCredentialsResponse.NoCredentials); + } + + var credentials = credentialStore.ReadCredentials(serverUri); + + if (credentials == null) + { + return Task.FromResult(GetCredentialsResponse.NoCredentials); + } + + return Task.FromResult(string.IsNullOrEmpty(credentials.Password) + ? new GetCredentialsResponse(new TokenDto(credentials.Username)) + : new GetCredentialsResponse(new UsernamePasswordDto(credentials.Username, credentials.Password))); + } + + private static Uri GetServerUriFromConnectionId(string connectionId) + { + if (connectionId == null) + { + return null; + } + + if (connectionId.StartsWith(ConnectionIdPrefix.SonarCloudPrefix)) + { + return SonarCloudUri; + } + + if (connectionId.StartsWith(ConnectionIdPrefix.SonarQubePrefix)) + { + return Uri.TryCreate(connectionId.Substring(ConnectionIdPrefix.SonarQubePrefix.Length), UriKind.Absolute, + out var uri) + ? uri + : null; + } + + return null; + } + } +} diff --git a/src/SLCore.Listeners/InternalsVisibleTo.cs b/src/SLCore.Listeners/InternalsVisibleTo.cs new file mode 100644 index 000000000..d31b04b06 --- /dev/null +++ b/src/SLCore.Listeners/InternalsVisibleTo.cs @@ -0,0 +1,32 @@ +/* + * 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.Runtime.CompilerServices; + +#if SignAssembly +[assembly: InternalsVisibleTo("SonarLint.VisualStudio.SLCore.UnitTests,PublicKey=002400000480000094000000060200000024000052534131000400000100010081b4345a022cc0f4b42bdc795a5a7a1623c1e58dc2246645d751ad41ba98f2749dc5c4e0da3a9e09febcb2cd5b088a0f041f8ac24b20e736d8ae523061733782f9c4cd75b44f17a63714aced0b29a59cd1ce58d8e10ccdb6012c7098c39871043b7241ac4ab9f6b34f183db716082cd57c1ff648135bece256357ba735e67dc6")] +[assembly: InternalsVisibleTo("SonarLint.VisualStudio.SLCore.Listeners.UnitTests,PublicKey=002400000480000094000000060200000024000052534131000400000100010081b4345a022cc0f4b42bdc795a5a7a1623c1e58dc2246645d751ad41ba98f2749dc5c4e0da3a9e09febcb2cd5b088a0f041f8ac24b20e736d8ae523061733782f9c4cd75b44f17a63714aced0b29a59cd1ce58d8e10ccdb6012c7098c39871043b7241ac4ab9f6b34f183db716082cd57c1ff648135bece256357ba735e67dc6")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] +#else + +[assembly: InternalsVisibleTo("SonarLint.VisualStudio.SLCore.UnitTests")] +[assembly: InternalsVisibleTo("SonarLint.VisualStudio.SLCore.Listeners.UnitTests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] +#endif diff --git a/src/SLCore.UnitTests/Listener/Credentials/CredentialsListenerTests.cs b/src/SLCore.UnitTests/Listener/Credentials/CredentialsListenerTests.cs deleted file mode 100644 index 47c462234..000000000 --- a/src/SLCore.UnitTests/Listener/Credentials/CredentialsListenerTests.cs +++ /dev/null @@ -1,58 +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; -using System.Threading.Tasks; -using SonarLint.VisualStudio.SLCore.Core; -using SonarLint.VisualStudio.SLCore.Listener.Credentials; -using SonarLint.VisualStudio.TestInfrastructure; - -namespace SonarLint.VisualStudio.SLCore.UnitTests.Listener.Credentials; - -[TestClass] -public class CredentialsListenerTests -{ - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported(); - } - - [TestMethod] - public void MefCtor_CheckIsSingleton() - { - MefTestHelpers.CheckIsSingletonMefComponent(); - } - - [TestMethod] - public async Task GetCredentialsAsync_StubImpl_ReturnsNoCredentials() - { - var testSubject = CreateTestSubject(); - - var response = await testSubject.GetCredentialsAsync(new GetCredentialsParams($"randomstring-{Guid.NewGuid()}")); - - response.Should().BeSameAs(GetCredentialsResponse.NoCredentials); - } - - private CredentialsListener CreateTestSubject() - { - return new CredentialsListener(); - } -} diff --git a/src/SLCore/Common/ConnectionIdPrefix.cs b/src/SLCore/Common/ConnectionIdPrefix.cs new file mode 100644 index 000000000..120e1daac --- /dev/null +++ b/src/SLCore/Common/ConnectionIdPrefix.cs @@ -0,0 +1,28 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2023 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. + */ + +namespace SonarLint.VisualStudio.SLCore.Common +{ + internal static class ConnectionIdPrefix + { + public const string SonarCloudPrefix = "sc|"; + public const string SonarQubePrefix = "sq|"; + } +} diff --git a/src/SLCore/InternalsVisibleTo.cs b/src/SLCore/InternalsVisibleTo.cs index 160118d8c..f5603cdf4 100644 --- a/src/SLCore/InternalsVisibleTo.cs +++ b/src/SLCore/InternalsVisibleTo.cs @@ -22,9 +22,13 @@ #if SignAssembly [assembly: InternalsVisibleTo("SonarLint.VisualStudio.SLCore.UnitTests,PublicKey=002400000480000094000000060200000024000052534131000400000100010081b4345a022cc0f4b42bdc795a5a7a1623c1e58dc2246645d751ad41ba98f2749dc5c4e0da3a9e09febcb2cd5b088a0f041f8ac24b20e736d8ae523061733782f9c4cd75b44f17a63714aced0b29a59cd1ce58d8e10ccdb6012c7098c39871043b7241ac4ab9f6b34f183db716082cd57c1ff648135bece256357ba735e67dc6")] +[assembly: InternalsVisibleTo("SonarLint.VisualStudio.SLCore.Listeners,PublicKey=002400000480000094000000060200000024000052534131000400000100010081b4345a022cc0f4b42bdc795a5a7a1623c1e58dc2246645d751ad41ba98f2749dc5c4e0da3a9e09febcb2cd5b088a0f041f8ac24b20e736d8ae523061733782f9c4cd75b44f17a63714aced0b29a59cd1ce58d8e10ccdb6012c7098c39871043b7241ac4ab9f6b34f183db716082cd57c1ff648135bece256357ba735e67dc6")] +[assembly: InternalsVisibleTo("SonarLint.VisualStudio.SLCore.Listeners.UnitTests,PublicKey=002400000480000094000000060200000024000052534131000400000100010081b4345a022cc0f4b42bdc795a5a7a1623c1e58dc2246645d751ad41ba98f2749dc5c4e0da3a9e09febcb2cd5b088a0f041f8ac24b20e736d8ae523061733782f9c4cd75b44f17a63714aced0b29a59cd1ce58d8e10ccdb6012c7098c39871043b7241ac4ab9f6b34f183db716082cd57c1ff648135bece256357ba735e67dc6")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] #else [assembly: InternalsVisibleTo("SonarLint.VisualStudio.SLCore.UnitTests")] +[assembly: InternalsVisibleTo("SonarLint.VisualStudio.SLCore.Listeners")] +[assembly: InternalsVisibleTo("SonarLint.VisualStudio.SLCore.Listeners.UnitTests")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] #endif diff --git a/src/SLCore/Listener/Credentials/CredentialsListener.cs b/src/SLCore/Listener/Credentials/ICredentialsListener.cs similarity index 67% rename from src/SLCore/Listener/Credentials/CredentialsListener.cs rename to src/SLCore/Listener/Credentials/ICredentialsListener.cs index 8233a4cba..883e6fe5f 100644 --- a/src/SLCore/Listener/Credentials/CredentialsListener.cs +++ b/src/SLCore/Listener/Credentials/ICredentialsListener.cs @@ -18,23 +18,13 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.ComponentModel.Composition; using System.Threading.Tasks; using SonarLint.VisualStudio.SLCore.Core; namespace SonarLint.VisualStudio.SLCore.Listener.Credentials { - /// - /// Credentials provider for SLCore - /// - [Export(typeof(ISLCoreListener))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal class CredentialsListener : ISLCoreListener + internal interface ICredentialsListener : ISLCoreListener { - public Task GetCredentialsAsync(GetCredentialsParams parameters) - { - // stub implementation - return Task.FromResult(GetCredentialsResponse.NoCredentials); - } + Task GetCredentialsAsync(GetCredentialsParams parameters); } } From 0faac527f064d2999139b0812f566defaec79fbe Mon Sep 17 00:00:00 2001 From: Georgii Borovinskikh Date: Wed, 7 Feb 2024 15:20:40 +0100 Subject: [PATCH 2/4] fix filename --- .../{ICredentialsListener.cs => CredentialsListener.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/SLCore.Listeners/Implementation/{ICredentialsListener.cs => CredentialsListener.cs} (100%) diff --git a/src/SLCore.Listeners/Implementation/ICredentialsListener.cs b/src/SLCore.Listeners/Implementation/CredentialsListener.cs similarity index 100% rename from src/SLCore.Listeners/Implementation/ICredentialsListener.cs rename to src/SLCore.Listeners/Implementation/CredentialsListener.cs From fc7ecf71d4f546cb510ef9b5848a977d77ab0ddf Mon Sep 17 00:00:00 2001 From: Georgii Borovinskikh Date: Wed, 7 Feb 2024 15:35:07 +0100 Subject: [PATCH 3/4] fix license --- src/SLCore.Listeners/Implementation/CredentialsListener.cs | 2 +- src/SLCore/Common/ConnectionIdPrefix.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SLCore.Listeners/Implementation/CredentialsListener.cs b/src/SLCore.Listeners/Implementation/CredentialsListener.cs index e48a839e3..3773d1b14 100644 --- a/src/SLCore.Listeners/Implementation/CredentialsListener.cs +++ b/src/SLCore.Listeners/Implementation/CredentialsListener.cs @@ -1,6 +1,6 @@ /* * SonarLint for Visual Studio - * Copyright (C) 2016-2023 SonarSource SA + * Copyright (C) 2016-2024 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or diff --git a/src/SLCore/Common/ConnectionIdPrefix.cs b/src/SLCore/Common/ConnectionIdPrefix.cs index 120e1daac..840378d6e 100644 --- a/src/SLCore/Common/ConnectionIdPrefix.cs +++ b/src/SLCore/Common/ConnectionIdPrefix.cs @@ -1,6 +1,6 @@ /* * SonarLint for Visual Studio - * Copyright (C) 2016-2023 SonarSource SA + * Copyright (C) 2016-2024 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or From f7c3c9a4f5e297b41530ffa0869d547ba5e64b4a Mon Sep 17 00:00:00 2001 From: Georgii Borovinskikh Date: Thu, 8 Feb 2024 10:58:03 +0100 Subject: [PATCH 4/4] Separate ConnectionIdHelper from CredentialsListener --- .../CredentialsListenerTests.cs | 87 +++++++------------ .../Implementation/CredentialsListener.cs | 40 +++------ .../Common/Helpers/ConnectionIdHelperTests.cs | 49 +++++++++++ src/SLCore/Common/ConnectionIdPrefix.cs | 28 ------ .../Common/Helpers/ConnectionIdHelper.cs | 60 +++++++++++++ 5 files changed, 151 insertions(+), 113 deletions(-) create mode 100644 src/SLCore.UnitTests/Common/Helpers/ConnectionIdHelperTests.cs delete mode 100644 src/SLCore/Common/ConnectionIdPrefix.cs create mode 100644 src/SLCore/Common/Helpers/ConnectionIdHelper.cs diff --git a/src/SLCore.Listeners.UnitTests/CredentialsListenerTests.cs b/src/SLCore.Listeners.UnitTests/CredentialsListenerTests.cs index fc468f7af..92dd566ef 100644 --- a/src/SLCore.Listeners.UnitTests/CredentialsListenerTests.cs +++ b/src/SLCore.Listeners.UnitTests/CredentialsListenerTests.cs @@ -25,7 +25,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using SonarLint.VisualStudio.ConnectedMode.Binding; -using SonarLint.VisualStudio.SLCore.Common; +using SonarLint.VisualStudio.SLCore.Common.Helpers; using SonarLint.VisualStudio.SLCore.Common.Models; using SonarLint.VisualStudio.SLCore.Core; using SonarLint.VisualStudio.SLCore.Listener.Credentials; @@ -37,10 +37,8 @@ namespace SonarLint.VisualStudio.SLCore.Listeners.UnitTests; [TestClass] public class CredentialsListenerTests { - private static readonly Uri SonarQubeUri = new("https://next.sonarqube.com"); - private static readonly Uri SonarCloudUri = new("https://sonarcloud.io"); - private static readonly string SonarQubeConnectionId = ConnectionIdPrefix.SonarQubePrefix + SonarQubeUri; - private const string SonarCloudConnectionId = ConnectionIdPrefix.SonarCloudPrefix + "myorganization"; + private const string ConnectionId = "connectionId123"; + private static readonly Uri Uri = new("http://myfavouriteuri.nonexistingdomain"); [TestMethod] public void MefCtor_CheckIsExported() @@ -58,8 +56,9 @@ public void MefCtor_CheckIsSingleton() [TestMethod] public async Task GetCredentialsAsync_NullConnectionId_ReturnsNoCredentials() { - var testSubject = CreateTestSubject(out _); - + var testSubject = CreateTestSubject(out _, out var connectionIdHelperMock); + connectionIdHelperMock.Setup(x => x.GetUriFromConnectionId(null)).Returns((Uri)null); + var response = await testSubject.GetCredentialsAsync(new GetCredentialsParams(null)); response.Should().BeSameAs(GetCredentialsResponse.NoCredentials); @@ -68,7 +67,8 @@ public async Task GetCredentialsAsync_NullConnectionId_ReturnsNoCredentials() [TestMethod] public async Task GetCredentialsAsync_NullParams_ReturnsNoCredentials() { - var testSubject = CreateTestSubject(out _); + var testSubject = CreateTestSubject(out _, out var connectionIdHelperMock); + connectionIdHelperMock.Setup(x => x.GetUriFromConnectionId(null)).Returns((Uri)null); var response = await testSubject.GetCredentialsAsync(null); @@ -76,37 +76,29 @@ public async Task GetCredentialsAsync_NullParams_ReturnsNoCredentials() } [TestMethod] - public async Task GetCredentialsAsync_SonarQubeCredentialsNotFound_ReturnsNoCredentials() + public async Task GetCredentialsAsync_CredentialsNotFound_ReturnsNoCredentials() { - var testSubject = CreateTestSubject(out var credentialStoreMock); - credentialStoreMock.Setup(x => x.ReadCredentials(It.Is((TargetUri uri) => UriEquals(uri, SonarQubeUri)))).Returns((Credential)null); + var testSubject = CreateTestSubject(out var credentialStoreMock, out var connectionIdHelperMock); + SetUpConnectionIdHelper(connectionIdHelperMock); + credentialStoreMock.Setup(x => x.ReadCredentials(It.Is((TargetUri targetUri) => UriEquals(targetUri, Uri)))).Returns((Credential)null); - var response = await testSubject.GetCredentialsAsync(new GetCredentialsParams(SonarQubeConnectionId)); + var response = await testSubject.GetCredentialsAsync(new GetCredentialsParams(ConnectionId)); response.Should().BeSameAs(GetCredentialsResponse.NoCredentials); } - - [TestMethod] - public async Task GetCredentialsAsync_SonarCloudCredentialsNotFound_ReturnsNoCredentials() - { - var testSubject = CreateTestSubject(out var credentialStoreMock); - credentialStoreMock.Setup(x => x.ReadCredentials(It.Is((TargetUri uri) => UriEquals(uri, SonarCloudUri)))).Returns((Credential)null); - var response = await testSubject.GetCredentialsAsync(new GetCredentialsParams(SonarQubeConnectionId)); - response.Should().BeSameAs(GetCredentialsResponse.NoCredentials); - } - [TestMethod] public async Task GetCredentialsAsync_SonarQubeUsernameAndPasswordFound_ReturnsUsernameAndPassword() { const string username = "user1"; const string password = "password123"; - var testSubject = CreateTestSubject(out var credentialStoreMock); - credentialStoreMock.Setup(x => x.ReadCredentials(It.Is((TargetUri uri) => UriEquals(uri, SonarQubeUri)))).Returns(new Credential(username, password)); + var testSubject = CreateTestSubject(out var credentialStoreMock, out var connectionIdHelperMock); + SetUpConnectionIdHelper(connectionIdHelperMock); + credentialStoreMock.Setup(x => x.ReadCredentials(It.Is((TargetUri targetUri) => UriEquals(targetUri, Uri)))).Returns(new Credential(username, password)); - var response = await testSubject.GetCredentialsAsync(new GetCredentialsParams(SonarQubeConnectionId)); + var response = await testSubject.GetCredentialsAsync(new GetCredentialsParams(ConnectionId)); response.Should().BeEquivalentTo(new GetCredentialsResponse(new UsernamePasswordDto(username, password))); } @@ -116,47 +108,30 @@ public async Task GetCredentialsAsync_SonarQubeTokenFound_ReturnsToken() { const string token = "token123"; - var testSubject = CreateTestSubject(out var credentialStoreMock); - credentialStoreMock.Setup(x => x.ReadCredentials(It.Is((TargetUri uri) => UriEquals(uri, SonarQubeUri)))).Returns(new Credential(token)); + var testSubject = CreateTestSubject(out var credentialStoreMock, out var connectionIdHelperMock); + SetUpConnectionIdHelper(connectionIdHelperMock); + credentialStoreMock.Setup(x => x.ReadCredentials(It.Is((TargetUri targetUri) => UriEquals(targetUri, Uri)))).Returns(new Credential(token)); - var response = await testSubject.GetCredentialsAsync(new GetCredentialsParams(SonarQubeConnectionId)); + var response = await testSubject.GetCredentialsAsync(new GetCredentialsParams(ConnectionId)); response.Should().BeEquivalentTo(new GetCredentialsResponse(new TokenDto(token))); } - - [TestMethod] - public async Task GetCredentialsAsync_SonarCloudUsernameAndPasswordFound_ReturnsUsernameAndPassword() + + private CredentialsListener CreateTestSubject(out Mock credentialStoreMock, out Mock connectionIdHelperMock) { - const string username = "user1"; - const string password = "password123"; + credentialStoreMock = new Mock(); + connectionIdHelperMock = new Mock(); - var testSubject = CreateTestSubject(out var credentialStoreMock); - credentialStoreMock.Setup(x => x.ReadCredentials(It.Is((TargetUri uri) => UriEquals(uri, SonarCloudUri)))).Returns(new Credential(username, password)); - - var response = await testSubject.GetCredentialsAsync(new GetCredentialsParams(SonarCloudConnectionId)); - - response.Should().BeEquivalentTo(new GetCredentialsResponse(new UsernamePasswordDto(username, password))); + return new CredentialsListener(credentialStoreMock.Object, connectionIdHelperMock.Object); } - [TestMethod] - public async Task GetCredentialsAsync_SonarCloudTokenFound_ReturnsToken() + private static void SetUpConnectionIdHelper(Mock connectionIdHelperMock) { - const string token = "token123"; - - var testSubject = CreateTestSubject(out var credentialStoreMock); - credentialStoreMock.Setup(x => x.ReadCredentials(It.Is((TargetUri uri) => UriEquals(uri, SonarCloudUri)))).Returns(new Credential(token)); - - var response = await testSubject.GetCredentialsAsync(new GetCredentialsParams(SonarCloudConnectionId)); - - response.Should().BeEquivalentTo(new GetCredentialsResponse(new TokenDto(token))); + connectionIdHelperMock.Setup(x => x.GetUriFromConnectionId(ConnectionId)).Returns(Uri); } - - private CredentialsListener CreateTestSubject(out Mock credentialStoreMock) + + private static bool UriEquals(TargetUri uri, Uri serverUri) { - credentialStoreMock = new Mock(); - - return new CredentialsListener(credentialStoreMock.Object); + return serverUri.Equals((Uri)uri); } - - private bool UriEquals(TargetUri uri, Uri serverUri) => serverUri.Equals((Uri)uri); } diff --git a/src/SLCore.Listeners/Implementation/CredentialsListener.cs b/src/SLCore.Listeners/Implementation/CredentialsListener.cs index 3773d1b14..e5bc93f71 100644 --- a/src/SLCore.Listeners/Implementation/CredentialsListener.cs +++ b/src/SLCore.Listeners/Implementation/CredentialsListener.cs @@ -18,11 +18,10 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; using System.ComponentModel.Composition; using System.Threading.Tasks; using SonarLint.VisualStudio.ConnectedMode.Binding; -using SonarLint.VisualStudio.SLCore.Common; +using SonarLint.VisualStudio.SLCore.Common.Helpers; using SonarLint.VisualStudio.SLCore.Common.Models; using SonarLint.VisualStudio.SLCore.Core; using SonarLint.VisualStudio.SLCore.Listener.Credentials; @@ -36,24 +35,30 @@ namespace SonarLint.VisualStudio.SLCore.Listeners.Implementation [PartCreationPolicy(CreationPolicy.Shared)] internal class CredentialsListener : ICredentialsListener { - private static readonly Uri SonarCloudUri = new Uri("https://sonarcloud.io"); private readonly ICredentialStoreService credentialStore; + private readonly IConnectionIdHelper connectionIdHelper; [ImportingConstructor] - public CredentialsListener(ICredentialStoreService credentialStore) + public CredentialsListener(ICredentialStoreService credentialStore) + : this(credentialStore, new ConnectionIdHelper()) + { + } + + public CredentialsListener(ICredentialStoreService credentialStore, IConnectionIdHelper connectionIdHelper) { this.credentialStore = credentialStore; + this.connectionIdHelper = connectionIdHelper; } public Task GetCredentialsAsync(GetCredentialsParams parameters) { - var serverUri = GetServerUriFromConnectionId(parameters?.connectionId); + var serverUri = connectionIdHelper.GetUriFromConnectionId(parameters?.connectionId); if (serverUri == null) { return Task.FromResult(GetCredentialsResponse.NoCredentials); } - + var credentials = credentialStore.ReadCredentials(serverUri); if (credentials == null) @@ -65,28 +70,5 @@ public Task GetCredentialsAsync(GetCredentialsParams par ? new GetCredentialsResponse(new TokenDto(credentials.Username)) : new GetCredentialsResponse(new UsernamePasswordDto(credentials.Username, credentials.Password))); } - - private static Uri GetServerUriFromConnectionId(string connectionId) - { - if (connectionId == null) - { - return null; - } - - if (connectionId.StartsWith(ConnectionIdPrefix.SonarCloudPrefix)) - { - return SonarCloudUri; - } - - if (connectionId.StartsWith(ConnectionIdPrefix.SonarQubePrefix)) - { - return Uri.TryCreate(connectionId.Substring(ConnectionIdPrefix.SonarQubePrefix.Length), UriKind.Absolute, - out var uri) - ? uri - : null; - } - - return null; - } } } diff --git a/src/SLCore.UnitTests/Common/Helpers/ConnectionIdHelperTests.cs b/src/SLCore.UnitTests/Common/Helpers/ConnectionIdHelperTests.cs new file mode 100644 index 000000000..9898e2c1b --- /dev/null +++ b/src/SLCore.UnitTests/Common/Helpers/ConnectionIdHelperTests.cs @@ -0,0 +1,49 @@ +/* + * 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.SLCore.Common.Helpers; + +namespace SonarLint.VisualStudio.SLCore.UnitTests.Common.Helpers; + +[TestClass] +public class ConnectionIdHelperTests +{ + [DataTestMethod] + [DataRow(null, null)] + [DataRow("", null)] + [DataRow("http://someuri.abcdef", null)] + [DataRow("sq", null)] + [DataRow("sq|", null)] + [DataRow("sq|http://someuri.abcdef", "http://someuri.abcdef")] + [DataRow("sq|https://someuri.abcdef", "https://someuri.abcdef")] + [DataRow("sq|https://someuri.abcdef/123", "https://someuri.abcdef/123")] + [DataRow("sc", null)] + [DataRow("sc|", null)] + [DataRow("sc|myorganization", "https://sonarcloud.io")] + [DataRow("sc|https://someuri.abcdef/123", "https://sonarcloud.io")] // should not happen, but we ignore any value after "sc|" + public void ReturnsAsExpected(string connectionId, string expectedUri) + { + var uri = new ConnectionIdHelper().GetUriFromConnectionId(connectionId); + + var stringUri = uri?.ToString().Trim('/'); //trim is because uri.ToString sometimes adds / at the end. This won't be a problem in real code since we use Uri-s and not strings, but for simplicity this tests asserts string equality + + stringUri.Should().BeEquivalentTo(expectedUri); + } +} diff --git a/src/SLCore/Common/ConnectionIdPrefix.cs b/src/SLCore/Common/ConnectionIdPrefix.cs deleted file mode 100644 index 840378d6e..000000000 --- a/src/SLCore/Common/ConnectionIdPrefix.cs +++ /dev/null @@ -1,28 +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. - */ - -namespace SonarLint.VisualStudio.SLCore.Common -{ - internal static class ConnectionIdPrefix - { - public const string SonarCloudPrefix = "sc|"; - public const string SonarQubePrefix = "sq|"; - } -} diff --git a/src/SLCore/Common/Helpers/ConnectionIdHelper.cs b/src/SLCore/Common/Helpers/ConnectionIdHelper.cs new file mode 100644 index 000000000..b27c9540d --- /dev/null +++ b/src/SLCore/Common/Helpers/ConnectionIdHelper.cs @@ -0,0 +1,60 @@ +/* + * 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; + +namespace SonarLint.VisualStudio.SLCore.Common.Helpers +{ + internal interface IConnectionIdHelper + { + Uri GetUriFromConnectionId(string connectionId); + } + + internal class ConnectionIdHelper : IConnectionIdHelper + { + private const string SonarCloudPrefix = "sc|"; + private const string SonarQubePrefix = "sq|"; + private static readonly Uri SonarCloudUri = new Uri("https://sonarcloud.io"); + + public Uri GetUriFromConnectionId(string connectionId) + { + if (connectionId == null) + { + return null; + } + + if (connectionId.StartsWith(SonarCloudPrefix)) + { + var uriString = connectionId.Substring(SonarCloudPrefix.Length); + + return !string.IsNullOrWhiteSpace(uriString) ? SonarCloudUri : null; + } + + if (connectionId.StartsWith(SonarQubePrefix)) + { + return Uri.TryCreate(connectionId.Substring(SonarQubePrefix.Length), UriKind.Absolute, out var uri) + ? uri + : null; + } + + return null; + } + } +}