diff --git a/src/ConnectedMode.UnitTests/Persistence/ServerConnectionsRepositoryTests.cs b/src/ConnectedMode.UnitTests/Persistence/ServerConnectionsRepositoryTests.cs new file mode 100644 index 0000000000..7f838cda6a --- /dev/null +++ b/src/ConnectedMode.UnitTests/Persistence/ServerConnectionsRepositoryTests.cs @@ -0,0 +1,598 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.IO; +using SonarLint.VisualStudio.ConnectedMode.Binding; +using SonarLint.VisualStudio.ConnectedMode.Persistence; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Binding; +using SonarLint.VisualStudio.Core.Persistence; +using SonarLint.VisualStudio.TestInfrastructure; +using static SonarLint.VisualStudio.Core.Binding.ServerConnection; + +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Persistence; + +[TestClass] +public class ServerConnectionsRepositoryTests +{ + private ServerConnectionsRepository testSubject; + private IJsonFileHandler jsonFileHandler; + private ILogger logger; + private IEnvironmentVariableProvider environmentVariableProvider; + private IServerConnectionModelMapper serverConnectionModelMapper; + private ISolutionBindingCredentialsLoader credentialsLoader; + private readonly SonarCloud sonarCloudServerConnection = new("myOrganization", new ServerConnectionSettings(true), Substitute.For()); + private readonly ServerConnection.SonarQube sonarQubeServerConnection = new(new Uri("http://localhost"), new ServerConnectionSettings(true), Substitute.For()); + + [TestInitialize] + public void TestInitialize() + { + jsonFileHandler = Substitute.For(); + serverConnectionModelMapper = Substitute.For(); + credentialsLoader = Substitute.For(); + environmentVariableProvider = Substitute.For(); + logger = Substitute.For(); + + testSubject = new ServerConnectionsRepository(jsonFileHandler, serverConnectionModelMapper, credentialsLoader, environmentVariableProvider, logger); + } + + [TestMethod] + public void MefCtor_CheckExports() + { + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + } + + [TestMethod] + public void Mef_CheckIsSingleton() + { + MefTestHelpers.CheckIsSingletonMefComponent(); + } + + [TestMethod] + public void TryGet_FileDoesNotExist_ReturnsFalse() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.When(x => x.ReadFile(Arg.Any())).Do(x => throw new FileNotFoundException()); + + var succeeded = testSubject.TryGet("myId", out ServerConnection serverConnection); + + succeeded.Should().BeFalse(); + serverConnection.Should().BeNull(); + jsonFileHandler.Received(1).ReadFile(Arg.Any()); + } + + [TestMethod] + public void TryGet_FileCanNotBeRead_ReturnsFalse() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.When(x => x.ReadFile(Arg.Any())).Do(x => throw new Exception()); + + var succeeded = testSubject.TryGet("myId", out ServerConnection serverConnection); + + succeeded.Should().BeFalse(); + serverConnection.Should().BeNull(); + jsonFileHandler.Received(1).ReadFile(Arg.Any()); + } + + [TestMethod] + public void TryGet_FileExistsAndConnectionDoesNotExist_ReturnsFalse() + { + MockFileWithOneSonarCloudConnection(); + + var succeeded = testSubject.TryGet("non-existing connectionId", out ServerConnection serverConnection); + + succeeded.Should().BeFalse(); + serverConnection.Should().BeNull(); + } + + [TestMethod] + public void TryGet_FileExistsAndConnectionIsSonarCloud_ReturnsSonarCloudConnection() + { + var sonarCloudModel = GetSonarCloudJsonModel("myOrg"); + var expectedConnection = new SonarCloud(sonarCloudModel.Id); + MockReadingFile(new ServerConnectionsListJsonModel { ServerConnections = [sonarCloudModel] }); + serverConnectionModelMapper.GetServerConnection(sonarCloudModel).Returns(expectedConnection); + + var succeeded = testSubject.TryGet(sonarCloudModel.Id, out ServerConnection serverConnection); + + succeeded.Should().BeTrue(); + serverConnection.Should().Be(expectedConnection); + } + + [TestMethod] + public void TryGet_FileExistsAndConnectionIsSonarCloud_FillsCredentials() + { + var expectedConnection = MockFileWithOneSonarCloudConnection(); + var credentials = Substitute.For(); + credentialsLoader.Load(expectedConnection.CredentialsUri).Returns(credentials); + + var succeeded = testSubject.TryGet(expectedConnection.Id, out ServerConnection serverConnection); + + succeeded.Should().BeTrue(); + serverConnection.Should().Be(expectedConnection); + serverConnection.Credentials.Should().Be(credentials); + credentialsLoader.Received(1).Load(expectedConnection.CredentialsUri); + } + + [TestMethod] + public void TryGet_FileExistsAndConnectionIsSonarQube_ReturnsSonarQubeConnection() + { + var sonarQubeModel = GetSonarQubeJsonModel(new Uri("http://localhost:9000")); + var expectedConnection = new ServerConnection.SonarQube(new Uri(sonarQubeModel.ServerUri)); + MockReadingFile(new ServerConnectionsListJsonModel { ServerConnections = [sonarQubeModel] }); + serverConnectionModelMapper.GetServerConnection(sonarQubeModel).Returns(expectedConnection); + + var succeeded = testSubject.TryGet(sonarQubeModel.Id, out ServerConnection serverConnection); + + succeeded.Should().BeTrue(); + serverConnection.Should().Be(expectedConnection); + } + + [TestMethod] + public void TryGet_FileExistsAndConnectionIsSonarQube_FillsCredentials() + { + var expectedConnection = MockFileWithOneSonarQubeConnection(); + var credentials = Substitute.For(); + credentialsLoader.Load(expectedConnection.CredentialsUri).Returns(credentials); + + var succeeded = testSubject.TryGet(expectedConnection.Id, out ServerConnection serverConnection); + + succeeded.Should().BeTrue(); + serverConnection.Should().Be(expectedConnection); + serverConnection.Credentials.Should().Be(credentials); + credentialsLoader.Received(1).Load(expectedConnection.CredentialsUri); + } + + [TestMethod] + public void TryGetAll_FileDoesNotExist_ReturnsEmptyList() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.When(x => x.ReadFile(Arg.Any())).Do(x => throw new FileNotFoundException()); + + testSubject.TryGetAll(out var connections); + + connections.Should().BeEmpty(); + } + + [TestMethod] + public void TryGetAll_FileCouldNotBeRead_ThrowsException() + { + var exceptionMsg = "failed"; + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.When(x => x.ReadFile(Arg.Any())).Do(x => throw new Exception(exceptionMsg)); + + var succeeded = testSubject.TryGetAll(out var connections); + + succeeded.Should().BeFalse(); + } + + [TestMethod] + public void TryGetAll_FileExistsAndIsEmpty_ReturnsEmptyList() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + + testSubject.TryGetAll(out var connections); + + connections.Should().BeEmpty(); + } + + [TestMethod] + public void TryGetAll_FileExistsAndHasConnection_MapsModel() + { + var cloudModel = GetSonarCloudJsonModel("myOrg"); + MockReadingFile(new ServerConnectionsListJsonModel { ServerConnections = [cloudModel] }); + + testSubject.TryGetAll(out _); + + serverConnectionModelMapper.Received(1).GetServerConnection(cloudModel); + } + + [TestMethod] + public void TryGetAll_ConnectionsExist_DoesNotFillCredentials() + { + MockFileWithOneSonarCloudConnection(); + + testSubject.TryGetAll(out _); + + credentialsLoader.DidNotReceive().Load(Arg.Any()); + } + + [TestMethod] + public void TryAdd_FileCouldNotBeRead_DoesNotAddConnection() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + + var succeeded = testSubject.TryAdd(sonarCloudServerConnection); + + succeeded.Should().BeFalse(); + Received.InOrder(() => + { + jsonFileHandler.ReadFile(Arg.Any()); + serverConnectionModelMapper.GetServerConnectionsListJsonModel(Arg.Is>(x => x.Contains(sonarCloudServerConnection))); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()); + }); + } + + [TestMethod] + public void TryAdd_FileExistsAndConnectionIsNew_AddsConnection() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(true); + + var succeeded = testSubject.TryAdd(sonarCloudServerConnection); + + succeeded.Should().BeTrue(); + Received.InOrder(() => + { + jsonFileHandler.ReadFile(Arg.Any()); + serverConnectionModelMapper.GetServerConnectionsListJsonModel(Arg.Is>(x => x.Contains(sonarCloudServerConnection))); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()); + }); + } + + [TestMethod] + public void TryAdd_SonarCloudConnectionIsAddedAndCredentialsAreNotNull_SavesCredentials() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(true); + + var succeeded = testSubject.TryAdd(sonarCloudServerConnection); + + succeeded.Should().BeTrue(); + credentialsLoader.Received(1).Save(sonarCloudServerConnection.Credentials, sonarCloudServerConnection.CredentialsUri); + } + + [TestMethod] + public void TryAdd_SonarQubeConnectionIsAddedAndCredentialsAreNotNull_SavesCredentials() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(true); + + var succeeded = testSubject.TryAdd(sonarQubeServerConnection); + + succeeded.Should().BeTrue(); + credentialsLoader.Received(1).Save(sonarQubeServerConnection.Credentials, sonarQubeServerConnection.CredentialsUri); + } + + [TestMethod] + public void TryAdd_ConnectionIsAddedAndCredentialsAreNull_ReturnsFalse() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(true); + sonarCloudServerConnection.Credentials = null; + + var succeeded = testSubject.TryAdd(sonarCloudServerConnection); + + succeeded.Should().BeFalse(); + credentialsLoader.DidNotReceive().Save(Arg.Any(), Arg.Any()); + } + + [TestMethod] + public void TryAdd_ConnectionIsNotAdded_DoesNotSaveCredentials() + { + var sonarCloud = MockFileWithOneSonarCloudConnection(); + + var succeeded = testSubject.TryAdd(sonarCloud); + + succeeded.Should().BeFalse(); + credentialsLoader.DidNotReceive().Save(Arg.Any(), Arg.Any()); + } + + [TestMethod] + public void TryAdd_FileExistsAndConnectionIsDuplicate_DoesNotAddConnection() + { + var sonarCloud = MockFileWithOneSonarCloudConnection(); + + var succeeded = testSubject.TryAdd(sonarCloud); + + succeeded.Should().BeFalse(); + jsonFileHandler.DidNotReceive().TryWriteToFile(Arg.Any(), Arg.Any()); + } + + [TestMethod] + public void TryAdd_WritingToFileFails_DoesNotAddConnection() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(false); + var sonarCloud = new SonarCloud("myOrg"); + + var succeeded = testSubject.TryAdd(sonarCloud); + + succeeded.Should().BeFalse(); + } + + [TestMethod] + public void TryAdd_WritingThrowsException_DoesNotUpdateConnectionAndWritesLog() + { + var exceptionMsg = "IO exception"; + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.When(x => x.TryWriteToFile(Arg.Any(), Arg.Any())).Do(x => throw new Exception(exceptionMsg)); + + var succeeded = testSubject.TryAdd(sonarCloudServerConnection); + + succeeded.Should().BeFalse(); + logger.Received(1).WriteLine($"Failed updating the {ServerConnectionsRepository.ConnectionsFileName}: {exceptionMsg}"); + } + + [TestMethod] + public void TryDelete_FileCouldNotBeRead_ReturnsFalse() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.When(x => x.ReadFile(Arg.Any())).Do(x => throw new Exception()); + + var succeeded = testSubject.TryDelete("myOrg"); + + succeeded.Should().BeFalse(); + jsonFileHandler.DidNotReceive().TryWriteToFile(Arg.Any(), Arg.Any()); + } + + [TestMethod] + public void TryDelete_FileExistsAndHasNoConnection_ReturnsFalse() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + + var succeeded = testSubject.TryDelete("myOrg"); + + succeeded.Should().BeFalse(); + jsonFileHandler.DidNotReceive().TryWriteToFile(Arg.Any(), Arg.Any()); + } + + [TestMethod] + public void TryDelete_FileExistsAndConnectionExists_RemovesConnection() + { + var sonarCloud = MockFileWithOneSonarCloudConnection(); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(true); + + var succeeded = testSubject.TryDelete(sonarCloud.Id); + + succeeded.Should().BeTrue(); + Received.InOrder(() => + { + jsonFileHandler.ReadFile(Arg.Any()); + serverConnectionModelMapper.GetServerConnectionsListJsonModel(Arg.Is>(x => !x.Any())); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()); + credentialsLoader.DeleteCredentials(sonarCloud.CredentialsUri); + }); + } + + [TestMethod] + public void TryDelete_SonarCloudConnectionWasRemoved_RemovesCredentials() + { + var sonarCloud = MockFileWithOneSonarCloudConnection(); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(true); + + var succeeded = testSubject.TryDelete(sonarCloud.Id); + + succeeded.Should().BeTrue(); + credentialsLoader.Received(1).DeleteCredentials(sonarCloud.CredentialsUri); + } + + [TestMethod] + public void TryDelete_SonarQubeConnectionWasRemoved_RemovesCredentials() + { + var sonarQube = MockFileWithOneSonarQubeConnection(); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(true); + + var succeeded = testSubject.TryDelete(sonarQube.Id); + + succeeded.Should().BeTrue(); + credentialsLoader.Received(1).DeleteCredentials(sonarQube.CredentialsUri); + } + + [TestMethod] + public void TryDelete_ConnectionWasNotRemoved_DoesNotRemoveCredentials() + { + var sonarCloud = MockFileWithOneSonarCloudConnection(); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(false); + + var succeeded = testSubject.TryDelete(sonarCloud.Id); + + succeeded.Should().BeFalse(); + credentialsLoader.DidNotReceive().DeleteCredentials(Arg.Any()); + } + + [TestMethod] + public void TryDelete_WritingToFileFails_ReturnsFalse() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(false); + + var succeeded = testSubject.TryDelete("myOrg"); + + succeeded.Should().BeFalse(); + } + + [TestMethod] + public void TryDelete_WritingThrowsException_DoesNotUpdateConnectionAndWritesLog() + { + var exceptionMsg = "IO exception"; + var sonarCloud = MockFileWithOneSonarCloudConnection(); + jsonFileHandler.When(x => x.TryWriteToFile(Arg.Any(), Arg.Any())).Do(x => throw new Exception(exceptionMsg)); + + var succeeded = testSubject.TryDelete(sonarCloud.Id); + + succeeded.Should().BeFalse(); + logger.Received(1).WriteLine($"Failed updating the {ServerConnectionsRepository.ConnectionsFileName}: {exceptionMsg}"); + } + + [TestMethod] + public void TryUpdateSettingsById_FileCouldNotBeRead_ReturnsFalse() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.When(x => x.ReadFile(Arg.Any())).Do(x => throw new Exception()); + + var succeeded = testSubject.TryUpdateSettingsById("myOrg", new ServerConnectionSettings(true)); + + succeeded.Should().BeFalse(); + jsonFileHandler.DidNotReceive().TryWriteToFile(Arg.Any(), Arg.Any()); + } + + [TestMethod] + public void TryUpdateSettingsById_FileExistsAndHasNoConnection_ReturnsFalse() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + + var succeeded = testSubject.TryUpdateSettingsById("myOrg", new ServerConnectionSettings(true)); + + succeeded.Should().BeFalse(); + jsonFileHandler.DidNotReceive().TryWriteToFile(Arg.Any(), Arg.Any()); + } + + [TestMethod] + [DataRow(false, true)] + [DataRow(true, false)] + public void TryUpdateSettingsById_FileExistsAndConnectionExists_UpdatesSettings(bool oldSmartNotifications, bool newSmartNotifications) + { + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(true); + MockFileWithOneSonarCloudConnection(oldSmartNotifications); + + var succeeded = testSubject.TryUpdateSettingsById("myOrg", new ServerConnectionSettings(newSmartNotifications)); + + succeeded.Should().BeTrue(); + Received.InOrder(() => + { + jsonFileHandler.ReadFile(Arg.Any()); + serverConnectionModelMapper.GetServerConnectionsListJsonModel(Arg.Is>(x => x.Count() == 1 && x.Single().Settings.IsSmartNotificationsEnabled == newSmartNotifications)); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()); + }); + } + + [TestMethod] + public void TryUpdateSettingsById_WritingToFileFails_DoesNotUpdateConnection() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(false); + + var succeeded = testSubject.TryUpdateSettingsById("myOrg", new ServerConnectionSettings(true)); + + succeeded.Should().BeFalse(); + } + + [TestMethod] + public void TryUpdateSettingsById_WritingThrowsException_DoesNotUpdateConnectionAndWritesLog() + { + var exceptionMsg = "IO exception"; + var sonarCloud = MockFileWithOneSonarCloudConnection(); + jsonFileHandler.When(x => x.TryWriteToFile(Arg.Any(), Arg.Any())).Do(x => throw new Exception(exceptionMsg)); + + var succeeded = testSubject.TryUpdateSettingsById(sonarCloud.Id, new ServerConnectionSettings(true)); + + succeeded.Should().BeFalse(); + logger.Received(1).WriteLine($"Failed updating the {ServerConnectionsRepository.ConnectionsFileName}: {exceptionMsg}"); + } + + [TestMethod] + public void TryUpdateCredentialsById_ConnectionDoesNotExist_DoesNotUpdateCredentials() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + + var succeeded = testSubject.TryUpdateCredentialsById("myConn", Substitute.For()); + + succeeded.Should().BeFalse(); + credentialsLoader.DidNotReceive().Save(Arg.Any(), Arg.Any()); + } + + [TestMethod] + public void TryUpdateCredentialsById_SonarCloudConnectionExists_UpdatesCredentials() + { + var sonarCloud = MockFileWithOneSonarCloudConnection(); + var newCredentials = Substitute.For(); + + var succeeded = testSubject.TryUpdateCredentialsById(sonarCloud.Id, newCredentials); + + succeeded.Should().BeTrue(); + credentialsLoader.Received(1).Save(newCredentials, sonarCloud.CredentialsUri); + } + + [TestMethod] + public void TryUpdateCredentialsById_SonarQubeConnectionExists_UpdatesCredentials() + { + var sonarQube = MockFileWithOneSonarQubeConnection(); + var newCredentials = Substitute.For(); + + var succeeded = testSubject.TryUpdateCredentialsById(sonarQube.Id, newCredentials); + + succeeded.Should().BeTrue(); + credentialsLoader.Received(1).Save(newCredentials, sonarQube.ServerUri); + } + + [TestMethod] + public void TryUpdateCredentialsById_SavingCredentialsThrows_ReturnsFalseAndLogs() + { + var exceptionMsg = "failed"; + var connection = MockFileWithOneSonarCloudConnection(); + credentialsLoader.When(x => x.Save(Arg.Any(), Arg.Any())).Do(x => throw new Exception(exceptionMsg)); + + var succeeded = testSubject.TryUpdateCredentialsById(connection.Id, Substitute.For()); + + succeeded.Should().BeFalse(); + logger.Received(1).WriteLine($"Failed updating credentials: {exceptionMsg}"); + } + + private SonarCloud MockFileWithOneSonarCloudConnection(bool isSmartNotificationsEnabled = true) + { + var sonarCloudModel = GetSonarCloudJsonModel("myOrg", isSmartNotificationsEnabled); + var sonarCloud = new SonarCloud(sonarCloudModel.Id, sonarCloudModel.Settings, Substitute.For()); + MockReadingFile(new ServerConnectionsListJsonModel { ServerConnections = [sonarCloudModel] }); + serverConnectionModelMapper.GetServerConnection(sonarCloudModel).Returns(sonarCloud); + + return sonarCloud; + } + + private ServerConnection.SonarQube MockFileWithOneSonarQubeConnection(bool isSmartNotificationsEnabled = true) + { + var sonarQubeModel = GetSonarQubeJsonModel(new Uri("http://localhost"), isSmartNotificationsEnabled); + var sonarQube = new ServerConnection.SonarQube(new Uri(sonarQubeModel.ServerUri), sonarQubeModel.Settings, Substitute.For()); + MockReadingFile(new ServerConnectionsListJsonModel { ServerConnections = [sonarQubeModel] }); + serverConnectionModelMapper.GetServerConnection(sonarQubeModel).Returns(sonarQube); + + return sonarQube; + } + + private void MockReadingFile(ServerConnectionsListJsonModel modelToReturn) + { + jsonFileHandler.ReadFile(Arg.Any()).Returns(modelToReturn); + } + + private static ServerConnectionJsonModel GetSonarCloudJsonModel(string id, bool isSmartNotificationsEnabled = false) + { + return new ServerConnectionJsonModel + { + Id = id, + OrganizationKey = id, + Settings = new ServerConnectionSettings(isSmartNotificationsEnabled) + }; + } + + private static ServerConnectionJsonModel GetSonarQubeJsonModel(Uri id, bool isSmartNotificationsEnabled = false) + { + return new ServerConnectionJsonModel + { + Id = id.ToString(), + ServerUri = id.ToString(), + Settings = new ServerConnectionSettings(isSmartNotificationsEnabled) + }; + } + +} diff --git a/src/ConnectedMode.UnitTests/ServerConnectionsRepositoryAdapterTests.cs b/src/ConnectedMode.UnitTests/ServerConnectionsRepositoryAdapterTests.cs index 195877ca47..3aaf133966 100644 --- a/src/ConnectedMode.UnitTests/ServerConnectionsRepositoryAdapterTests.cs +++ b/src/ConnectedMode.UnitTests/ServerConnectionsRepositoryAdapterTests.cs @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; using SonarLint.VisualStudio.TestInfrastructure; using static SonarLint.VisualStudio.Core.Binding.ServerConnection; @@ -43,25 +42,25 @@ public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported(MefTestHelpers.CreateExport()); [TestMethod] - public void GetAllConnections_CallServerConnectionsRepository() + public void TryGetAllConnections_CallServerConnectionsRepository() { - serverConnectionsRepository.GetAll().Returns([]); + MockServerConnections([]); - var connections = testSubject.GetAllConnections(); + testSubject.TryGetAllConnections(out var connections); - serverConnectionsRepository.Received(1).GetAll(); + serverConnectionsRepository.Received(1).TryGetAll(out Arg.Any>()); connections.Should().BeEmpty(); } [TestMethod] [DataRow(true)] [DataRow(false)] - public void GetAllConnections_HasOneSonarCloudConnection_ReturnsOneMappedConnection(bool isSmartNotificationsEnabled) + public void TryGetAllConnections_HasOneSonarCloudConnection_ReturnsOneMappedConnection(bool isSmartNotificationsEnabled) { var sonarCloud = CreateSonarCloudServerConnection(isSmartNotificationsEnabled); - serverConnectionsRepository.GetAll().Returns([sonarCloud]); + MockServerConnections([sonarCloud]); - var connections = testSubject.GetAllConnections(); + testSubject.TryGetAllConnections(out var connections); connections.Should().BeEquivalentTo([new Connection(new ConnectionInfo(sonarCloud.Id, ConnectionServerType.SonarCloud), isSmartNotificationsEnabled)]); } @@ -69,49 +68,75 @@ public void GetAllConnections_HasOneSonarCloudConnection_ReturnsOneMappedConnect [TestMethod] [DataRow(true)] [DataRow(false)] - public void GetAllConnections_HasOneSonarQubeConnection_ReturnsOneMappedConnection(bool isSmartNotificationsEnabled) + public void TryGetAllConnections_HasOneSonarQubeConnection_ReturnsOneMappedConnection(bool isSmartNotificationsEnabled) { var sonarQube = CreateSonarQubeServerConnection(isSmartNotificationsEnabled); - serverConnectionsRepository.GetAll().Returns([sonarQube]); + MockServerConnections([sonarQube]); - var connections = testSubject.GetAllConnections(); + testSubject.TryGetAllConnections(out var connections); connections.Should().BeEquivalentTo([new Connection(new ConnectionInfo(sonarQube.Id, ConnectionServerType.SonarQube), isSmartNotificationsEnabled)]); } [TestMethod] - public void GetAllConnectionsInfo_CallServerConnectionsRepository() + [DataRow(true)] + [DataRow(false)] + public void TryGetAllConnections_ReturnsStatusFromSlCore(bool expectedStatus) { - serverConnectionsRepository.GetAll().Returns([]); + var sonarCloud = CreateSonarCloudServerConnection(); + MockServerConnections([sonarCloud], succeeded:expectedStatus); - var connections = testSubject.GetAllConnectionsInfo(); + var succeeded = testSubject.TryGetAllConnections(out _); - serverConnectionsRepository.Received(1).GetAll(); + succeeded.Should().Be(expectedStatus); + } + + [TestMethod] + public void TryGetAllConnectionsInfo_CallServerConnectionsRepository() + { + MockServerConnections([]); + + testSubject.TryGetAllConnectionsInfo(out var connections); + + serverConnectionsRepository.Received(1).TryGetAll(out Arg.Any>()); connections.Should().BeEmpty(); } [TestMethod] - public void GetAllConnectionsInfo_HasOneSonarCloudConnection_ReturnsOneMappedConnection() + public void TryGetAllConnectionsInfo_HasOneSonarCloudConnection_ReturnsOneMappedConnection() { var sonarCloud = CreateSonarCloudServerConnection(); - serverConnectionsRepository.GetAll().Returns([sonarCloud]); + MockServerConnections([sonarCloud]); - var connections = testSubject.GetAllConnectionsInfo(); + testSubject.TryGetAllConnectionsInfo(out var connections); connections.Should().BeEquivalentTo([new ConnectionInfo(sonarCloud.Id, ConnectionServerType.SonarCloud)]); } [TestMethod] - public void GetAllConnectionsInfo_HasOneSonarQubeConnection_ReturnsOneMappedConnection() + public void TryGetAllConnectionsInfo_HasOneSonarQubeConnection_ReturnsOneMappedConnection() { var sonarQube = CreateSonarQubeServerConnection(); - serverConnectionsRepository.GetAll().Returns([sonarQube]); + MockServerConnections([sonarQube]); - var connections = testSubject.GetAllConnectionsInfo(); + testSubject.TryGetAllConnectionsInfo(out var connections); connections.Should().BeEquivalentTo([new ConnectionInfo(sonarQube.Id, ConnectionServerType.SonarQube)]); } + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void TryGetAllConnectionsInfo_ReturnsStatusFromSlCore(bool expectedStatus) + { + var sonarQube = CreateSonarQubeServerConnection(); + MockServerConnections([sonarQube], succeeded: expectedStatus); + + var succeeded = testSubject.TryGetAllConnectionsInfo(out _); + + succeeded.Should().Be(expectedStatus); + } + private static SonarCloud CreateSonarCloudServerConnection(bool isSmartNotificationsEnabled = true) { return new SonarCloud("myOrg", new ServerConnectionSettings(isSmartNotificationsEnabled), Substitute.For()); @@ -122,4 +147,13 @@ private static ServerConnection.SonarQube CreateSonarQubeServerConnection(bool i var sonarQube = new ServerConnection.SonarQube(new Uri("http://localhost"), new ServerConnectionSettings(isSmartNotificationsEnabled), Substitute.For()); return sonarQube; } + + private void MockServerConnections(List connections, bool succeeded = true) + { + serverConnectionsRepository.TryGetAll(out Arg.Any>()).Returns(callInfo => + { + callInfo[0] = connections; + return succeeded; + }); + } } diff --git a/src/ConnectedMode.UnitTests/UI/ManageBinding/ManageBindingViewModelTests.cs b/src/ConnectedMode.UnitTests/UI/ManageBinding/ManageBindingViewModelTests.cs index 01957d2cc7..3367155012 100644 --- a/src/ConnectedMode.UnitTests/UI/ManageBinding/ManageBindingViewModelTests.cs +++ b/src/ConnectedMode.UnitTests/UI/ManageBinding/ManageBindingViewModelTests.cs @@ -370,7 +370,7 @@ public void IsConnectionSelectionEnabled_ProjectIsNotBoundAndBindingIsNotInProgr { testSubject.BoundProject = null; progressReporterViewModel.IsOperationInProgress.Returns(false); - connectedModeServices.ServerConnectionsRepositoryAdapter.GetAllConnectionsInfo().Returns([sonarCloudConnectionInfo]); + MockTryGetAllConnectionsInfo([sonarCloudConnectionInfo]); testSubject.LoadConnections(); testSubject.IsConnectionSelectionEnabled.Should().BeTrue(); @@ -434,7 +434,7 @@ public void IsExportButtonEnabled_ProjectIsBoundAndBindingIsNotInProgress_Return public void LoadConnections_FillsConnections() { List existingConnections = [sonarQubeConnectionInfo, sonarCloudConnectionInfo]; - serverConnectionsRepositoryAdapter.GetAllConnectionsInfo().Returns(existingConnections); + MockTryGetAllConnectionsInfo(existingConnections); testSubject.LoadConnections(); @@ -444,7 +444,7 @@ public void LoadConnections_FillsConnections() [TestMethod] public void LoadConnections_ClearsPreviousConnections() { - serverConnectionsRepositoryAdapter.GetAllConnectionsInfo().Returns([sonarQubeConnectionInfo]); + MockTryGetAllConnectionsInfo([sonarQubeConnectionInfo]); testSubject.Connections.Add(sonarCloudConnectionInfo); testSubject.LoadConnections(); @@ -467,6 +467,18 @@ public void LoadConnections_RaisesEvents() Arg.Is(x => x.PropertyName == nameof(testSubject.ConnectionSelectionCaptionText))); } + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void LoadConnectionsAsync_ReturnsResponseFromAdapter(bool expectedStatus) + { + serverConnectionsRepositoryAdapter.TryGetAllConnectionsInfo(out Arg.Any>()).Returns(expectedStatus); + + var succeeded = testSubject.LoadConnections(); + + succeeded.Should().Be(expectedStatus); + } + [TestMethod] public async Task InitializeDataAsync_InitializesDataAndReportsProgress() { @@ -482,19 +494,11 @@ await progressReporterViewModel.Received(1) } [TestMethod] - public async Task LoadDataAsync_LoadsConnections() + public async Task LoadDataAsync_LoadsConnectionsOnUIThread() { await testSubject.LoadDataAsync(); - await threadHandling.Received(1).RunOnUIThreadAsync(Arg.Is(op => op == testSubject.LoadConnections)); - } - - [TestMethod] - public async Task LoadDataAsync_ConnectionsLoadedSuccessfully_ReturnsTrue() - { - var adapterResponse = await testSubject.LoadDataAsync(); - - adapterResponse.Success.Should().BeTrue(); + await threadHandling.Received(1).RunOnUIThreadAsync(Arg.Any()); } [TestMethod] @@ -537,6 +541,15 @@ private void MockServices() connectedModeServices.ThreadHandling.Returns(threadHandling); connectedModeServices.Logger.Returns(logger); - serverConnectionsRepositoryAdapter.GetAllConnectionsInfo().Returns([]); + MockTryGetAllConnectionsInfo([]); + } + + private void MockTryGetAllConnectionsInfo(List connectionInfos) + { + connectedModeServices.ServerConnectionsRepositoryAdapter.TryGetAllConnectionsInfo(out _).Returns(callInfo => + { + callInfo[0] = connectionInfos; + return true; + }); } } diff --git a/src/ConnectedMode.UnitTests/UI/ManageConnections/ManageConnectionsViewModelTest.cs b/src/ConnectedMode.UnitTests/UI/ManageConnections/ManageConnectionsViewModelTest.cs index f30f83b0dd..5c4f3ec417 100644 --- a/src/ConnectedMode.UnitTests/UI/ManageConnections/ManageConnectionsViewModelTest.cs +++ b/src/ConnectedMode.UnitTests/UI/ManageConnections/ManageConnectionsViewModelTest.cs @@ -30,7 +30,7 @@ namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.UI.ManageConnections; public class ManageConnectionsViewModelTest { private ManageConnectionsViewModel testSubject; - private List connections; + private List twoConnections; private IProgressReporterViewModel progressReporterViewModel; private IConnectedModeServices connectedModeServices; private IServerConnectionsRepositoryAdapter serverConnectionsRepositoryAdapter; @@ -40,7 +40,7 @@ public class ManageConnectionsViewModelTest [TestInitialize] public void TestInitialize() { - connections = + twoConnections = [ new Connection(new ConnectionInfo("http://localhost:9000", ConnectionServerType.SonarQube), true), new Connection(new ConnectionInfo("https://sonarcloud.io/myOrg", ConnectionServerType.SonarCloud), false) @@ -63,11 +63,11 @@ public void ConnectionViewModels_NoInitialization_HasEmptyList() [TestMethod] public void InitializeConnections_InitializesConnectionsCorrectly() { - serverConnectionsRepositoryAdapter.GetAllConnections().Returns(connections); + MockTryGetConnections(twoConnections); testSubject.InitializeConnections(); - HasExpectedConnections(connections); + HasExpectedConnections(twoConnections); } [TestMethod] @@ -78,7 +78,7 @@ public void RemoveConnection_RemovesProvidedConnection() testSubject.RemoveConnection(connectionToRemove); - testSubject.ConnectionViewModels.Count.Should().Be(connections.Count() - 1); + testSubject.ConnectionViewModels.Count.Should().Be(twoConnections.Count - 1); testSubject.ConnectionViewModels.Should().NotContain(connectionToRemove); } @@ -119,6 +119,8 @@ public void AddConnection_RaisesEvents() [TestMethod] public void NoConnectionExists_NoConnections_ReturnsTrue() { + MockTryGetConnections([]); + testSubject.InitializeConnections(); testSubject.NoConnectionExists.Should().BeTrue(); @@ -146,19 +148,11 @@ await progressReporterViewModel.Received(1) } [TestMethod] - public async Task LoadConnectionsAsync_LoadsConnections() + public async Task LoadConnectionsAsync_LoadsConnectionsOnUIThread() { await testSubject.LoadConnectionsAsync(); - await threadHandling.Received(1).RunOnUIThreadAsync(Arg.Is(op => op == testSubject.InitializeConnections)); - } - - [TestMethod] - public async Task LoadConnectionsAsync_ConnectionsLoadedSuccessfully_ReturnsTrue() - { - var adapterResponse = await testSubject.LoadConnectionsAsync(); - - adapterResponse.Success.Should().BeTrue(); + await threadHandling.Received(1).RunOnUIThreadAsync(Arg.Any()); } [TestMethod] @@ -175,10 +169,22 @@ public async Task LoadConnectionsAsync_LoadingConnectionsThrows_ReturnsFalse() logger.Received(1).WriteLine(exceptionMsg); } + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void InitializeConnections_ReturnsResponseFromAdapter(bool expectedStatus) + { + serverConnectionsRepositoryAdapter.TryGetAllConnections(out _).Returns(expectedStatus); + + var adapterResponse = testSubject.InitializeConnections(); + + adapterResponse.Should().Be(expectedStatus); + } + private void HasExpectedConnections(IEnumerable expectedConnections) { testSubject.ConnectionViewModels.Should().NotBeNull(); - testSubject.ConnectionViewModels.Count.Should().Be(connections.Count()); + testSubject.ConnectionViewModels.Count.Should().Be(twoConnections.Count); foreach (var connection in expectedConnections) { var connectionViewModel = testSubject.ConnectionViewModels.SingleOrDefault(c => c.Name == connection.Info.Id); @@ -190,7 +196,11 @@ private void HasExpectedConnections(IEnumerable expectedConnections) private void InitializeTwoConnections() { - serverConnectionsRepositoryAdapter.GetAllConnections().Returns(connections); + serverConnectionsRepositoryAdapter.TryGetAllConnections(out _).Returns(callInfo => + { + callInfo[0] = twoConnections; + return true; + }); testSubject.InitializeConnections(); } @@ -203,6 +213,15 @@ private void MockServices() connectedModeServices.ServerConnectionsRepositoryAdapter.Returns(serverConnectionsRepositoryAdapter); connectedModeServices.ThreadHandling.Returns(threadHandling); connectedModeServices.Logger.Returns(logger); - serverConnectionsRepositoryAdapter.GetAllConnections().Returns([]); + MockTryGetConnections(twoConnections); + } + + private void MockTryGetConnections(List connections) + { + serverConnectionsRepositoryAdapter.TryGetAllConnections(out _).Returns(callInfo => + { + callInfo[0] = connections; + return true; + }); } } diff --git a/src/ConnectedMode/Persistence/ServerConnectionsRepository.cs b/src/ConnectedMode/Persistence/ServerConnectionsRepository.cs new file mode 100644 index 0000000000..1910b8ab50 --- /dev/null +++ b/src/ConnectedMode/Persistence/ServerConnectionsRepository.cs @@ -0,0 +1,236 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Binding; +using System.ComponentModel.Composition; +using System.IO; +using SonarLint.VisualStudio.Core.Persistence; +using SonarLint.VisualStudio.ConnectedMode.Binding; + +namespace SonarLint.VisualStudio.ConnectedMode.Persistence; + + +[Export(typeof(IServerConnectionsRepository))] +[PartCreationPolicy(CreationPolicy.Shared)] +internal class ServerConnectionsRepository : IServerConnectionsRepository +{ + internal const string ConnectionsFileName = "connections.json"; + + private readonly ISolutionBindingCredentialsLoader credentialsLoader; + private readonly ILogger logger; + private readonly IJsonFileHandler jsonFileHandle; + private readonly IServerConnectionModelMapper serverConnectionModelMapper; + private readonly string storageFilePath; + private static readonly object LockObject = new(); + + [ImportingConstructor] + public ServerConnectionsRepository( + IJsonFileHandler jsonFileHandle, + IServerConnectionModelMapper serverConnectionModelMapper, + ICredentialStoreService credentialStoreService, + ILogger logger) : this(jsonFileHandle, + serverConnectionModelMapper, + new SolutionBindingCredentialsLoader(credentialStoreService), + EnvironmentVariableProvider.Instance, + logger) { } + + internal /* for testing */ ServerConnectionsRepository( + IJsonFileHandler jsonFileHandle, + IServerConnectionModelMapper serverConnectionModelMapper, + ISolutionBindingCredentialsLoader credentialsLoader, + IEnvironmentVariableProvider environmentVariables, + ILogger logger) + { + this.jsonFileHandle = jsonFileHandle; + this.serverConnectionModelMapper = serverConnectionModelMapper; + this.credentialsLoader = credentialsLoader; + this.logger = logger; + storageFilePath = GetStorageFilePath(environmentVariables); + } + + public bool TryGet(string connectionId, out ServerConnection serverConnection) + { + serverConnection = ReadServerConnectionsFromFile()?.Find(c => c.Id == connectionId); + if (serverConnection is null) + { + return false; + } + + serverConnection.Credentials = credentialsLoader.Load(serverConnection.CredentialsUri); + return true; + + } + + public bool TryGetAll(out IReadOnlyList serverConnections) + { + serverConnections = ReadServerConnectionsFromFile(); + + return serverConnections != null; + } + + public bool TryAdd(ServerConnection connectionToAdd) + { + return SafeUpdateConnectionsFile(connections => TryAddConnection(connections, connectionToAdd)); + } + + public bool TryDelete(string connectionId) + { + ServerConnection removedConnection = null; + var wasDeleted = SafeUpdateConnectionsFile(connections => + { + removedConnection = connections?.Find(c => c.Id == connectionId); + connections?.Remove(removedConnection); + + return removedConnection != null; + }); + if (wasDeleted) + { + TryDeleteCredentials(removedConnection); + } + + return wasDeleted; + } + + public bool TryUpdateSettingsById(string connectionId, ServerConnectionSettings connectionSettings) + { + return SafeUpdateConnectionsFile(connections => TryUpdateConnectionSettings(connections, connectionId, connectionSettings)); + } + + public bool TryUpdateCredentialsById(string connectionId, ICredentials credentials) + { + try + { + var wasFound = TryGet(connectionId, out ServerConnection serverConnection); + if (wasFound) + { + credentialsLoader.Save(credentials, serverConnection.CredentialsUri); + return true; + } + } + catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) + { + logger.WriteLine($"Failed updating credentials: {ex.Message}"); + } + return false; + } + + private bool TryAddConnection(List connections, ServerConnection connection) + { + if (connection.Credentials is null) + { + logger.LogVerbose($"Connection was not added.{nameof(ServerConnection.Credentials)} is not filled"); + return false; + } + + if (connections.Find(x => x.Id == connection.Id) is not null) + { + logger.LogVerbose($"Connection was not added.{nameof(ServerConnection.Id)} already exist"); + return false; + } + + try + { + connections.Add(connection); + credentialsLoader.Save(connection.Credentials, connection.CredentialsUri); + return true; + } + catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) + { + logger.WriteLine($"Failed adding server connection: {ex.Message}"); + } + + return false; + } + + + private void TryDeleteCredentials(ServerConnection removedConnection) + { + try + { + credentialsLoader.DeleteCredentials(removedConnection.CredentialsUri); + } + catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) + { + logger.WriteLine($"Failed deleting credentials: {ex.Message}"); + } + } + + private static bool TryUpdateConnectionSettings(List connections, string connectionId, ServerConnectionSettings connectionSettings) + { + var serverConnectionToUpdate = connections?.Find(c => c.Id == connectionId); + if (serverConnectionToUpdate == null) + { + return false; + } + + serverConnectionToUpdate.Settings = connectionSettings; + return true; + } + + private static string GetStorageFilePath(IEnvironmentVariableProvider environmentVariables) + { + var appDataFolder = environmentVariables.GetSLVSAppDataRootPath(); + return Path.Combine(appDataFolder, ConnectionsFileName); + } + + private List ReadServerConnectionsFromFile() + { + try + { + var model = jsonFileHandle.ReadFile(storageFilePath); + return model.ServerConnections.Select(serverConnectionModelMapper.GetServerConnection).ToList(); + } + catch (FileNotFoundException) + { + // file not existing should not be treated as an error, as it will be created at the first write + return []; + } + catch(Exception ex) when (!ErrorHandler.IsCriticalException(ex)) + { + logger.WriteLine($"Failed reading the {ConnectionsFileName}: {ex.Message}"); + } + + return null; + } + + private bool SafeUpdateConnectionsFile(Func, bool> tryUpdateConnectionModels) + { + lock (LockObject) + { + try + { + var serverConnections = ReadServerConnectionsFromFile(); + + if (tryUpdateConnectionModels(serverConnections)) + { + var model = serverConnectionModelMapper.GetServerConnectionsListJsonModel(serverConnections); + return jsonFileHandle.TryWriteToFile(storageFilePath, model); + } + } + catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) + { + logger.WriteLine($"Failed updating the {ConnectionsFileName}: {ex.Message}"); + } + + return false; + } + } +} diff --git a/src/ConnectedMode/ServerConnectionsRepositoryAdapter.cs b/src/ConnectedMode/ServerConnectionsRepositoryAdapter.cs index 1e4fce217e..8bd6ab0157 100644 --- a/src/ConnectedMode/ServerConnectionsRepositoryAdapter.cs +++ b/src/ConnectedMode/ServerConnectionsRepositoryAdapter.cs @@ -25,23 +25,26 @@ namespace SonarLint.VisualStudio.ConnectedMode; public interface IServerConnectionsRepositoryAdapter { - List GetAllConnections(); - List GetAllConnectionsInfo(); + bool TryGetAllConnections(out List connections); + bool TryGetAllConnectionsInfo(out List connectionInfos); } [Export(typeof(IServerConnectionsRepositoryAdapter))] [method: ImportingConstructor] internal class ServerConnectionsRepositoryAdapter(IServerConnectionsRepository serverConnectionsRepository) : IServerConnectionsRepositoryAdapter { - public List GetAllConnections() + public bool TryGetAllConnections(out List connections) { - var connections = serverConnectionsRepository.GetAll(); - return connections.Select(MapServerConnectionModel).ToList(); + var succeeded = serverConnectionsRepository.TryGetAll(out var serverConnections); + connections = serverConnections?.Select(MapServerConnectionModel).ToList(); + return succeeded; } - public List GetAllConnectionsInfo() + public bool TryGetAllConnectionsInfo(out List connectionInfos) { - return GetAllConnections().Select(conn => conn.Info).ToList(); + var succeeded = TryGetAllConnections(out var connections); + connectionInfos = connections?.Select(conn => conn.Info).ToList(); + return succeeded; } private static Connection MapServerConnectionModel(ServerConnection serverConnection) diff --git a/src/ConnectedMode/UI/ManageBinding/ManageBindingViewModel.cs b/src/ConnectedMode/UI/ManageBinding/ManageBindingViewModel.cs index a9c3074efe..dc50d3fa09 100644 --- a/src/ConnectedMode/UI/ManageBinding/ManageBindingViewModel.cs +++ b/src/ConnectedMode/UI/ManageBinding/ManageBindingViewModel.cs @@ -175,26 +175,28 @@ internal void OnProgressUpdated() internal async Task LoadDataAsync() { + var succeeded = false; try { - await connectedModeServices.ThreadHandling.RunOnUIThreadAsync(LoadConnections); - return new AdapterResponse(true); + await connectedModeServices.ThreadHandling.RunOnUIThreadAsync(() => succeeded = LoadConnections()); } catch (Exception ex) { connectedModeServices.Logger.WriteLine(ex.Message); + succeeded = false; } - return new AdapterResponse(false); + return new AdapterResponse(succeeded); } - internal void LoadConnections() + internal bool LoadConnections() { Connections.Clear(); - var slCoreConnections = connectedModeServices.ServerConnectionsRepositoryAdapter.GetAllConnectionsInfo(); - slCoreConnections.ForEach(Connections.Add); + var succeeded = connectedModeServices.ServerConnectionsRepositoryAdapter.TryGetAllConnectionsInfo(out var slCoreConnections); + slCoreConnections?.ForEach(Connections.Add); RaisePropertyChanged(nameof(IsConnectionSelectionEnabled)); RaisePropertyChanged(nameof(ConnectionSelectionCaptionText)); + return succeeded; } } diff --git a/src/ConnectedMode/UI/ManageConnections/ManageConnectionsViewModel.cs b/src/ConnectedMode/UI/ManageConnections/ManageConnectionsViewModel.cs index 7287667504..ec85b1466c 100644 --- a/src/ConnectedMode/UI/ManageConnections/ManageConnectionsViewModel.cs +++ b/src/ConnectedMode/UI/ManageConnections/ManageConnectionsViewModel.cs @@ -38,24 +38,26 @@ public async Task LoadConnectionsWithProgressAsync() internal async Task LoadConnectionsAsync() { + var succeeded = false; try { - await connectedModeServices.ThreadHandling.RunOnUIThreadAsync(InitializeConnections); - return new AdapterResponse(true); + await connectedModeServices.ThreadHandling.RunOnUIThreadAsync(() => succeeded = InitializeConnections()); } catch (Exception ex) { connectedModeServices.Logger.WriteLine(ex.Message); + succeeded = false; } - return new AdapterResponse(false); + return new AdapterResponse(succeeded); } - internal void InitializeConnections() + internal bool InitializeConnections() { ConnectionViewModels.Clear(); - var connections = connectedModeServices.ServerConnectionsRepositoryAdapter.GetAllConnections(); - connections.ToList().ForEach(AddConnection); + var succeeded = connectedModeServices.ServerConnectionsRepositoryAdapter.TryGetAllConnections(out var connections); + connections?.ForEach(AddConnection); + return succeeded; } public void RemoveConnection(ConnectionViewModel connectionViewModel) diff --git a/src/Core/Binding/IServerConnectionsRepository.cs b/src/Core/Binding/IServerConnectionsRepository.cs index 38767e4472..9604fbec99 100644 --- a/src/Core/Binding/IServerConnectionsRepository.cs +++ b/src/Core/Binding/IServerConnectionsRepository.cs @@ -26,45 +26,9 @@ namespace SonarLint.VisualStudio.Core.Binding; public interface IServerConnectionsRepository { bool TryGet(string connectionId, out ServerConnection serverConnection); - List GetAll(); - bool TryAdd(ServerConnection connection); + bool TryGetAll(out IReadOnlyList serverConnections); + bool TryAdd(ServerConnection connectionToAdd); bool TryDelete(string connectionId); - Task TryUpdateSettingsById(string connectionId, ServerConnectionSettings connectionSettings); - Task TryUpdateCredentialsById(string connectionId, ICredentials credentials); -} - -[ExcludeFromCodeCoverage] // todo: remove this class in https://sonarsource.atlassian.net/browse/SLVS-1399 -[Export(typeof(IServerConnectionsRepository))] -[PartCreationPolicy(CreationPolicy.Shared)] -public class DummyServerConnectionsRepository : IServerConnectionsRepository -{ - public bool TryGet(string connectionId, out ServerConnection serverConnection) - { - throw new NotImplementedException(); - } - - public List GetAll() - { - return [new ServerConnection.SonarCloud("a")]; - } - - public bool TryAdd(ServerConnection connection) - { - throw new NotImplementedException(); - } - - public bool TryDelete(string connectionId) - { - throw new NotImplementedException(); - } - - public Task TryUpdateSettingsById(string connectionId, ServerConnectionSettings connectionSettings) - { - throw new NotImplementedException(); - } - - public Task TryUpdateCredentialsById(string connectionId, ICredentials credentials) - { - throw new NotImplementedException(); - } + bool TryUpdateSettingsById(string connectionId, ServerConnectionSettings connectionSettings); + bool TryUpdateCredentialsById(string connectionId, ICredentials credentials); }