diff --git a/src/ConnectedMode.UnitTests/Persistence/ServerConnectionsRepositoryTests.cs b/src/ConnectedMode.UnitTests/Persistence/ServerConnectionsRepositoryTests.cs index bc214603e..03e2e2a20 100644 --- a/src/ConnectedMode.UnitTests/Persistence/ServerConnectionsRepositoryTests.cs +++ b/src/ConnectedMode.UnitTests/Persistence/ServerConnectionsRepositoryTests.cs @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using System.ComponentModel; using System.IO; using System.IO.Abstractions; using SonarLint.VisualStudio.ConnectedMode.Binding; @@ -338,6 +339,32 @@ public void TryAdd_WritingThrowsException_DoesNotUpdateConnectionAndWritesLog() logger.Received(1).WriteLine($"Failed updating the {ServerConnectionsRepository.ConnectionsFileName}: {exceptionMsg}"); } + [TestMethod] + public void TryAdd_DoesNotAddConnection_DoesNotInvokeConnectionChangedEvent() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(false); + var eventHandler = Substitute.For(); + testSubject.ConnectionChanged += eventHandler; + + testSubject.TryAdd(sonarCloudServerConnection); + + eventHandler.DidNotReceive().Invoke(testSubject, Arg.Any()); + } + + [TestMethod] + public void TryAdd_AddsConnection_InvokesConnectionChangedEvent() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(true); + var eventHandler = Substitute.For(); + testSubject.ConnectionChanged += eventHandler; + + testSubject.TryAdd(sonarCloudServerConnection); + + eventHandler.Received().Invoke(testSubject, Arg.Any()); + } + [TestMethod] public void TryDelete_FileCouldNotBeRead_ReturnsFalse() { @@ -439,6 +466,32 @@ public void TryDelete_WritingThrowsException_DoesNotUpdateConnectionAndWritesLog logger.Received(1).WriteLine($"Failed updating the {ServerConnectionsRepository.ConnectionsFileName}: {exceptionMsg}"); } + [TestMethod] + public void TryDelete_DoesNotDeleteConnection_DoesNotInvokeConnectionChangedEvent() + { + var sonarQube = MockFileWithOneSonarQubeConnection(); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(false); + var eventHandler = Substitute.For(); + testSubject.ConnectionChanged += eventHandler; + + testSubject.TryDelete(sonarQube.Id); + + eventHandler.DidNotReceive().Invoke(testSubject, Arg.Any()); + } + + [TestMethod] + public void TryDelete_DeletesConnection_InvokesConnectionChangedEvent() + { + var sonarQube = MockFileWithOneSonarQubeConnection(); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(true); + var eventHandler = Substitute.For(); + testSubject.ConnectionChanged += eventHandler; + + testSubject.TryDelete(sonarQube.Id); + + eventHandler.Received(1).Invoke(testSubject, Arg.Any()); + } + [TestMethod] public void TryUpdateSettingsById_FileCouldNotBeRead_ReturnsFalse() { @@ -505,6 +558,32 @@ public void TryUpdateSettingsById_WritingThrowsException_DoesNotUpdateConnection logger.Received(1).WriteLine($"Failed updating the {ServerConnectionsRepository.ConnectionsFileName}: {exceptionMsg}"); } + [TestMethod] + public void TryUpdateSettingsById_DoesNotUpdateConnection_DoesNotInvokeConnectionChangedEvent() + { + var sonarQube = MockFileWithOneSonarQubeConnection(); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(false); + var eventHandler = Substitute.For(); + testSubject.ConnectionChanged += eventHandler; + + testSubject.TryDelete(sonarQube.Id); + + eventHandler.DidNotReceive().Invoke(testSubject, Arg.Any()); + } + + [TestMethod] + public void TryUpdateSettingsById_UpdatesConnection_InvokesConnectionChangedEvent() + { + var sonarQube = MockFileWithOneSonarQubeConnection(); + jsonFileHandler.TryWriteToFile(Arg.Any(), Arg.Any()).Returns(true); + var eventHandler = Substitute.For(); + testSubject.ConnectionChanged += eventHandler; + + testSubject.TryUpdateSettingsById(sonarQube.Id, new ServerConnectionSettings(true)); + + eventHandler.Received(1).Invoke(testSubject, Arg.Any()); + } + [TestMethod] public void TryUpdateCredentialsById_ConnectionDoesNotExist_DoesNotUpdateCredentials() { @@ -540,6 +619,30 @@ public void TryUpdateCredentialsById_SonarQubeConnectionExists_UpdatesCredential credentialsLoader.Received(1).Save(newCredentials, sonarQube.ServerUri); } + [TestMethod] + public void TryUpdateCredentialsById_DoesNotUpdateCredentials_DoesNotInvokeConnectionChangedEvent() + { + MockReadingFile(new ServerConnectionsListJsonModel()); + var eventHandler = Substitute.For>(); + testSubject.CredentialsChanged += eventHandler; + + testSubject.TryUpdateCredentialsById("non-existingConn", Substitute.For()); + + eventHandler.DidNotReceive().Invoke(testSubject, Arg.Any()); + } + + [TestMethod] + public void TryUpdateCredentialsById_UpdatesCredentials_InvokesConnectionChangedEvent() + { + var sonarQube = MockFileWithOneSonarQubeConnection(); + var eventHandler = Substitute.For>(); + testSubject.CredentialsChanged += eventHandler; + + testSubject.TryUpdateCredentialsById(sonarQube.Id, Substitute.For()); + + eventHandler.Received(1).Invoke(testSubject, Arg.Is(args => args.ServerConnection == sonarQube)); + } + [TestMethod] [DataRow(true)] [DataRow(false)] diff --git a/src/ConnectedMode/Persistence/ServerConnectionsRepository.cs b/src/ConnectedMode/Persistence/ServerConnectionsRepository.cs index 3578bc375..7554307cf 100644 --- a/src/ConnectedMode/Persistence/ServerConnectionsRepository.cs +++ b/src/ConnectedMode/Persistence/ServerConnectionsRepository.cs @@ -43,6 +43,9 @@ internal class ServerConnectionsRepository : IServerConnectionsRepository private readonly string connectionsStorageFilePath; private static readonly object LockObject = new(); + public event EventHandler ConnectionChanged; + public event EventHandler CredentialsChanged; + [ImportingConstructor] public ServerConnectionsRepository( IJsonFileHandler jsonFileHandle, @@ -127,6 +130,7 @@ public bool TryUpdateCredentialsById(string connectionId, ICredentials credentia if (wasFound) { credentialsLoader.Save(credentials, serverConnection.CredentialsUri); + OnCredentialsChanged(serverConnection); return true; } } @@ -229,7 +233,12 @@ private bool SafeUpdateConnectionsFile(Func, bool> tryUpd if (tryUpdateConnectionModels(serverConnections)) { var model = serverConnectionModelMapper.GetServerConnectionsListJsonModel(serverConnections); - return jsonFileHandle.TryWriteToFile(connectionsStorageFilePath, model); + var wasSaved = jsonFileHandle.TryWriteToFile(connectionsStorageFilePath, model); + if (wasSaved) + { + OnConnectionChanged(); + } + return wasSaved; } } catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) @@ -240,4 +249,7 @@ private bool SafeUpdateConnectionsFile(Func, bool> tryUpd return false; } } + + private void OnConnectionChanged() => ConnectionChanged?.Invoke(this, EventArgs.Empty); + private void OnCredentialsChanged(ServerConnection serverConnection) => CredentialsChanged?.Invoke(this, new ServerConnectionUpdatedEventArgs(serverConnection)); } diff --git a/src/Core/Binding/IServerConnectionsRepository.cs b/src/Core/Binding/IServerConnectionsRepository.cs index 13a859b1b..22e988257 100644 --- a/src/Core/Binding/IServerConnectionsRepository.cs +++ b/src/Core/Binding/IServerConnectionsRepository.cs @@ -29,4 +29,17 @@ public interface IServerConnectionsRepository bool TryUpdateSettingsById(string connectionId, ServerConnectionSettings connectionSettings); bool TryUpdateCredentialsById(string connectionId, ICredentials credentials); bool ConnectionsFileExists(); + event EventHandler ConnectionChanged; + event EventHandler CredentialsChanged; } + +public class ServerConnectionUpdatedEventArgs : EventArgs +{ + public ServerConnectionUpdatedEventArgs(ServerConnection serverConnection) + { + ServerConnection = serverConnection; + } + + public ServerConnection ServerConnection { get; } +} + diff --git a/src/SLCore.Listeners.UnitTests/CredentialsListenerTests.cs b/src/SLCore.Listeners.UnitTests/CredentialsListenerTests.cs index cf0f6a3ff..9516f6f07 100644 --- a/src/SLCore.Listeners.UnitTests/CredentialsListenerTests.cs +++ b/src/SLCore.Listeners.UnitTests/CredentialsListenerTests.cs @@ -18,11 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Threading.Tasks; -using NSubstitute; using SonarLint.VisualStudio.ConnectedMode.Binding; -using SonarLint.VisualStudio.SLCore.Common.Helpers; using SonarLint.VisualStudio.SLCore.Common.Models; using SonarLint.VisualStudio.SLCore.Core; using SonarLint.VisualStudio.SLCore.Listener.Credentials; @@ -32,15 +28,14 @@ namespace SonarLint.VisualStudio.SLCore.Listeners.UnitTests; [TestClass] public class CredentialsListenerTests { - private const string ConnectionId = "connectionId123"; - private static readonly Uri Uri = new("http://myfavouriteuri.nonexistingdomain"); + private const string ConnectionId = "http://myfavouriteuri.nonexistingdomain"; + private static readonly Uri Uri = new(ConnectionId); [TestMethod] public void MefCtor_CheckIsExported() { MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); + MefTestHelpers.CreateExport()); } [TestMethod] @@ -52,8 +47,7 @@ public void MefCtor_CheckIsSingleton() [TestMethod] public async Task GetCredentialsAsync_NullConnectionId_ReturnsNoCredentials() { - var testSubject = CreateTestSubject(out _, out var connectionIdHelperMock); - connectionIdHelperMock.GetUriFromConnectionId(null).Returns((Uri)null); + var testSubject = CreateTestSubject(out _); var response = await testSubject.GetCredentialsAsync(new GetCredentialsParams(null)); @@ -63,8 +57,7 @@ public async Task GetCredentialsAsync_NullConnectionId_ReturnsNoCredentials() [TestMethod] public async Task GetCredentialsAsync_NullParams_ReturnsNoCredentials() { - var testSubject = CreateTestSubject(out _, out var connectionIdHelperMock); - connectionIdHelperMock.GetUriFromConnectionId(null).Returns((Uri)null); + var testSubject = CreateTestSubject(out _); var response = await testSubject.GetCredentialsAsync(null); @@ -74,8 +67,7 @@ public async Task GetCredentialsAsync_NullParams_ReturnsNoCredentials() [TestMethod] public async Task GetCredentialsAsync_CredentialsNotFound_ReturnsNoCredentials() { - var testSubject = CreateTestSubject(out var credentialStoreMock, out var connectionIdHelperMock); - SetUpConnectionIdHelper(connectionIdHelperMock); + var testSubject = CreateTestSubject(out var credentialStoreMock); credentialStoreMock.GetCredentials(Arg.Is(x => UriEquals(x, Uri))).Returns((ConnectionCredentials)null); var response = await testSubject.GetCredentialsAsync(new GetCredentialsParams(ConnectionId)); @@ -90,8 +82,7 @@ public async Task GetCredentialsAsync_SonarQubeUsernameAndPasswordFound_ReturnsU const string username = "user1"; const string password = "password123"; - var testSubject = CreateTestSubject(out var credentialStoreMock, out var connectionIdHelperMock); - SetUpConnectionIdHelper(connectionIdHelperMock); + var testSubject = CreateTestSubject(out var credentialStoreMock); credentialStoreMock.GetCredentials(Arg.Is(x => UriEquals(x, Uri))).Returns(new ConnectionCredentials(username, password)); var response = await testSubject.GetCredentialsAsync(new GetCredentialsParams(ConnectionId)); @@ -104,8 +95,7 @@ public async Task GetCredentialsAsync_SonarQubeTokenFound_ReturnsToken() { const string token = "token123"; - var testSubject = CreateTestSubject(out var credentialStoreMock, out var connectionIdHelperMock); - SetUpConnectionIdHelper(connectionIdHelperMock); + var testSubject = CreateTestSubject(out var credentialStoreMock); credentialStoreMock.GetCredentials(Arg.Is(x => UriEquals(x, Uri))).Returns(new ConnectionCredentials(token)); var response = await testSubject.GetCredentialsAsync(new GetCredentialsParams(ConnectionId)); @@ -113,17 +103,11 @@ public async Task GetCredentialsAsync_SonarQubeTokenFound_ReturnsToken() response.Should().BeEquivalentTo(new GetCredentialsResponse(new TokenDto(token))); } - private CredentialsListener CreateTestSubject(out ICredentialProvider credentialStoreMock, out IConnectionIdHelper connectionIdHelperMock) + private CredentialsListener CreateTestSubject(out ICredentialProvider credentialStoreMock) { credentialStoreMock = Substitute.For(); - connectionIdHelperMock = Substitute.For(); - return new CredentialsListener(credentialStoreMock, connectionIdHelperMock); - } - - private static void SetUpConnectionIdHelper(IConnectionIdHelper connectionIdHelperMock) - { - connectionIdHelperMock.GetUriFromConnectionId(ConnectionId).Returns(Uri); + return new CredentialsListener(credentialStoreMock); } private static bool UriEquals(Uri uri, Uri serverUri) diff --git a/src/SLCore.Listeners/Implementation/CredentialsListener.cs b/src/SLCore.Listeners/Implementation/CredentialsListener.cs index 55b390963..10ea65554 100644 --- a/src/SLCore.Listeners/Implementation/CredentialsListener.cs +++ b/src/SLCore.Listeners/Implementation/CredentialsListener.cs @@ -19,9 +19,7 @@ */ using System.ComponentModel.Composition; -using System.Threading.Tasks; using SonarLint.VisualStudio.ConnectedMode.Binding; -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 +34,21 @@ namespace SonarLint.VisualStudio.SLCore.Listeners.Implementation internal class CredentialsListener : ICredentialsListener { private readonly ICredentialProvider credentialProvider; - private readonly IConnectionIdHelper connectionIdHelper; [ImportingConstructor] - public CredentialsListener(ICredentialProvider credentialProvider, IConnectionIdHelper connectionIdHelper) + public CredentialsListener(ICredentialProvider credentialProvider) { this.credentialProvider = credentialProvider; - this.connectionIdHelper = connectionIdHelper; } public Task GetCredentialsAsync(GetCredentialsParams parameters) { - var serverUri = connectionIdHelper.GetUriFromConnectionId(parameters?.connectionId); - - if (serverUri == null) + if (parameters?.connectionId == null) { return Task.FromResult(GetCredentialsResponse.NoCredentials); } + var serverUri = new Uri(parameters.connectionId); var credentials = credentialProvider.GetCredentials(serverUri); if (credentials == null) diff --git a/src/SLCore.UnitTests/Common/Helpers/ConnectionIdHelperTests.cs b/src/SLCore.UnitTests/Common/Helpers/ConnectionIdHelperTests.cs deleted file mode 100644 index 59e437723..000000000 --- a/src/SLCore.UnitTests/Common/Helpers/ConnectionIdHelperTests.cs +++ /dev/null @@ -1,111 +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 SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.SLCore.Common.Helpers; - -namespace SonarLint.VisualStudio.SLCore.UnitTests.Common.Helpers; - -[TestClass] -public class ConnectionIdHelperTests -{ - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported(); - } - - [TestMethod] - public void MefCtor_CheckIsSingleton() - { - MefTestHelpers.CheckIsSingletonMefComponent(); - } - - [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 GetUriFromConnectionId_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); - } - - [TestMethod] - [DataRow("http://someuri.com", null, "sq|http://someuri.com/")] - [DataRow("https://sonarcloud.io", "something", "sc|https://sonarcloud.io/organizations/something")] - public void GetConnectionIdFromServerConnection_PassUri_ReturnsAsExpected(string uriString, string organisation, string expectedConnectionId) - { - var uri = new Uri(uriString); - - var testSubject = new ConnectionIdHelper(); - - var actualConnectionId = testSubject.GetConnectionIdFromServerConnection(organisation is null ? new ServerConnection.SonarQube(new Uri(uriString)) : new ServerConnection.SonarCloud(organisation)); - - actualConnectionId.Should().Be(expectedConnectionId); - } - - [TestMethod] - public void GetConnectionIdFromServerConnection_ConnectionIsNull_ReturnsNull() - { - var testSubject = new ConnectionIdHelper(); - - var actualConnectionId = testSubject.GetConnectionIdFromServerConnection(null); - - actualConnectionId.Should().BeNull(); - } - - [TestMethod] - public void MethodsBackToBack_SonarQube_ShouldCreateSameUri() - { - var uri = new Uri("http://someuri.com"); - - var testSubject = new ConnectionIdHelper(); - - var resultUri = testSubject.GetUriFromConnectionId(testSubject.GetConnectionIdFromServerConnection(new ServerConnection.SonarQube(uri))); - - resultUri.Should().Be(uri); - } - - [TestMethod] - public void MethodsBackToBack_SonarCloud_ShouldCreateSameUri() - { - var uri = new Uri("https://sonarcloud.io"); - - var testSubject = new ConnectionIdHelper(); - - var resultUri = testSubject.GetUriFromConnectionId(testSubject.GetConnectionIdFromServerConnection(new ServerConnection.SonarCloud("my org"))); - - resultUri.Should().Be(uri); - } -} diff --git a/src/SLCore.UnitTests/State/AliveConnectionTrackerTests.cs b/src/SLCore.UnitTests/State/AliveConnectionTrackerTests.cs index 58673494b..2dc605fbc 100644 --- a/src/SLCore.UnitTests/State/AliveConnectionTrackerTests.cs +++ b/src/SLCore.UnitTests/State/AliveConnectionTrackerTests.cs @@ -18,12 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.Linq; -using System.Threading.Tasks; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; using SonarLint.VisualStudio.Core.Synchronization; -using SonarLint.VisualStudio.SLCore.Common.Helpers; using SonarLint.VisualStudio.SLCore.Core; using SonarLint.VisualStudio.SLCore.Service.Connection; using SonarLint.VisualStudio.SLCore.Service.Connection.Models; @@ -39,8 +36,8 @@ public void MefCtor_CheckIsExported() { MefTestHelpers.CheckTypeCanBeImported( MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport()); } @@ -54,13 +51,14 @@ public void MefCtor_CheckIsSingleton() [TestMethod] public void Ctor_SubscribesToEvents() { - var bindingRepositoryMock = new Mock(); + var serverConnectionsRepository = new Mock(); ConfigureAsyncLockFactory(out var asyncLockFactoryMock, out _, out _); - CreateTestSubject(Mock.Of(), Mock.Of(), bindingRepositoryMock.Object, + CreateTestSubject(Mock.Of(), Mock.Of(), serverConnectionsRepository.Object, asyncLockFactoryMock.Object); - bindingRepositoryMock.VerifyAdd(x => x.BindingUpdated += It.IsAny()); + serverConnectionsRepository.VerifyAdd(x => x.ConnectionChanged += It.IsAny()); + serverConnectionsRepository.VerifyAdd(x => x.CredentialsChanged += It.IsAny>()); asyncLockFactoryMock.Verify(x => x.Create()); } @@ -74,7 +72,7 @@ public void Refresh_SonarQubeConnection_CorrectlyUpdated() ConfigureAsyncLockFactory(out var asyncLockFactoryMock, out var asyncLockMock, out var asyncLockReleaseMock); ConfigureConnectionProvider(out var connectionProviderMock, sonarQubeConnection); var testSubject = CreateTestSubject(serviceProviderMock.Object, connectionProviderMock.Object, - Mock.Of(), asyncLockFactoryMock.Object); + Mock.Of(), asyncLockFactoryMock.Object); testSubject.RefreshConnectionList(); @@ -100,7 +98,7 @@ public void Refresh_SonarCloudConnection_CorrectlyUpdated() ConfigureAsyncLockFactory(out var asyncLockFactoryMock, out var asyncLockMock, out var asyncLockReleaseMock); ConfigureConnectionProvider(out var connectionProviderMock, sonarCloudConnection); var testSubject = CreateTestSubject(serviceProviderMock.Object, connectionProviderMock.Object, - Mock.Of(), asyncLockFactoryMock.Object); + Mock.Of(), asyncLockFactoryMock.Object); testSubject.RefreshConnectionList(); @@ -124,7 +122,7 @@ public void Refresh_ChecksThread() ConfigureConnectionProvider(out var connectionProviderMock); var threadHandlingMock = new Mock(); var testSubject = CreateTestSubject(serviceProviderMock.Object, connectionProviderMock.Object, - Mock.Of(), asyncLockFactoryMock.Object, threadHandlingMock.Object); + Mock.Of(), asyncLockFactoryMock.Object, threadHandlingMock.Object); testSubject.RefreshConnectionList(); @@ -136,7 +134,7 @@ public void Refresh_ServiceUnavailable_Throws() { var serviceProviderMock = new Mock(); var testSubject = CreateTestSubject(serviceProviderMock.Object, Mock.Of(), - Mock.Of(), Mock.Of()); + Mock.Of(), Mock.Of()); var act = () => testSubject.RefreshConnectionList(); @@ -146,16 +144,16 @@ public void Refresh_ServiceUnavailable_Throws() [TestMethod] public void Event_TriggersRefresh() { - var bindingRepositoryMock = new Mock(); + var serverConnectionsRepository = new Mock(); var sonarQubeConnection = new SonarQubeConnectionConfigurationDto("sq1", true, "http://localhost/"); var sonarCloudConnection = new SonarCloudConnectionConfigurationDto("sc2", true, "org"); ConfigureServiceProvider(out var serviceProviderMock, out var connectionServiceMock); ConfigureAsyncLockFactory(out var asyncLockFactoryMock, out var asyncLockMock, out var asyncLockReleaseMock); ConfigureConnectionProvider(out var connectionProviderMock, sonarQubeConnection, sonarCloudConnection); - var testSubject = CreateTestSubject(serviceProviderMock.Object, connectionProviderMock.Object, - bindingRepositoryMock.Object, asyncLockFactoryMock.Object); + CreateTestSubject(serviceProviderMock.Object, connectionProviderMock.Object, + serverConnectionsRepository.Object, asyncLockFactoryMock.Object); - bindingRepositoryMock.Raise(x => x.BindingUpdated += It.IsAny(), EventArgs.Empty); + serverConnectionsRepository.Raise(x => x.ConnectionChanged += It.IsAny(), EventArgs.Empty); connectionServiceMock.Verify(x => x.DidUpdateConnections( It.Is(p => @@ -169,13 +167,13 @@ public void Event_TriggersRefresh() [TestMethod] public void Event_RunsOnBackgroundThread() { - var bindingRepositoryMock = new Mock(); + var serverConnectionsRepository = new Mock(); var threadHandlingMock = new Mock(); ConfigureConnectionProvider(out var connectionProviderMock); - var testSubject = CreateTestSubject(Mock.Of(), connectionProviderMock.Object, - bindingRepositoryMock.Object, Mock.Of(), threadHandlingMock.Object); + CreateTestSubject(Mock.Of(), connectionProviderMock.Object, + serverConnectionsRepository.Object, Mock.Of(), threadHandlingMock.Object); - bindingRepositoryMock.Raise(x => x.BindingUpdated += It.IsAny(), EventArgs.Empty); + serverConnectionsRepository.Raise(x => x.ConnectionChanged += It.IsAny(), EventArgs.Empty); threadHandlingMock.Verify(x => x.RunOnBackgroundThread(It.IsAny>>())); } @@ -183,17 +181,76 @@ public void Event_RunsOnBackgroundThread() [TestMethod] public void Dispose_UnsubscribesAndDisposesLock() { - var bindingRepositoryMock = new Mock(); + var serverConnectionsRepository = new Mock(); ConfigureAsyncLockFactory(out var asyncLockFactoryMock, out var asyncLockMock, out _); var testSubject = CreateTestSubject(Mock.Of(), Mock.Of(), - bindingRepositoryMock.Object, asyncLockFactoryMock.Object); + serverConnectionsRepository.Object, asyncLockFactoryMock.Object); testSubject.Dispose(); - bindingRepositoryMock.VerifyRemove(x => x.BindingUpdated -= It.IsAny()); + serverConnectionsRepository.VerifyRemove(x => x.ConnectionChanged -= It.IsAny()); asyncLockMock.Verify(x => x.Dispose()); } + [TestMethod] + public void UpdateCredentials_ChecksThread() + { + ConfigureServiceProvider(out var serviceProviderMock, out _); + ConfigureAsyncLockFactory(out var asyncLockFactoryMock, out _, out _); + ConfigureConnectionProvider(out var connectionProviderMock); + var threadHandlingMock = new Mock(); + var testSubject = CreateTestSubject(serviceProviderMock.Object, connectionProviderMock.Object, Mock.Of(), asyncLockFactory: asyncLockFactoryMock.Object, threadHandlingMock.Object); + + testSubject.UpdateCredentials("connId"); + + threadHandlingMock.Verify(x => x.ThrowIfOnUIThread()); + } + + [TestMethod] + public void UpdateCredentials_ServiceUnavailable_Throws() + { + var serviceProviderMock = new Mock(); + var testSubject = CreateTestSubject(serviceProviderMock.Object, Mock.Of(), Mock.Of(), Mock.Of()); + + var act = () => testSubject.UpdateCredentials("connId"); + + act.Should().ThrowExactly().WithMessage(SLCoreStrings.ServiceProviderNotInitialized); + } + + [TestMethod] + public void UpdateCredentials_SonarCloud_TriggersRefreshCredentials() + { + ConfigureServiceProvider(out var serviceProviderMock, out var connectionServiceMock); + ConfigureAsyncLockFactory(out var asyncLockFactoryMock, out var asyncLockMock, out var asyncLockReleaseMock); + var serverConnectionRepository = new Mock(); + CreateTestSubject(serviceProviderMock.Object, Mock.Of(), serverConnectionRepository.Object, asyncLockFactoryMock.Object); + var sonarCloud = new ServerConnection.SonarCloud("myOrg"); + + serverConnectionRepository.Raise( + x => x.CredentialsChanged += It.IsAny>(), new ServerConnectionUpdatedEventArgs(sonarCloud)); + + connectionServiceMock.Verify( + x => x.DidChangeCredentials(It.Is(args => args.connectionId == sonarCloud.Id)), Times.Once); + VerifyLockTakenAndReleased(asyncLockMock, asyncLockReleaseMock); + } + + [TestMethod] + public void UpdateCredentials_SonarQube_TriggersRefreshCredentials() + { + ConfigureServiceProvider(out var serviceProviderMock, out var connectionServiceMock); + ConfigureAsyncLockFactory(out var asyncLockFactoryMock, out var asyncLockMock, out var asyncLockReleaseMock); + var serverConnectionRepository = new Mock(); + CreateTestSubject(serviceProviderMock.Object, Mock.Of(), serverConnectionRepository.Object, asyncLockFactoryMock.Object); + var sonarQube = new ServerConnection.SonarQube(new Uri("http://localhost:9000")); + + serverConnectionRepository.Raise( + x => x.CredentialsChanged += It.IsAny>(), new ServerConnectionUpdatedEventArgs(sonarQube)); + + connectionServiceMock.Verify( + x => x.DidChangeCredentials(It.Is(args => args.connectionId == sonarQube.Id)), Times.Once); + VerifyLockTakenAndReleased(asyncLockMock, asyncLockReleaseMock); + } + private static void VerifyLockTakenAndReleased(Mock asyncLock, Mock lockRelease) { asyncLock.Verify(x => x.Acquire(), Times.Once); @@ -228,13 +285,13 @@ private static void ConfigureServiceProvider(out Mock se private static AliveConnectionTracker CreateTestSubject(ISLCoreServiceProvider slCoreServiceProvider, IServerConnectionsProvider serverConnectionsProvider, - ISolutionBindingRepository bindingRepository, + IServerConnectionsRepository connectionsRepository, IAsyncLockFactory asyncLockFactory, IThreadHandling threadHandling = null) { return new AliveConnectionTracker(slCoreServiceProvider, serverConnectionsProvider, - bindingRepository, + connectionsRepository, asyncLockFactory, threadHandling ?? new NoOpThreadHandler()); } diff --git a/src/SLCore.UnitTests/State/ConfigScopeUpdaterTests.cs b/src/SLCore.UnitTests/State/ConfigScopeUpdaterTests.cs index 15bccc76f..a4abed4fd 100644 --- a/src/SLCore.UnitTests/State/ConfigScopeUpdaterTests.cs +++ b/src/SLCore.UnitTests/State/ConfigScopeUpdaterTests.cs @@ -18,12 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.Threading.Tasks; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.SLCore.Common.Helpers; using SonarLint.VisualStudio.SLCore.State; -using SonarQube.Client.Models; namespace SonarLint.VisualStudio.SLCore.UnitTests.State; @@ -36,7 +33,6 @@ public void MefCtor_CheckIsExported() MefTestHelpers.CheckTypeCanBeImported( MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport()); } @@ -86,34 +82,32 @@ public void UpdateConfigScopeForCurrentSolution_UnboundSolutionOpen_SetsCurrentC [TestMethod] public void UpdateConfigScopeForCurrentSolution_BoundSolutionOpen_SetsCurrentConfigScope() { - var binding = new BoundServerProject("solution", "projectKey", new ServerConnection.SonarQube(new Uri("http://localhost"))); + var serverConnection = new ServerConnection.SonarQube(new Uri("http://localhost")); + var binding = new BoundServerProject("solution", "projectKey", serverConnection); var activeConfigScopeTrackerMock = new Mock(); var solutionInfoProviderMock = new Mock(); solutionInfoProviderMock.Setup(x => x.GetSolutionName()).Returns("sln"); - var connectionIdHelperMock = new Mock(); - connectionIdHelperMock.Setup(x => x.GetConnectionIdFromServerConnection(binding.ServerConnection)).Returns("conid"); - var testSubject = CreateTestSubject(activeConfigScopeTrackerMock.Object, solutionInfoProviderMock.Object, connectionIdHelperMock.Object); + var testSubject = CreateTestSubject(activeConfigScopeTrackerMock.Object, solutionInfoProviderMock.Object); testSubject.UpdateConfigScopeForCurrentSolution(binding); - activeConfigScopeTrackerMock.Verify(x => x.SetCurrentConfigScope("sln", "conid", binding.ServerProjectKey)); + activeConfigScopeTrackerMock.Verify(x => x.SetCurrentConfigScope("sln", serverConnection.Id, binding.ServerProjectKey)); activeConfigScopeTrackerMock.VerifyNoOtherCalls(); } [TestMethod] public void UpdateConfigScopeForCurrentSolution_BoundSolutionWithOrganizationOpen_SetsCurrentConfigScope() { - var binding = new BoundServerProject("solution", "projectKey", new ServerConnection.SonarCloud("org")); + var serverConnection = new ServerConnection.SonarCloud("org"); + var binding = new BoundServerProject("solution", "projectKey", serverConnection); var activeConfigScopeTrackerMock = new Mock(); var solutionInfoProviderMock = new Mock(); solutionInfoProviderMock.Setup(x => x.GetSolutionName()).Returns("sln"); - var connectionIdHelperMock = new Mock(); - connectionIdHelperMock.Setup(x => x.GetConnectionIdFromServerConnection(binding.ServerConnection)).Returns("conid"); - var testSubject = CreateTestSubject(activeConfigScopeTrackerMock.Object, solutionInfoProviderMock.Object, connectionIdHelperMock.Object); + var testSubject = CreateTestSubject(activeConfigScopeTrackerMock.Object, solutionInfoProviderMock.Object); testSubject.UpdateConfigScopeForCurrentSolution(binding); - activeConfigScopeTrackerMock.Verify(x => x.SetCurrentConfigScope("sln", "conid", binding.ServerProjectKey)); + activeConfigScopeTrackerMock.Verify(x => x.SetCurrentConfigScope("sln", serverConnection.Id, binding.ServerProjectKey)); activeConfigScopeTrackerMock.VerifyNoOtherCalls(); } @@ -131,13 +125,11 @@ public void UpdateConfigScopeForCurrentSolution_SolutionClosed_RemovesCurrentCon private static ConfigScopeUpdater CreateTestSubject(IActiveConfigScopeTracker activeConfigScopeTracker = null, ISolutionInfoProvider solutionInfoProvider = null, - IConnectionIdHelper connectionIdHelper = null, IThreadHandling threadHandling = null) { activeConfigScopeTracker ??= Mock.Of(); solutionInfoProvider ??= Mock.Of(); - connectionIdHelper ??= new ConnectionIdHelper(); threadHandling ??= new NoOpThreadHandler(); - return new ConfigScopeUpdater(activeConfigScopeTracker, solutionInfoProvider, connectionIdHelper, threadHandling); + return new ConfigScopeUpdater(activeConfigScopeTracker, solutionInfoProvider, threadHandling); } } diff --git a/src/SLCore.UnitTests/State/ServerConnectionsProviderTests.cs b/src/SLCore.UnitTests/State/ServerConnectionsProviderTests.cs index 3eefe1a1f..1670fc737 100644 --- a/src/SLCore.UnitTests/State/ServerConnectionsProviderTests.cs +++ b/src/SLCore.UnitTests/State/ServerConnectionsProviderTests.cs @@ -19,7 +19,6 @@ */ using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.SLCore.Common.Helpers; using SonarLint.VisualStudio.SLCore.Service.Connection.Models; using SonarLint.VisualStudio.SLCore.State; @@ -32,8 +31,7 @@ public class ServerConnectionsProviderTests public void MefCtor_CheckIsExported() { MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); + MefTestHelpers.CreateExport()); } [TestMethod] @@ -45,77 +43,118 @@ public void MefCtor_CheckIsSingleton() [TestMethod] public void GetServerConnections_CorrectlyReturnsSonarQubeConnection() { - const string connectionId = "connectionId"; const string servierUriString = "http://localhost/"; var serverUri = new Uri(servierUriString); - var binding = new BoundServerProject("solution", "project", new ServerConnection.SonarQube(serverUri)); - var solutionBindingRepository = SetUpBindingRepository(binding); - var connectionIdHelper = Substitute.For(); - connectionIdHelper.GetConnectionIdFromServerConnection(Arg.Is(s => s.ServerUri == serverUri)).Returns(connectionId); - var testSubject = CreateTestSubject(solutionBindingRepository, connectionIdHelper); + var connection = new ServerConnection.SonarQube(serverUri); + var serverConnectionsRepository = SetServerConnectionsRepository(succeeded: true, connection); + var testSubject = CreateTestSubject(serverConnectionsRepository); var serverConnections = testSubject.GetServerConnections(); serverConnections.Should().HaveCount(1); - serverConnections[connectionId].Should().BeOfType().Which.serverUrl.Should().Be(servierUriString); + serverConnections[connection.Id].Should().BeOfType().Which.serverUrl.Should().Be(servierUriString); } - + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void GetServerConnections_CorrectlyReturnsSonarQubeNotifications(bool isSmartNotificationsEnabled) + { + const string servierUriString = "http://localhost/"; + var serverUri = new Uri(servierUriString); + var connection = new ServerConnection.SonarQube(serverUri, settings: new ServerConnectionSettings(isSmartNotificationsEnabled)); + var serverConnectionsRepository = SetServerConnectionsRepository(succeeded: true, connection); + var testSubject = CreateTestSubject(serverConnectionsRepository); + + var serverConnections = testSubject.GetServerConnections(); + + serverConnections.Should().HaveCount(1); + serverConnections[connection.Id].Should().BeOfType().Which.disableNotification.Should().Be(!isSmartNotificationsEnabled); + } + [TestMethod] public void GetServerConnections_CorrectlyReturnsSonarCloudConnection() { - const string connectionId = "https://sonarcloud.io/organizations/org"; - var serverUri = new Uri("https://sonarcloud.io/"); const string organizationKey = "org"; - var binding = new BoundServerProject("solution", "project", new ServerConnection.SonarCloud(organizationKey)); - var solutionBindingRepository = SetUpBindingRepository(binding); - var connectionIdHelper = Substitute.For(); - connectionIdHelper.GetConnectionIdFromServerConnection(Arg.Is(s => s.ServerUri.Equals(serverUri) && s.Id == connectionId)).Returns(connectionId); - var testSubject = CreateTestSubject(solutionBindingRepository, connectionIdHelper); + var connection = new ServerConnection.SonarCloud(organizationKey); + var serverConnectionsRepository = SetServerConnectionsRepository(succeeded: true, connection); + var testSubject = CreateTestSubject(serverConnectionsRepository); var serverConnections = testSubject.GetServerConnections(); serverConnections.Should().HaveCount(1); - serverConnections[connectionId].Should().BeOfType().Which.organization.Should().Be(organizationKey); + serverConnections[connection.Id].Should().BeOfType().Which.organization.Should().Be(organizationKey); } - + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void GetServerConnections_CorrectlyReturnsSonarCloudNotifications(bool isSmartNotificationsEnabled) + { + const string organizationKey = "org"; + var connection = new ServerConnection.SonarCloud(organizationKey, settings: new ServerConnectionSettings(isSmartNotificationsEnabled)); + var serverConnectionsRepository = SetServerConnectionsRepository(succeeded: true, connection); + var testSubject = CreateTestSubject(serverConnectionsRepository); + + var serverConnections = testSubject.GetServerConnections(); + + serverConnections.Should().HaveCount(1); + serverConnections[connection.Id].Should().BeOfType().Which.disableNotification.Should().Be(!isSmartNotificationsEnabled); + } + [TestMethod] public void GetServerConnections_CorrectlyHandlesMultipleConnections() { - var bindingSQ1 = new BoundServerProject("solution1", "project1", new ServerConnection.SonarQube(new Uri("http://localhost/"))); - var bindingSQ2 = new BoundServerProject("solution2", "project2", new ServerConnection.SonarQube(new Uri("https://next.sonarqube.org/sonarqube/"))); - var bindingSC = new BoundServerProject("solution3", "project3", new ServerConnection.SonarCloud("myorg")); - var solutionBindingRepository = SetUpBindingRepository(bindingSQ1, bindingSQ2, bindingSC); - var connectionIdHelper = Substitute.ForPartsOf(); - var testSubject = CreateTestSubject(solutionBindingRepository, connectionIdHelper); + var connectionSQ1 = new ServerConnection.SonarQube(new Uri("http://localhost/")); + var connectionSQ2 =new ServerConnection.SonarQube(new Uri("https://next.sonarqube.org/sonarqube/")); + var connectionSC = new ServerConnection.SonarCloud("myorg"); + var serverConnectionsRepository = SetServerConnectionsRepository(succeeded:true, connectionSQ1, connectionSQ2, connectionSC); + var testSubject = CreateTestSubject(serverConnectionsRepository); var serverConnections = testSubject.GetServerConnections(); serverConnections.Should().HaveCount(3); - serverConnections["sc|https://sonarcloud.io/organizations/myorg"].Should().BeOfType(); - serverConnections["sq|http://localhost/"].Should().BeOfType(); - serverConnections["sq|https://next.sonarqube.org/sonarqube/"].Should().BeOfType(); + serverConnections["https://sonarcloud.io/organizations/myorg"].Should().BeOfType(); + serverConnections["http://localhost/"].Should().BeOfType(); + serverConnections["https://next.sonarqube.org/sonarqube/"].Should().BeOfType(); } [TestMethod] public void GetServerConnections_CorrectlyHandlesNoConnections() { - var solutionBindingRepository = SetUpBindingRepository(); - var connectionIdHelper = Substitute.ForPartsOf(); - var testSubject = CreateTestSubject(solutionBindingRepository, connectionIdHelper); + var serverConnectionsRepository = SetServerConnectionsRepository(succeeded: true); + var testSubject = CreateTestSubject(serverConnectionsRepository); + + var serverConnections = testSubject.GetServerConnections(); + + serverConnections.Should().HaveCount(0); + } + + [TestMethod] + public void GetServerConnections_ConnectionsCouldNotBeRead_ReturnsNoConnection() + { + var serverConnectionsRepository = SetServerConnectionsRepository(succeeded:false); + var testSubject = CreateTestSubject(serverConnectionsRepository); var serverConnections = testSubject.GetServerConnections(); serverConnections.Should().HaveCount(0); } - private static ISolutionBindingRepository SetUpBindingRepository( - params BoundServerProject[] bindings) + private static IServerConnectionsRepository SetServerConnectionsRepository(bool succeeded, + params ServerConnection[] serverConnections) { - var bindingRepository = Substitute.For(); - bindingRepository.List().Returns(bindings); + var bindingRepository = Substitute.For(); + bindingRepository.TryGetAll(out var _).Returns(callInfo => + { + callInfo[0] = serverConnections; + return succeeded; + }); + + return bindingRepository; } - private static IServerConnectionsProvider CreateTestSubject(ISolutionBindingRepository bindingRepository, IConnectionIdHelper connectionIdHelper) => - new ServerConnectionsProvider(bindingRepository, connectionIdHelper); + private static IServerConnectionsProvider CreateTestSubject(IServerConnectionsRepository serverConnectionsRepository) => + new ServerConnectionsProvider(serverConnectionsRepository); } diff --git a/src/SLCore/Common/Helpers/ConnectionIdHelper.cs b/src/SLCore/Common/Helpers/ConnectionIdHelper.cs deleted file mode 100644 index 2759d005f..000000000 --- a/src/SLCore/Common/Helpers/ConnectionIdHelper.cs +++ /dev/null @@ -1,74 +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.ComponentModel.Composition; -using SonarLint.VisualStudio.Core.Binding; - -namespace SonarLint.VisualStudio.SLCore.Common.Helpers -{ - public interface IConnectionIdHelper - { - Uri GetUriFromConnectionId(string connectionId); - - string GetConnectionIdFromServerConnection(ServerConnection serverConnection); - } - - [Export(typeof(IConnectionIdHelper))] - [PartCreationPolicy(CreationPolicy.Shared)] - public class ConnectionIdHelper : IConnectionIdHelper - { - private const string SonarCloudPrefix = "sc|"; - private const string SonarQubePrefix = "sq|"; - public static readonly Uri SonarCloudUri = new Uri("https://sonarcloud.io"); - - public string GetConnectionIdFromServerConnection(ServerConnection serverConnection) => - serverConnection switch - { // todo https://sonarsource.atlassian.net/browse/SLVS-1419 - ServerConnection.SonarQube sonarQube => SonarQubePrefix + sonarQube.Id, - ServerConnection.SonarCloud sonarCloud => SonarCloudPrefix + sonarCloud.Id, - _ => null - }; - - 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; - } - } -} diff --git a/src/SLCore/SLCoreStrings.Designer.cs b/src/SLCore/SLCoreStrings.Designer.cs index 7863eb6b0..29c438e71 100644 --- a/src/SLCore/SLCoreStrings.Designer.cs +++ b/src/SLCore/SLCoreStrings.Designer.cs @@ -1,6 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. +// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -211,5 +212,14 @@ public static string SloopRestartFailedNotificationService_Restart { return ResourceManager.GetString("SloopRestartFailedNotificationService_Restart", resourceCulture); } } + + /// + /// Looks up a localized string similar to Unexpected server connection type. + /// + public static string UnexpectedServerConnectionType { + get { + return ResourceManager.GetString("UnexpectedServerConnectionType", resourceCulture); + } + } } } diff --git a/src/SLCore/SLCoreStrings.resx b/src/SLCore/SLCoreStrings.resx index 165e61462..207abf9a6 100644 --- a/src/SLCore/SLCoreStrings.resx +++ b/src/SLCore/SLCoreStrings.resx @@ -168,4 +168,7 @@ [SLCore.Http] Server verification result: {0} + + Unexpected server connection type + \ No newline at end of file diff --git a/src/SLCore/State/AliveConnectionTracker.cs b/src/SLCore/State/AliveConnectionTracker.cs index 146445ad4..1c47ac0fc 100644 --- a/src/SLCore/State/AliveConnectionTracker.cs +++ b/src/SLCore/State/AliveConnectionTracker.cs @@ -18,16 +18,11 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Collections.Generic; using System.ComponentModel.Composition; -using System.Linq; -using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; using SonarLint.VisualStudio.Core.Synchronization; -using SonarLint.VisualStudio.SLCore.Common.Helpers; using SonarLint.VisualStudio.SLCore.Core; using SonarLint.VisualStudio.SLCore.Service.Connection; using SonarLint.VisualStudio.SLCore.Service.Connection.Models; @@ -47,20 +42,21 @@ internal sealed class AliveConnectionTracker : IAliveConnectionTracker private readonly IThreadHandling threadHandling; private readonly IAsyncLock asyncLock; private readonly IServerConnectionsProvider serverConnectionsProvider; - private readonly ISolutionBindingRepository solutionBindingRepository; + private readonly IServerConnectionsRepository serverConnectionsRepository; [ImportingConstructor] public AliveConnectionTracker(ISLCoreServiceProvider serviceProvider, IServerConnectionsProvider serverConnectionsProvider, - ISolutionBindingRepository solutionBindingRepository, + IServerConnectionsRepository serverConnectionsRepository, IAsyncLockFactory asyncLockFactory, IThreadHandling threadHandling) { this.serviceProvider = serviceProvider; this.serverConnectionsProvider = serverConnectionsProvider; this.threadHandling = threadHandling; - this.solutionBindingRepository = solutionBindingRepository; - this.solutionBindingRepository.BindingUpdated += BindingUpdateHandler; + this.serverConnectionsRepository = serverConnectionsRepository; + this.serverConnectionsRepository.ConnectionChanged += ConnectionUpdateHandler; + this.serverConnectionsRepository.CredentialsChanged += CredentialsUpdateHandler; asyncLock = asyncLockFactory.Create(); } @@ -83,14 +79,27 @@ internal void RefreshConnectionList() foreach (var connectionId in serverConnections.Keys) { - // we don't manage connections as separate entities and we don't know when credentials actually change - connectionConfigurationService.DidChangeCredentials( - new DidChangeCredentialsParams(connectionId)); + connectionConfigurationService.DidChangeCredentials(new DidChangeCredentialsParams(connectionId)); } } } - private void BindingUpdateHandler(object sender, EventArgs arg) + internal void UpdateCredentials(string connectionId) + { + threadHandling.ThrowIfOnUIThread(); + + if (!serviceProvider.TryGetTransientService(out IConnectionConfigurationSLCoreService connectionConfigurationService)) + { + throw new InvalidOperationException(SLCoreStrings.ServiceProviderNotInitialized); + } + + using (asyncLock.Acquire()) + { + connectionConfigurationService.DidChangeCredentials(new DidChangeCredentialsParams(connectionId)); + } + } + + private void ConnectionUpdateHandler(object sender, EventArgs arg) { threadHandling.RunOnBackgroundThread(() => { @@ -100,9 +109,20 @@ private void BindingUpdateHandler(object sender, EventArgs arg) }).Forget(); } + private void CredentialsUpdateHandler(object sender, ServerConnectionUpdatedEventArgs e) + { + threadHandling.RunOnBackgroundThread(() => + { + UpdateCredentials(e.ServerConnection.Id); + + return Task.FromResult(0); + }).Forget(); + } + public void Dispose() { - solutionBindingRepository.BindingUpdated -= BindingUpdateHandler; + serverConnectionsRepository.ConnectionChanged -= ConnectionUpdateHandler; + serverConnectionsRepository.CredentialsChanged -= CredentialsUpdateHandler; asyncLock.Dispose(); } } diff --git a/src/SLCore/State/ConfigScopeUpdater.cs b/src/SLCore/State/ConfigScopeUpdater.cs index aaf6db411..95dfabfe8 100644 --- a/src/SLCore/State/ConfigScopeUpdater.cs +++ b/src/SLCore/State/ConfigScopeUpdater.cs @@ -19,11 +19,9 @@ */ using System.ComponentModel.Composition; -using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.SLCore.Common.Helpers; namespace SonarLint.VisualStudio.SLCore.State; @@ -32,16 +30,14 @@ namespace SonarLint.VisualStudio.SLCore.State; internal class ConfigScopeUpdater : IConfigScopeUpdater { private readonly IActiveConfigScopeTracker activeConfigScopeTracker; - private readonly IConnectionIdHelper connectionIdHelper; private readonly ISolutionInfoProvider solutionInfoProvider; private readonly IThreadHandling threadHandling; [ImportingConstructor] - public ConfigScopeUpdater(IActiveConfigScopeTracker activeConfigScopeTracker, ISolutionInfoProvider solutionInfoProvider, IConnectionIdHelper connectionIdHelper, IThreadHandling threadHandling) + public ConfigScopeUpdater(IActiveConfigScopeTracker activeConfigScopeTracker, ISolutionInfoProvider solutionInfoProvider, IThreadHandling threadHandling) { this.activeConfigScopeTracker = activeConfigScopeTracker; this.solutionInfoProvider = solutionInfoProvider; - this.connectionIdHelper = connectionIdHelper; this.threadHandling = threadHandling; } @@ -52,7 +48,7 @@ public void UpdateConfigScopeForCurrentSolution(BoundServerProject currentBindin threadHandling.RunOnBackgroundThread(() => { HandleConfigScopeUpdateInternal(solutionName, - connectionIdHelper.GetConnectionIdFromServerConnection(currentBinding?.ServerConnection), + currentBinding?.ServerConnection.Id, currentBinding?.ServerProjectKey); return Task.FromResult(0); }).Forget(); diff --git a/src/SLCore/State/ServerConnectionsProvider.cs b/src/SLCore/State/ServerConnectionsProvider.cs index 9fc107136..3e6276fab 100644 --- a/src/SLCore/State/ServerConnectionsProvider.cs +++ b/src/SLCore/State/ServerConnectionsProvider.cs @@ -18,10 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.Collections.Generic; using System.ComponentModel.Composition; using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.SLCore.Common.Helpers; using SonarLint.VisualStudio.SLCore.Service.Connection.Models; namespace SonarLint.VisualStudio.SLCore.State; @@ -35,51 +33,37 @@ internal interface IServerConnectionsProvider [PartCreationPolicy(CreationPolicy.Shared)] internal class ServerConnectionsProvider : IServerConnectionsProvider { - private readonly ISolutionBindingRepository solutionBindingRepository; - private readonly IConnectionIdHelper connectionIdHelper; + private readonly IServerConnectionsRepository serverConnectionsRepository; [ImportingConstructor] - public ServerConnectionsProvider(ISolutionBindingRepository solutionBindingRepository, IConnectionIdHelper connectionIdHelper) + public ServerConnectionsProvider(IServerConnectionsRepository serverConnectionsRepository) { - this.solutionBindingRepository = solutionBindingRepository; - this.connectionIdHelper = connectionIdHelper; + this.serverConnectionsRepository = serverConnectionsRepository; } public Dictionary GetServerConnections() { - return GetUniqueConnections(solutionBindingRepository.List()); + var succeeded = serverConnectionsRepository.TryGetAll(out var serverConnections); + return succeeded ? GetServerConnectionConfigurations(serverConnections).ToDictionary(conf => conf.connectionId) : []; } - private Dictionary GetUniqueConnections(IEnumerable bindings) + private static List GetServerConnectionConfigurations(IReadOnlyList serverConnections) { - var connections = new Dictionary(); - - foreach (var serverConnection in bindings.Select(b => b.ServerConnection)) - { - var serverUri = serverConnection.ServerUri; - var organization = (serverConnection as ServerConnection.SonarCloud)?.OrganizationKey; - var connectionId = connectionIdHelper.GetConnectionIdFromServerConnection(GetServerConnection(serverUri, organization)); - - connections[connectionId] = serverUri == ConnectionIdHelper.SonarCloudUri - ? new SonarCloudConnectionConfigurationDto(connectionId, true, organization) - : new SonarQubeConnectionConfigurationDto(connectionId, true, serverUri.ToString()); - } - - return connections; - } - - private static ServerConnection GetServerConnection(Uri serverUri, string organization) - { - if (organization is not null) - { - return new ServerConnection.SonarCloud(organization); - } - - if (serverUri is not null) + var serverConnectionConfigurations = new List(); + foreach (var serverConnection in serverConnections) { - return new ServerConnection.SonarQube(serverUri); + switch (serverConnection) + { + case ServerConnection.SonarQube sonarQubeConnection: + serverConnectionConfigurations.Add(new SonarQubeConnectionConfigurationDto(sonarQubeConnection.Id, !sonarQubeConnection.Settings.IsSmartNotificationsEnabled, sonarQubeConnection.ServerUri.ToString())); + break; + case ServerConnection.SonarCloud sonarCloudConnection: + serverConnectionConfigurations.Add(new SonarCloudConnectionConfigurationDto(sonarCloudConnection.Id, !sonarCloudConnection.Settings.IsSmartNotificationsEnabled, sonarCloudConnection.OrganizationKey)); + break; + default: + throw new InvalidOperationException(SLCoreStrings.UnexpectedServerConnectionType); + } } - - return null; + return serverConnectionConfigurations; } }