From e98a72a316a10ec8bb069e74c09cdc109948844f Mon Sep 17 00:00:00 2001 From: Gabriela Trutan Date: Mon, 2 Sep 2024 10:17:39 +0200 Subject: [PATCH 1/5] SLVS-1431 Add json model for the ServerConnection and a mapper that maps the model from the logic model to the json model and vice-versa --- .../ServerConnectionModelMapperTest.cs | 239 ++++++++++++++++++ .../Persistence/ServerConnectionJsonModel.cs | 46 ++++ .../ServerConnectionModelMapper.cs | 123 +++++++++ src/Core/Binding/ServerConnection.cs | 2 +- 4 files changed, 409 insertions(+), 1 deletion(-) create mode 100644 src/ConnectedMode.UnitTests/Persistence/ServerConnectionModelMapperTest.cs create mode 100644 src/ConnectedMode/Persistence/ServerConnectionJsonModel.cs create mode 100644 src/ConnectedMode/Persistence/ServerConnectionModelMapper.cs diff --git a/src/ConnectedMode.UnitTests/Persistence/ServerConnectionModelMapperTest.cs b/src/ConnectedMode.UnitTests/Persistence/ServerConnectionModelMapperTest.cs new file mode 100644 index 0000000000..f578b15413 --- /dev/null +++ b/src/ConnectedMode.UnitTests/Persistence/ServerConnectionModelMapperTest.cs @@ -0,0 +1,239 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.ConnectedMode.Persistence; +using SonarLint.VisualStudio.Core.Binding; +using SonarLint.VisualStudio.TestInfrastructure; +using static SonarLint.VisualStudio.ConnectedMode.Persistence.ServerConnectionModelMapper; + +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Persistence; + +[TestClass] +public class ServerConnectionModelMapperTest +{ + private ServerConnectionModelMapper testSubject; + + [TestInitialize] + public void TestInitialize() + { + testSubject = new ServerConnectionModelMapper(); + } + + [TestMethod] + public void MefCtor_CheckExports() + { + MefTestHelpers.CheckTypeCanBeImported(); + } + + [TestMethod] + public void Mef_CheckIsSingleton() + { + MefTestHelpers.CheckIsSingletonMefComponent(); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void GetServerConnection_SonarCloud_ReturnsSonarCloudConnection(bool isSmartNotificationsEnabled) + { + var connectionsModel = GetSonarCloudJsonModel("myOrg", isSmartNotificationsEnabled); + + var serverConnection = testSubject.GetServerConnection(connectionsModel); + + IsExpectedSonarCloudConnection(serverConnection, connectionsModel); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void GetServerConnection_SonarQube_ReturnsSonarQubeConnection(bool isSmartNotificationsEnabled) + { + var connectionsModel = GetSonarQubeJsonModel("http://localhost:9000", isSmartNotificationsEnabled); + + var serverConnection = testSubject.GetServerConnection(connectionsModel); + + IsExpectedSonarQubeConnection(serverConnection, connectionsModel); + } + + [TestMethod] + public void GetServerConnection_BothOrganizationKeyAndServerUriAreSet_ThrowsException() + { + var connectionsModel = GetSonarCloudJsonModel("myOrg"); + connectionsModel.ServerUri = "http://localhost:9000"; + + Action act = () => testSubject.GetServerConnection(connectionsModel); + + act.Should().Throw($"Invalid {nameof(ServerConnectionJsonModel)}. {nameof(ServerConnection)} could not be created"); + } + + [TestMethod] + public void GetServerConnection_BothOrganizationKeyAndServerUriAreNull_ThrowsException() + { + var connectionsModel = GetSonarCloudJsonModel("myOrg"); + connectionsModel.OrganizationKey = null; + + Action act = () => testSubject.GetServerConnection(connectionsModel); + + act.Should().Throw($"Invalid {nameof(ServerConnectionJsonModel)}. {nameof(ServerConnection)} could not be created"); + } + + [TestMethod] + [DataRow("org", null, true)] + [DataRow(null, "http://localhost", false)] + [DataRow(null, null, false)] + [DataRow("org", "http://localhost", false)] + public void IsServerConnectionForSonarCloud_OrganizationKeySetAndServerUriNotSet_ReturnsTrue(string organizationKey, string serverUi, bool expectedResult) + { + var connectionsModel = GetSonarCloudJsonModel(organizationKey); + connectionsModel.ServerUri = serverUi; + + var isSonarCloud = IsServerConnectionForSonarCloud(connectionsModel); + + isSonarCloud.Should().Be(expectedResult); + } + + [TestMethod] + [DataRow("org", null, false)] + [DataRow(null, "http://localhost", true)] + [DataRow(null, null, false)] + [DataRow("org", "http://localhost", false)] + public void IsServerConnectionForSonarCloud_OrganizationKeyNotSetAndServerUriSet_ReturnsTrue(string organizationKey, string serverUi, bool expectedResult) + { + var connectionsModel = GetSonarCloudJsonModel(organizationKey); + connectionsModel.ServerUri = serverUi; + + var isSonarQube = IsServerConnectionForSonarQube(connectionsModel); + + isSonarQube.Should().Be(expectedResult); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void GetServerConnectionsListJsonModel_OneSonarCloudConnection_ReturnsServerConnectionModelForSonarCloud(bool isSmartNotifications) + { + var sonarCloud = new ServerConnection.SonarCloud("myOrg", new ServerConnectionSettings(isSmartNotifications)); + + var model = testSubject.GetServerConnectionsListJsonModel([sonarCloud]); + + model.Should().NotBeNull(); + model.ServerConnections.Count.Should().Be(1); + model.ServerConnections[0].Should().BeEquivalentTo(new ServerConnectionJsonModel + { + Id = sonarCloud.Id, + OrganizationKey = sonarCloud.OrganizationKey, + Settings = sonarCloud.Settings, + ServerUri = null + }); + } + + [TestMethod] + public void GetServerConnectionsListJsonModel_OneSonarCloudConnectionWithNullSettings_ThrowsExceptions() + { + var sonarCloud = new ServerConnection.SonarCloud("myOrg") + { + Settings = null + }; + + Action act = () => testSubject.GetServerConnectionsListJsonModel([sonarCloud]); + + act.Should().Throw($"{nameof(ServerConnection.Settings)} can not be null"); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void GetServerConnectionsListJsonModel_OneSonarQubeConnection_ReturnsServerConnectionModelForSonarQube(bool isSmartNotifications) + { + var sonarQube = new ServerConnection.SonarQube(new Uri("http://localhost"), new ServerConnectionSettings(isSmartNotifications)); + + var model = testSubject.GetServerConnectionsListJsonModel([sonarQube]); + + model.Should().NotBeNull(); + model.ServerConnections.Count.Should().Be(1); + model.ServerConnections[0].Should().BeEquivalentTo(new ServerConnectionJsonModel + { + Id = sonarQube.Id, + OrganizationKey = null, + ServerUri = sonarQube.ServerUri.ToString(), + Settings = sonarQube.Settings + }); + } + + [TestMethod] + public void GetServerConnectionsListJsonModel_OneSonarQubeConnectionWithNullSettings_ThrowsExceptions() + { + var sonarQube = new ServerConnection.SonarQube(new Uri("http://localhost")) + { + Settings = null + }; + + Action act = () => testSubject.GetServerConnectionsListJsonModel([sonarQube]); + + act.Should().Throw($"{nameof(ServerConnection.Settings)} can not be null"); + } + + [TestMethod] + public void GetServerConnectionsListJsonModel_NoConnection_ReturnsServerConnectionModelWithNoConnection() + { + var model = testSubject.GetServerConnectionsListJsonModel([]); + + model.Should().NotBeNull(); + model.ServerConnections.Should().BeEmpty(); + } + + private static ServerConnectionJsonModel GetSonarCloudJsonModel(string id, bool isSmartNotificationsEnabled = false) + { + return new ServerConnectionJsonModel + { + Id = id, + OrganizationKey = id, + Settings = new ServerConnectionSettings(isSmartNotificationsEnabled) + }; + } + + private static ServerConnectionJsonModel GetSonarQubeJsonModel(string id, bool isSmartNotificationsEnabled = false) + { + return new ServerConnectionJsonModel + { + Id = id, + ServerUri = id, + Settings = new ServerConnectionSettings(isSmartNotificationsEnabled) + }; + } + + private static void IsExpectedSonarCloudConnection(ServerConnection serverConnection, ServerConnectionJsonModel connectionsModel) + { + serverConnection.Should().BeOfType(); + serverConnection.Id.Should().Be(serverConnection.Id); + serverConnection.Settings.Should().NotBeNull(); + serverConnection.Settings.IsSmartNotificationsEnabled.Should().Be(connectionsModel.Settings.IsSmartNotificationsEnabled); + ((ServerConnection.SonarCloud)serverConnection).OrganizationKey.Should().Be(connectionsModel.OrganizationKey); + } + + private static void IsExpectedSonarQubeConnection(ServerConnection serverConnection, ServerConnectionJsonModel connectionsModel) + { + serverConnection.Should().BeOfType(); + serverConnection.Id.Should().Be(serverConnection.Id); + serverConnection.Settings.Should().NotBeNull(); + serverConnection.Settings.IsSmartNotificationsEnabled.Should().Be(connectionsModel.Settings.IsSmartNotificationsEnabled); + ((ServerConnection.SonarQube)serverConnection).ServerUri.Should().Be(connectionsModel.ServerUri); + } +} diff --git a/src/ConnectedMode/Persistence/ServerConnectionJsonModel.cs b/src/ConnectedMode/Persistence/ServerConnectionJsonModel.cs new file mode 100644 index 0000000000..ef0142852e --- /dev/null +++ b/src/ConnectedMode/Persistence/ServerConnectionJsonModel.cs @@ -0,0 +1,46 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Newtonsoft.Json.Converters; +using Newtonsoft.Json; +using SonarLint.VisualStudio.Core.Binding; + +namespace SonarLint.VisualStudio.ConnectedMode.Persistence; + +public class ServerConnectionsListJsonModel +{ + [JsonProperty("serverConnections")] + public List ServerConnections { get; set; } = new(); +} + +public record ServerConnectionJsonModel +{ + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("setting")] + public ServerConnectionSettings Settings { get; set; } + + [JsonProperty("organizationKey")] + public string OrganizationKey { get; set; } + + [JsonProperty("serverUri")] + public string ServerUri { get; set; } +} diff --git a/src/ConnectedMode/Persistence/ServerConnectionModelMapper.cs b/src/ConnectedMode/Persistence/ServerConnectionModelMapper.cs new file mode 100644 index 0000000000..bf7a79d6ef --- /dev/null +++ b/src/ConnectedMode/Persistence/ServerConnectionModelMapper.cs @@ -0,0 +1,123 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using SonarLint.VisualStudio.Core.Binding; + +namespace SonarLint.VisualStudio.ConnectedMode.Persistence; + +public interface IServerConnectionModelMapper +{ + ServerConnection GetServerConnection(ServerConnectionJsonModel jsonModel); + ServerConnectionsListJsonModel GetServerConnectionsListJsonModel(IEnumerable serverConnections); +} + +[Export(typeof(IServerConnectionModelMapper))] +[PartCreationPolicy(CreationPolicy.Shared)] +public class ServerConnectionModelMapper : IServerConnectionModelMapper +{ + [ImportingConstructor] + public ServerConnectionModelMapper() { } + + public ServerConnection GetServerConnection(ServerConnectionJsonModel jsonModel) + { + if (IsServerConnectionForSonarCloud(jsonModel)) + { + return new ServerConnection.SonarCloud(jsonModel.OrganizationKey, jsonModel.Settings); + } + if (IsServerConnectionForSonarQube(jsonModel)) + { + return new ServerConnection.SonarQube(new Uri(jsonModel.ServerUri), jsonModel.Settings); + } + + throw new InvalidOperationException($"Invalid {nameof(ServerConnectionJsonModel)}. {nameof(ServerConnection)} could not be created"); + } + + public ServerConnectionsListJsonModel GetServerConnectionsListJsonModel(IEnumerable serverConnections) + { + var model = new ServerConnectionsListJsonModel + { + ServerConnections = serverConnections.Select(GetServerConnectionJsonModel).ToList() + }; + + return model; + } + + internal static bool IsServerConnectionForSonarCloud(ServerConnectionJsonModel jsonModel) + { + return IsOrganizationKeyFilled(jsonModel) && !IsServerUriFilled(jsonModel); + } + + internal static bool IsServerConnectionForSonarQube(ServerConnectionJsonModel jsonModel) + { + return IsServerUriFilled(jsonModel) && !IsOrganizationKeyFilled(jsonModel); + } + + private static ServerConnectionJsonModel GetServerConnectionJsonModel(ServerConnection serverConnection) + { + return new ServerConnectionJsonModel + { + Id = serverConnection.Id, + Settings = serverConnection.Settings ?? throw new InvalidOperationException($"{nameof(ServerConnection.Settings)} can not be null"), + OrganizationKey = GetOrganizationKey(serverConnection), + ServerUri = GetServerUri(serverConnection) + }; + } + + private static string GetOrganizationKey(ServerConnection serverConnection) + { + if (serverConnection is not ServerConnection.SonarCloud sonarCloud) + { + return null; + } + + if(string.IsNullOrWhiteSpace(sonarCloud.OrganizationKey)) + { + throw new InvalidOperationException($"{nameof(ServerConnection.SonarCloud.OrganizationKey)} can not be null"); + } + + return sonarCloud.OrganizationKey; + } + + private static string GetServerUri(ServerConnection serverConnection) + { + if (serverConnection is not ServerConnection.SonarQube sonarQube) + { + return null; + } + + if (sonarQube.ServerUri == null) + { + throw new InvalidOperationException($"{nameof(ServerConnection.SonarQube.ServerUri)} can not be null"); + } + + return sonarQube.ServerUri.ToString(); + } + + private static bool IsServerUriFilled(ServerConnectionJsonModel jsonModel) + { + return !string.IsNullOrWhiteSpace(jsonModel.ServerUri); + } + + private static bool IsOrganizationKeyFilled(ServerConnectionJsonModel jsonModel) + { + return !string.IsNullOrWhiteSpace(jsonModel.OrganizationKey); + } +} diff --git a/src/Core/Binding/ServerConnection.cs b/src/Core/Binding/ServerConnection.cs index 3d65b054ec..e21238fda3 100644 --- a/src/Core/Binding/ServerConnection.cs +++ b/src/Core/Binding/ServerConnection.cs @@ -25,7 +25,7 @@ public abstract class ServerConnection internal static readonly ServerConnectionSettings DefaultSettings = new(true); public string Id { get; } - public ServerConnectionSettings Settings { get; } + public ServerConnectionSettings Settings { get; set; } public ICredentials Credentials { get; set; } public abstract Uri ServerUri { get; } From 30d9cf18e9c10e4af2517b6f1da73767cccf1e41 Mon Sep 17 00:00:00 2001 From: Gabriela Trutan Date: Mon, 2 Sep 2024 10:27:10 +0200 Subject: [PATCH 2/5] SLVS-1432 Support removing credentials from SolutionBindingCredentialsLoader --- .../SolutionBindingCredentialsLoaderTests.cs | 43 ++++++++++++------- .../ISolutionBindingCredentialsLoader.cs | 1 + .../SolutionBindingCredentialsLoader.cs | 11 ++++- 3 files changed, 38 insertions(+), 17 deletions(-) diff --git a/src/ConnectedMode.UnitTests/Persistence/SolutionBindingCredentialsLoaderTests.cs b/src/ConnectedMode.UnitTests/Persistence/SolutionBindingCredentialsLoaderTests.cs index f7f8a9de1e..d92a4bd945 100644 --- a/src/ConnectedMode.UnitTests/Persistence/SolutionBindingCredentialsLoaderTests.cs +++ b/src/ConnectedMode.UnitTests/Persistence/SolutionBindingCredentialsLoaderTests.cs @@ -18,10 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; using Microsoft.Alm.Authentication; using SonarLint.VisualStudio.ConnectedMode.Binding; -using Moq; using SonarLint.VisualStudio.ConnectedMode.Persistence; using SonarLint.VisualStudio.Core.Binding; using SonarQube.Client.Helpers; @@ -31,16 +29,16 @@ namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Persistence [TestClass] public class SolutionBindingCredentialsLoaderTests { - private Mock store; + private ICredentialStoreService store; private Uri mockUri; private SolutionBindingCredentialsLoader testSubject; [TestInitialize] public void Setup() { - store = new Mock(); + store = Substitute.For(); mockUri = new Uri("http://sonarsource.com"); - testSubject = new SolutionBindingCredentialsLoader(store.Object); + testSubject = new SolutionBindingCredentialsLoader(store); } [TestMethod] @@ -61,7 +59,7 @@ public void Load_ServerUriIsNull_Null() [TestMethod] public void Load_NoCredentials_Null() { - store.Setup(x => x.ReadCredentials(mockUri)).Returns(null as Credential); + store.ReadCredentials(mockUri).Returns(null as Credential); var actual = testSubject.Load(mockUri); actual.Should().Be(null); @@ -72,7 +70,7 @@ public void Load_CredentialsExist_CredentialsWithSecuredString() { var credentials = new Credential("user", "password"); store - .Setup(x => x.ReadCredentials(It.Is(t => t.ActualUri == mockUri))) + .ReadCredentials(Arg.Is(t => t.ActualUri == mockUri)) .Returns(credentials); var actual = testSubject.Load(mockUri); @@ -86,7 +84,7 @@ public void Save_ServerUriIsNull_CredentialsNotSaved() testSubject.Save(credentials, null); - store.Verify(x=> x.WriteCredentials(It.IsAny(), It.IsAny()), Times.Never); + store.DidNotReceive().WriteCredentials(Arg.Any(), Arg.Any()); } [TestMethod] @@ -94,7 +92,7 @@ public void Save_CredentialsAreNull_CredentialsNotSaved() { testSubject.Save(null, mockUri); - store.Verify(x => x.WriteCredentials(It.IsAny(), It.IsAny()), Times.Never); + store.DidNotReceive().WriteCredentials(Arg.Any(), Arg.Any()); } [TestMethod] @@ -103,7 +101,7 @@ public void Save_CredentialsAreNotBasicAuth_CredentialsNotSaved() var mockCredentials = new Mock(); testSubject.Save(mockCredentials.Object, mockUri); - store.Verify(x => x.WriteCredentials(It.IsAny(), It.IsAny()), Times.Never); + store.DidNotReceive().WriteCredentials(Arg.Any(), Arg.Any()); } [TestMethod] @@ -112,11 +110,26 @@ public void Save_CredentialsAreBasicAuth_CredentialsSavedWithUnsecuredString() var credentials = new BasicAuthCredentials("user", "password".ToSecureString()); testSubject.Save(credentials, mockUri); - store.Verify(x => - x.WriteCredentials( - It.Is(t => t.ActualUri == mockUri), - It.Is(c=> c.Username == "user" && c.Password == "password")), - Times.Once); + store.Received(1) + .WriteCredentials( + Arg.Is(t => t.ActualUri == mockUri), + Arg.Is(c=> c.Username == "user" && c.Password == "password")); + } + + [TestMethod] + public void DeleteCredentials_UriNull_DoesNotCallStoreDeleteCredentials() + { + testSubject.DeleteCredentials(null); + + store.DidNotReceive().DeleteCredentials(Arg.Any()); + } + + [TestMethod] + public void DeleteCredentials_UriProvided_CallsStoreDeleteCredentials() + { + testSubject.DeleteCredentials(mockUri); + + store.Received(1).DeleteCredentials(Arg.Any()); } } } diff --git a/src/ConnectedMode/Persistence/ISolutionBindingCredentialsLoader.cs b/src/ConnectedMode/Persistence/ISolutionBindingCredentialsLoader.cs index bbbdc79d0c..d3bfb0c31b 100644 --- a/src/ConnectedMode/Persistence/ISolutionBindingCredentialsLoader.cs +++ b/src/ConnectedMode/Persistence/ISolutionBindingCredentialsLoader.cs @@ -25,6 +25,7 @@ namespace SonarLint.VisualStudio.ConnectedMode.Persistence { interface ISolutionBindingCredentialsLoader { + void DeleteCredentials(Uri boundServerUri); ICredentials Load(Uri boundServerUri); void Save(ICredentials credentials, Uri boundServerUri); } diff --git a/src/ConnectedMode/Persistence/SolutionBindingCredentialsLoader.cs b/src/ConnectedMode/Persistence/SolutionBindingCredentialsLoader.cs index 74899b32f6..6232873efd 100644 --- a/src/ConnectedMode/Persistence/SolutionBindingCredentialsLoader.cs +++ b/src/ConnectedMode/Persistence/SolutionBindingCredentialsLoader.cs @@ -18,8 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Diagnostics; using Microsoft.Alm.Authentication; using SonarLint.VisualStudio.ConnectedMode.Binding; using SonarLint.VisualStudio.Core.Binding; @@ -36,6 +34,15 @@ public SolutionBindingCredentialsLoader(ICredentialStoreService store) this.store = store ?? throw new ArgumentNullException(nameof(store)); } + public void DeleteCredentials(Uri boundServerUri) + { + if(boundServerUri == null) + { + return; + } + store.DeleteCredentials(boundServerUri); + } + public ICredentials Load(Uri boundServerUri) { if (boundServerUri == null) From ac49d08c9cdf6c17ed498a8c0be95adb5b019c4d Mon Sep 17 00:00:00 2001 From: Gabriela Trutan Date: Mon, 2 Sep 2024 10:54:58 +0200 Subject: [PATCH 3/5] SLVS-1433 Add credentials uri to the ServerConnection model --- .../Binding/ServerConnectionTests.cs | 28 +++++++++++-------- src/Core/Binding/ServerConnection.cs | 19 +++++++++---- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/Core.UnitTests/Binding/ServerConnectionTests.cs b/src/Core.UnitTests/Binding/ServerConnectionTests.cs index ae2a6848aa..9d0516fd05 100644 --- a/src/Core.UnitTests/Binding/ServerConnectionTests.cs +++ b/src/Core.UnitTests/Binding/ServerConnectionTests.cs @@ -19,6 +19,7 @@ */ using SonarLint.VisualStudio.Core.Binding; +using static SonarLint.VisualStudio.Core.Binding.ServerConnection; using ICredentials = SonarLint.VisualStudio.Core.Binding.ICredentials; namespace SonarLint.VisualStudio.Core.UnitTests.Binding; @@ -26,8 +27,8 @@ namespace SonarLint.VisualStudio.Core.UnitTests.Binding; [TestClass] public class ServerConnectionTests { - private static readonly Uri localhost = new Uri("http://localhost:5000"); - private static readonly string org = "myOrg"; + private static readonly Uri Localhost = new("http://localhost:5000"); + private const string Org = "myOrg"; [TestMethod] public void Ctor_SonarCloud_NullOrganization_Throws() @@ -40,7 +41,7 @@ public void Ctor_SonarCloud_NullOrganization_Throws() [TestMethod] public void Ctor_SonarCloud_NullSettings_SetDefault() { - var sonarCloud = new ServerConnection.SonarCloud(org, null); + var sonarCloud = new ServerConnection.SonarCloud(Org, null); sonarCloud.Settings.Should().BeSameAs(ServerConnection.DefaultSettings); } @@ -48,7 +49,7 @@ public void Ctor_SonarCloud_NullSettings_SetDefault() [TestMethod] public void Ctor_SonarCloud_NullCredentials_SetsNull() { - var sonarCloud = new ServerConnection.SonarCloud(org, credentials: null); + var sonarCloud = new ServerConnection.SonarCloud(Org, credentials: null); sonarCloud.Credentials.Should().BeNull(); } @@ -56,15 +57,17 @@ public void Ctor_SonarCloud_NullCredentials_SetsNull() [TestMethod] public void Ctor_SonarCloud_SetsProperties() { + var expectedServerUri = new Uri("https://sonarcloud.io"); var serverConnectionSettings = new ServerConnectionSettings(false); var credentials = Substitute.For(); - var sonarCloud = new ServerConnection.SonarCloud(org, serverConnectionSettings, credentials); + var sonarCloud = new ServerConnection.SonarCloud(Org, serverConnectionSettings, credentials); - sonarCloud.Id.Should().BeSameAs(org); - sonarCloud.OrganizationKey.Should().BeSameAs(org); + sonarCloud.Id.Should().BeSameAs(Org); + sonarCloud.OrganizationKey.Should().BeSameAs(Org); sonarCloud.ServerUri.Should().Be(new Uri("https://sonarcloud.io")); sonarCloud.Settings.Should().BeSameAs(serverConnectionSettings); sonarCloud.Credentials.Should().BeSameAs(credentials); + sonarCloud.CredentialsUri.Should().Be(new Uri(expectedServerUri, $"{SonarCloud.Organizations}/{sonarCloud.OrganizationKey}")); } [TestMethod] @@ -78,7 +81,7 @@ public void Ctor_SonarQube_NullUri_Throws() [TestMethod] public void Ctor_SonarQube_NullSettings_SetDefault() { - var sonarQube = new ServerConnection.SonarQube(localhost, null); + var sonarQube = new ServerConnection.SonarQube(Localhost, null); sonarQube.Settings.Should().BeSameAs(ServerConnection.DefaultSettings); } @@ -86,7 +89,7 @@ public void Ctor_SonarQube_NullSettings_SetDefault() [TestMethod] public void Ctor_SonarQube_NullCredentials_SetsNull() { - var sonarQube = new ServerConnection.SonarQube(localhost, credentials: null); + var sonarQube = new ServerConnection.SonarQube(Localhost, credentials: null); sonarQube.Credentials.Should().BeNull(); } @@ -96,11 +99,12 @@ public void Ctor_SonarQube_SetsProperties() { var serverConnectionSettings = new ServerConnectionSettings(false); var credentials = Substitute.For(); - var sonarQube = new ServerConnection.SonarQube(localhost, serverConnectionSettings, credentials); + var sonarQube = new ServerConnection.SonarQube(Localhost, serverConnectionSettings, credentials); - sonarQube.Id.Should().Be(localhost.ToString()); - sonarQube.ServerUri.Should().BeSameAs(localhost); + sonarQube.Id.Should().Be(Localhost.ToString()); + sonarQube.ServerUri.Should().BeSameAs(Localhost); sonarQube.Settings.Should().BeSameAs(serverConnectionSettings); sonarQube.Credentials.Should().BeSameAs(credentials); + sonarQube.CredentialsUri.Should().BeSameAs(sonarQube.ServerUri); } } diff --git a/src/Core/Binding/ServerConnection.cs b/src/Core/Binding/ServerConnection.cs index e21238fda3..50c425a7c4 100644 --- a/src/Core/Binding/ServerConnection.cs +++ b/src/Core/Binding/ServerConnection.cs @@ -29,6 +29,7 @@ public abstract class ServerConnection public ICredentials Credentials { get; set; } public abstract Uri ServerUri { get; } + public abstract Uri CredentialsUri { get; } private ServerConnection(string id, ServerConnectionSettings settings = null, ICredentials credentials = null) { @@ -36,18 +37,26 @@ private ServerConnection(string id, ServerConnectionSettings settings = null, IC Settings = settings ?? DefaultSettings; Credentials = credentials; } - - public sealed class SonarCloud(string organizationKey, ServerConnectionSettings settings = null, ICredentials credentials = null) - : ServerConnection(organizationKey, settings, credentials) + + public sealed class SonarCloud : ServerConnection { - public string OrganizationKey { get; } = organizationKey; + public const string Organizations = "organizations"; + public SonarCloud(string organizationKey, ServerConnectionSettings settings = null, ICredentials credentials = null) : base(organizationKey, settings, credentials) + { + OrganizationKey = organizationKey ?? throw new ArgumentNullException(nameof(organizationKey)); + CredentialsUri = new Uri(ServerUri, $"{Organizations}/{organizationKey}"); + } + + public string OrganizationKey { get; } - public override Uri ServerUri { get; } = new Uri("https://sonarcloud.io"); + public override Uri ServerUri { get; } = new("https://sonarcloud.io"); + public override Uri CredentialsUri { get; } } public sealed class SonarQube(Uri serverUri, ServerConnectionSettings settings = null, ICredentials credentials = null) : ServerConnection(serverUri?.ToString(), settings, credentials) { public override Uri ServerUri { get; } = serverUri; + public override Uri CredentialsUri { get; } = serverUri; } } From c06a6844ef87b86a9cd5396c1065661e95984619 Mon Sep 17 00:00:00 2001 From: Gabriela Trutan Date: Mon, 2 Sep 2024 11:04:27 +0200 Subject: [PATCH 4/5] SLVS-1434 Extend JsonFileHandler --- .../Persistence/JsonFileHandlerTest.cs | 24 ++++++- .../Persistence/JsonSerializerTests.cs | 20 ++++++ src/Core/Persistence/JsonFileHandler.cs | 66 +++++++++++++------ src/Core/Persistence/JsonSerializer.cs | 8 ++- 4 files changed, 96 insertions(+), 22 deletions(-) diff --git a/src/Core.UnitTests/Persistence/JsonFileHandlerTest.cs b/src/Core.UnitTests/Persistence/JsonFileHandlerTest.cs index 385a484d50..c58baf91bd 100644 --- a/src/Core.UnitTests/Persistence/JsonFileHandlerTest.cs +++ b/src/Core.UnitTests/Persistence/JsonFileHandlerTest.cs @@ -55,7 +55,7 @@ public void MefCtor_CheckExports() [TestMethod] public void Mef_CheckIsSingleton() { - MefTestHelpers.CheckIsSingletonMefComponent(); + MefTestHelpers.CheckIsNonSharedMefComponent(); } [TestMethod] @@ -127,6 +127,28 @@ public void TryReadFile_DeserializationFails_WritesLogAndReturnsFalse() succeeded.Should().BeFalse(); } + [TestMethod] + public void ReadFile_ReadingFileThrowsException_TrowsException() + { + var exceptionMsg = "IO failed"; + fileSystem.File.When(x => x.ReadAllText(FilePath)).Do(x => throw new Exception(exceptionMsg)); + + Action act = () => testSubject.ReadFile(FilePath); + + act.Should().Throw().WithMessage(exceptionMsg); + } + + [TestMethod] + public void ReadFile_DeserializationThrowsException_TrowsException() + { + var exceptionMsg = "IO failed"; + serializer.When(x => x.Deserialize(Arg.Any())).Do(x => throw new Exception(exceptionMsg)); + + Action act = () => testSubject.ReadFile(FilePath); + + act.Should().Throw().WithMessage(exceptionMsg); + } + [TestMethod] public void TryWriteToFile_FolderDoesNotExist_CreatesFolder() { diff --git a/src/Core.UnitTests/Persistence/JsonSerializerTests.cs b/src/Core.UnitTests/Persistence/JsonSerializerTests.cs index 50d87bcf55..00411e046c 100644 --- a/src/Core.UnitTests/Persistence/JsonSerializerTests.cs +++ b/src/Core.UnitTests/Persistence/JsonSerializerTests.cs @@ -96,4 +96,24 @@ public void TryDeserialize_Fails_LogsAndReturnsFalse() deserializedObj.Should().BeNull(); logger.Received(1).WriteLine(string.Format(PersistenceStrings.FailedToDeserializeObject, nameof(TestType))); } + + [TestMethod] + public void Deserialize_Succeeds_ReturnsString() + { + var serializedString = "{\"PropName\":\"abc\"}"; + + var deserializedObj = testSubject.Deserialize(serializedString); + + deserializedObj.Should().BeEquivalentTo(new TestType("abc")); + } + + [TestMethod] + public void Deserialize_Fails_ThrowsException() + { + var serializedString = "invalid"; + + Action act = () => testSubject.Deserialize(serializedString); + + act.Should().Throw(); + } } diff --git a/src/Core/Persistence/JsonFileHandler.cs b/src/Core/Persistence/JsonFileHandler.cs index 2673e49f4a..05940c399c 100644 --- a/src/Core/Persistence/JsonFileHandler.cs +++ b/src/Core/Persistence/JsonFileHandler.cs @@ -27,18 +27,41 @@ namespace SonarLint.VisualStudio.Core.Persistence; public interface IJsonFileHandler { + /// + /// Tries to read the json file and to deserialize the model. + /// + /// The type of the model that will be serialized + /// The path to the file + /// The content of the file deserialized to the provided type + /// True if the file could be read and the model could be serialized successfully. False otherwise bool TryReadFile(string filePath, out T content) where T : class; + + /// + /// Reads the json file and deserializes its content to the provided type. + /// + /// The type of the model that will be serialized + /// The path to the file + /// Returns the content of the json file deserialized to the provided type. + T ReadFile(string filePath) where T : class; + + /// + /// Tries to deserialize the model and write it to the json file. + /// If the file does not exist, it will be created. + /// + /// The type of the model that will be deserialized + /// The path to the file + /// The model that will be deserialized + /// True if the model was deserialized successfully and written to the file. False otherwise bool TryWriteToFile(string filePath, T model) where T : class; } [Export(typeof(IJsonFileHandler))] -[PartCreationPolicy(CreationPolicy.Shared)] +[PartCreationPolicy(CreationPolicy.NonShared)] public class JsonFileHandler : IJsonFileHandler { private readonly ILogger logger; private readonly IFileSystem fileSystem; private readonly IJsonSerializer jsonSerializer; - private static readonly object Locker = new(); [ImportingConstructor] public JsonFileHandler(IJsonSerializer jsonSerializer, ILogger logger) : this(new FileSystem(), jsonSerializer, logger) { } @@ -71,31 +94,34 @@ public bool TryReadFile(string filePath, out T content) where T: class } } + public T ReadFile(string filePath) where T : class + { + var jsonContent = fileSystem.File.ReadAllText(filePath); + return jsonSerializer.Deserialize(jsonContent); + } + public bool TryWriteToFile(string filePath, T model) where T : class { - lock (Locker) + try { - try + var directoryName = Path.GetDirectoryName(filePath); + if (!fileSystem.Directory.Exists(directoryName)) { - var directoryName = Path.GetDirectoryName(filePath); - if (!fileSystem.Directory.Exists(directoryName)) - { - fileSystem.Directory.CreateDirectory(directoryName); - } - - var wasContentDeserialized = jsonSerializer.TrySerialize(model, out string serializedObj, Formatting.Indented); - if (wasContentDeserialized) - { - fileSystem.File.WriteAllText(filePath, serializedObj); - return true; - } + fileSystem.Directory.CreateDirectory(directoryName); } - catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) + + var wasContentDeserialized = jsonSerializer.TrySerialize(model, out string serializedObj, Formatting.Indented); + if (wasContentDeserialized) { - logger.WriteLine(ex.Message); + fileSystem.File.WriteAllText(filePath, serializedObj); + return true; } - - return false; } + catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) + { + logger.WriteLine(ex.Message); + } + + return false; } } diff --git a/src/Core/Persistence/JsonSerializer.cs b/src/Core/Persistence/JsonSerializer.cs index a604f86036..6ec3f0be5f 100644 --- a/src/Core/Persistence/JsonSerializer.cs +++ b/src/Core/Persistence/JsonSerializer.cs @@ -27,6 +27,7 @@ namespace SonarLint.VisualStudio.Core.Persistence; public interface IJsonSerializer { bool TryDeserialize(string json, out T deserializedObj, JsonSerializerSettings serializerSettings = null) where T : class; + T Deserialize(string json, JsonSerializerSettings serializerSettings = null) where T : class; bool TrySerialize(T objectToSerialize, out string serializedObj, Formatting formatting = Formatting.None, JsonSerializerSettings serializerSettings = null) where T : class; } @@ -55,7 +56,7 @@ public bool TryDeserialize(string json, out T deserializedObj, JsonSerializer deserializedObj = null; try { - deserializedObj = JsonConvert.DeserializeObject(json, serializerSettings); + deserializedObj = Deserialize(json, serializerSettings); return true; } catch (Exception) @@ -65,6 +66,11 @@ public bool TryDeserialize(string json, out T deserializedObj, JsonSerializer } } + public T Deserialize(string json, JsonSerializerSettings serializerSettings = null) where T : class + { + return JsonConvert.DeserializeObject(json, serializerSettings); + } + public bool TrySerialize(T objectToSerialize, out string serializedObj, Formatting formatting = Formatting.None, JsonSerializerSettings serializerSettings = null) where T: class { serializedObj = null; From a633a18d478f777f518dfd2aab30351cde9f786c Mon Sep 17 00:00:00 2001 From: Gabriela Trutan Date: Mon, 2 Sep 2024 11:40:08 +0200 Subject: [PATCH 5/5] SLVS-1434 Remove method used only in tests --- .../Persistence/JsonFileHandlerTest.cs | 69 ------------------- src/Core/Persistence/JsonFileHandler.cs | 30 -------- 2 files changed, 99 deletions(-) diff --git a/src/Core.UnitTests/Persistence/JsonFileHandlerTest.cs b/src/Core.UnitTests/Persistence/JsonFileHandlerTest.cs index c58baf91bd..93a99ffdd1 100644 --- a/src/Core.UnitTests/Persistence/JsonFileHandlerTest.cs +++ b/src/Core.UnitTests/Persistence/JsonFileHandlerTest.cs @@ -58,75 +58,6 @@ public void Mef_CheckIsSingleton() MefTestHelpers.CheckIsNonSharedMefComponent(); } - [TestMethod] - public void TryReadFile_FileDoesNotExist_ReturnsFalse() - { - fileSystem.File.Exists(FilePath).Returns(false); - - var succeeded = testSubject.TryReadFile(FilePath, out TestType deserializedContent); - - succeeded.Should().BeFalse(); - deserializedContent.Should().BeNull(); - fileSystem.File.Received(1).Exists(FilePath); - } - - [TestMethod] - public void TryReadFile_FileExists_ReturnsTrueAndDeserializeContent() - { - var expectedContent = new TestType("test"); - var serializedContent = JsonConvert.SerializeObject(expectedContent); - fileSystem.File.Exists(FilePath).Returns(true); - fileSystem.File.ReadAllText(FilePath).Returns(serializedContent); - serializer.TryDeserialize(Arg.Any(), out Arg.Any()).Returns(true); - - var succeeded = testSubject.TryReadFile(FilePath, out TestType _); - - succeeded.Should().BeTrue(); - Received.InOrder(() => - { - fileSystem.File.Exists(FilePath); - fileSystem.File.ReadAllText(FilePath); - serializer.TryDeserialize(Arg.Any(), out Arg.Any()); - }); - } - - [TestMethod] - public void TryReadFile_ReadingFileThrowsException_WritesLogAndReturnsFalse() - { - var exceptionMsg = "IO failed"; - fileSystem.File.Exists(FilePath).Returns(true); - fileSystem.File.When(x => x.ReadAllText(FilePath)).Do(x => throw new Exception(exceptionMsg)); - - var succeeded = testSubject.TryReadFile(FilePath, out TestType _); - - succeeded.Should().BeFalse(); - logger.Received(1).WriteLine(exceptionMsg); - } - - [TestMethod] - public void TryReadFile_DeserializationThrowsException_WritesLogAndReturnsFalse() - { - var exceptionMsg = "deserialization failed"; - fileSystem.File.Exists(FilePath).Returns(true); - serializer.When(x => x.TryDeserialize(Arg.Any(), out Arg.Any())).Do(x => throw new Exception(exceptionMsg)); - - var succeeded = testSubject.TryReadFile(FilePath, out TestType _); - - succeeded.Should().BeFalse(); - logger.Received(1).WriteLine(exceptionMsg); - } - - [TestMethod] - public void TryReadFile_DeserializationFails_WritesLogAndReturnsFalse() - { - fileSystem.File.Exists(FilePath).Returns(true); - serializer.TryDeserialize(Arg.Any(), out Arg.Any()).Returns(false); - - var succeeded = testSubject.TryReadFile(FilePath, out TestType _); - - succeeded.Should().BeFalse(); - } - [TestMethod] public void ReadFile_ReadingFileThrowsException_TrowsException() { diff --git a/src/Core/Persistence/JsonFileHandler.cs b/src/Core/Persistence/JsonFileHandler.cs index 05940c399c..7444cbd358 100644 --- a/src/Core/Persistence/JsonFileHandler.cs +++ b/src/Core/Persistence/JsonFileHandler.cs @@ -27,15 +27,6 @@ namespace SonarLint.VisualStudio.Core.Persistence; public interface IJsonFileHandler { - /// - /// Tries to read the json file and to deserialize the model. - /// - /// The type of the model that will be serialized - /// The path to the file - /// The content of the file deserialized to the provided type - /// True if the file could be read and the model could be serialized successfully. False otherwise - bool TryReadFile(string filePath, out T content) where T : class; - /// /// Reads the json file and deserializes its content to the provided type. /// @@ -73,27 +64,6 @@ public JsonFileHandler(IJsonSerializer jsonSerializer, ILogger logger) : this(ne this.logger = logger; } - public bool TryReadFile(string filePath, out T content) where T: class - { - content = null; - if (!fileSystem.File.Exists(filePath)) - { - return false; - } - - try - { - var jsonContent = fileSystem.File.ReadAllText(filePath); - return jsonSerializer.TryDeserialize(jsonContent, out content); - - } - catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) - { - logger.WriteLine(ex.Message); - return false; - } - } - public T ReadFile(string filePath) where T : class { var jsonContent = fileSystem.File.ReadAllText(filePath);