diff --git a/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionFactoryTests.cs b/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionFactoryTests.cs index d129bd54e9..cdd3e32cf3 100644 --- a/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionFactoryTests.cs +++ b/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionFactoryTests.cs @@ -18,81 +18,70 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ - -using System; using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.TestInfrastructure; using SonarQube.Client; -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Taint; using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Issue; using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.QualityProfile; -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.ServerSentEvents +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.ServerSentEvents; + +[TestClass] +public class SSESessionFactoryTests { - [TestClass] - public class SSESessionFactoryTests + [TestMethod] + public void MefCtor_CheckIsExported() { - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + } - [TestMethod] - public void Create_ReturnsCorrectType() - { - var testSubject = CreateTestSubject(); + [TestMethod] + public void Create_ReturnsCorrectType() + { + var testSubject = CreateTestSubject(); - var sseSession = testSubject.Create("MyProjectName", null); + var sseSession = testSubject.Create("MyProjectName", null); - sseSession.Should().NotBeNull().And.BeOfType(); - } + sseSession.Should().NotBeNull().And.BeOfType(); + } - [TestMethod] - public void Create_AfterDispose_Throws() - { - var testSubject = CreateTestSubject(); + [TestMethod] + public void Create_AfterDispose_Throws() + { + var testSubject = CreateTestSubject(); - testSubject.Dispose(); - Action act = () => testSubject.Create("MyProjectName", null); + testSubject.Dispose(); + Action act = () => testSubject.Create("MyProjectName", null); - act.Should().Throw(); - } + act.Should().Throw(); + } - [TestMethod] - public void Dispose_IdempotentAndDisposesPublishers() - { - var taintPublisherMock = new Mock(); - var issuesPublisherMock = new Mock(); - var qualityProfilePublisherMock = new Mock(); - var testSubject = CreateTestSubject(taintPublisherMock, issuesPublisherMock, qualityProfilePublisherMock); + [TestMethod] + public void Dispose_IdempotentAndDisposesPublishers() + { + var issuesPublisherMock = new Mock(); + var qualityProfilePublisherMock = new Mock(); + var testSubject = CreateTestSubject(issuesPublisherMock, qualityProfilePublisherMock); - testSubject.Dispose(); - testSubject.Dispose(); - testSubject.Dispose(); + testSubject.Dispose(); + testSubject.Dispose(); + testSubject.Dispose(); - taintPublisherMock.Verify(p => p.Dispose(), Times.Once); - issuesPublisherMock.Verify(p => p.Dispose(), Times.Once); - qualityProfilePublisherMock.Verify(p => p.Dispose(), Times.Once); - } + issuesPublisherMock.Verify(p => p.Dispose(), Times.Once); + qualityProfilePublisherMock.Verify(p => p.Dispose(), Times.Once); + } - private SSESessionFactory CreateTestSubject(Mock taintPublisher = null, - Mock issuePublisher = null, - Mock qualityProfileServerEventSourcePublisher = null) - { - return new SSESessionFactory(Mock.Of(), - taintPublisher?.Object ?? Mock.Of(), - issuePublisher?.Object ?? Mock.Of(), - qualityProfileServerEventSourcePublisher?.Object ?? Mock.Of(), + private SSESessionFactory CreateTestSubject(Mock issuePublisher = null, + Mock qualityProfileServerEventSourcePublisher = null) => + new(Mock.Of(), + issuePublisher?.Object ?? Mock.Of(), + qualityProfileServerEventSourcePublisher?.Object ?? Mock.Of(), Mock.Of(), - Mock.Of()); - } - } + Mock.Of()); } diff --git a/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionTests.cs b/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionTests.cs index d65364da23..7c148a5510 100644 --- a/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionTests.cs +++ b/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionTests.cs @@ -18,19 +18,12 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.TestInfrastructure; using SonarQube.Client; using SonarQube.Client.Models.ServerSentEvents; using SonarQube.Client.Models.ServerSentEvents.ClientContract; -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Taint; using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Issue; using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.QualityProfile; @@ -62,14 +55,10 @@ public async Task PumpAllAsync_SelectsPublisherCorrectlyAndPreservesOrderWithinT var inputSequence = new IServerEvent[] { Mock.Of(), - Mock.Of(), Mock.Of(), Mock.Of(), - Mock.Of(), - Mock.Of(), Mock.Of(), Mock.Of(), - Mock.Of() }; testScope.SetUpSwitchToBackgroundThread(); var sseStreamMock = testScope.SetUpSQServiceToSuccessfullyReturnSSEStreamReader(); @@ -77,7 +66,6 @@ public async Task PumpAllAsync_SelectsPublisherCorrectlyAndPreservesOrderWithinT await testScope.TestSubject.PumpAllAsync(); - CheckEventsSequence(testScope.TaintPublisherMock.Invocations); CheckEventsSequence(testScope.IssuePublisherMock.Invocations); CheckEventsSequence(testScope.QualityProfilePublisherMock.Invocations); @@ -100,10 +88,8 @@ public async Task PumpAllAsync_WhenNullEvent_Ignores() testScope.SetUpSSEStreamReaderToReturnEventsSequenceAndExit(sseStreamMock, new IServerEvent[] { - Mock.Of(), Mock.Of(), null, - Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of() @@ -111,7 +97,6 @@ public async Task PumpAllAsync_WhenNullEvent_Ignores() await testScope.TestSubject.PumpAllAsync(); - testScope.TaintPublisherMock.Verify(publisher => publisher.Publish(It.IsAny()), Times.Exactly(2)); testScope.IssuePublisherMock.Verify(publisher => publisher.Publish(It.IsAny()), Times.Exactly(2)); testScope.QualityProfilePublisherMock.Verify(publisher => publisher.Publish(It.IsAny()), Times.Exactly(2)); } @@ -126,16 +111,13 @@ public async Task PumpAllAsync_WhenUnsupportedEvent_Ignores() testScope.SetUpSSEStreamReaderToReturnEventsSequenceAndExit(sseStreamMock, new IServerEvent[] { - Mock.Of(), Mock.Of(), Mock.Of(), - Mock.Of(), Mock.Of() }); await testScope.TestSubject.PumpAllAsync(); - testScope.TaintPublisherMock.Verify(publisher => publisher.Publish(It.IsAny()), Times.Exactly(2)); testScope.IssuePublisherMock.Verify(publisher => publisher.Publish(It.IsAny()), Times.Exactly(1)); testScope.QualityProfilePublisherMock.Verify(publisher => publisher.Publish(It.IsAny()), Times.Exactly(1)); } @@ -232,7 +214,6 @@ public TestScope() { mockRepository = new MockRepository(MockBehavior.Strict); SonarQubeServiceMock = mockRepository.Create(); - TaintPublisherMock = mockRepository.Create(MockBehavior.Loose); IssuePublisherMock = mockRepository.Create(MockBehavior.Loose); QualityProfilePublisherMock = mockRepository.Create(MockBehavior.Loose); ThreadHandlingMock = mockRepository.Create(); @@ -241,7 +222,6 @@ public TestScope() var factory = new SSESessionFactory( SonarQubeServiceMock.Object, - TaintPublisherMock.Object, IssuePublisherMock.Object, QualityProfilePublisherMock.Object, ThreadHandlingMock.Object, @@ -249,10 +229,9 @@ public TestScope() TestSubject = factory.Create("blalala", OnSessionFailedAsyncMock.Object); } - + private Mock ThreadHandlingMock { get; } public Mock SonarQubeServiceMock { get; } - public Mock TaintPublisherMock { get; } public Mock IssuePublisherMock { get; } public Mock QualityProfilePublisherMock { get; } public Mock LoggerMock { get; } diff --git a/src/ConnectedMode.UnitTests/TestFilterableIssue.cs b/src/ConnectedMode.UnitTests/TestFilterableIssue.cs index 845a0531ed..49a776772a 100644 --- a/src/ConnectedMode.UnitTests/TestFilterableIssue.cs +++ b/src/ConnectedMode.UnitTests/TestFilterableIssue.cs @@ -25,6 +25,7 @@ namespace SonarLint.VisualStudio.ConnectedMode.UnitTests; internal class TestFilterableIssue : IFilterableIssue { + public Guid? IssueId { get; set; } public string RuleId { get; set; } public string LineHash { get; set; } public int? StartLine { get; set; } diff --git a/src/ConnectedMode/ServerSentEvents/SSESessionFactory.cs b/src/ConnectedMode/ServerSentEvents/SSESessionFactory.cs index 88659ad8b3..464c4371d8 100644 --- a/src/ConnectedMode/ServerSentEvents/SSESessionFactory.cs +++ b/src/ConnectedMode/ServerSentEvents/SSESessionFactory.cs @@ -18,213 +18,195 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; using System.ComponentModel.Composition; -using System.Threading; -using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Issue; using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.QualityProfile; -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Taint; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.ServerSentEvents; using SonarQube.Client; using SonarQube.Client.Models.ServerSentEvents; using SonarQube.Client.Models.ServerSentEvents.ClientContract; -namespace SonarLint.VisualStudio.ConnectedMode.ServerSentEvents +namespace SonarLint.VisualStudio.ConnectedMode.ServerSentEvents; + +internal delegate Task OnSessionFailedAsync(ISSESession failedSession); + +/// +/// Factory for . Responsible for disposing EventSourcePublishers +/// +internal interface ISSESessionFactory : IDisposable +{ + ISSESession Create(string projectKey, OnSessionFailedAsync onSessionFailedCallback); +} + +/// +/// Represents the session entity, that is responsible for dealing with reader +/// and propagating events to correct topic event publishers +/// +internal interface ISSESession : IDisposable +{ + Task PumpAllAsync(); +} + +[Export(typeof(ISSESessionFactory))] +[PartCreationPolicy(CreationPolicy.Shared)] +internal sealed class SSESessionFactory : ISSESessionFactory { - internal delegate Task OnSessionFailedAsync(ISSESession failedSession); + private readonly ISonarQubeService sonarQubeClient; + private readonly IIssueServerEventSourcePublisher issueServerEventSourcePublisher; + private readonly IQualityProfileServerEventSourcePublisher qualityProfileServerEventSourcePublisher; + private readonly IThreadHandling threadHandling; + + private bool disposed; + private readonly ILogger logger; + + [ImportingConstructor] + public SSESessionFactory(ISonarQubeService sonarQubeClient, + IIssueServerEventSourcePublisher issueServerEventSourcePublisher, + IQualityProfileServerEventSourcePublisher qualityProfileServerEventSourcePublisher, + IThreadHandling threadHandling, + ILogger logger) + { + this.sonarQubeClient = sonarQubeClient; + this.issueServerEventSourcePublisher = issueServerEventSourcePublisher; + this.qualityProfileServerEventSourcePublisher = qualityProfileServerEventSourcePublisher; + this.threadHandling = threadHandling; + this.logger = logger; + } - /// - /// Factory for . Responsible for disposing EventSourcePublishers - /// - internal interface ISSESessionFactory : IDisposable + public ISSESession Create(string projectKey, OnSessionFailedAsync onSessionFailedCallback) { - ISSESession Create(string projectKey, OnSessionFailedAsync onSessionFailedCallback); + if (disposed) + { + throw new ObjectDisposedException(nameof(SSESessionFactory)); + } + + var session = new SSESession( + issueServerEventSourcePublisher, + qualityProfileServerEventSourcePublisher, + projectKey, + threadHandling, + sonarQubeClient, + onSessionFailedCallback, + logger); + + return session; } - /// - /// Represents the session entity, that is responsible for dealing with reader - /// and propagating events to correct topic event publishers - /// - internal interface ISSESession : IDisposable + public void Dispose() { - Task PumpAllAsync(); + if (disposed) + { + return; + } + + issueServerEventSourcePublisher.Dispose(); + qualityProfileServerEventSourcePublisher.Dispose(); + disposed = true; } - [Export(typeof(ISSESessionFactory))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal sealed class SSESessionFactory : ISSESessionFactory + internal sealed class SSESession : ISSESession { - private readonly ISonarQubeService sonarQubeClient; - private readonly ITaintServerEventSourcePublisher taintServerEventSourcePublisher; private readonly IIssueServerEventSourcePublisher issueServerEventSourcePublisher; private readonly IQualityProfileServerEventSourcePublisher qualityProfileServerEventSourcePublisher; + private readonly string projectKey; private readonly IThreadHandling threadHandling; + private readonly ISonarQubeService sonarQubeService; + private readonly OnSessionFailedAsync onSessionFailedCallback; + private readonly ILogger logger; + private readonly CancellationTokenSource sessionTokenSource; private bool disposed; - private readonly ILogger logger; - [ImportingConstructor] - public SSESessionFactory(ISonarQubeService sonarQubeClient, - ITaintServerEventSourcePublisher taintServerEventSourcePublisher, - IIssueServerEventSourcePublisher issueServerEventSourcePublisher, - IQualityProfileServerEventSourcePublisher qualityProfileServerEventSourcePublisher, - IThreadHandling threadHandling, + internal SSESession(IIssueServerEventSourcePublisher issueServerEventSourcePublisher, + IQualityProfileServerEventSourcePublisher qualityProfileServerEventSourcePublisher, + string projectKey, + IThreadHandling threadHandling, + ISonarQubeService sonarQubeService, + OnSessionFailedAsync onSessionFailedCallback, ILogger logger) { - this.sonarQubeClient = sonarQubeClient; - this.taintServerEventSourcePublisher = taintServerEventSourcePublisher; this.issueServerEventSourcePublisher = issueServerEventSourcePublisher; this.qualityProfileServerEventSourcePublisher = qualityProfileServerEventSourcePublisher; + this.projectKey = projectKey; this.threadHandling = threadHandling; + this.sonarQubeService = sonarQubeService; + this.onSessionFailedCallback = onSessionFailedCallback; this.logger = logger; + this.sessionTokenSource = new CancellationTokenSource(); } - public ISSESession Create(string projectKey, OnSessionFailedAsync onSessionFailedCallback) + public async Task PumpAllAsync() { if (disposed) { - throw new ObjectDisposedException(nameof(SSESessionFactory)); + logger.LogVerbose("[SSESession] Session {0} is disposed", GetHashCode()); + throw new ObjectDisposedException(nameof(SSESession)); } - var session = new SSESession(taintServerEventSourcePublisher, - issueServerEventSourcePublisher, - qualityProfileServerEventSourcePublisher, - projectKey, - threadHandling, - sonarQubeClient, - onSessionFailedCallback, - logger); - - return session; - } - - public void Dispose() - { - if (disposed) - { - return; - } + await threadHandling.SwitchToBackgroundThread(); - taintServerEventSourcePublisher.Dispose(); - issueServerEventSourcePublisher.Dispose(); - qualityProfileServerEventSourcePublisher.Dispose(); - disposed = true; - } + var sseStreamReader = await sonarQubeService.CreateSSEStreamReader(projectKey, sessionTokenSource.Token); - internal sealed class SSESession : ISSESession - { - private readonly ITaintServerEventSourcePublisher taintServerEventSourcePublisher; - private readonly IIssueServerEventSourcePublisher issueServerEventSourcePublisher; - private readonly IQualityProfileServerEventSourcePublisher qualityProfileServerEventSourcePublisher; - private readonly string projectKey; - private readonly IThreadHandling threadHandling; - private readonly ISonarQubeService sonarQubeService; - private readonly OnSessionFailedAsync onSessionFailedCallback; - private readonly ILogger logger; - private readonly CancellationTokenSource sessionTokenSource; - - private bool disposed; - - internal SSESession(ITaintServerEventSourcePublisher taintServerEventSourcePublisher, - IIssueServerEventSourcePublisher issueServerEventSourcePublisher, - IQualityProfileServerEventSourcePublisher qualityProfileServerEventSourcePublisher, - string projectKey, - IThreadHandling threadHandling, - ISonarQubeService sonarQubeService, - OnSessionFailedAsync onSessionFailedCallback, - ILogger logger) + if (sseStreamReader == null) { - this.taintServerEventSourcePublisher = taintServerEventSourcePublisher; - this.issueServerEventSourcePublisher = issueServerEventSourcePublisher; - this.qualityProfileServerEventSourcePublisher = qualityProfileServerEventSourcePublisher; - this.projectKey = projectKey; - this.threadHandling = threadHandling; - this.sonarQubeService = sonarQubeService; - this.onSessionFailedCallback = onSessionFailedCallback; - this.logger = logger; - this.sessionTokenSource = new CancellationTokenSource(); + logger.LogVerbose("[SSESession] Failed to create CreateSSEStreamReader"); + return; } - public async Task PumpAllAsync() + while (!sessionTokenSource.IsCancellationRequested) { - if (disposed) + try { - logger.LogVerbose("[SSESession] Session {0} is disposed", GetHashCode()); - throw new ObjectDisposedException(nameof(SSESession)); - } + var serverEvent = await sseStreamReader.ReadAsync(); - await threadHandling.SwitchToBackgroundThread(); - - var sseStreamReader = await sonarQubeService.CreateSSEStreamReader(projectKey, sessionTokenSource.Token); - - if (sseStreamReader == null) - { - logger.LogVerbose("[SSESession] Failed to create CreateSSEStreamReader"); - return; - } - - while (!sessionTokenSource.IsCancellationRequested) - { - try + if (serverEvent == null) { - var serverEvent = await sseStreamReader.ReadAsync(); - - if (serverEvent == null) - { - continue; - } + continue; + } - logger.LogVerbose("[SSESession] Received server event: {0}", serverEvent.GetType()); + logger.LogVerbose("[SSESession] Received server event: {0}", serverEvent.GetType()); - switch (serverEvent) - { - case ITaintServerEvent taintServerEvent: - { - logger.LogVerbose("[SSESession] Publishing taint event..."); - taintServerEventSourcePublisher.Publish(taintServerEvent); - break; - } - case IIssueChangedServerEvent issueChangedServerEvent: + switch (serverEvent) + { + case IIssueChangedServerEvent issueChangedServerEvent: { logger.LogVerbose("[SSESession] Publishing issue changed event..."); issueServerEventSourcePublisher.Publish(issueChangedServerEvent); break; } - case IQualityProfileEvent qualityProfileEvent: + case IQualityProfileEvent qualityProfileEvent: { logger.LogVerbose("[SSESession] Publishing quality profile event..."); qualityProfileServerEventSourcePublisher.Publish(qualityProfileEvent); break; } - } - } - catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) - { - logger.LogVerbose($"[SSESession] Failed to handle events: {ex}"); - onSessionFailedCallback(this).Forget(); - Dispose(); - return; } } - - logger.LogVerbose("[SSESession] Session stopped, session token was canceled"); - } - - public void Dispose() - { - logger.LogVerbose("[SSESession] Disposing session: {0}", GetHashCode()); - - if (disposed) + catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) { + logger.LogVerbose($"[SSESession] Failed to handle events: {ex}"); + onSessionFailedCallback(this).Forget(); + Dispose(); return; } + } + + logger.LogVerbose("[SSESession] Session stopped, session token was canceled"); + } - disposed = true; - sessionTokenSource.Cancel(); + public void Dispose() + { + logger.LogVerbose("[SSESession] Disposing session: {0}", GetHashCode()); + + if (disposed) + { + return; } + + disposed = true; + sessionTokenSource.Cancel(); } } } diff --git a/src/Core/Analysis/AnalysisIssue.cs b/src/Core/Analysis/AnalysisIssue.cs index 4fe9c3fb32..fc02ddd91a 100644 --- a/src/Core/Analysis/AnalysisIssue.cs +++ b/src/Core/Analysis/AnalysisIssue.cs @@ -26,19 +26,21 @@ public class AnalysisIssue : IAnalysisIssue private static readonly IReadOnlyList EmptyFixes = []; public AnalysisIssue( + Guid? id, string ruleKey, - AnalysisIssueSeverity severity, - AnalysisIssueType type, - SoftwareQualitySeverity? highestSoftwareQualitySeverity, + AnalysisIssueSeverity? severity, + AnalysisIssueType? type, + Impact highestImpact, IAnalysisIssueLocation primaryLocation, IReadOnlyList flows, IReadOnlyList fixes = null, string context = null ) { + Id = id; RuleKey = ruleKey; Severity = severity; - HighestSoftwareQualitySeverity = highestSoftwareQualitySeverity; + HighestImpact = highestImpact; Type = type; PrimaryLocation = primaryLocation ?? throw new ArgumentNullException(nameof(primaryLocation)); Flows = flows ?? EmptyFlows; @@ -46,35 +48,37 @@ public AnalysisIssue( RuleDescriptionContextKey = context; } + public Guid? Id { get; } + public string RuleKey { get; } - public AnalysisIssueSeverity Severity { get; } - - public SoftwareQualitySeverity? HighestSoftwareQualitySeverity { get; } + public AnalysisIssueSeverity? Severity { get; } - public AnalysisIssueType Type { get; } + public AnalysisIssueType? Type { get; } public IReadOnlyList Flows { get; } public IAnalysisIssueLocation PrimaryLocation { get; } public IReadOnlyList Fixes { get; } + public Impact HighestImpact { get; } public string RuleDescriptionContextKey { get; } } public class AnalysisHotspotIssue : AnalysisIssue, IAnalysisHotspotIssue { - public AnalysisHotspotIssue(string ruleKey, - AnalysisIssueSeverity severity, - AnalysisIssueType type, - SoftwareQualitySeverity? highestSoftwareQualitySeverity, + public AnalysisHotspotIssue(Guid? id, + string ruleKey, + AnalysisIssueSeverity? severity, + AnalysisIssueType? type, + Impact highestImpact, IAnalysisIssueLocation primaryLocation, IReadOnlyList flows, IReadOnlyList fixes = null, string context = null, HotspotPriority? hotspotPriority = null) : - base(ruleKey, severity, type, highestSoftwareQualitySeverity, primaryLocation, flows, fixes, context) + base(id, ruleKey, severity, type, highestImpact, primaryLocation, flows, fixes, context) { HotspotPriority = hotspotPriority; } diff --git a/src/Core/Analysis/CleanCodeTaxonomy.cs b/src/Core/Analysis/CleanCodeTaxonomy.cs index 17f9ba01ab..18cc8ed4d4 100644 --- a/src/Core/Analysis/CleanCodeTaxonomy.cs +++ b/src/Core/Analysis/CleanCodeTaxonomy.cs @@ -108,4 +108,6 @@ public enum SoftwareQualitySeverity High = 3, Blocker = 4 } + + public record Impact(SoftwareQuality Quality, SoftwareQualitySeverity Severity); } diff --git a/src/Core/Analysis/IAnalysisIssue.cs b/src/Core/Analysis/IAnalysisIssue.cs index 5a80e85007..7945aa3b52 100644 --- a/src/Core/Analysis/IAnalysisIssue.cs +++ b/src/Core/Analysis/IAnalysisIssue.cs @@ -22,13 +22,13 @@ namespace SonarLint.VisualStudio.Core.Analysis { public interface IAnalysisIssue : IAnalysisIssueBase { - AnalysisIssueSeverity Severity { get; } - - SoftwareQualitySeverity? HighestSoftwareQualitySeverity { get; } + AnalysisIssueSeverity? Severity { get; } - AnalysisIssueType Type { get; } + AnalysisIssueType? Type { get; } IReadOnlyList Fixes { get; } + + Impact HighestImpact { get; } } public interface IAnalysisHotspotIssue : IAnalysisIssue @@ -38,6 +38,11 @@ public interface IAnalysisHotspotIssue : IAnalysisIssue public interface IAnalysisIssueBase { + /// + /// The id of the issue that comes from SlCore + /// + Guid? Id { get; } + string RuleKey { get; } IReadOnlyList Flows { get; } diff --git a/src/Core/IEducation.cs b/src/Core/IEducation.cs index ac802a37da..2b848bd09f 100644 --- a/src/Core/IEducation.cs +++ b/src/Core/IEducation.cs @@ -18,6 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using SonarLint.VisualStudio.Core.Suppressions; + namespace SonarLint.VisualStudio.Core { /// @@ -32,6 +34,7 @@ public interface IEducation /// be displayed in the IDE. Otherwise, the rule help will be displayed in the /// browser i.e. at rules.sonarsource.com /// Key for the How to fix it Context acquired from a specific issue. Can be null. - void ShowRuleHelp(SonarCompositeRuleId ruleId, string issueContext); + /// The SlCore issue ID for which the rule help should be shown.s + void ShowRuleHelp(SonarCompositeRuleId ruleId, Guid? issueId, string issueContext); } } diff --git a/src/Core/Suppressions/FilterableRoslynIssue.cs b/src/Core/Suppressions/FilterableRoslynIssue.cs index e72f86399c..1bc9fcd74c 100644 --- a/src/Core/Suppressions/FilterableRoslynIssue.cs +++ b/src/Core/Suppressions/FilterableRoslynIssue.cs @@ -40,6 +40,10 @@ public FilterableRoslynIssue(string ruleId, string filePath, int startLine, int RoslynStartColumn = startColumn; } + /// + /// Always null, as this Id is specific to SlCore + /// + public Guid? IssueId => null; public string RuleId { get; } public string FilePath { get; } public int? StartLine => RoslynStartLine; diff --git a/src/Core/Suppressions/IFilterableIssue.cs b/src/Core/Suppressions/IFilterableIssue.cs index 34ef449f47..bb5e5256ef 100644 --- a/src/Core/Suppressions/IFilterableIssue.cs +++ b/src/Core/Suppressions/IFilterableIssue.cs @@ -26,6 +26,11 @@ namespace SonarLint.VisualStudio.Core.Suppressions /// public interface IFilterableIssue { + /// + /// The id of the issue that comes from SlCore + /// Nullable due to the fact that some issues do not come from SlCore (e.g. Roslyn) + /// + Guid? IssueId { get; } string RuleId { get; } string FilePath { get; } string LineHash { get; } diff --git a/src/Core/ThreadHandlingExtensions.cs b/src/Core/ThreadHandlingExtensions.cs index 1ebb7feaa5..a704257eab 100644 --- a/src/Core/ThreadHandlingExtensions.cs +++ b/src/Core/ThreadHandlingExtensions.cs @@ -27,10 +27,10 @@ public static Task RunOnBackgroundThread(this IThreadHandling threadHandling, Ac syncMethod(); return Task.CompletedTask; }); - - public static Task RunOnBackgroundThread(this IThreadHandling threadHandling, Func asyncMethod) => Task.FromResult(threadHandling.RunOnBackgroundThread(async () => + + public static Task RunOnBackgroundThread(this IThreadHandling threadHandling, Func asyncMethod) => threadHandling.RunOnBackgroundThread(async () => { await asyncMethod(); return 0; - })); + }); } diff --git a/src/Education.UnitTests/EducationTests.cs b/src/Education.UnitTests/EducationTests.cs index 8a0548916c..c049485d9e 100644 --- a/src/Education.UnitTests/EducationTests.cs +++ b/src/Education.UnitTests/EducationTests.cs @@ -18,14 +18,10 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Threading; using System.Windows.Documents; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Education.Commands; +using SonarLint.VisualStudio.Core.Suppressions; using SonarLint.VisualStudio.Education.Rule; using SonarLint.VisualStudio.Education.XamlGenerator; using SonarLint.VisualStudio.TestInfrastructure; @@ -53,11 +49,11 @@ public void ShowRuleHelp_KnownRule_DocumentIsDisplayedInToolWindow() var ruleId = new SonarCompositeRuleId("repoKey", "ruleKey"); var ruleInfo = Mock.Of(); - ruleMetaDataProvider.Setup(x => x.GetRuleInfoAsync(It.IsAny())).ReturnsAsync(ruleInfo); + ruleMetaDataProvider.Setup(x => x.GetRuleInfoAsync(It.IsAny(), It.IsAny())).ReturnsAsync(ruleInfo); var flowDocument = Mock.Of(); var ruleHelpXamlBuilder = new Mock(); - ruleHelpXamlBuilder.Setup(x => x.Create(ruleInfo, /* todo */ null)).Returns(flowDocument); + ruleHelpXamlBuilder.Setup(x => x.Create(ruleInfo, /* todo by SLVS-1630 */ null)).Returns(flowDocument); var ruleDescriptionToolWindow = new Mock(); @@ -74,10 +70,10 @@ public void ShowRuleHelp_KnownRule_DocumentIsDisplayedInToolWindow() toolWindowService.Invocations.Should().HaveCount(0); // Act - testSubject.ShowRuleHelp(ruleId, null); + testSubject.ShowRuleHelp(ruleId, null, null); - ruleMetaDataProvider.Verify(x => x.GetRuleInfoAsync(ruleId), Times.Once); - ruleHelpXamlBuilder.Verify(x => x.Create(ruleInfo, /* todo */ null), Times.Once); + ruleMetaDataProvider.Verify(x => x.GetRuleInfoAsync(ruleId, It.IsAny()), Times.Once); + ruleHelpXamlBuilder.Verify(x => x.Create(ruleInfo, /* todo by SLVS-1630 */ null), Times.Once); ruleDescriptionToolWindow.Verify(x => x.UpdateContent(flowDocument), Times.Once); toolWindowService.Verify(x => x.Show(RuleHelpToolWindow.ToolWindowId), Times.Once); @@ -95,9 +91,9 @@ public void ShowRuleHelp_FailedToDisplayRule_RuleIsShownInBrowser() var ruleId = new SonarCompositeRuleId("repoKey", "ruleKey"); var ruleInfo = Mock.Of(); - ruleMetadataProvider.Setup(x => x.GetRuleInfoAsync(It.IsAny())).ReturnsAsync(ruleInfo); + ruleMetadataProvider.Setup(x => x.GetRuleInfoAsync(It.IsAny(), It.IsAny())).ReturnsAsync(ruleInfo); - ruleHelpXamlBuilder.Setup(x => x.Create(ruleInfo, /* todo */ null)).Throws(new Exception("some layout error")); + ruleHelpXamlBuilder.Setup(x => x.Create(ruleInfo, /* todo by SLVS-1630 */ null)).Throws(new Exception("some layout error")); var testSubject = CreateEducation( toolWindowService.Object, @@ -107,9 +103,9 @@ public void ShowRuleHelp_FailedToDisplayRule_RuleIsShownInBrowser() toolWindowService.Reset(); // Called in the constructor, so need to reset to clear the list of invocations - testSubject.ShowRuleHelp(ruleId, /* todo */ null); + testSubject.ShowRuleHelp(ruleId, null, /* todo by SLVS-1630 */null); - ruleMetadataProvider.Verify(x => x.GetRuleInfoAsync(ruleId), Times.Once); + ruleMetadataProvider.Verify(x => x.GetRuleInfoAsync(ruleId, It.IsAny()), Times.Once); showRuleInBrowser.Verify(x => x.ShowRuleDescription(ruleId), Times.Once); // should have attempted to build the rule, but failed @@ -126,7 +122,7 @@ public void ShowRuleHelp_UnknownRule_RuleIsShownInBrowser() var showRuleInBrowser = new Mock(); var unknownRule = new SonarCompositeRuleId("known", "xxx"); - ruleMetadataProvider.Setup(x => x.GetRuleInfoAsync(unknownRule)).ReturnsAsync((IRuleInfo)null); + ruleMetadataProvider.Setup(x => x.GetRuleInfoAsync(unknownRule, It.IsAny())).ReturnsAsync((IRuleInfo)null); var testSubject = CreateEducation( toolWindowService.Object, @@ -136,9 +132,9 @@ public void ShowRuleHelp_UnknownRule_RuleIsShownInBrowser() toolWindowService.Reset(); // Called in the constructor, so need to reset to clear the list of invocations - testSubject.ShowRuleHelp(unknownRule, /* todo */ null); + testSubject.ShowRuleHelp(unknownRule, null, /* todo by SLVS-1630 */ null); - ruleMetadataProvider.Verify(x => x.GetRuleInfoAsync(unknownRule), Times.Once); + ruleMetadataProvider.Verify(x => x.GetRuleInfoAsync(unknownRule, It.IsAny()), Times.Once); showRuleInBrowser.Verify(x => x.ShowRuleDescription(unknownRule), Times.Once); // Should not have attempted to build the rule @@ -146,6 +142,27 @@ public void ShowRuleHelp_UnknownRule_RuleIsShownInBrowser() toolWindowService.Invocations.Should().HaveCount(0); } + [TestMethod] + public void ShowRuleHelp_FilterableIssueProvided_CallsGetRuleInfoForIssue() + { + var toolWindowService = new Mock(); + var ruleMetadataProvider = new Mock(); + var ruleHelpXamlBuilder = new Mock(); + var showRuleInBrowser = new Mock(); + var issueId = Guid.NewGuid(); + var ruleId = new SonarCompositeRuleId("repoKey", "ruleKey"); + ruleMetadataProvider.Setup(x => x.GetRuleInfoAsync(ruleId, issueId)).ReturnsAsync((IRuleInfo)null); + var testSubject = CreateEducation( + toolWindowService.Object, + ruleMetadataProvider.Object, + showRuleInBrowser.Object, + ruleHelpXamlBuilder.Object); + + testSubject.ShowRuleHelp(ruleId,issueId, null); + + ruleMetadataProvider.Verify(x => x.GetRuleInfoAsync(ruleId, issueId), Times.Once); + } + private Education CreateEducation(IToolWindowService toolWindowService = null, IRuleMetaDataProvider ruleMetadataProvider = null, IShowRuleInBrowser showRuleInBrowser = null, diff --git a/src/Education.UnitTests/ErrorList/SonarErrorListEventProcessorTests.cs b/src/Education.UnitTests/ErrorList/SonarErrorListEventProcessorTests.cs index f81eeeb0c5..8ce2c980d8 100644 --- a/src/Education.UnitTests/ErrorList/SonarErrorListEventProcessorTests.cs +++ b/src/Education.UnitTests/ErrorList/SonarErrorListEventProcessorTests.cs @@ -18,11 +18,10 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using FluentAssertions; using Microsoft.VisualStudio.Shell.TableControl; -using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Suppressions; using SonarLint.VisualStudio.Education.SonarLint.VisualStudio.Education.ErrorList; using SonarLint.VisualStudio.Infrastructure.VS; using SonarLint.VisualStudio.TestInfrastructure; @@ -68,10 +67,30 @@ public void PreprocessNavigateToHelp_IsASonarRule_EventIsHandledAndEducationServ errorListHelper.Verify(x => x.TryGetRuleId(handle, out ruleId)); education.Invocations.Should().HaveCount(1); - education.Verify(x => x.ShowRuleHelp(ruleId, /* todo */ null)); + education.Verify(x => x.ShowRuleHelp(ruleId, null, /* todo by SLVS-1630 */ null)); eventArgs.Handled.Should().BeTrue(); } + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void PreprocessNavigateToHelp_IsASonarRule_EducationServiceIsCalledWithIssueId(bool getFilterableIssueResult) + { + SonarCompositeRuleId ruleId; + IFilterableIssue filterableIssue = new Mock().Object; + SonarCompositeRuleId.TryParse("cpp:S123", out ruleId); + var handle = Mock.Of(); + var errorListHelper = CreateErrorListHelper(isSonarRule: true, ruleId); + errorListHelper.Setup(x => x.TryGetFilterableIssue(It.IsAny(), out filterableIssue)).Returns(getFilterableIssueResult); + var education = new Mock(); + var testSubject = CreateTestSubject(education.Object, errorListHelper.Object); + + testSubject.PreprocessNavigateToHelp(handle, new TableEntryEventArgs()); + + education.Invocations.Should().HaveCount(1); + education.Verify(x => x.ShowRuleHelp(ruleId, filterableIssue.IssueId, null)); + } + private static Mock CreateErrorListHelper(bool isSonarRule, SonarCompositeRuleId ruleId) { var mock = new Mock(); diff --git a/src/Education.UnitTests/Rule/RuleInfoConverterTests.cs b/src/Education.UnitTests/Rule/RuleInfoConverterTests.cs new file mode 100644 index 0000000000..a857fe0145 --- /dev/null +++ b/src/Education.UnitTests/Rule/RuleInfoConverterTests.cs @@ -0,0 +1,430 @@ +/* + * 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.Education.Rule; +using SonarLint.VisualStudio.SLCore.Common.Models; +using SonarLint.VisualStudio.SLCore.Protocol; +using SonarLint.VisualStudio.SLCore.Service.Issue.Models; +using SonarLint.VisualStudio.SLCore.Service.Rules.Models; +using SonarLint.VisualStudio.TestInfrastructure; +using CleanCodeAttribute = SonarLint.VisualStudio.SLCore.Common.Models.CleanCodeAttribute; +using IssueSeverity = SonarLint.VisualStudio.SLCore.Common.Models.IssueSeverity; +using Language = SonarLint.VisualStudio.SLCore.Common.Models.Language; +using SoftwareQuality = SonarLint.VisualStudio.SLCore.Common.Models.SoftwareQuality; +using RuleCleanCodeAttribute = SonarLint.VisualStudio.Core.Analysis.CleanCodeAttribute; +using RuleSoftwareQuality = SonarLint.VisualStudio.Core.Analysis.SoftwareQuality; +using RuleSoftwareQualitySeverity = SonarLint.VisualStudio.Core.Analysis.SoftwareQualitySeverity; + +namespace SonarLint.VisualStudio.Education.UnitTests.Rule; + +[TestClass] +public class RuleInfoConverterTests +{ + private RuleInfoConverter testSubject; + + [TestInitialize] + public void TestInitialize() => testSubject = new RuleInfoConverter(); + + [TestMethod] + public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported(); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [DataTestMethod] + [DataRow(IssueSeverity.INFO, RuleIssueSeverity.Info)] + [DataRow(IssueSeverity.MAJOR, RuleIssueSeverity.Major)] + [DataRow(IssueSeverity.BLOCKER, RuleIssueSeverity.Blocker)] + [DataRow(IssueSeverity.CRITICAL, RuleIssueSeverity.Critical)] + [DataRow(IssueSeverity.MINOR, RuleIssueSeverity.Minor)] + public void Convert_RuleDetails_CorrectlyConvertsSeverity(IssueSeverity slCore, RuleIssueSeverity expected) + { + var ruleDetails = new EffectiveRuleDetailsDto( + default, + default, + default, + new StandardModeDetails(slCore, default), + default, + default, + default); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.Severity.Should().Be(expected); + } + + [DataTestMethod] + [DataRow(RuleType.CODE_SMELL, RuleIssueType.CodeSmell)] + [DataRow(RuleType.VULNERABILITY, RuleIssueType.Vulnerability)] + [DataRow(RuleType.BUG, RuleIssueType.Bug)] + [DataRow(RuleType.SECURITY_HOTSPOT, RuleIssueType.Hotspot)] + public void Convert_RuleDetails_CorrectlyConvertsType(RuleType slCore, RuleIssueType expected) + { + var ruleDetails = new EffectiveRuleDetailsDto( + default, + default, + default, + new StandardModeDetails(default, slCore), + default, + default, + default); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.IssueType.Should().Be(expected); + } + + [DataTestMethod] + [DataRow(CleanCodeAttribute.CONVENTIONAL, RuleCleanCodeAttribute.Conventional)] + [DataRow(CleanCodeAttribute.FORMATTED, RuleCleanCodeAttribute.Formatted)] + [DataRow(CleanCodeAttribute.IDENTIFIABLE, RuleCleanCodeAttribute.Identifiable)] + [DataRow(CleanCodeAttribute.CLEAR, RuleCleanCodeAttribute.Clear)] + [DataRow(CleanCodeAttribute.COMPLETE, RuleCleanCodeAttribute.Complete)] + [DataRow(CleanCodeAttribute.EFFICIENT, RuleCleanCodeAttribute.Efficient)] + [DataRow(CleanCodeAttribute.LOGICAL, RuleCleanCodeAttribute.Logical)] + [DataRow(CleanCodeAttribute.DISTINCT, RuleCleanCodeAttribute.Distinct)] + [DataRow(CleanCodeAttribute.FOCUSED, RuleCleanCodeAttribute.Focused)] + [DataRow(CleanCodeAttribute.MODULAR, RuleCleanCodeAttribute.Modular)] + [DataRow(CleanCodeAttribute.TESTED, RuleCleanCodeAttribute.Tested)] + [DataRow(CleanCodeAttribute.LAWFUL, RuleCleanCodeAttribute.Lawful)] + [DataRow(CleanCodeAttribute.RESPECTFUL, RuleCleanCodeAttribute.Respectful)] + [DataRow(CleanCodeAttribute.TRUSTWORTHY, RuleCleanCodeAttribute.Trustworthy)] + public void Convert_RuleDetails_CorrectlyConvertsCleanCodeAttribute(CleanCodeAttribute slCore, RuleCleanCodeAttribute expected) + { + var ruleDetails = new EffectiveRuleDetailsDto( + default, + default, + default, + new MQRModeDetails(slCore, default), + default, + default, + default); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.CleanCodeAttribute.Should().Be(expected); + } + + [TestMethod] + public void Convert_RuleDetails_CorrectlyConvertsImpacts() + { + var ruleDetails = new EffectiveRuleDetailsDto( + default, + default, + default, + new MQRModeDetails(default, [ + new ImpactDto(SoftwareQuality.SECURITY, ImpactSeverity.HIGH), + new ImpactDto(SoftwareQuality.RELIABILITY, ImpactSeverity.LOW), + new ImpactDto(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.MEDIUM) + ]), + default, + default, + default); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.DefaultImpacts.Should().BeEquivalentTo(new Dictionary + { + { RuleSoftwareQuality.Security, RuleSoftwareQualitySeverity.High }, + { RuleSoftwareQuality.Reliability, RuleSoftwareQualitySeverity.Low }, + { RuleSoftwareQuality.Maintainability, RuleSoftwareQualitySeverity.Medium } + }); + } + + [TestMethod] + public void Convert_RuleDetails_Standard_SimpleRuleDescription() + { + const string rulekey = "rule:key1"; + var ruleDetails = new EffectiveRuleDetailsDto( + rulekey, + "name", + Language.JS, + new StandardModeDetails(IssueSeverity.CRITICAL, RuleType.VULNERABILITY), + VulnerabilityProbability.MEDIUM, + Either.CreateLeft( + new RuleMonolithicDescriptionDto("content")), + new List()); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.Should().BeEquivalentTo(new RuleInfo(rulekey, + "content", + "name", + RuleIssueSeverity.Critical, + RuleIssueType.Vulnerability, + null, + null, + null)); + } + + [TestMethod] + public void Convert_RuleDetails_MQR_SimpleRuleDescription() + { + const string rulekey = "rule:key1"; + var ruleDetails = new EffectiveRuleDetailsDto( + rulekey, + "name", + Language.JS, + new MQRModeDetails(CleanCodeAttribute.MODULAR, [ + new ImpactDto(SoftwareQuality.SECURITY, ImpactSeverity.HIGH), + new ImpactDto(SoftwareQuality.RELIABILITY, ImpactSeverity.LOW) + ]), + VulnerabilityProbability.MEDIUM, + Either.CreateLeft( + new RuleMonolithicDescriptionDto("content")), + new List()); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.Should().BeEquivalentTo(new RuleInfo(rulekey, + "content", + "name", + null, + null, + null, + RuleCleanCodeAttribute.Modular, + new Dictionary + { + { RuleSoftwareQuality.Security, RuleSoftwareQualitySeverity.High }, { RuleSoftwareQuality.Reliability, RuleSoftwareQualitySeverity.Low } + })); + } + + [TestMethod] + public void Convert_RuleDetails_Standard_RichRuleDescription() + { + const string rulekey = "rule:key1"; + var ruleSplitDescriptionDto = new RuleSplitDescriptionDto("intro", new List()); + var ruleDetails = new EffectiveRuleDetailsDto( + rulekey, + "name", + Language.CPP, + new StandardModeDetails(IssueSeverity.MINOR, RuleType.BUG), + null, + Either.CreateRight(ruleSplitDescriptionDto), + new List { new("ignored", default, default, default) }); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.Should().BeEquivalentTo(new RuleInfo(rulekey, + null, + "name", + RuleIssueSeverity.Minor, + RuleIssueType.Bug, + ruleSplitDescriptionDto, + null, + null)); + } + + [TestMethod] + public void Convert_RuleDetails_MQR_RichRuleDescription() + { + const string rulekey = "rule:key1"; + var ruleSplitDescriptionDto = new RuleSplitDescriptionDto("intro", new List()); + var ruleDetails = new EffectiveRuleDetailsDto( + rulekey, + "name", + Language.CPP, + new MQRModeDetails(CleanCodeAttribute.RESPECTFUL, [ + new ImpactDto(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.MEDIUM) + ]), + null, + Either.CreateRight(ruleSplitDescriptionDto), + new List { new("ignored", default, default, default) }); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.Should().BeEquivalentTo(new RuleInfo(rulekey, + null, + "name", + null, + null, + ruleSplitDescriptionDto, + RuleCleanCodeAttribute.Respectful, + new Dictionary { { RuleSoftwareQuality.Maintainability, RuleSoftwareQualitySeverity.Medium } })); + } + + [DataTestMethod] + [DataRow(IssueSeverity.INFO, RuleIssueSeverity.Info)] + [DataRow(IssueSeverity.MAJOR, RuleIssueSeverity.Major)] + [DataRow(IssueSeverity.BLOCKER, RuleIssueSeverity.Blocker)] + [DataRow(IssueSeverity.CRITICAL, RuleIssueSeverity.Critical)] + [DataRow(IssueSeverity.MINOR, RuleIssueSeverity.Minor)] + public void Convert_IssueDetails_CorrectlyConvertsSeverity(IssueSeverity slCore, RuleIssueSeverity expected) + { + Guid.NewGuid(); + var ruleDetails = CreateEffectiveIssueDetailsDto(new StandardModeDetails(slCore, default)); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.Severity.Should().Be(expected); + } + + [DataTestMethod] + [DataRow(RuleType.CODE_SMELL, RuleIssueType.CodeSmell)] + [DataRow(RuleType.VULNERABILITY, RuleIssueType.Vulnerability)] + [DataRow(RuleType.BUG, RuleIssueType.Bug)] + [DataRow(RuleType.SECURITY_HOTSPOT, RuleIssueType.Hotspot)] + public void Convert_IssueDetails_CorrectlyConvertsType(RuleType slCore, RuleIssueType expected) + { + Guid.NewGuid(); + var ruleDetails = CreateEffectiveIssueDetailsDto(new StandardModeDetails(default, slCore)); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.IssueType.Should().Be(expected); + } + + [DataTestMethod] + [DataRow(CleanCodeAttribute.CONVENTIONAL, RuleCleanCodeAttribute.Conventional)] + [DataRow(CleanCodeAttribute.FORMATTED, RuleCleanCodeAttribute.Formatted)] + [DataRow(CleanCodeAttribute.IDENTIFIABLE, RuleCleanCodeAttribute.Identifiable)] + [DataRow(CleanCodeAttribute.CLEAR, RuleCleanCodeAttribute.Clear)] + [DataRow(CleanCodeAttribute.COMPLETE, RuleCleanCodeAttribute.Complete)] + [DataRow(CleanCodeAttribute.EFFICIENT, RuleCleanCodeAttribute.Efficient)] + [DataRow(CleanCodeAttribute.LOGICAL, RuleCleanCodeAttribute.Logical)] + [DataRow(CleanCodeAttribute.DISTINCT, RuleCleanCodeAttribute.Distinct)] + [DataRow(CleanCodeAttribute.FOCUSED, RuleCleanCodeAttribute.Focused)] + [DataRow(CleanCodeAttribute.MODULAR, RuleCleanCodeAttribute.Modular)] + [DataRow(CleanCodeAttribute.TESTED, RuleCleanCodeAttribute.Tested)] + [DataRow(CleanCodeAttribute.LAWFUL, RuleCleanCodeAttribute.Lawful)] + [DataRow(CleanCodeAttribute.RESPECTFUL, RuleCleanCodeAttribute.Respectful)] + [DataRow(CleanCodeAttribute.TRUSTWORTHY, RuleCleanCodeAttribute.Trustworthy)] + public void Convert_IssueDetails_CorrectlyConvertsCleanCodeAttribute(CleanCodeAttribute slCore, RuleCleanCodeAttribute expected) + { + Guid.NewGuid(); + var ruleDetails = CreateEffectiveIssueDetailsDto(new MQRModeDetails(slCore, default)); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.CleanCodeAttribute.Should().Be(expected); + } + + [TestMethod] + public void Convert_IssueDetails_CorrectlyConvertsImpacts() + { + Guid.NewGuid(); + var ruleDetails = CreateEffectiveIssueDetailsDto(new MQRModeDetails(default, [ + new ImpactDto(SoftwareQuality.SECURITY, ImpactSeverity.HIGH), + new ImpactDto(SoftwareQuality.RELIABILITY, ImpactSeverity.LOW), + new ImpactDto(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.MEDIUM) + ])); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.DefaultImpacts.Should().BeEquivalentTo(new Dictionary + { + { RuleSoftwareQuality.Security, RuleSoftwareQualitySeverity.High }, + { RuleSoftwareQuality.Reliability, RuleSoftwareQualitySeverity.Low }, + { RuleSoftwareQuality.Maintainability, RuleSoftwareQualitySeverity.Medium } + }); + } + + [TestMethod] + public void Convert_IssueDetails_Standard_SimpleRuleDescription() + { + Guid.NewGuid(); + var ruleDetails = CreateEffectiveIssueDetailsDto(new StandardModeDetails(IssueSeverity.CRITICAL, RuleType.VULNERABILITY), + Either.CreateLeft( + new RuleMonolithicDescriptionDto("content"))); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.Should().BeEquivalentTo(new RuleInfo(null, + "content", + null, + RuleIssueSeverity.Critical, + RuleIssueType.Vulnerability, + null, + null, + null)); + } + + [TestMethod] + public void Convert_IssueDetails_MQR_SimpleRuleDescription() + { + Guid.NewGuid(); + var ruleDetails = CreateEffectiveIssueDetailsDto(new MQRModeDetails(CleanCodeAttribute.MODULAR, default), Either.CreateLeft( + new RuleMonolithicDescriptionDto("content"))); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.Should().BeEquivalentTo(new RuleInfo(null, + "content", + null, + null, + null, + null, + RuleCleanCodeAttribute.Modular, + null)); + } + + [TestMethod] + public void Convert_IssueDetails_Standard_RichRuleDescription() + { + Guid.NewGuid(); + var ruleSplitDescriptionDto = new RuleSplitDescriptionDto("intro", new List()); + var ruleDetails = CreateEffectiveIssueDetailsDto(new StandardModeDetails(IssueSeverity.MINOR, RuleType.BUG), + Either.CreateRight(ruleSplitDescriptionDto)); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.Should().BeEquivalentTo(new RuleInfo(null, + null, + null, + RuleIssueSeverity.Minor, + RuleIssueType.Bug, + ruleSplitDescriptionDto, + null, + null)); + } + + [TestMethod] + public void Convert_IssueDetails_MQR_RichRuleDescription() + { + Guid.NewGuid(); + var ruleSplitDescriptionDto = new RuleSplitDescriptionDto("intro", new List()); + var ruleDetails = CreateEffectiveIssueDetailsDto(new MQRModeDetails(CleanCodeAttribute.RESPECTFUL, default), + Either.CreateRight(ruleSplitDescriptionDto)); + + var ruleInfo = testSubject.Convert(ruleDetails); + + ruleInfo.Should().BeEquivalentTo(new RuleInfo(null, + null, + null, + null, + null, + ruleSplitDescriptionDto, + RuleCleanCodeAttribute.Respectful, + null)); + } + + private static EffectiveIssueDetailsDto CreateEffectiveIssueDetailsDto( + Either severityDetails, + Either description = default) => + new( + default, + default, + default, + default, + description, + default, + severityDetails, + default); +} diff --git a/src/Education.UnitTests/Rule/SLCoreRuleMetaDataProviderTests.cs b/src/Education.UnitTests/Rule/SLCoreRuleMetaDataProviderTests.cs index 6cad5562b5..676b139045 100644 --- a/src/Education.UnitTests/Rule/SLCoreRuleMetaDataProviderTests.cs +++ b/src/Education.UnitTests/Rule/SLCoreRuleMetaDataProviderTests.cs @@ -18,358 +18,268 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Analysis; using SonarLint.VisualStudio.Education.Rule; -using SonarLint.VisualStudio.SLCore.Common.Models; using SonarLint.VisualStudio.SLCore.Core; using SonarLint.VisualStudio.SLCore.Protocol; using SonarLint.VisualStudio.SLCore.Service.Rules; using SonarLint.VisualStudio.SLCore.Service.Rules.Models; using SonarLint.VisualStudio.SLCore.State; using SonarLint.VisualStudio.TestInfrastructure; -using CleanCodeAttribute = SonarLint.VisualStudio.SLCore.Common.Models.CleanCodeAttribute; -using IssueSeverity = SonarLint.VisualStudio.SLCore.Common.Models.IssueSeverity; -using Language = SonarLint.VisualStudio.SLCore.Common.Models.Language; -using SoftwareQuality = SonarLint.VisualStudio.SLCore.Common.Models.SoftwareQuality; -using RuleCleanCodeAttribute = SonarLint.VisualStudio.Core.Analysis.CleanCodeAttribute; -using RuleSoftwareQuality = SonarLint.VisualStudio.Core.Analysis.SoftwareQuality; -using RuleSoftwareQualitySeverity = SonarLint.VisualStudio.Core.Analysis.SoftwareQualitySeverity; +using SonarLint.VisualStudio.SLCore.Service.Issue; +using SonarLint.VisualStudio.SLCore.Service.Issue.Models; namespace SonarLint.VisualStudio.Education.UnitTests.Rule; [TestClass] public class SLCoreRuleMetaDataProviderTests { + private static readonly SonarCompositeRuleId CompositeRuleId = new("rule", "key1"); + [TestMethod] - public void MefCtor_CheckIsExported() - { + public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported( MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport()); - } [TestMethod] - public void MefCtor_CheckIsSingleton() - { - MefTestHelpers.CheckIsSingletonMefComponent(); - } - - [DataTestMethod] - [DataRow(IssueSeverity.INFO, RuleIssueSeverity.Info)] - [DataRow(IssueSeverity.MAJOR, RuleIssueSeverity.Major)] - [DataRow(IssueSeverity.BLOCKER, RuleIssueSeverity.Blocker)] - [DataRow(IssueSeverity.CRITICAL, RuleIssueSeverity.Critical)] - [DataRow(IssueSeverity.MINOR, RuleIssueSeverity.Minor)] - public async Task GetRuleInfoAsync_CorrectlyConvertsSeverity(IssueSeverity slCore, RuleIssueSeverity expected) - { - const string rulekey = "rule:key1"; - const string configScopeId = "configscope"; + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + [TestMethod] + public async Task GetRuleInfoAsync_NoActiveScope_ReturnsNull() + { var testSubject = - CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out _); - SetUpServiceProvider(serviceProviderMock, out var rulesServiceMock); - SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope(configScopeId)); - SetupRulesService(rulesServiceMock, rulekey, configScopeId, new EffectiveRuleDetailsDto( - default, - default, - slCore, - default, - default, - default, - default, - default, - default, - default, - default)); + CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out var logger); + SetUpServiceProvider(serviceProviderMock, out _); + SetUpConfigScopeTracker(configScopeTrackerMock, null); - var ruleInfo = await testSubject.GetRuleInfoAsync(new SonarCompositeRuleId("rule", "key1")); + var ruleInfo = await testSubject.GetRuleInfoAsync(CompositeRuleId); - ruleInfo.Severity.Should().Be(expected); + ruleInfo.Should().BeNull(); + logger.AssertNoOutputMessages(); } - - [DataTestMethod] - [DataRow(RuleType.CODE_SMELL, RuleIssueType.CodeSmell)] - [DataRow(RuleType.VULNERABILITY, RuleIssueType.Vulnerability)] - [DataRow(RuleType.BUG, RuleIssueType.Bug)] - [DataRow(RuleType.SECURITY_HOTSPOT, RuleIssueType.Hotspot)] - public async Task GetRuleInfoAsync_CorrectlyConvertsType(RuleType slCore, RuleIssueType expected) + + [TestMethod] + public async Task GetRuleInfoAsync_ServiceUnavailable_ReturnsNull() { - const string rulekey = "rule:key1"; - const string configScopeId = "configscope"; + var testSubject = CreateTestSubject(out _, out var configScopeTrackerMock, out var logger); + SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope("id")); + + var ruleInfo = await testSubject.GetRuleInfoAsync(CompositeRuleId); + + ruleInfo.Should().BeNull(); + logger.AssertNoOutputMessages(); + } + [TestMethod] + public void GetRuleInfoAsync_ServiceThrows_ReturnsNullAndLogs() + { var testSubject = - CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out _); + CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out var logger); + SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope("id")); SetUpServiceProvider(serviceProviderMock, out var rulesServiceMock); - SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope(configScopeId)); - SetupRulesService(rulesServiceMock, rulekey, configScopeId, new EffectiveRuleDetailsDto( - default, - default, - default, - slCore, - default, - default, - default, - default, - default, - default, - default)); + rulesServiceMock + .Setup(x => x.GetEffectiveRuleDetailsAsync(It.IsAny())) + .ThrowsAsync(new Exception("my message")); - var ruleInfo = await testSubject.GetRuleInfoAsync(new SonarCompositeRuleId("rule", "key1")); + var act = () => testSubject.GetRuleInfoAsync(CompositeRuleId); - ruleInfo.IssueType.Should().Be(expected); + act.Should().NotThrow(); + logger.AssertPartialOutputStringExists("my message"); } - [DataTestMethod] - [DataRow(CleanCodeAttribute.CONVENTIONAL, RuleCleanCodeAttribute.Conventional)] - [DataRow(CleanCodeAttribute.FORMATTED, RuleCleanCodeAttribute.Formatted)] - [DataRow(CleanCodeAttribute.IDENTIFIABLE, RuleCleanCodeAttribute.Identifiable)] - [DataRow(CleanCodeAttribute.CLEAR, RuleCleanCodeAttribute.Clear)] - [DataRow(CleanCodeAttribute.COMPLETE, RuleCleanCodeAttribute.Complete)] - [DataRow(CleanCodeAttribute.EFFICIENT, RuleCleanCodeAttribute.Efficient)] - [DataRow(CleanCodeAttribute.LOGICAL, RuleCleanCodeAttribute.Logical)] - [DataRow(CleanCodeAttribute.DISTINCT, RuleCleanCodeAttribute.Distinct)] - [DataRow(CleanCodeAttribute.FOCUSED, RuleCleanCodeAttribute.Focused)] - [DataRow(CleanCodeAttribute.MODULAR, RuleCleanCodeAttribute.Modular)] - [DataRow(CleanCodeAttribute.TESTED, RuleCleanCodeAttribute.Tested)] - [DataRow(CleanCodeAttribute.LAWFUL, RuleCleanCodeAttribute.Lawful)] - [DataRow(CleanCodeAttribute.RESPECTFUL, RuleCleanCodeAttribute.Respectful)] - [DataRow(CleanCodeAttribute.TRUSTWORTHY, RuleCleanCodeAttribute.Trustworthy)] - public async Task GetRuleInfoAsync_CorrectlyConvertsCleanCodeAttribute(CleanCodeAttribute slCore, RuleCleanCodeAttribute expected) + [TestMethod] + public async Task GetRuleInfoAsync_ForIssue_NoActiveScope_ReturnsNull() { - const string rulekey = "rule:key1"; - const string configScopeId = "configscope"; - var testSubject = - CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out _); - SetUpServiceProvider(serviceProviderMock, out var rulesServiceMock); - SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope(configScopeId)); - SetupRulesService(rulesServiceMock, rulekey, configScopeId, new EffectiveRuleDetailsDto( - default, - default, - default, - default, - slCore, - default, - default, - default, - default, - default, - default)); + CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out var logger); + SetUpIssueServiceProvider(serviceProviderMock, out _); + SetUpConfigScopeTracker(configScopeTrackerMock, null); + var issueId = Guid.NewGuid(); - var ruleInfo = await testSubject.GetRuleInfoAsync(new SonarCompositeRuleId("rule", "key1")); + var ruleInfo = await testSubject.GetRuleInfoAsync(default,issueId); - ruleInfo.CleanCodeAttribute.Should().Be(expected); + ruleInfo.Should().BeNull(); + logger.AssertNoOutputMessages(); } - + [TestMethod] - public async Task GetRuleInfoAsync_CorrectlyConvertsImpacts() + public async Task GetRuleInfoAsync_ForIssue_ServiceUnavailable_ReturnsNull() { - const string rulekey = "rule:key1"; - const string configScopeId = "configscope"; + var testSubject = CreateTestSubject(out _, out var configScopeTrackerMock, out var logger); + SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope("id")); + + var ruleInfo = await testSubject.GetRuleInfoAsync(default,Guid.NewGuid()); + ruleInfo.Should().BeNull(); + logger.AssertNoOutputMessages(); + } + + [TestMethod] + public void GetRuleInfoAsync_ForIssue_ServiceThrows_ReturnsNullAndLogs() + { var testSubject = - CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out _); - SetUpServiceProvider(serviceProviderMock, out var rulesServiceMock); - SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope(configScopeId)); - SetupRulesService(rulesServiceMock, rulekey, configScopeId, new EffectiveRuleDetailsDto( - default, - default, - default, - default, - default, - default, - new List - { - new(SoftwareQuality.SECURITY, ImpactSeverity.HIGH), - new(SoftwareQuality.RELIABILITY, ImpactSeverity.LOW), - new(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.MEDIUM), - }, - default, - default, - default, - default)); + CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out var logger); + SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope("id")); + SetUpIssueServiceProvider(serviceProviderMock, out var issueServiceMock); + issueServiceMock + .Setup(x => x.GetEffectiveIssueDetailsAsync(It.IsAny())) + .ThrowsAsync(new Exception("my message")); - var ruleInfo = await testSubject.GetRuleInfoAsync(new SonarCompositeRuleId("rule", "key1")); + var act = () => testSubject.GetRuleInfoAsync(default,Guid.NewGuid()); - ruleInfo.DefaultImpacts.Should().BeEquivalentTo(new Dictionary - { - { RuleSoftwareQuality.Security , SoftwareQualitySeverity.High}, - { RuleSoftwareQuality.Reliability , SoftwareQualitySeverity.Low}, - { RuleSoftwareQuality.Maintainability , SoftwareQualitySeverity.Medium}, - }); + act.Should().NotThrow(); + logger.AssertPartialOutputStringExists("my message"); } - + [TestMethod] - public async Task GetRuleInfoAsync_SimpleRuleDescription() + public async Task GetRuleInfoAsync_FilterableIssueNull_CallsGetEffectiveRuleDetailsAsync() { - const string rulekey = "rule:key1"; - const string configScopeId = "configscope"; - var testSubject = - CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out var logger); + CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out _); + SetUpIssueServiceProvider(serviceProviderMock, out var issueServiceMock); SetUpServiceProvider(serviceProviderMock, out var rulesServiceMock); - SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope(configScopeId)); - SetupRulesService(rulesServiceMock, rulekey, configScopeId, new EffectiveRuleDetailsDto( - rulekey, - "name", - IssueSeverity.CRITICAL, - RuleType.SECURITY_HOTSPOT, - CleanCodeAttribute.MODULAR, - CleanCodeAttributeCategory.INTENTIONAL, - new List - { - new(SoftwareQuality.SECURITY, ImpactSeverity.HIGH), - new(SoftwareQuality.RELIABILITY, ImpactSeverity.LOW) - }, - Language.JS, - VulnerabilityProbability.MEDIUM, - Either.CreateLeft( - new RuleMonolithicDescriptionDto("content")), - new List())); - - var ruleInfo = await testSubject.GetRuleInfoAsync(new SonarCompositeRuleId("rule", "key1")); - - ruleInfo.Should().BeEquivalentTo(new RuleInfo(rulekey, - "content", - "name", - RuleIssueSeverity.Critical, - RuleIssueType.Hotspot, - null, - Core.Analysis.CleanCodeAttribute.Modular, - new Dictionary - { - { Core.Analysis.SoftwareQuality.Security, SoftwareQualitySeverity.High }, - { Core.Analysis.SoftwareQuality.Reliability, SoftwareQualitySeverity.Low } - })); + SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope("configscope")); + + await testSubject.GetRuleInfoAsync(CompositeRuleId, null); + + rulesServiceMock.Verify(x => x.GetEffectiveRuleDetailsAsync(It.Is(p => p.ruleKey == CompositeRuleId.ToString())), Times.Once); + issueServiceMock.Verify(x => x.GetEffectiveIssueDetailsAsync(It.IsAny()), Times.Never); } [TestMethod] - public async Task GetRuleInfoAsync_RichRuleDescription() + public async Task GetRuleInfoAsync_FilterableIssueIdNull_CallsGetEffectiveRuleDetailsAsync() { - const string rulekey = "rule:key1"; - const string configScopeId = "configscope"; - var testSubject = - CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out var logger); + CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out _); + SetUpIssueServiceProvider(serviceProviderMock, out var issueServiceMock); SetUpServiceProvider(serviceProviderMock, out var rulesServiceMock); - SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope(configScopeId)); - var ruleSplitDescriptionDto = new RuleSplitDescriptionDto("intro", new List()); - SetupRulesService(rulesServiceMock, rulekey, configScopeId, new EffectiveRuleDetailsDto( - rulekey, - "name", - IssueSeverity.MINOR, - RuleType.BUG, - CleanCodeAttribute.RESPECTFUL, - CleanCodeAttributeCategory.ADAPTABLE, - new List - { - new(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.MEDIUM) - }, - Language.CPP, - null, - Either.CreateRight(ruleSplitDescriptionDto), - new List - { - new("ignored", default, default, default) - })); - - var ruleInfo = await testSubject.GetRuleInfoAsync(new SonarCompositeRuleId("rule", "key1")); - - ruleInfo.Should().BeEquivalentTo(new RuleInfo(rulekey, - null, - "name", - RuleIssueSeverity.Minor, - RuleIssueType.Bug, - ruleSplitDescriptionDto, - Core.Analysis.CleanCodeAttribute.Respectful, - new Dictionary - { - { Core.Analysis.SoftwareQuality.Maintainability, SoftwareQualitySeverity.Medium } - })); - logger.AssertNoOutputMessages(); + SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope("configscope")); + Guid? issueId = null; + + await testSubject.GetRuleInfoAsync(CompositeRuleId, issueId); + + rulesServiceMock.Verify(x => x.GetEffectiveRuleDetailsAsync(It.Is(p => p.ruleKey == CompositeRuleId.ToString())), Times.Once); + issueServiceMock.Verify(x => x.GetEffectiveIssueDetailsAsync(It.IsAny()), Times.Never); } [TestMethod] - public async Task GetRuleInfoAsync_NoActiveScope_ReturnsNull() + public async Task GetRuleInfoAsync_FilterableIssueIdNotNull_CallsGetEffectiveIssueDetailsAsync() { - var testSubject = - CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out var logger); - SetUpServiceProvider(serviceProviderMock, out _); - SetUpConfigScopeTracker(configScopeTrackerMock, null); + var configScopeId = "configscope"; + var issueId = Guid.NewGuid(); + var testSubject = CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out _); + SetUpIssueServiceProvider(serviceProviderMock, out var issueServiceMock); + SetUpServiceProvider(serviceProviderMock, out var rulesServiceMock); + SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope(configScopeId)); + SetupIssuesService(issueServiceMock, issueId, configScopeId, CreateEffectiveIssueDetailsDto(new MQRModeDetails(default, default))); - var ruleInfo = await testSubject.GetRuleInfoAsync(new SonarCompositeRuleId("rule", "key1")); + await testSubject.GetRuleInfoAsync(CompositeRuleId, issueId); - ruleInfo.Should().BeNull(); - logger.AssertNoOutputMessages(); + rulesServiceMock.Verify(x => x.GetEffectiveRuleDetailsAsync(It.IsAny()), Times.Never); + issueServiceMock.Verify(x => x.GetEffectiveIssueDetailsAsync(It.Is(p => p.issueId == issueId)), Times.Once); } [TestMethod] - public async Task GetRuleInfoAsync_ServiceUnavailable_ReturnsNull() + public async Task GetRuleInfoAsync_GetEffectiveIssueDetailsAsyncThrows_CallsGetEffectiveRuleDetailsAsync() { - var testSubject = CreateTestSubject(out _, out var configScopeTrackerMock, out var logger); - SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope("id")); + var testSubject = + CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out _); + SetUpIssueServiceProvider(serviceProviderMock, out var issueServiceMock); + SetUpServiceProvider(serviceProviderMock, out var rulesServiceMock); + SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope("configscope")); + var issueId = Guid.NewGuid(); + issueServiceMock + .Setup(x => x.GetEffectiveIssueDetailsAsync(It.IsAny())) + .ThrowsAsync(new Exception("my message")); - var ruleInfo = await testSubject.GetRuleInfoAsync(new SonarCompositeRuleId("rule", "key1")); + await testSubject.GetRuleInfoAsync(CompositeRuleId, issueId); - ruleInfo.Should().BeNull(); - logger.AssertNoOutputMessages(); + rulesServiceMock.Verify(x => x.GetEffectiveRuleDetailsAsync(It.Is(p => p.ruleKey == CompositeRuleId.ToString())), Times.Once); + issueServiceMock.Verify(x => x.GetEffectiveIssueDetailsAsync(It.Is(p => p.issueId == issueId)), Times.Once); } [TestMethod] - public void GetRuleInfoAsync_ServiceThrows_ReturnsNullAndLogs() + public async Task GetRuleInfoAsync_BothServicesThrow_ReturnsNull() { var testSubject = - CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out var logger); - SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope("id")); + CreateTestSubject(out var serviceProviderMock, out var configScopeTrackerMock, out _); + SetUpIssueServiceProvider(serviceProviderMock, out var issueServiceMock); SetUpServiceProvider(serviceProviderMock, out var rulesServiceMock); + SetUpConfigScopeTracker(configScopeTrackerMock, new ConfigurationScope("configscope")); + var issueId = Guid.NewGuid(); + issueServiceMock + .Setup(x => x.GetEffectiveIssueDetailsAsync(It.IsAny())) + .ThrowsAsync(new Exception("my message")); rulesServiceMock .Setup(x => x.GetEffectiveRuleDetailsAsync(It.IsAny())) .ThrowsAsync(new Exception("my message")); - var act = () => testSubject.GetRuleInfoAsync(new SonarCompositeRuleId("rule", "key1")); + var result = await testSubject.GetRuleInfoAsync(CompositeRuleId, issueId); - act.Should().NotThrow(); - logger.AssertPartialOutputStringExists("my message"); + result.Should().BeNull(); + rulesServiceMock.Verify(x => x.GetEffectiveRuleDetailsAsync(It.Is(p => p.ruleKey == CompositeRuleId.ToString())), Times.Once); + issueServiceMock.Verify(x => x.GetEffectiveIssueDetailsAsync(It.Is(p => p.issueId == issueId)), Times.Once); } - private static void SetUpConfigScopeTracker(Mock configScopeTrackerMock, - ConfigurationScope scope) - { + private static void SetUpConfigScopeTracker( + Mock configScopeTrackerMock, + ConfigurationScope scope) => configScopeTrackerMock.SetupGet(x => x.Current).Returns(scope); - } - private static void SetupRulesService(Mock rulesServiceMock, string rulekey, string configScopeId, - EffectiveRuleDetailsDto response) + private static void SetupIssuesService( + Mock issuesServiceMock, + Guid id, + string configScopeId, + EffectiveIssueDetailsDto response) => + issuesServiceMock + .Setup(r => r.GetEffectiveIssueDetailsAsync(It.Is(p => p.configurationScopeId == configScopeId && p.issueId == id))) + .ReturnsAsync(new GetEffectiveIssueDetailsResponse(response)); + + private static void SetUpServiceProvider( + Mock serviceProviderMock, + out Mock rulesServiceMock) { - rulesServiceMock - .Setup(r => r.GetEffectiveRuleDetailsAsync(It.Is(p => - p.ruleKey == rulekey && p.configurationScopeId == configScopeId))) - .ReturnsAsync(new GetEffectiveRuleDetailsResponse(response)); + rulesServiceMock = new Mock(); + var rulesService = rulesServiceMock.Object; + serviceProviderMock.Setup(x => x.TryGetTransientService(out rulesService)).Returns(true); } - private static void SetUpServiceProvider(Mock serviceProviderMock, - out Mock rulesServiceMock) + private static void SetUpIssueServiceProvider( + Mock serviceProviderMock, + out Mock rulesServiceMock) { - rulesServiceMock = new Mock(); + rulesServiceMock = new Mock(); var rulesService = rulesServiceMock.Object; serviceProviderMock.Setup(x => x.TryGetTransientService(out rulesService)).Returns(true); } - private static SLCoreRuleMetaDataProvider CreateTestSubject(out Mock serviceProviderMock, + private static SLCoreRuleMetaDataProvider CreateTestSubject( + out Mock serviceProviderMock, out Mock configScopeTrackerMock, out TestLogger logger) { serviceProviderMock = new Mock(); configScopeTrackerMock = new Mock(); + configScopeTrackerMock = new Mock(); + var ruleInfoConverter = new Mock(); + ruleInfoConverter.Setup(x => x.Convert(It.IsAny())).Returns(new RuleInfo(default, default, default, default, default, default, default, default)); logger = new TestLogger(); - return new SLCoreRuleMetaDataProvider(serviceProviderMock.Object, configScopeTrackerMock.Object, logger); + return new SLCoreRuleMetaDataProvider(serviceProviderMock.Object, configScopeTrackerMock.Object, ruleInfoConverter.Object, logger); } + + private static EffectiveIssueDetailsDto CreateEffectiveIssueDetailsDto(Either severityDetails, + Either description = default) => + new( + default, + default, + default, + default, + description, + default, + severityDetails, + default); } diff --git a/src/Education/Controls/RuleHelpUserControl.xaml.cs b/src/Education/Controls/RuleHelpUserControl.xaml.cs index 1eab623ec3..1566f7b904 100644 --- a/src/Education/Controls/RuleHelpUserControl.xaml.cs +++ b/src/Education/Controls/RuleHelpUserControl.xaml.cs @@ -57,7 +57,7 @@ public void HandleRequestNavigate(object sender, RequestNavigateEventArgs e) // in which case it needs to be handed over to the education service. if (SonarRuleIdUriEncoderDecoder.TryDecodeToCompositeRuleId(e.Uri, out SonarCompositeRuleId compositeRuleId)) { - education.ShowRuleHelp(compositeRuleId, null); + education.ShowRuleHelp(compositeRuleId, null, null); return; } diff --git a/src/Education/Education.cs b/src/Education/Education.cs index f1902994e7..c2bb19d406 100644 --- a/src/Education/Education.cs +++ b/src/Education/Education.cs @@ -18,13 +18,10 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; using System.ComponentModel.Composition; -using System.Threading; -using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Education.Commands; +using SonarLint.VisualStudio.Core.Suppressions; using SonarLint.VisualStudio.Education.Rule; using SonarLint.VisualStudio.Education.XamlGenerator; using SonarLint.VisualStudio.Infrastructure.VS; @@ -68,16 +65,16 @@ public Education(IToolWindowService toolWindowService, IRuleMetaDataProvider rul this.threadHandling = threadHandling; } - public void ShowRuleHelp(SonarCompositeRuleId ruleId, string issueContext) + public void ShowRuleHelp(SonarCompositeRuleId ruleId, Guid? issueId, string issueContext) { - ShowRuleHelpAsync(ruleId, issueContext).Forget(); + ShowRuleHelpAsync(ruleId, issueId, issueContext).Forget(); } - private async Task ShowRuleHelpAsync(SonarCompositeRuleId ruleId, string issueContext) + private async Task ShowRuleHelpAsync(SonarCompositeRuleId ruleId, Guid? issueId, string issueContext) { await threadHandling.SwitchToBackgroundThread(); - var ruleInfo = await ruleMetadataProvider.GetRuleInfoAsync(ruleId); + var ruleInfo = await ruleMetadataProvider.GetRuleInfoAsync(ruleId, issueId); await threadHandling.RunOnUIThreadAsync(() => { diff --git a/src/Education/ErrorList/SonarErrorListEventProcessor.cs b/src/Education/ErrorList/SonarErrorListEventProcessor.cs index 539a2eb7f7..5c3828a5a2 100644 --- a/src/Education/ErrorList/SonarErrorListEventProcessor.cs +++ b/src/Education/ErrorList/SonarErrorListEventProcessor.cs @@ -56,9 +56,10 @@ public override void PreprocessNavigateToHelp( if (errorListHelper.TryGetRuleId(entry, out var ruleId)) { + errorListHelper.TryGetFilterableIssue(entry, out var filterableIssue); logger.LogVerbose(Resources.ErrorList_Processor_SonarRuleDetected, ruleId); - educationService.ShowRuleHelp(ruleId, /* todo */ null); + educationService.ShowRuleHelp(ruleId, filterableIssue?.IssueId, /* todo by SLVS-1630 */null); // Mark the event as handled to stop the normal VS "show help in browser" behaviour handled = true; diff --git a/src/Education/Rule/IRuleInfo.cs b/src/Education/Rule/IRuleInfo.cs index 4e618df639..12d789e6f8 100644 --- a/src/Education/Rule/IRuleInfo.cs +++ b/src/Education/Rule/IRuleInfo.cs @@ -48,7 +48,7 @@ public enum RuleIssueType Hotspot, // SonarQube serialization = SECURITY_HOTSPOT Unknown } - + /// /// Help data about a single rule, extracted using the Java plugin API /// @@ -60,16 +60,16 @@ public interface IRuleInfo string FullRuleKey { get; } string Name { get; } - - RuleIssueSeverity Severity { get; } - RuleIssueType IssueType { get; } + RuleIssueSeverity? Severity { get; } + + RuleIssueType? IssueType { get; } /// /// The HTML description, tweaked so it can be parsed as XML /// string Description { get; } - + RuleSplitDescriptionDto RichRuleDescriptionDto { get; } CleanCodeAttribute? CleanCodeAttribute { get; } @@ -80,7 +80,7 @@ public interface IRuleInfo public class RuleInfo : IRuleInfo { public RuleInfo(string fullRuleKey, string description, string name, - RuleIssueSeverity severity, RuleIssueType issueType, RuleSplitDescriptionDto richRuleDescriptionDto, + RuleIssueSeverity? severity, RuleIssueType? issueType, RuleSplitDescriptionDto richRuleDescriptionDto, CleanCodeAttribute? cleanCodeAttribute, Dictionary defaultImpacts) { FullRuleKey = fullRuleKey; @@ -97,9 +97,9 @@ public RuleInfo(string fullRuleKey, string description, string name, public string Name { get; private set; } - public RuleIssueSeverity Severity { get; set; } + public RuleIssueSeverity? Severity { get; set; } - public RuleIssueType IssueType { get; private set; } + public RuleIssueType? IssueType { get; private set; } public string Description { get; private set; } diff --git a/src/Education/Rule/IRuleInfoConverter.cs b/src/Education/Rule/IRuleInfoConverter.cs new file mode 100644 index 0000000000..39c4571258 --- /dev/null +++ b/src/Education/Rule/IRuleInfoConverter.cs @@ -0,0 +1,100 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using SonarLint.VisualStudio.Core.Analysis; +using SonarLint.VisualStudio.SLCore.Common.Helpers; +using SonarLint.VisualStudio.SLCore.Common.Models; +using SonarLint.VisualStudio.SLCore.Service.Rules.Models; +using CleanCodeAttribute = SonarLint.VisualStudio.Core.Analysis.CleanCodeAttribute; +using IssueSeverity = SonarLint.VisualStudio.SLCore.Common.Models.IssueSeverity; +using SoftwareQuality = SonarLint.VisualStudio.Core.Analysis.SoftwareQuality; + +namespace SonarLint.VisualStudio.Education.Rule; + +internal interface IRuleInfoConverter +{ + IRuleInfo Convert(IRuleDetails details); +} + +[Export(typeof(IRuleInfoConverter))] +[PartCreationPolicy(CreationPolicy.Shared)] +public class RuleInfoConverter : IRuleInfoConverter +{ + [ImportingConstructor] + public RuleInfoConverter() { } + + public IRuleInfo Convert(IRuleDetails details) => + new RuleInfo(details.key, + HtmlXmlCompatibilityHelper.EnsureHtmlIsXml(details.description?.Left?.htmlContent), + details.name, + Convert(details.severityDetails.Left?.severity), + Convert(details.severityDetails.Left?.type), + details.description?.Right, + Convert(details.severityDetails.Right?.cleanCodeAttribute), + Convert(details.severityDetails.Right?.impacts)); + + private static RuleIssueSeverity? Convert(IssueSeverity? issueSeverity) => + issueSeverity switch + { + IssueSeverity.BLOCKER => RuleIssueSeverity.Blocker, + IssueSeverity.CRITICAL => RuleIssueSeverity.Critical, + IssueSeverity.MAJOR => RuleIssueSeverity.Major, + IssueSeverity.MINOR => RuleIssueSeverity.Minor, + IssueSeverity.INFO => RuleIssueSeverity.Info, + null => null, + _ => throw new ArgumentOutOfRangeException(nameof(issueSeverity), issueSeverity, null) + }; + + private static RuleIssueType? Convert(RuleType? ruleType) => + ruleType switch + { + RuleType.CODE_SMELL => RuleIssueType.CodeSmell, + RuleType.BUG => RuleIssueType.Bug, + RuleType.VULNERABILITY => RuleIssueType.Vulnerability, + RuleType.SECURITY_HOTSPOT => RuleIssueType.Hotspot, + null => null, + _ => throw new ArgumentOutOfRangeException(nameof(ruleType), ruleType, null) + }; + + private static CleanCodeAttribute? Convert(SLCore.Common.Models.CleanCodeAttribute? cleanCodeAttribute) => + cleanCodeAttribute switch + { + SLCore.Common.Models.CleanCodeAttribute.CONVENTIONAL => CleanCodeAttribute.Conventional, + SLCore.Common.Models.CleanCodeAttribute.FORMATTED => CleanCodeAttribute.Formatted, + SLCore.Common.Models.CleanCodeAttribute.IDENTIFIABLE => CleanCodeAttribute.Identifiable, + SLCore.Common.Models.CleanCodeAttribute.CLEAR => CleanCodeAttribute.Clear, + SLCore.Common.Models.CleanCodeAttribute.COMPLETE => CleanCodeAttribute.Complete, + SLCore.Common.Models.CleanCodeAttribute.EFFICIENT => CleanCodeAttribute.Efficient, + SLCore.Common.Models.CleanCodeAttribute.LOGICAL => CleanCodeAttribute.Logical, + SLCore.Common.Models.CleanCodeAttribute.DISTINCT => CleanCodeAttribute.Distinct, + SLCore.Common.Models.CleanCodeAttribute.FOCUSED => CleanCodeAttribute.Focused, + SLCore.Common.Models.CleanCodeAttribute.MODULAR => CleanCodeAttribute.Modular, + SLCore.Common.Models.CleanCodeAttribute.TESTED => CleanCodeAttribute.Tested, + SLCore.Common.Models.CleanCodeAttribute.LAWFUL => CleanCodeAttribute.Lawful, + SLCore.Common.Models.CleanCodeAttribute.RESPECTFUL => CleanCodeAttribute.Respectful, + SLCore.Common.Models.CleanCodeAttribute.TRUSTWORTHY => CleanCodeAttribute.Trustworthy, + null => null, + _ => throw new ArgumentOutOfRangeException(nameof(cleanCodeAttribute), cleanCodeAttribute, null) + }; + + private static Dictionary Convert(List cleanCodeAttribute) => + cleanCodeAttribute?.ToDictionary(x => x.softwareQuality.ToSoftwareQuality(), x => x.impactSeverity.ToSoftwareQualitySeverity()); +} diff --git a/src/Education/Rule/IRuleMetaDataProvider.cs b/src/Education/Rule/IRuleMetaDataProvider.cs index 50f063940a..bf1c9e056e 100644 --- a/src/Education/Rule/IRuleMetaDataProvider.cs +++ b/src/Education/Rule/IRuleMetaDataProvider.cs @@ -18,8 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.Threading; -using System.Threading.Tasks; using SonarLint.VisualStudio.Core; namespace SonarLint.VisualStudio.Education.Rule @@ -27,9 +25,10 @@ namespace SonarLint.VisualStudio.Education.Rule public interface IRuleMetaDataProvider { /// - /// Returns rule information for the specified rule ID, or null if a rule description - /// could not be found. + /// If is NOT null, returns rule information for the specified issue ID + /// If is null, returns the rule information for the specified rule ID + /// If no rule information can be found, null is returned. /// - Task GetRuleInfoAsync(SonarCompositeRuleId ruleId); + Task GetRuleInfoAsync(SonarCompositeRuleId ruleId, Guid? issueId = null); } } diff --git a/src/Education/Rule/SLCoreRuleMetaDataProvider.cs b/src/Education/Rule/SLCoreRuleMetaDataProvider.cs index 131cb9b91f..8986f656e9 100644 --- a/src/Education/Rule/SLCoreRuleMetaDataProvider.cs +++ b/src/Education/Rule/SLCoreRuleMetaDataProvider.cs @@ -18,22 +18,12 @@ * 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 SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Analysis; -using SonarLint.VisualStudio.SLCore.Common.Helpers; -using SonarLint.VisualStudio.SLCore.Common.Models; using SonarLint.VisualStudio.SLCore.Core; +using SonarLint.VisualStudio.SLCore.Service.Issue; using SonarLint.VisualStudio.SLCore.Service.Rules; -using SonarLint.VisualStudio.SLCore.Service.Rules.Models; using SonarLint.VisualStudio.SLCore.State; -using CleanCodeAttribute = SonarLint.VisualStudio.Core.Analysis.CleanCodeAttribute; -using IssueSeverity = SonarLint.VisualStudio.SLCore.Common.Models.IssueSeverity; -using SoftwareQuality = SonarLint.VisualStudio.Core.Analysis.SoftwareQuality; namespace SonarLint.VisualStudio.Education.Rule; @@ -42,28 +32,44 @@ namespace SonarLint.VisualStudio.Education.Rule; internal class SLCoreRuleMetaDataProvider : IRuleMetaDataProvider { private readonly IActiveConfigScopeTracker activeConfigScopeTracker; + private readonly IRuleInfoConverter ruleInfoConverter; private readonly ILogger logger; private readonly ISLCoreServiceProvider slCoreServiceProvider; [ImportingConstructor] public SLCoreRuleMetaDataProvider(ISLCoreServiceProvider slCoreServiceProvider, - IActiveConfigScopeTracker activeConfigScopeTracker, ILogger logger) + IActiveConfigScopeTracker activeConfigScopeTracker, + IRuleInfoConverter ruleInfoConverter, + ILogger logger) { this.slCoreServiceProvider = slCoreServiceProvider; this.activeConfigScopeTracker = activeConfigScopeTracker; + this.ruleInfoConverter = ruleInfoConverter; this.logger = logger; } - public async Task GetRuleInfoAsync(SonarCompositeRuleId ruleId) + /// + public async Task GetRuleInfoAsync(SonarCompositeRuleId ruleId, Guid? issueId = null) { - if (activeConfigScopeTracker.Current is { Id: var configurationScopeId } - && slCoreServiceProvider.TryGetTransientService(out IRulesSLCoreService rulesRpcService)) + if (activeConfigScopeTracker.Current is not { Id: var configurationScopeId }) + { + return null; + } + + var ruleInfoFromIssue = issueId != null ? await GetEffectiveIssueDetailsAsync(configurationScopeId, issueId.Value) : null; + + return ruleInfoFromIssue ?? await GetEffectiveRuleDetailsAsync(configurationScopeId, ruleId); + } + + private async Task GetEffectiveIssueDetailsAsync(string configurationScopeId, Guid issueId) + { + if (slCoreServiceProvider.TryGetTransientService(out IIssueSLCoreService rulesRpcService)) { try { - var ruleDetailsResponse = await rulesRpcService.GetEffectiveRuleDetailsAsync( - new GetEffectiveRuleDetailsParams(configurationScopeId, ruleId.ToString())); - return Convert(ruleDetailsResponse.details); + var issueDetailsResponse = await rulesRpcService.GetEffectiveIssueDetailsAsync( + new GetEffectiveIssueDetailsParams(configurationScopeId, issueId)); + return ruleInfoConverter.Convert(issueDetailsResponse.details); } catch (Exception e) { @@ -74,68 +80,22 @@ public async Task GetRuleInfoAsync(SonarCompositeRuleId ruleId) return null; } - private static RuleInfo Convert(EffectiveRuleDetailsDto effectiveRuleDetailsAsync) => - new(effectiveRuleDetailsAsync.key, - HtmlXmlCompatibilityHelper.EnsureHtmlIsXml(effectiveRuleDetailsAsync.description?.Left?.htmlContent), - effectiveRuleDetailsAsync.name, - Convert(effectiveRuleDetailsAsync.severity), - Convert(effectiveRuleDetailsAsync.type), - effectiveRuleDetailsAsync.description?.Right, - Convert(effectiveRuleDetailsAsync.cleanCodeAttribute), - Convert(effectiveRuleDetailsAsync.defaultImpacts)); - - private static Dictionary Convert(List cleanCodeAttribute) => - cleanCodeAttribute?.ToDictionary(x => Convert(x.softwareQuality), x => x.impactSeverity.ToSoftwareQualitySeverity()); - - - private static RuleIssueSeverity Convert(IssueSeverity issueSeverity) => - issueSeverity switch - { - IssueSeverity.BLOCKER => RuleIssueSeverity.Blocker, - IssueSeverity.CRITICAL => RuleIssueSeverity.Critical, - IssueSeverity.MAJOR => RuleIssueSeverity.Major, - IssueSeverity.MINOR => RuleIssueSeverity.Minor, - IssueSeverity.INFO => RuleIssueSeverity.Info, - _ => throw new ArgumentOutOfRangeException(nameof(issueSeverity), issueSeverity, null) - }; - - private static RuleIssueType Convert(RuleType ruleType) => - ruleType switch - { - RuleType.CODE_SMELL => RuleIssueType.CodeSmell, - RuleType.BUG => RuleIssueType.Bug, - RuleType.VULNERABILITY => RuleIssueType.Vulnerability, - RuleType.SECURITY_HOTSPOT => RuleIssueType.Hotspot, - _ => throw new ArgumentOutOfRangeException(nameof(ruleType), ruleType, null) - }; - - private static SoftwareQuality Convert(SLCore.Common.Models.SoftwareQuality argSoftwareQuality) => - argSoftwareQuality switch + private async Task GetEffectiveRuleDetailsAsync(string configurationScopeId, SonarCompositeRuleId ruleId) + { + if (slCoreServiceProvider.TryGetTransientService(out IRulesSLCoreService rulesRpcService)) { - SLCore.Common.Models.SoftwareQuality.MAINTAINABILITY => SoftwareQuality.Maintainability, - SLCore.Common.Models.SoftwareQuality.RELIABILITY => SoftwareQuality.Reliability, - SLCore.Common.Models.SoftwareQuality.SECURITY => SoftwareQuality.Security, - _ => throw new ArgumentOutOfRangeException(nameof(argSoftwareQuality), argSoftwareQuality, null) - }; + try + { + var ruleDetailsResponse = await rulesRpcService.GetEffectiveRuleDetailsAsync( + new GetEffectiveRuleDetailsParams(configurationScopeId, ruleId.ToString())); + return ruleInfoConverter.Convert(ruleDetailsResponse.details); + } + catch (Exception e) + { + logger.WriteLine(e.ToString()); + } + } - private static CleanCodeAttribute? Convert(SLCore.Common.Models.CleanCodeAttribute? cleanCodeAttribute) => - cleanCodeAttribute switch - { - SLCore.Common.Models.CleanCodeAttribute.CONVENTIONAL => CleanCodeAttribute.Conventional, - SLCore.Common.Models.CleanCodeAttribute.FORMATTED => CleanCodeAttribute.Formatted, - SLCore.Common.Models.CleanCodeAttribute.IDENTIFIABLE => CleanCodeAttribute.Identifiable, - SLCore.Common.Models.CleanCodeAttribute.CLEAR => CleanCodeAttribute.Clear, - SLCore.Common.Models.CleanCodeAttribute.COMPLETE => CleanCodeAttribute.Complete, - SLCore.Common.Models.CleanCodeAttribute.EFFICIENT => CleanCodeAttribute.Efficient, - SLCore.Common.Models.CleanCodeAttribute.LOGICAL => CleanCodeAttribute.Logical, - SLCore.Common.Models.CleanCodeAttribute.DISTINCT => CleanCodeAttribute.Distinct, - SLCore.Common.Models.CleanCodeAttribute.FOCUSED => CleanCodeAttribute.Focused, - SLCore.Common.Models.CleanCodeAttribute.MODULAR => CleanCodeAttribute.Modular, - SLCore.Common.Models.CleanCodeAttribute.TESTED => CleanCodeAttribute.Tested, - SLCore.Common.Models.CleanCodeAttribute.LAWFUL => CleanCodeAttribute.Lawful, - SLCore.Common.Models.CleanCodeAttribute.RESPECTFUL => CleanCodeAttribute.Respectful, - SLCore.Common.Models.CleanCodeAttribute.TRUSTWORTHY => CleanCodeAttribute.Trustworthy, - null => null, - _ => throw new ArgumentOutOfRangeException(nameof(cleanCodeAttribute), cleanCodeAttribute, null) - }; + return null; + } } diff --git a/src/Education/XamlGenerator/XamlGeneratorHelper.cs b/src/Education/XamlGenerator/XamlGeneratorHelper.cs index 002553b8b4..c98d421ff5 100644 --- a/src/Education/XamlGenerator/XamlGeneratorHelper.cs +++ b/src/Education/XamlGenerator/XamlGeneratorHelper.cs @@ -140,7 +140,7 @@ private void WriteCleanCodeHeader_Url() private void WriteCleanCodeHeader_CleanCodeAttribute(IRuleInfo ruleInfo) { - var cleanCodeCategory = CleanCodeAttributeToCategoryMapping.Map[ruleInfo.CleanCodeAttribute.Value]; + var cleanCodeCategory = CleanCodeAttributeToCategoryMapping.Map[ruleInfo.CleanCodeAttribute!.Value]; var cleanCodeAttribute = ruleInfo.CleanCodeAttribute; WriteBubble(StyleResourceNames.CleanCodeAttributeBubble, @@ -200,13 +200,13 @@ private void WriteBubble(StyleResourceNames borderStyle, Action writeContent) private void WriteSubTitleElement_IssueType(IRuleInfo ruleInfo) { - var imageInfo = SubTitleImageInfo.IssueTypeImages[ruleInfo.IssueType]; + var imageInfo = SubTitleImageInfo.IssueTypeImages[ruleInfo.IssueType!.Value]; WriteSubTitleElementWithImage(imageInfo); } private void WriteSubTitleElement_Severity(IRuleInfo ruleInfo) { - var imageInfo = SubTitleImageInfo.SeverityImages[ruleInfo.Severity]; + var imageInfo = SubTitleImageInfo.SeverityImages[ruleInfo.Severity!.Value]; WriteSubTitleElementWithImage(imageInfo); } diff --git a/src/EmbeddedSonarAnalyzer.props b/src/EmbeddedSonarAnalyzer.props index 5d79b21ca6..a429434134 100644 --- a/src/EmbeddedSonarAnalyzer.props +++ b/src/EmbeddedSonarAnalyzer.props @@ -9,6 +9,6 @@ 10.16.0.27621 2.16.0.4008 - 10.7.1.79146 + 10.9.0.79478 \ No newline at end of file diff --git a/src/Infrastructure.VS.UnitTests/ErrorListHelperTests.cs b/src/Infrastructure.VS.UnitTests/ErrorListHelperTests.cs index 83845ae333..ec70d46d49 100644 --- a/src/Infrastructure.VS.UnitTests/ErrorListHelperTests.cs +++ b/src/Infrastructure.VS.UnitTests/ErrorListHelperTests.cs @@ -446,6 +446,44 @@ public void TryGetRuleIdAndSuppressionStateFromSelectedRow_NoSuppressionState_Re isSuppressed.Should().Be(expectedSuppression); } + [TestMethod] + public void TryGetFilterableIssue_SonarIssue_IssueReturned() + { + var issueMock = Mock.Of(); + var issueHandle = CreateIssueHandle(111, new Dictionary + { + { StandardTableKeyNames.BuildTool, "SonarLint" }, + { StandardTableKeyNames.ErrorCode, "javascript:S333"}, + { SonarLintTableControlConstants.IssueVizColumnName, issueMock } + }); + var errorList = CreateErrorList(issueHandle); + var serviceProvider = CreateServiceOperation(errorList); + var testSubject = new ErrorListHelper(serviceProvider); + + bool result = testSubject.TryGetFilterableIssue(issueHandle, out var issue); + + result.Should().BeTrue(); + issue.Should().BeSameAs(issueMock); + } + + [TestMethod] + public void TryGetFilterableIssue_NoAnalysisIssue_IssueNotReturned() + { + var issueHandle = CreateIssueHandle(111, new Dictionary + { + { StandardTableKeyNames.BuildTool, "SonarLint" }, + { StandardTableKeyNames.ErrorCode, "javascript:S333"}, + { SonarLintTableControlConstants.IssueVizColumnName, null } + }); + var errorList = CreateErrorList(issueHandle); + var serviceProvider = CreateServiceOperation(errorList); + + var testSubject = new ErrorListHelper(serviceProvider); + var result = testSubject.TryGetFilterableIssue(issueHandle,out _); + + result.Should().BeFalse(); + } + private IVsUIServiceOperation CreateServiceOperation(IErrorList svcToPassToCallback) { var serviceOp = new Mock(); diff --git a/src/Infrastructure.VS/ErrorListHelper.cs b/src/Infrastructure.VS/ErrorListHelper.cs index 463d14a3ba..4beb5f0b80 100644 --- a/src/Infrastructure.VS/ErrorListHelper.cs +++ b/src/Infrastructure.VS/ErrorListHelper.cs @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; using System.ComponentModel.Composition; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; @@ -89,8 +88,19 @@ public bool TryGetIssueFromSelectedRow(out IFilterableIssue issue) { IFilterableIssue issueOut = null; var result = vSServiceOperation.Execute( - errorList => TryGetSelectedSnapshotAndIndex(errorList, out var snapshot, out var index) - && TryGetValue(snapshot, index, SonarLintTableControlConstants.IssueVizColumnName, out issueOut)); + errorList => TryGetSelectedTableEntry(errorList, out var handle) && TryGetFilterableIssue(handle, out issueOut)); + + issue = issueOut; + + return result; + } + + public bool TryGetFilterableIssue(ITableEntryHandle handle, out IFilterableIssue issue) + { + IFilterableIssue issueOut = null; + var result = vSServiceOperation.Execute( + _ => handle.TryGetSnapshot(out var snapshot, out var index) + && TryGetValue(snapshot, index, SonarLintTableControlConstants.IssueVizColumnName, out issueOut)); issue = issueOut; @@ -155,7 +165,7 @@ private static string FindErrorCodeForEntry(ITableEntriesSnapshot snapshot, int { return $"{SonarRuleRepoKeys.CSharpRules}:{errorCode}"; } - + if (helpLink.Contains("rules.sonarsource.com/vbnet/")) { return $"{SonarRuleRepoKeys.VBNetRules}:{errorCode}"; @@ -197,7 +207,11 @@ private static bool TryGetSelectedTableEntry(IErrorList errorList, out ITableEnt return true; } - private static bool TryGetValue(ITableEntriesSnapshot snapshot, int index, string columnName, out T value) + private static bool TryGetValue( + ITableEntriesSnapshot snapshot, + int index, + string columnName, + out T value) { value = default; diff --git a/src/Infrastructure.VS/IErrorListHelper.cs b/src/Infrastructure.VS/IErrorListHelper.cs index 5661e5eb00..1bf57fdb64 100644 --- a/src/Infrastructure.VS/IErrorListHelper.cs +++ b/src/Infrastructure.VS/IErrorListHelper.cs @@ -51,6 +51,13 @@ public interface IErrorListHelper /// True if issue is present in the selected row, False if not present or multiple rows selected bool TryGetIssueFromSelectedRow(out IFilterableIssue issue); + /// + /// Extracts, if present, from the hidden column + /// The method will only return a rule key if the row represents a Sonar analysis issue for any supported language (including Roslyn languages i.e. C# and VB.NET) + /// + /// True if issue is present in the provided row + bool TryGetFilterableIssue(ITableEntryHandle handle, out IFilterableIssue issue); + /// /// Extracts from error code, line number and file path. Does not calculate line hash. /// diff --git a/src/Integration.Vsix.UnitTests/CFamily/CFamilyIssueToAnalysisIssueConverterTests.cs b/src/Integration.Vsix.UnitTests/CFamily/CFamilyIssueToAnalysisIssueConverterTests.cs index bb3d190706..2bd53d3259 100644 --- a/src/Integration.Vsix.UnitTests/CFamily/CFamilyIssueToAnalysisIssueConverterTests.cs +++ b/src/Integration.Vsix.UnitTests/CFamily/CFamilyIssueToAnalysisIssueConverterTests.cs @@ -18,18 +18,11 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Collections.Generic; using System.IO.Abstractions; -using System.Linq; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Utilities; -using Moq; using SonarLint.VisualStudio.CFamily.Rules; using SonarLint.VisualStudio.CFamily.SubProcess; -using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Analysis; using SonarLint.VisualStudio.Core.Configuration; using SonarLint.VisualStudio.Core.UserRuleSettings; @@ -444,6 +437,7 @@ public void Convert_SeverityAndTypeLookup(string ruleKey, AnalysisIssueSeverity var testSubject = CreateTestSubject(); var issue = Convert(testSubject, message); + issue.Id.Should().BeNull(); issue.RuleKey.Should().Be($"lang1:{ruleKey}"); issue.Severity.Should().Be(severity); issue.Type.Should().Be(type); @@ -477,11 +471,12 @@ public void Convert_NewCCTEnabled_FillsSoftwareQualitySeverity(bool isCCTEnabled var testSubject = CreateTestSubject(connectedModeFeaturesConfiguration: CMConfig.Object); var issue = testSubject.Convert(message, "lang", CFamilyconfig.Object); - issue.HighestSoftwareQualitySeverity.Should().Be(expectedSoftwareQualitySeverity); + var highestSoftwareQualitySeverity = issue.HighestImpact?.Severity; + highestSoftwareQualitySeverity.Should().Be(expectedSoftwareQualitySeverity); } [TestMethod] - [Description("Regression test for https://github.com/SonarSource/sonarlint-visualstudio/issues/2149")] + [Microsoft.VisualStudio.TestTools.UnitTesting.Description("Regression test for https://github.com/SonarSource/sonarlint-visualstudio/issues/2149")] [DataRow("", "")] // empty should not throw [DataRow("a.txt", "a.txt")] // not-rooted should stay the same [DataRow("c:\\a.txt", "c:\\a.txt")] @@ -504,7 +499,7 @@ public void Convert_HasMessageParts_QualifiedFilePath(string originalPath, strin } [TestMethod] - [Description("Regression test for https://github.com/SonarSource/sonarlint-visualstudio/issues/2557")] + [Microsoft.VisualStudio.TestTools.UnitTesting.Description("Regression test for https://github.com/SonarSource/sonarlint-visualstudio/issues/2557")] [DataRow("", "")] // empty should not throw [DataRow("a.txt", "a.txt")] // not-rooted should stay the same [DataRow("c:\\a.txt", "c:\\a.txt")] @@ -554,39 +549,40 @@ public void ConvertFromIssueType(IssueType cfamilyIssueType, AnalysisIssueType a [DataRow(SoftwareQualitySeverity.Medium, SoftwareQualitySeverity.Medium)] [DataRow(SoftwareQualitySeverity.Low, SoftwareQualitySeverity.Low)] [DataRow(null, null)] - public void GetHighestSoftwareQualitySeverity(SoftwareQualitySeverity? softwareQualitySeverity, SoftwareQualitySeverity? highestSoftwareQualitySeverity) + public void GetHighestImpact_ReturnsImpactWithHighestSeverity(SoftwareQualitySeverity? softwareQualitySeverity, SoftwareQualitySeverity? expectedHighestSoftwareQualitySeverity) { var impacts = new Dictionary(); - if (softwareQualitySeverity.HasValue) { impacts.Add(SoftwareQuality.Maintainability, softwareQualitySeverity.Value); } - RuleMetadata ruleMetaData = CreateRuleMetaData(impacts); - CFamilyIssueToAnalysisIssueConverter.GetHighestSoftwareQualitySeverity(ruleMetaData).Should().Be(highestSoftwareQualitySeverity); + var highestSoftwareQualitySeverity = CFamilyIssueToAnalysisIssueConverter.GetHighestImpact(ruleMetaData)?.Severity; + + highestSoftwareQualitySeverity.Should().Be(expectedHighestSoftwareQualitySeverity); } [TestMethod] [DataRow(new SoftwareQualitySeverity[] { SoftwareQualitySeverity.Low, SoftwareQualitySeverity.Medium }, SoftwareQualitySeverity.Medium)] [DataRow(new SoftwareQualitySeverity[] { SoftwareQualitySeverity.Low, SoftwareQualitySeverity.High }, SoftwareQualitySeverity.High)] [DataRow(new SoftwareQualitySeverity[] { SoftwareQualitySeverity.Medium, SoftwareQualitySeverity.High }, SoftwareQualitySeverity.High)] - public void GetHighestSoftwareQualitySeverity_HasTwoImpacts_GetsTheHighestOne(SoftwareQualitySeverity[] softwareQualitySeverities, SoftwareQualitySeverity? highestSoftwareQualitySeverity) + public void GetHighestImpact_HasTwoImpacts_GetsTheHighestOne(SoftwareQualitySeverity[] softwareQualitySeverities, SoftwareQualitySeverity? expectedHighestSoftwareQualitySeverity) { var impacts = new Dictionary { { SoftwareQuality.Maintainability, softwareQualitySeverities[0] }, { SoftwareQuality.Reliability, softwareQualitySeverities[1] } }; - RuleMetadata ruleMetaData = CreateRuleMetaData(impacts); - CFamilyIssueToAnalysisIssueConverter.GetHighestSoftwareQualitySeverity(ruleMetaData).Should().Be(highestSoftwareQualitySeverity); + var highestSoftwareQualitySeverity = CFamilyIssueToAnalysisIssueConverter.GetHighestImpact(ruleMetaData)?.Severity; + + highestSoftwareQualitySeverity.Should().Be(expectedHighestSoftwareQualitySeverity); } [TestMethod] - public void GetHighestSoftwareQualitySeverity_HasThreeImpacts_GetsTheHighestOne() + public void GetHighestImpact_HasThreeImpacts_GetsTheHighestOne() { var impacts = new Dictionary { @@ -594,10 +590,34 @@ public void GetHighestSoftwareQualitySeverity_HasThreeImpacts_GetsTheHighestOne( { SoftwareQuality.Reliability, SoftwareQualitySeverity.High }, { SoftwareQuality.Security, SoftwareQualitySeverity.Medium } }; + RuleMetadata ruleMetaData = CreateRuleMetaData(impacts); + var highestImpact = CFamilyIssueToAnalysisIssueConverter.GetHighestImpact(ruleMetaData); + + highestImpact.Severity.Should().Be(SoftwareQualitySeverity.High); + highestImpact.Quality.Should().Be(SoftwareQuality.Reliability); + } + + [TestMethod] + [DataRow(SoftwareQualitySeverity.Blocker)] + [DataRow(SoftwareQualitySeverity.High)] + [DataRow(SoftwareQualitySeverity.Medium)] + [DataRow(SoftwareQualitySeverity.Low)] + [DataRow(SoftwareQualitySeverity.Info)] + public void GetHighestImpact_HasTwoHighImpactsForDifferentQualities_GetsTheHighestSoftwareQuality(SoftwareQualitySeverity softwareQualitySeverity) + { + var impacts = new Dictionary + { + { SoftwareQuality.Maintainability, SoftwareQualitySeverity.Info }, + { SoftwareQuality.Reliability, softwareQualitySeverity }, + { SoftwareQuality.Security, softwareQualitySeverity } + }; RuleMetadata ruleMetaData = CreateRuleMetaData(impacts); - CFamilyIssueToAnalysisIssueConverter.GetHighestSoftwareQualitySeverity(ruleMetaData).Should().Be(SoftwareQualitySeverity.High); + var highestImpact = CFamilyIssueToAnalysisIssueConverter.GetHighestImpact(ruleMetaData); + + highestImpact.Severity.Should().Be(softwareQualitySeverity); + highestImpact.Quality.Should().Be(SoftwareQuality.Security); } [TestMethod] diff --git a/src/Integration.Vsix.UnitTests/ErrorList/IssuesSnapshotTests _GetValue.cs b/src/Integration.Vsix.UnitTests/ErrorList/IssuesSnapshotTests _GetValue.cs index 7ca63be191..fed2b36a5a 100644 --- a/src/Integration.Vsix.UnitTests/ErrorList/IssuesSnapshotTests _GetValue.cs +++ b/src/Integration.Vsix.UnitTests/ErrorList/IssuesSnapshotTests _GetValue.cs @@ -206,7 +206,7 @@ public void GetValue_ErrorCategory_Is_CodeSmell_By_Default() } [TestMethod] - public void GetValue_ErrorCategory_Is_Issue_Type() + public void GetValue_StandardMode_ErrorCategory_IsSeverityAndType() { issue.Type = AnalysisIssueType.Bug; issue.Severity = AnalysisIssueSeverity.Blocker; @@ -217,6 +217,19 @@ public void GetValue_ErrorCategory_Is_Issue_Type() GetValue(StandardTableKeyNames.ErrorCategory).Should().Be("Blocker Vulnerability"); } + [TestMethod] + [DataRow(SoftwareQuality.Security, SoftwareQualitySeverity.Blocker)] + [DataRow(SoftwareQuality.Security, SoftwareQualitySeverity.High)] + [DataRow(SoftwareQuality.Maintainability, SoftwareQualitySeverity.Medium)] + [DataRow(SoftwareQuality.Maintainability, SoftwareQualitySeverity.Low)] + [DataRow(SoftwareQuality.Reliability, SoftwareQualitySeverity.Info)] + public void GetValue_MqrMode_ErrorCategory_IsSoftwareQualityAndSeverity(SoftwareQuality quality, SoftwareQualitySeverity severity) + { + issue.HighestImpact = new Impact(quality, severity); + + GetValue(StandardTableKeyNames.ErrorCategory).Should().Be(severity + " " + quality); + } + [TestMethod] public void GetValue_ErrorCodeToolTip() { diff --git a/src/Integration.Vsix/CFamily/CFamilyIssueToAnalysisIssueConverter.cs b/src/Integration.Vsix/CFamily/CFamilyIssueToAnalysisIssueConverter.cs index 16536c75dc..d5c7852285 100644 --- a/src/Integration.Vsix/CFamily/CFamilyIssueToAnalysisIssueConverter.cs +++ b/src/Integration.Vsix/CFamily/CFamilyIssueToAnalysisIssueConverter.cs @@ -122,11 +122,11 @@ public IAnalysisIssue Convert(Message cFamilyIssue, string sqLanguage, ICFamilyR // Look up default severity and type var defaultSeverity = ruleMetaData.DefaultSeverity; var defaultType = ruleMetaData.Type; - SoftwareQualitySeverity? highestSoftwareQualitySeverity = null; + Impact highestImpact = null; if (ruleMetaData.Type != IssueType.SecurityHotspot && connectedModeFeaturesConfiguration.IsNewCctAvailable()) { - highestSoftwareQualitySeverity = GetHighestSoftwareQualitySeverity(ruleMetaData); + highestImpact = GetHighestImpact(ruleMetaData); } var fileContents = GetFileContentsOfReportedFiles(cFamilyIssue); @@ -143,7 +143,7 @@ public IAnalysisIssue Convert(Message cFamilyIssue, string sqLanguage, ICFamilyR var flows = locations.Any() ? new[] { new AnalysisIssueFlow(locations) } : null; - var result = ToAnalysisIssue(cFamilyIssue, sqLanguage, defaultSeverity, defaultType, flows, fileContents, highestSoftwareQualitySeverity); + var result = ToAnalysisIssue(cFamilyIssue, sqLanguage, defaultSeverity, defaultType, flows, fileContents, highestImpact); CodeMarkers.Instance.CFamilyConvertIssueStop(); @@ -193,14 +193,15 @@ private IAnalysisIssue ToAnalysisIssue(Message cFamilyIssue, IssueType defaultType, IReadOnlyList flows, IReadOnlyDictionary fileContents, - SoftwareQualitySeverity? highestSoftwareQualitySeverity) + Impact highestImpact) { return new AnalysisIssue ( + id: null, // until CFamily is migrated to SlCore, its ID will be null ruleKey: sqLanguage + ":" + cFamilyIssue.RuleKey, severity: Convert(defaultSeverity), type: Convert(defaultType), - highestSoftwareQualitySeverity, + highestImpact, primaryLocation: ToAnalysisIssueLocation(cFamilyIssue, fileContents), flows: flows, fixes: ToQuickFixes(cFamilyIssue) @@ -311,7 +312,15 @@ private AnalysisIssueLocation ToAnalysisIssueLocation(MessagePart cFamilyIssueLo } } - internal /* for testing */ static SoftwareQualitySeverity? GetHighestSoftwareQualitySeverity(RuleMetadata ruleMetadata) - => ruleMetadata.Code?.Impacts?.Count > 0 ? (SoftwareQualitySeverity?)ruleMetadata.Code.Impacts.Max(r => r.Value) : null; + internal /* for testing */ static Impact GetHighestImpact(RuleMetadata ruleMetadata) + { + if (ruleMetadata?.Code?.Impacts == null || ruleMetadata.Code.Impacts.Count == 0) + { + return null; + } + + var highestImpact = ruleMetadata.Code.Impacts.OrderByDescending(kvp => kvp.Value).ThenByDescending(kvp => kvp.Key).First(); + return new Impact(highestImpact.Key, highestImpact.Value); + } } } diff --git a/src/Integration.Vsix/ErrorList/IssuesSnapshot.cs b/src/Integration.Vsix/ErrorList/IssuesSnapshot.cs index 969c0e4c84..9e1f3e49ab 100644 --- a/src/Integration.Vsix/ErrorList/IssuesSnapshot.cs +++ b/src/Integration.Vsix/ErrorList/IssuesSnapshot.cs @@ -227,7 +227,7 @@ public override bool TryGetValue(int index, string keyName, out object content) return true; case StandardTableKeyNames.ErrorCategory: - content = $"{issue.Severity} {ToString(issue.Type)}"; + content = GetErrorCategory(issue); return true; case StandardTableKeyNames.ErrorCodeToolTip: @@ -262,6 +262,15 @@ public override bool TryGetValue(int index, string keyName, out object content) } } + private string GetErrorCategory(IAnalysisIssue issue) + { + if (issue.HighestImpact != null) + { + return $"{issue.HighestImpact.Severity} {issue.HighestImpact.Quality}"; + } + return $"{issue.Severity} {ToString(issue.Type)}"; + } + /// /// Returns true/false if the ErrorList should hide the requested issue. /// @@ -276,7 +285,7 @@ private bool ShouldHideIssue(IAnalysisIssueVisualization issue) return !issue.Span.HasValue || issue.Span.Value.IsEmpty; } - private object ToString(AnalysisIssueType type) + private object ToString(AnalysisIssueType? type) { switch (type) { diff --git a/src/IssueViz.Security.UnitTests/Hotspots/Models/HotspotTests.cs b/src/IssueViz.Security.UnitTests/Hotspots/Models/HotspotTests.cs index 6b5ade8213..9e6389890c 100644 --- a/src/IssueViz.Security.UnitTests/Hotspots/Models/HotspotTests.cs +++ b/src/IssueViz.Security.UnitTests/Hotspots/Models/HotspotTests.cs @@ -36,7 +36,7 @@ public class HotspotTests [TestMethod] public void Ctor_NullLocation_ArgumentNullException() { - Action act = () => new Hotspot( + Action act = () => new Hotspot(id: null, "hotspot key", "server-path", primaryLocation: null, @@ -49,7 +49,7 @@ public void Ctor_NullLocation_ArgumentNullException() [TestMethod] public void Ctor_PropertiesSet() { - var hotspot = new Hotspot( + var hotspot = new Hotspot(id: null, "hotspot key", "server-path", primaryLocation: new AnalysisIssueLocation( @@ -84,7 +84,8 @@ public void Ctor_PropertiesSet() public void Ctor_NoFlows_EmptyFlows() { IReadOnlyList flows = null; - var hotspot = new Hotspot("hotspot key", + var hotspot = new Hotspot(id: null, + "hotspot key", "server-path", new AnalysisIssueLocation( "message", @@ -105,7 +106,8 @@ public void Ctor_NoFlows_EmptyFlows() public void Ctor_HasFlows_CorrectFlows() { var flows = new[] { Mock.Of(), Mock.Of() }; - var hotspot = new Hotspot("hotspot key", + var hotspot = new Hotspot(id: null, + "hotspot key", "server-path", new AnalysisIssueLocation( "message", diff --git a/src/IssueViz.Security.UnitTests/Taint/Models/TaintIssueTests.cs b/src/IssueViz.Security.UnitTests/Taint/Models/TaintIssueTests.cs index 26581da401..ac9ccf8a62 100644 --- a/src/IssueViz.Security.UnitTests/Taint/Models/TaintIssueTests.cs +++ b/src/IssueViz.Security.UnitTests/Taint/Models/TaintIssueTests.cs @@ -18,74 +18,87 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Collections.Generic; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; using SonarLint.VisualStudio.Core.Analysis; +using SonarLint.VisualStudio.IssueVisualization.Security.Taint; using SonarLint.VisualStudio.IssueVisualization.Security.Taint.Models; -namespace SonarLint.VisualStudio.IssueVisualization.Security.UnitTests.Taint.Models +namespace SonarLint.VisualStudio.IssueVisualization.Security.UnitTests.Taint.Models; + +[TestClass] +public class TaintIssueTests { - [TestClass] - public class TaintIssueTests + [TestMethod] + public void Ctor_NullLocation_ArgumentNullException() { - [TestMethod] - public void Ctor_NullLocation_ArgumentNullException() - { - Action act = () => new TaintIssue("issue key", "rule key", - null, - AnalysisIssueSeverity.Major, SoftwareQualitySeverity.High, DateTimeOffset.MinValue, DateTimeOffset.MinValue, null, null); + Action act = () => new TaintIssue( Guid.Empty,"issue key", "rule key", + null, + AnalysisIssueSeverity.Major, SoftwareQualitySeverity.High, DateTimeOffset.MinValue, null, null); - act.Should().Throw().And.ParamName.Should().Be("primaryLocation"); - } + act.Should().Throw().And.ParamName.Should().Be("primaryLocation"); + } - [TestMethod] - public void Ctor_PropertiesSet() - { - var created = DateTimeOffset.Parse("2001-01-31T01:02:03+0200"); - var lastUpdated = DateTimeOffset.UtcNow; - var issue = new TaintIssue("issue key", "rule key", - new AnalysisIssueLocation("message", "local-path.cpp", new TextRange(1, 2, 3, 4, "hash")), - AnalysisIssueSeverity.Major, SoftwareQualitySeverity.High, created, lastUpdated, null, "contextKey"); + [TestMethod] + public void Ctor_PropertiesSet() + { + var created = DateTimeOffset.Parse("2001-01-31T01:02:03+0200"); + var id = Guid.NewGuid(); + var issue = new TaintIssue(id, "issue key", "rule key", + new AnalysisIssueLocation("message", "local-path.cpp", new TextRange(1, 2, 3, 4, "hash")), + AnalysisIssueSeverity.Major, SoftwareQualitySeverity.High, created, null, "contextKey"); - issue.IssueKey.Should().Be("issue key"); - issue.RuleKey.Should().Be("rule key"); - issue.Severity.Should().Be(AnalysisIssueSeverity.Major); - issue.CreationTimestamp.Should().Be(created); - issue.LastUpdateTimestamp.Should().Be(lastUpdated); - issue.RuleDescriptionContextKey.Should().Be("contextKey"); + issue.Id.Should().Be(id); + issue.IssueKey.Should().Be("issue key"); + issue.RuleKey.Should().Be("rule key"); + issue.Severity.Should().Be(AnalysisIssueSeverity.Major); + issue.CreationTimestamp.Should().Be(created); + issue.RuleDescriptionContextKey.Should().Be("contextKey"); - issue.PrimaryLocation.FilePath.Should().Be("local-path.cpp"); - issue.PrimaryLocation.Message.Should().Be("message"); - issue.PrimaryLocation.TextRange.StartLine.Should().Be(1); - issue.PrimaryLocation.TextRange.EndLine.Should().Be(2); - issue.PrimaryLocation.TextRange.StartLineOffset.Should().Be(3); - issue.PrimaryLocation.TextRange.EndLineOffset.Should().Be(4); - issue.PrimaryLocation.TextRange.LineHash.Should().Be("hash"); - } + issue.PrimaryLocation.FilePath.Should().Be("local-path.cpp"); + issue.PrimaryLocation.Message.Should().Be("message"); + issue.PrimaryLocation.TextRange.StartLine.Should().Be(1); + issue.PrimaryLocation.TextRange.EndLine.Should().Be(2); + issue.PrimaryLocation.TextRange.StartLineOffset.Should().Be(3); + issue.PrimaryLocation.TextRange.EndLineOffset.Should().Be(4); + issue.PrimaryLocation.TextRange.LineHash.Should().Be("hash"); + } - [TestMethod] - public void Ctor_NoFlows_EmptyFlows() - { - IReadOnlyList flows = null; - var issue = new TaintIssue("issue key", "rule key", - new AnalysisIssueLocation("message", "local-path.cpp", new TextRange(1, 2, 3, 4, "hash")), - AnalysisIssueSeverity.Major, SoftwareQualitySeverity.High, DateTimeOffset.MinValue, DateTimeOffset.MaxValue, flows, null); + [TestMethod] + public void Ctor_NoFlows_EmptyFlows() + { + IReadOnlyList flows = null; + var issue = new TaintIssue(Guid.NewGuid(), "issue key", "rule key", + new AnalysisIssueLocation("message", "local-path.cpp", new TextRange(1, 2, 3, 4, "hash")), + AnalysisIssueSeverity.Major, SoftwareQualitySeverity.High, DateTimeOffset.MinValue, flows, null); - issue.Flows.Should().BeEmpty(); - } + issue.Flows.Should().BeEmpty(); + } - [TestMethod] - public void Ctor_HasFlows_CorrectFlows() - { - var flows = new[] { Mock.Of(), Mock.Of() }; - var issue = new TaintIssue("issue key", "rule key", - new AnalysisIssueLocation("message", "local-path.cpp", new TextRange(1, 2, 3, 4, "hash")), - AnalysisIssueSeverity.Major, SoftwareQualitySeverity.High, DateTimeOffset.MinValue, DateTimeOffset.MaxValue, flows, null); + [TestMethod] + public void Ctor_HasFlows_CorrectFlows() + { + var flows = new[] { Substitute.For(), Substitute.For() }; + var issue = new TaintIssue(Guid.NewGuid(), "issue key", "rule key", + new AnalysisIssueLocation("message", "local-path.cpp", new TextRange(1, 2, 3, 4, "hash")), + AnalysisIssueSeverity.Major, SoftwareQualitySeverity.High, DateTimeOffset.MinValue, flows, null); + + issue.Flows.Should().BeEquivalentTo(flows); + } + + [TestMethod] + public void Ctor_HasNoStandardAndNoCCTSeverity_Throws() + { + AnalysisIssueSeverity? analysisIssueSeverity = null; + SoftwareQualitySeverity? highestSoftwareQualitySeverity = null; + var act = () => new TaintIssue(Guid.NewGuid(), + "issue key", + "rule key", + new AnalysisIssueLocation("msg", "local-path.cpp", new TextRange(1, 2, 3, 4, "hash")), + analysisIssueSeverity, + highestSoftwareQualitySeverity, + DateTimeOffset.Now, + [], + null); - issue.Flows.Should().BeEquivalentTo(flows); - } + act.Should().Throw().WithMessage(string.Format(TaintResources.TaintIssue_SeverityUndefined, "issue key")); } } diff --git a/src/IssueViz.Security.UnitTests/Taint/ServerSentEvents/TaintServerEventsListenerTests.cs b/src/IssueViz.Security.UnitTests/Taint/ServerSentEvents/TaintServerEventsListenerTests.cs index 55de241bf2..ef6527091f 100644 --- a/src/IssueViz.Security.UnitTests/Taint/ServerSentEvents/TaintServerEventsListenerTests.cs +++ b/src/IssueViz.Security.UnitTests/Taint/ServerSentEvents/TaintServerEventsListenerTests.cs @@ -1,310 +1,310 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2024 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Threading; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.IssueVisualization.Models; -using SonarLint.VisualStudio.IssueVisualization.Security.Taint.ServerSentEvents; -using SonarLint.VisualStudio.IssueVisualization.Security.Taint; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarLint.VisualStudio.IssueVisualization.Security.UnitTests.Taint.ServerSentEvents -{ - [TestClass] - public class TaintServerEventsListenerTests - { - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public async Task OnEvent_UnrecognizedServerEvent_EventIsIgnored() - { - var taintStore = new Mock(); - var logger = new Mock(); - var taintServerEventSource = SetupTaintServerEventSource(Mock.Of()); - - var testSubject = CreateTestSubject( - taintServerEventSource: taintServerEventSource.Object, - taintStore: taintStore.Object, - logger: logger.Object); - - await testSubject.ListenAsync(); - - taintStore.Invocations.Count.Should().Be(0); - - logger.Verify(x => - x.LogVerbose(It.Is(s => s.Contains("ITestEvent"))), - Times.Once); - } - - [TestMethod] - public async Task OnEvent_TaintClosedServerEvent_IssueIsRemovedFromStore() - { - const string issueKey = "some issue1"; - - var serverEvent = CreateTaintClosedServerEvent(issueKey); - var taintServerEventSource = SetupTaintServerEventSource(serverEvent); - var taintStore = new Mock(); - - var testSubject = CreateTestSubject( - taintServerEventSource: taintServerEventSource.Object, - taintStore: taintStore.Object); - - await testSubject.ListenAsync(); - - taintStore.Verify(x => x.Remove(issueKey), Times.Once); - taintStore.VerifyNoOtherCalls(); - } - - [TestMethod] - public async Task OnEvent_TaintRaisedServerEvent_EventIsNotForCurrentBranch_IssueIgnored() - { - var taintIssue = Mock.Of(); - var serverEvent = CreateTaintRaisedServerEvent(taintIssue, "some branch"); - var taintServerEventSource = SetupTaintServerEventSource(serverEvent); - var serverBranchProvider = SetupBranchProvider("another branch"); - - var taintStore = new Mock(); - - var testSubject = CreateTestSubject( - taintServerEventSource: taintServerEventSource.Object, - taintStore: taintStore.Object, - serverBranchProvider: serverBranchProvider.Object); - - await testSubject.ListenAsync(); - - serverBranchProvider.Verify(x => x.GetServerBranchNameAsync(It.IsAny()), Times.Once); - taintStore.Invocations.Count.Should().Be(0); - } - - [TestMethod] - public async Task OnEvent_TaintRaisedServerEvent_EventIsForCurrentBranch_IssueIsAddedToStore() - { - const string branchName = "master"; - - var taintIssue = Mock.Of(); - var serverEvent = CreateTaintRaisedServerEvent(taintIssue, branchName); - var taintServerEventSource = SetupTaintServerEventSource(serverEvent); - var convertedIssueViz = Mock.Of(); - - var converter = new Mock(); - converter.Setup(x => x.Convert(taintIssue)) - .Returns(convertedIssueViz); - - var serverBranchProvider = SetupBranchProvider(branchName); - var taintStore = new Mock(); - - var testSubject = CreateTestSubject( - taintServerEventSource: taintServerEventSource.Object, - taintStore: taintStore.Object, - taintToIssueVizConverter: converter.Object, - serverBranchProvider: serverBranchProvider.Object); - - await testSubject.ListenAsync(); - - taintStore.Verify(x => x.Add(convertedIssueViz), Times.Once); - taintStore.VerifyNoOtherCalls(); - } - - [TestMethod] - public async Task OnEvent_FailureToProcessTaintEvent_NonCriticalException_EventIsIgnored() - { - var serverEvent1 = CreateTaintClosedServerEvent("some issue1"); - var serverEvent2 = CreateTaintClosedServerEvent("some issue2"); - var taintServerEventSource = SetupTaintServerEventSource(serverEvent1, serverEvent2); - var taintStore = new Mock(); - - taintStore - .Setup(x => x.Remove("some issue1")) - .Throws(new NotImplementedException("this is a test")); - - var logger = new Mock(); - - var testSubject = CreateTestSubject( - taintServerEventSource: taintServerEventSource.Object, - taintStore: taintStore.Object, - logger: logger.Object); - - await testSubject.ListenAsync(); - - taintStore.Verify(x => x.Remove("some issue1"), Times.Once); - taintStore.Verify(x => x.Remove("some issue2"), Times.Once); - taintStore.VerifyNoOtherCalls(); - logger.Verify(x=> x.LogVerbose(It.Is(s=> s.Contains("this is a test")), Array.Empty()), Times.Once()); - } - - [TestMethod] - public void OnEvent_FailureToProcessTaintEvent_CriticalException_StopsListeningToServerEventsSource() - { - var serverEvent1 = CreateTaintClosedServerEvent("some issue1"); - var serverEvent2 = CreateTaintClosedServerEvent("some issue2"); - var taintServerEventSource = SetupTaintServerEventSource(serverEvent1, serverEvent2); - var taintStore = new Mock(); - - taintStore - .Setup(x => x.Remove("some issue1")) - .Throws(new StackOverflowException("this is a test")); - - var testSubject = CreateTestSubject( - taintServerEventSource: taintServerEventSource.Object, - taintStore: taintStore.Object); - - Func func = async () => await testSubject.ListenAsync(); - - func.Should().Throw().And.Message.Should().Be("this is a test"); - - taintStore.Verify(x => x.Remove("some issue1"), Times.Once); - taintStore.Verify(x => x.Remove("some issue2"), Times.Never); - taintStore.VerifyNoOtherCalls(); - taintServerEventSource.Verify(x => x.GetNextEventOrNullAsync(), Times.Once); - taintServerEventSource.VerifyNoOtherCalls(); - } - - [TestMethod] - public async Task Dispose_StopsListeningToServerEventsSource() - { - var firstListenTask = new TaskCompletionSource(); - - var event1 = CreateTaintClosedServerEvent("issue1"); - var event2 = CreateTaintClosedServerEvent("issue2"); - - var taintServerEventSource = new Mock(); - taintServerEventSource - .SetupSequence(x => x.GetNextEventOrNullAsync()) - .Returns(firstListenTask.Task) - .ReturnsAsync(event2) - .ReturnsAsync((ITaintServerEvent) null); - - var taintStore = new Mock(); - - var testSubject = CreateTestSubject( - taintServerEventSource: taintServerEventSource.Object, - taintStore: taintStore.Object); - - var listenTask = testSubject.ListenAsync(); - - testSubject.Dispose(); - - firstListenTask.SetResult(event1); - - await listenTask; - - taintServerEventSource.Verify(x => x.GetNextEventOrNullAsync(), Times.Once); - taintStore.Verify(x=> x.Remove("issue1"), Times.Once); - taintStore.VerifyNoOtherCalls(); - } - - [TestMethod] - [Description("Regression test for https://github.com/SonarSource/sonarlint-visualstudio/issues/3946")] - public void Dispose_CalledASecondTime_NoException() - { - var testSubject = CreateTestSubject(); - - testSubject.Dispose(); - - Action act = () => testSubject.Dispose(); - act.Should().NotThrow(); - } - - private static ITaintVulnerabilityClosedServerEvent CreateTaintClosedServerEvent(string taintKey) - { - var serverEvent = new Mock(); - serverEvent.Setup(x => x.Key).Returns(taintKey); - - return serverEvent.Object; - } - - private static ITaintVulnerabilityRaisedServerEvent CreateTaintRaisedServerEvent(ITaintIssue taintIssue, string issueBranch) - { - var serverEvent = new Mock(); - serverEvent.Setup(x => x.Branch).Returns(issueBranch); - serverEvent.Setup(x => x.Issue).Returns(taintIssue); - - return serverEvent.Object; - } - - private static Mock SetupTaintServerEventSource(params ITaintServerEvent[] serverEvents) - { - var taintServerEventSource = new Mock(); - - var sequence = taintServerEventSource.SetupSequence(x => x.GetNextEventOrNullAsync()); - - foreach (var serverEvent in serverEvents) - { - sequence.ReturnsAsync(serverEvent); - } - - // Signal that the task is finished - sequence.ReturnsAsync((ITaintServerEvent)null); - - return taintServerEventSource; - } - - private static Mock SetupBranchProvider(string currentBranch) - { - var branchProvider = new Mock(); - branchProvider - .Setup(x => x.GetServerBranchNameAsync(It.IsAny())) - .ReturnsAsync(currentBranch); - - return branchProvider; - } - - private static TaintServerEventsListener CreateTestSubject( - IStatefulServerBranchProvider serverBranchProvider = null, - ITaintServerEventSource taintServerEventSource = null, - ITaintStore taintStore = null, - ITaintIssueToIssueVisualizationConverter taintToIssueVizConverter = null, - IThreadHandling threadHandling = null, - ILogger logger = null) - { - serverBranchProvider ??= Mock.Of(); - taintServerEventSource ??= Mock.Of(); - taintStore ??= Mock.Of(); - taintToIssueVizConverter ??= Mock.Of(); - threadHandling ??= new NoOpThreadHandler(); - logger ??= Mock.Of(); - - return new TaintServerEventsListener(serverBranchProvider, taintServerEventSource, taintStore, threadHandling, taintToIssueVizConverter, logger); - } - - /// - /// Used to test unrecognized taint events - /// - public interface ITestEvent : ITaintServerEvent - { - } - } -} +// /* +// * 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. +// */ +// // todo https://sonarsource.atlassian.net/browse/SLVS-1593 +// using System; +// using System.Threading; +// using System.Threading.Tasks; +// using FluentAssertions; +// using Microsoft.VisualStudio.TestTools.UnitTesting; +// using Moq; +// using SonarLint.VisualStudio.Core; +// using SonarLint.VisualStudio.IssueVisualization.Models; +// using SonarLint.VisualStudio.IssueVisualization.Security.Taint.ServerSentEvents; +// using SonarLint.VisualStudio.IssueVisualization.Security.Taint; +// using SonarLint.VisualStudio.TestInfrastructure; +// using SonarQube.Client.Models.ServerSentEvents.ClientContract; +// +// namespace SonarLint.VisualStudio.IssueVisualization.Security.UnitTests.Taint.ServerSentEvents +// { +// [TestClass] +// public class TaintServerEventsListenerTests +// { +// [TestMethod] +// public void MefCtor_CheckIsExported() +// { +// MefTestHelpers.CheckTypeCanBeImported( +// MefTestHelpers.CreateExport(), +// MefTestHelpers.CreateExport(), +// MefTestHelpers.CreateExport(), +// MefTestHelpers.CreateExport(), +// MefTestHelpers.CreateExport(), +// MefTestHelpers.CreateExport()); +// } +// +// [TestMethod] +// public async Task OnEvent_UnrecognizedServerEvent_EventIsIgnored() +// { +// var taintStore = new Mock(); +// var logger = new Mock(); +// var taintServerEventSource = SetupTaintServerEventSource(Mock.Of()); +// +// var testSubject = CreateTestSubject( +// taintServerEventSource: taintServerEventSource.Object, +// taintStore: taintStore.Object, +// logger: logger.Object); +// +// await testSubject.ListenAsync(); +// +// taintStore.Invocations.Count.Should().Be(0); +// +// logger.Verify(x => +// x.LogVerbose(It.Is(s => s.Contains("ITestEvent"))), +// Times.Once); +// } +// +// [TestMethod] +// public async Task OnEvent_TaintClosedServerEvent_IssueIsRemovedFromStore() +// { +// const string issueKey = "some issue1"; +// +// var serverEvent = CreateTaintClosedServerEvent(issueKey); +// var taintServerEventSource = SetupTaintServerEventSource(serverEvent); +// var taintStore = new Mock(); +// +// var testSubject = CreateTestSubject( +// taintServerEventSource: taintServerEventSource.Object, +// taintStore: taintStore.Object); +// +// await testSubject.ListenAsync(); +// +// taintStore.Verify(x => x.Remove(issueKey), Times.Once); +// taintStore.VerifyNoOtherCalls(); +// } +// +// [TestMethod] +// public async Task OnEvent_TaintRaisedServerEvent_EventIsNotForCurrentBranch_IssueIgnored() +// { +// var taintIssue = Mock.Of(); +// var serverEvent = CreateTaintRaisedServerEvent(taintIssue, "some branch"); +// var taintServerEventSource = SetupTaintServerEventSource(serverEvent); +// var serverBranchProvider = SetupBranchProvider("another branch"); +// +// var taintStore = new Mock(); +// +// var testSubject = CreateTestSubject( +// taintServerEventSource: taintServerEventSource.Object, +// taintStore: taintStore.Object, +// serverBranchProvider: serverBranchProvider.Object); +// +// await testSubject.ListenAsync(); +// +// serverBranchProvider.Verify(x => x.GetServerBranchNameAsync(It.IsAny()), Times.Once); +// taintStore.Invocations.Count.Should().Be(0); +// } +// +// [TestMethod] +// public async Task OnEvent_TaintRaisedServerEvent_EventIsForCurrentBranch_IssueIsAddedToStore() +// { +// const string branchName = "master"; +// +// var taintIssue = Mock.Of(); +// var serverEvent = CreateTaintRaisedServerEvent(taintIssue, branchName); +// var taintServerEventSource = SetupTaintServerEventSource(serverEvent); +// var convertedIssueViz = Mock.Of(); +// +// var converter = new Mock(); +// converter.Setup(x => x.Convert(taintIssue)) +// .Returns(convertedIssueViz); +// +// var serverBranchProvider = SetupBranchProvider(branchName); +// var taintStore = new Mock(); +// +// var testSubject = CreateTestSubject( +// taintServerEventSource: taintServerEventSource.Object, +// taintStore: taintStore.Object, +// taintToIssueVizConverter: converter.Object, +// serverBranchProvider: serverBranchProvider.Object); +// +// await testSubject.ListenAsync(); +// +// taintStore.Verify(x => x.Add(convertedIssueViz), Times.Once); +// taintStore.VerifyNoOtherCalls(); +// } +// +// [TestMethod] +// public async Task OnEvent_FailureToProcessTaintEvent_NonCriticalException_EventIsIgnored() +// { +// var serverEvent1 = CreateTaintClosedServerEvent("some issue1"); +// var serverEvent2 = CreateTaintClosedServerEvent("some issue2"); +// var taintServerEventSource = SetupTaintServerEventSource(serverEvent1, serverEvent2); +// var taintStore = new Mock(); +// +// taintStore +// .Setup(x => x.Remove("some issue1")) +// .Throws(new NotImplementedException("this is a test")); +// +// var logger = new Mock(); +// +// var testSubject = CreateTestSubject( +// taintServerEventSource: taintServerEventSource.Object, +// taintStore: taintStore.Object, +// logger: logger.Object); +// +// await testSubject.ListenAsync(); +// +// taintStore.Verify(x => x.Remove("some issue1"), Times.Once); +// taintStore.Verify(x => x.Remove("some issue2"), Times.Once); +// taintStore.VerifyNoOtherCalls(); +// logger.Verify(x=> x.LogVerbose(It.Is(s=> s.Contains("this is a test")), Array.Empty()), Times.Once()); +// } +// +// [TestMethod] +// public void OnEvent_FailureToProcessTaintEvent_CriticalException_StopsListeningToServerEventsSource() +// { +// var serverEvent1 = CreateTaintClosedServerEvent("some issue1"); +// var serverEvent2 = CreateTaintClosedServerEvent("some issue2"); +// var taintServerEventSource = SetupTaintServerEventSource(serverEvent1, serverEvent2); +// var taintStore = new Mock(); +// +// taintStore +// .Setup(x => x.Remove("some issue1")) +// .Throws(new StackOverflowException("this is a test")); +// +// var testSubject = CreateTestSubject( +// taintServerEventSource: taintServerEventSource.Object, +// taintStore: taintStore.Object); +// +// Func func = async () => await testSubject.ListenAsync(); +// +// func.Should().Throw().And.Message.Should().Be("this is a test"); +// +// taintStore.Verify(x => x.Remove("some issue1"), Times.Once); +// taintStore.Verify(x => x.Remove("some issue2"), Times.Never); +// taintStore.VerifyNoOtherCalls(); +// taintServerEventSource.Verify(x => x.GetNextEventOrNullAsync(), Times.Once); +// taintServerEventSource.VerifyNoOtherCalls(); +// } +// +// [TestMethod] +// public async Task Dispose_StopsListeningToServerEventsSource() +// { +// var firstListenTask = new TaskCompletionSource(); +// +// var event1 = CreateTaintClosedServerEvent("issue1"); +// var event2 = CreateTaintClosedServerEvent("issue2"); +// +// var taintServerEventSource = new Mock(); +// taintServerEventSource +// .SetupSequence(x => x.GetNextEventOrNullAsync()) +// .Returns(firstListenTask.Task) +// .ReturnsAsync(event2) +// .ReturnsAsync((ITaintServerEvent) null); +// +// var taintStore = new Mock(); +// +// var testSubject = CreateTestSubject( +// taintServerEventSource: taintServerEventSource.Object, +// taintStore: taintStore.Object); +// +// var listenTask = testSubject.ListenAsync(); +// +// testSubject.Dispose(); +// +// firstListenTask.SetResult(event1); +// +// await listenTask; +// +// taintServerEventSource.Verify(x => x.GetNextEventOrNullAsync(), Times.Once); +// taintStore.Verify(x=> x.Remove("issue1"), Times.Once); +// taintStore.VerifyNoOtherCalls(); +// } +// +// [TestMethod] +// [Description("Regression test for https://github.com/SonarSource/sonarlint-visualstudio/issues/3946")] +// public void Dispose_CalledASecondTime_NoException() +// { +// var testSubject = CreateTestSubject(); +// +// testSubject.Dispose(); +// +// Action act = () => testSubject.Dispose(); +// act.Should().NotThrow(); +// } +// +// private static ITaintVulnerabilityClosedServerEvent CreateTaintClosedServerEvent(string taintKey) +// { +// var serverEvent = new Mock(); +// serverEvent.Setup(x => x.Key).Returns(taintKey); +// +// return serverEvent.Object; +// } +// +// private static ITaintVulnerabilityRaisedServerEvent CreateTaintRaisedServerEvent(ITaintIssue taintIssue, string issueBranch) +// { +// var serverEvent = new Mock(); +// serverEvent.Setup(x => x.Branch).Returns(issueBranch); +// serverEvent.Setup(x => x.Issue).Returns(taintIssue); +// +// return serverEvent.Object; +// } +// +// private static Mock SetupTaintServerEventSource(params ITaintServerEvent[] serverEvents) +// { +// var taintServerEventSource = new Mock(); +// +// var sequence = taintServerEventSource.SetupSequence(x => x.GetNextEventOrNullAsync()); +// +// foreach (var serverEvent in serverEvents) +// { +// sequence.ReturnsAsync(serverEvent); +// } +// +// // Signal that the task is finished +// sequence.ReturnsAsync((ITaintServerEvent)null); +// +// return taintServerEventSource; +// } +// +// private static Mock SetupBranchProvider(string currentBranch) +// { +// var branchProvider = new Mock(); +// branchProvider +// .Setup(x => x.GetServerBranchNameAsync(It.IsAny())) +// .ReturnsAsync(currentBranch); +// +// return branchProvider; +// } +// +// private static TaintServerEventsListener CreateTestSubject( +// IStatefulServerBranchProvider serverBranchProvider = null, +// ITaintServerEventSource taintServerEventSource = null, +// ITaintStore taintStore = null, +// ITaintIssueToIssueVisualizationConverter taintToIssueVizConverter = null, +// IThreadHandling threadHandling = null, +// ILogger logger = null) +// { +// serverBranchProvider ??= Mock.Of(); +// taintServerEventSource ??= Mock.Of(); +// taintStore ??= Mock.Of(); +// taintToIssueVizConverter ??= Mock.Of(); +// threadHandling ??= new NoOpThreadHandler(); +// logger ??= Mock.Of(); +// +// return new TaintServerEventsListener(serverBranchProvider, taintServerEventSource, taintStore, threadHandling, taintToIssueVizConverter, logger); +// } +// +// /// +// /// Used to test unrecognized taint events +// /// +// public interface ITestEvent : ITaintServerEvent +// { +// } +// } +// } diff --git a/src/IssueViz.Security.UnitTests/Taint/TaintIssueToIssueVisualizationConverterTests.cs b/src/IssueViz.Security.UnitTests/Taint/TaintIssueToIssueVisualizationConverterTests.cs index de484bad35..9d850f09a1 100644 --- a/src/IssueViz.Security.UnitTests/Taint/TaintIssueToIssueVisualizationConverterTests.cs +++ b/src/IssueViz.Security.UnitTests/Taint/TaintIssueToIssueVisualizationConverterTests.cs @@ -18,523 +18,259 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Collections.Generic; -using System.Linq; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Microsoft.VisualStudio.Text; -using Moq; -using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Analysis; using SonarLint.VisualStudio.IssueVisualization.Models; using SonarLint.VisualStudio.IssueVisualization.Security.Taint; using SonarLint.VisualStudio.IssueVisualization.Security.Taint.Models; +using SonarLint.VisualStudio.SLCore.Common.Models; +using SonarLint.VisualStudio.SLCore.Protocol; +using SonarLint.VisualStudio.SLCore.Service.Rules.Models; using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client.Models; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; -using ITaintIssue = SonarQube.Client.Models.ServerSentEvents.ClientContract.ITaintIssue; -using ITextRange = SonarQube.Client.Models.ServerSentEvents.ClientContract.ITextRange; +using SoftwareQuality = SonarLint.VisualStudio.SLCore.Common.Models.SoftwareQuality; -namespace SonarLint.VisualStudio.IssueVisualization.Security.UnitTests.Taint -{ - [TestClass] - public class TaintIssueToIssueVisualizationConverterTests - { - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void Convert_FromSonarQubeIssue_ServerIssueHasNoTextRange_ArgumentNullException() - { - var serverIssue = CreateServerIssue(textRange: null); - - var testSubject = CreateTestSubject(); - - Action act = () => testSubject.Convert(serverIssue); - act.Should().Throw().And.ParamName.Should().Be("TextRange"); - } - - [TestMethod] - public void Convert_FromSonarQubeIssue_FlowLocationHasNoTextRange_ArgumentNullException() - { - var serverLocation = CreateServerLocation(textRange: null); - var serverFlow = CreateServerFlow(serverLocation); - var sonarQubeIssue = CreateServerIssue(textRange: new IssueTextRange(1, 1, 1, 1), flows: serverFlow); - - var testSubject = CreateTestSubject(); - - Action act = () => testSubject.Convert(sonarQubeIssue); - act.Should().Throw().And.ParamName.Should().Be("TextRange"); - } - - [TestMethod] - public void Convert_FromSonarQubeIssue_IssueVizConverterCalledWithCorrectParameters_ReturnsConvertedIssueVizWithReversedLocations() - { - var location1 = CreateServerLocation("path1", "message1", new IssueTextRange(1, 2, 3, 4)); - var location2 = CreateServerLocation("path2", "message2", new IssueTextRange(5, 6, 7, 8)); - var flow1 = CreateServerFlow(location1, location2); - - var location3 = CreateServerLocation("path3", "message3", new IssueTextRange(9, 10, 11, 12)); - var flow2 = CreateServerFlow(location3); - - var created = DateTimeOffset.Parse("2001-12-30T01:02:03+0000"); - var lastUpdate = DateTimeOffset.Parse("2009-02-01T13:14:15+0200"); - - var issue = CreateServerIssue("issue key", - "path4", - "hash", - "message4", - "rule", - SonarQubeIssueSeverity.Major, - cctSeverities: new Dictionary - { - { SonarQubeSoftwareQuality.Security, SonarQubeSoftwareQualitySeverity.Medium } - }, - new IssueTextRange(13, 14, 15, 16), - created, - lastUpdate, - resolved: true, - contextKey: "contextKey", - flow1, - flow2); - - var expectedConvertedIssueViz = CreateIssueViz(); - var issueVizConverter = new Mock(); - issueVizConverter - .Setup(x => x.Convert(It.IsAny(), null)) - .Returns(expectedConvertedIssueViz); - - var testSubject = CreateTestSubject(issueVizConverter.Object); - var result = testSubject.Convert(issue); - - result.Should().BeSameAs(expectedConvertedIssueViz); - - issueVizConverter.Verify(x => x.Convert( - It.Is((TaintIssue taintIssue) => - taintIssue.IssueKey == "issue key" && - taintIssue.RuleKey == "rule" && - taintIssue.Severity == AnalysisIssueSeverity.Major && - taintIssue.HighestSoftwareQualitySeverity == SoftwareQualitySeverity.Medium && - taintIssue.RuleDescriptionContextKey == "contextKey" && - - taintIssue.PrimaryLocation.FilePath == "path4" && - taintIssue.PrimaryLocation.Message == "message4" && - taintIssue.PrimaryLocation.TextRange.LineHash == "hash" && - taintIssue.PrimaryLocation.TextRange.StartLine == 13 && - taintIssue.PrimaryLocation.TextRange.EndLine == 14 && - taintIssue.PrimaryLocation.TextRange.StartLineOffset == 15 && - taintIssue.PrimaryLocation.TextRange.EndLineOffset == 16 && - - taintIssue.CreationTimestamp == created && - taintIssue.LastUpdateTimestamp == lastUpdate && - - taintIssue.Flows.Count == 2 && - taintIssue.Flows[0].Locations.Count == 2 && - taintIssue.Flows[1].Locations.Count == 1 && - - taintIssue.Flows[0].Locations[0].Message == "message2" && - taintIssue.Flows[0].Locations[0].FilePath == "path2" && - taintIssue.Flows[0].Locations[0].TextRange.LineHash == null && - taintIssue.Flows[0].Locations[0].TextRange.StartLine == 5 && - taintIssue.Flows[0].Locations[0].TextRange.EndLine == 6 && - taintIssue.Flows[0].Locations[0].TextRange.StartLineOffset == 7 && - taintIssue.Flows[0].Locations[0].TextRange.EndLineOffset == 8 && - - taintIssue.Flows[0].Locations[1].Message == "message1" && - taintIssue.Flows[0].Locations[1].FilePath == "path1" && - taintIssue.Flows[0].Locations[1].TextRange.LineHash == null && - taintIssue.Flows[0].Locations[1].TextRange.StartLine == 1 && - taintIssue.Flows[0].Locations[1].TextRange.EndLine == 2 && - taintIssue.Flows[0].Locations[1].TextRange.StartLineOffset == 3 && - taintIssue.Flows[0].Locations[1].TextRange.EndLineOffset == 4 && - - taintIssue.Flows[1].Locations[0].Message == "message3" && - taintIssue.Flows[1].Locations[0].FilePath == "path3" && - taintIssue.Flows[1].Locations[0].TextRange.LineHash == null && - taintIssue.Flows[1].Locations[0].TextRange.StartLine == 9 && - taintIssue.Flows[1].Locations[0].TextRange.EndLine == 10 && - taintIssue.Flows[1].Locations[0].TextRange.StartLineOffset == 11 && - taintIssue.Flows[1].Locations[0].TextRange.EndLineOffset == 12 - ), - It.IsAny()), - Times.Once); - } - - [TestMethod] - [DataRow(SonarQubeIssueSeverity.Blocker, AnalysisIssueSeverity.Blocker)] - [DataRow(SonarQubeIssueSeverity.Critical, AnalysisIssueSeverity.Critical)] - [DataRow(SonarQubeIssueSeverity.Info, AnalysisIssueSeverity.Info)] - [DataRow(SonarQubeIssueSeverity.Major, AnalysisIssueSeverity.Major)] - [DataRow(SonarQubeIssueSeverity.Minor, AnalysisIssueSeverity.Minor)] - public void Convert_KnownSeverity_ConvertedToAnalysisIssueSeverity(SonarQubeIssueSeverity sqSeverity, AnalysisIssueSeverity expectedSeverity) - { - var result = TaintIssueToIssueVisualizationConverter.Convert(sqSeverity); - - result.Should().Be(expectedSeverity); - } - - [TestMethod] - public void ConvertToHighestSeverity_NullSeverities_ReturnsNull() - { - var result = TaintIssueToIssueVisualizationConverter.ConvertToHighestSeverity(null); - - result.Should().Be(null); - } - - [TestMethod] - public void ConvertToHighestSeverity_EmptySeverities_ReturnsNull() - { - var result = TaintIssueToIssueVisualizationConverter.ConvertToHighestSeverity(new Dictionary()); - - result.Should().Be(null); - } - - [DataTestMethod] - [DataRow(SonarQubeSoftwareQualitySeverity.High, SoftwareQualitySeverity.High)] - [DataRow(SonarQubeSoftwareQualitySeverity.Medium, SoftwareQualitySeverity.Medium)] - [DataRow(SonarQubeSoftwareQualitySeverity.Low, SoftwareQualitySeverity.Low)] - public void ConvertToHighestSeverity_SingleSeverity_ReturnsIt(SonarQubeSoftwareQualitySeverity sqSeverity, - SoftwareQualitySeverity expectedSeverity) - { - var impacts = new Dictionary - { - { SonarQubeSoftwareQuality.Maintainability, sqSeverity } - }; - - var result = TaintIssueToIssueVisualizationConverter.ConvertToHighestSeverity(impacts); - - result.Should().Be(expectedSeverity); - } - - [TestMethod] - public void ConvertToHighestSeverity_InvalidSeverity_Throws() - { - var impacts = new Dictionary - { - { SonarQubeSoftwareQuality.Maintainability, (SonarQubeSoftwareQualitySeverity)999 } - }; - - var act = () => TaintIssueToIssueVisualizationConverter.ConvertToHighestSeverity(impacts); - - act.Should().Throw(); - } - - [DataTestMethod] - [DataRow(new []{SonarQubeSoftwareQualitySeverity.Low, SonarQubeSoftwareQualitySeverity.Low, SonarQubeSoftwareQualitySeverity.Low}, SoftwareQualitySeverity.Low)] - [DataRow(new []{SonarQubeSoftwareQualitySeverity.Low, SonarQubeSoftwareQualitySeverity.Medium, SonarQubeSoftwareQualitySeverity.Low}, SoftwareQualitySeverity.Medium)] - [DataRow(new []{SonarQubeSoftwareQualitySeverity.High, SonarQubeSoftwareQualitySeverity.Medium, SonarQubeSoftwareQualitySeverity.Low}, SoftwareQualitySeverity.High)] - public void ConvertToHighestSeverity_MultipleSeverities_ReturnsHighest(SonarQubeSoftwareQualitySeverity[] sqSeverities, SoftwareQualitySeverity expectedSeverity) - { - if (sqSeverities.Length != 3) - { - Assert.Fail("Wrong length of the list"); - } - - var impacts = new Dictionary - { - { SonarQubeSoftwareQuality.Maintainability, sqSeverities[0] }, - { SonarQubeSoftwareQuality.Reliability, sqSeverities[1] }, - { SonarQubeSoftwareQuality.Security, sqSeverities[2] }, - }; - - var result = TaintIssueToIssueVisualizationConverter.ConvertToHighestSeverity(impacts); - - result.Should().Be(expectedSeverity); - } - - [TestMethod] - [DataRow(SonarQubeIssueSeverity.Unknown)] - [DataRow((SonarQubeIssueSeverity)1234)] - public void Convert_UnknownSeverity_ArgumentOutOfRangeException(SonarQubeIssueSeverity sqSeverity) - { - Action act = () => TaintIssueToIssueVisualizationConverter.Convert(sqSeverity); - - act.Should().Throw().And.ParamName.Should().Be("issueSeverity"); - } - - public enum OriginalIssueType - { - SonarQubeIssue, - TaintSonarQubeIssue - } - - [TestMethod] - [DataRow(OriginalIssueType.SonarQubeIssue)] - [DataRow(OriginalIssueType.TaintSonarQubeIssue)] - public void Convert_CalculatesLocalFilePaths(OriginalIssueType originalIssueType) - { - var locationViz1 = CreateLocationViz("server-path1"); - var locationViz2 = CreateLocationViz("server-path2"); - var locationViz3 = CreateLocationViz("server-path3"); - var expectedIssueViz = CreateIssueViz("server-path4", locationViz1, locationViz2, locationViz3); - - var issueVizConverter = new Mock(); - issueVizConverter - .Setup(x => x.Convert(It.IsAny(), null)) - .Returns(expectedIssueViz); - - var absoluteFilePathLocator = new Mock(); - absoluteFilePathLocator.Setup(x => x.Locate("server-path1")).Returns("local1"); - absoluteFilePathLocator.Setup(x => x.Locate("server-path2")).Returns((string)null); - absoluteFilePathLocator.Setup(x => x.Locate("server-path3")).Returns("local3"); - absoluteFilePathLocator.Setup(x => x.Locate("server-path4")).Returns("local4"); - - var testSubject = CreateTestSubject(issueVizConverter.Object, absoluteFilePathLocator.Object); +namespace SonarLint.VisualStudio.IssueVisualization.Security.UnitTests.Taint; - var result = originalIssueType == OriginalIssueType.SonarQubeIssue - ? testSubject.Convert(CreateDummySonarQubeIssue()) - : testSubject.Convert(CreateDummyTaintSonarQubeIssue()); - - result.Should().Be(expectedIssueViz); - - expectedIssueViz.CurrentFilePath.Should().Be("local4"); - - var secondaryLocations = expectedIssueViz.GetSecondaryLocations().ToList(); - secondaryLocations[0].CurrentFilePath.Should().Be("local1"); - secondaryLocations[1].CurrentFilePath.Should().Be(null); - secondaryLocations[2].CurrentFilePath.Should().Be("local3"); - } - - [TestMethod] - public void Convert_FromTaintIssue_IssueVizConverterCalledWithCorrectParameters_ReturnsConvertedIssueVizWithReversedLocations() - { - var location1 = CreateTaintServerLocation("path1", "message1", CreateTaintTextRange(1, 2, 3, 4, "hash1")); - var location2 = CreateTaintServerLocation("path2", "message2", CreateTaintTextRange(5, 6, 7, 8, "hash2")); - var flow1 = CreateTaintServerFlow(location1, location2); - - var location3 = CreateTaintServerLocation("path3", "message3", CreateTaintTextRange(9, 10, 11, 12, "hash3")); - var flow2 = CreateTaintServerFlow(location3); - - var mainLocation = CreateTaintServerLocation("path4", "message4", CreateTaintTextRange(13, 14, 15, 16, "hash4")); - var creationDate = DateTimeOffset.UtcNow; - var issue = CreateTaintServerIssue("issue key", - "rule", - creationDate, - SonarQubeIssueSeverity.Major, - new Dictionary - { - { SonarQubeSoftwareQuality.Security, SonarQubeSoftwareQualitySeverity.Low}, - { SonarQubeSoftwareQuality.Maintainability, SonarQubeSoftwareQualitySeverity.Medium}, - }, - mainLocation, - flow1, - flow2); - - var expectedConvertedIssueViz = CreateIssueViz(); - var issueVizConverter = new Mock(); - issueVizConverter - .Setup(x => x.Convert(It.IsAny(), null)) - .Returns(expectedConvertedIssueViz); - - var testSubject = CreateTestSubject(issueVizConverter.Object); - var result = testSubject.Convert(issue); - - result.Should().BeSameAs(expectedConvertedIssueViz); - - issueVizConverter.Verify(x => x.Convert( - It.Is((TaintIssue taintIssue) => - taintIssue.IssueKey == "issue key" && - taintIssue.RuleKey == "rule" && - taintIssue.Severity == AnalysisIssueSeverity.Major && - taintIssue.HighestSoftwareQualitySeverity == SoftwareQualitySeverity.Medium && - - taintIssue.PrimaryLocation.FilePath == "path4" && - taintIssue.PrimaryLocation.Message == "message4" && - taintIssue.PrimaryLocation.TextRange.LineHash == "hash4" && - taintIssue.PrimaryLocation.TextRange.StartLine == 13 && - taintIssue.PrimaryLocation.TextRange.EndLine == 14 && - taintIssue.PrimaryLocation.TextRange.StartLineOffset == 15 && - taintIssue.PrimaryLocation.TextRange.EndLineOffset == 16 && - - taintIssue.CreationTimestamp == creationDate && - taintIssue.LastUpdateTimestamp == default && - - taintIssue.Flows.Count == 2 && - taintIssue.Flows[0].Locations.Count == 2 && - taintIssue.Flows[1].Locations.Count == 1 && - - taintIssue.Flows[0].Locations[0].Message == "message2" && - taintIssue.Flows[0].Locations[0].FilePath == "path2" && - taintIssue.Flows[0].Locations[0].TextRange.LineHash == "hash2" && - taintIssue.Flows[0].Locations[0].TextRange.StartLine == 5 && - taintIssue.Flows[0].Locations[0].TextRange.EndLine == 6 && - taintIssue.Flows[0].Locations[0].TextRange.StartLineOffset == 7 && - taintIssue.Flows[0].Locations[0].TextRange.EndLineOffset == 8 && - - taintIssue.Flows[0].Locations[1].Message == "message1" && - taintIssue.Flows[0].Locations[1].FilePath == "path1" && - taintIssue.Flows[0].Locations[1].TextRange.LineHash == "hash1" && - taintIssue.Flows[0].Locations[1].TextRange.StartLine == 1 && - taintIssue.Flows[0].Locations[1].TextRange.EndLine == 2 && - taintIssue.Flows[0].Locations[1].TextRange.StartLineOffset == 3 && - taintIssue.Flows[0].Locations[1].TextRange.EndLineOffset == 4 && - - taintIssue.Flows[1].Locations[0].Message == "message3" && - taintIssue.Flows[1].Locations[0].FilePath == "path3" && - taintIssue.Flows[1].Locations[0].TextRange.LineHash == "hash3" && - taintIssue.Flows[1].Locations[0].TextRange.StartLine == 9 && - taintIssue.Flows[1].Locations[0].TextRange.EndLine == 10 && - taintIssue.Flows[1].Locations[0].TextRange.StartLineOffset == 11 && - taintIssue.Flows[1].Locations[0].TextRange.EndLineOffset == 12 - ), - It.IsAny()), - Times.Once); - } - - [TestMethod] - [DataRow(true)] - [DataRow(false)] - public void Convert_FromSonarQubeIssue_IssueVizIsCorrectlyMarkedAsSuppressed(bool isIssueSuppressed) - { - var issue = CreateServerIssue(resolved: isIssueSuppressed, textRange: new IssueTextRange(1, 2, 3, 4)); +[TestClass] +public class TaintIssueToIssueVisualizationConverterTests +{ + private IAnalysisIssueVisualizationConverter issueVizConverter; + private TaintIssueToIssueVisualizationConverter testSubject; - var expectedConvertedIssueViz = CreateIssueViz(); + [TestInitialize] + public void TestInitialize() + { + issueVizConverter = Substitute.For(); + testSubject = new TaintIssueToIssueVisualizationConverter(issueVizConverter); + } - var issueVizConverter = new Mock(); - issueVizConverter - .Setup(x => x.Convert(It.IsAny(), null)) - .Returns(expectedConvertedIssueViz); + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport()); - var testSubject = CreateTestSubject(issueVizConverter.Object); - var result = testSubject.Convert(issue); + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); - result.Should().Be(expectedConvertedIssueViz); - result.IsSuppressed.Should().Be(isIssueSuppressed); - } - - [TestMethod] - public void Convert_FromTaintIssue_IssueVizIsNotSuppressed() - { - var taintIssue = CreateTaintServerIssue( - mainLocation: CreateTaintServerLocation( - textRange: CreateTaintTextRange(1, 2, 3, 4, null))); + [TestMethod] + public void Convert_IssueVizConverterCalledWithCorrectParameters_ReturnsConvertedIssueVizWithReversedLocations() + { + var created = DateTimeOffset.Parse("2001-12-30T01:02:03+0000"); + var id = Guid.Parse("efa697a2-9cfd-4faf-ba21-71b378667a81"); + var taintDto = new TaintVulnerabilityDto( + id, + "serverkey", + true, + "rulekey:S123", + "message1", + "file\\path\\1", + created, + new StandardModeDetails(IssueSeverity.MINOR, RuleType.VULNERABILITY), + [ + new TaintFlowDto( + [ + new TaintFlowLocationDto( + new TextRangeWithHashDto(5, 6, 7, 8, "hash2"), + "message2", + "file\\path\\2"), + new TaintFlowLocationDto( + new TextRangeWithHashDto(9, 10, 11, 12, "hash3"), + "message3", + "file\\path\\3") + ]), + new TaintFlowDto( + [ + new TaintFlowLocationDto( + new TextRangeWithHashDto(13, 14, 15, 16, "hash4"), + "message4", + "file\\path\\4") + ]) + ], + new TextRangeWithHashDto(1, 2, 3, 4, "hash1"), + "rulecontext", + false); + + var expectedConvertedIssueViz = CreateIssueViz(); + issueVizConverter.Convert(Arg.Any()) + .Returns(expectedConvertedIssueViz); + + var result = testSubject.Convert(taintDto, "C:\\root"); + + result.Should().BeSameAs(expectedConvertedIssueViz); + issueVizConverter.Received().Convert( + Arg.Is((TaintIssue taintIssue) => + taintIssue.Id == id && + taintIssue.IssueKey == "serverkey" && + taintIssue.RuleKey == "rulekey:S123" && + taintIssue.Severity == AnalysisIssueSeverity.Minor && + taintIssue.HighestSoftwareQualitySeverity == null && + taintIssue.RuleDescriptionContextKey == "rulecontext" && + taintIssue.PrimaryLocation.FilePath == @"C:\root\file\path\1" && + taintIssue.PrimaryLocation.Message == "message1" && + taintIssue.PrimaryLocation.TextRange.LineHash == "hash1" && + taintIssue.PrimaryLocation.TextRange.StartLine == 1 && + taintIssue.PrimaryLocation.TextRange.EndLine == 3 && + taintIssue.PrimaryLocation.TextRange.StartLineOffset == 2 && + taintIssue.PrimaryLocation.TextRange.EndLineOffset == 4 && + taintIssue.CreationTimestamp == created && + taintIssue.Flows.Count == 2 && + taintIssue.Flows[0].Locations.Count == 2 && + taintIssue.Flows[1].Locations.Count == 1 && + taintIssue.Flows[0].Locations[0].Message == "message2" && + taintIssue.Flows[0].Locations[0].FilePath == @"C:\root\file\path\2" && + taintIssue.Flows[0].Locations[0].TextRange.LineHash == "hash2" && + taintIssue.Flows[0].Locations[0].TextRange.StartLine == 5 && + taintIssue.Flows[0].Locations[0].TextRange.EndLine == 7 && + taintIssue.Flows[0].Locations[0].TextRange.StartLineOffset == 6 && + taintIssue.Flows[0].Locations[0].TextRange.EndLineOffset == 8 && + taintIssue.Flows[0].Locations[1].Message == "message3" && + taintIssue.Flows[0].Locations[1].FilePath == @"C:\root\file\path\3" && + taintIssue.Flows[0].Locations[1].TextRange.LineHash == "hash3" && + taintIssue.Flows[0].Locations[1].TextRange.StartLine == 9 && + taintIssue.Flows[0].Locations[1].TextRange.EndLine == 11 && + taintIssue.Flows[0].Locations[1].TextRange.StartLineOffset == 10 && + taintIssue.Flows[0].Locations[1].TextRange.EndLineOffset == 12 && + taintIssue.Flows[1].Locations[0].Message == "message4" && + taintIssue.Flows[1].Locations[0].FilePath == @"C:\root\file\path\4" && + taintIssue.Flows[1].Locations[0].TextRange.LineHash == "hash4" && + taintIssue.Flows[1].Locations[0].TextRange.StartLine == 13 && + taintIssue.Flows[1].Locations[0].TextRange.EndLine == 15 && + taintIssue.Flows[1].Locations[0].TextRange.StartLineOffset == 14 && + taintIssue.Flows[1].Locations[0].TextRange.EndLineOffset == 16 + )); + } - var expectedConvertedIssueViz = CreateIssueViz(); + [DataTestMethod] + [DataRow(IssueSeverity.INFO, AnalysisIssueSeverity.Info)] + [DataRow(IssueSeverity.MINOR, AnalysisIssueSeverity.Minor)] + [DataRow(IssueSeverity.CRITICAL, AnalysisIssueSeverity.Critical)] + [DataRow(IssueSeverity.MAJOR, AnalysisIssueSeverity.Major)] + [DataRow(IssueSeverity.BLOCKER, AnalysisIssueSeverity.Blocker)] + public void StandardSeverity_Converts(IssueSeverity slCoreSeverity, AnalysisIssueSeverity expectedSeverity) + { + var taintVulnerabilityDto = CreateDefaultTaintDto(new StandardModeDetails(slCoreSeverity, default)); - var issueVizConverter = new Mock(); - issueVizConverter - .Setup(x => x.Convert(It.IsAny(), null)) - .Returns(expectedConvertedIssueViz); + testSubject.Convert(taintVulnerabilityDto, "C:\\root"); - var testSubject = CreateTestSubject(issueVizConverter.Object); - var result = testSubject.Convert(taintIssue); + issueVizConverter.Received() + .Convert(Arg.Is(x => x.HighestSoftwareQualitySeverity == null && x.Severity == expectedSeverity)); + } - result.Should().Be(expectedConvertedIssueViz); - result.IsSuppressed.Should().BeFalse(); - } + [TestMethod] + public void StandardSeverity_InvalidSeverity_Throws() + { + var taintVulnerabilityDto = CreateDefaultTaintDto(new StandardModeDetails((IssueSeverity)999, default)); - private static TaintIssueToIssueVisualizationConverter CreateTestSubject(IAnalysisIssueVisualizationConverter issueVizConverter = null, IAbsoluteFilePathLocator absoluteFilePathLocator = null) - { - issueVizConverter ??= Mock.Of(); - absoluteFilePathLocator ??= Mock.Of(); + var act = () => testSubject.Convert(taintVulnerabilityDto, "C:\\root"); - return new TaintIssueToIssueVisualizationConverter(issueVizConverter, absoluteFilePathLocator); - } + act.Should().Throw(); + issueVizConverter.DidNotReceiveWithAnyArgs().Convert(default); + } - private static SonarQubeIssue CreateServerIssue(string issueKey = "issue key", string filePath = "test.cpp", string hash = "hash", string message = "message", string rule = "rule", - SonarQubeIssueSeverity severity = SonarQubeIssueSeverity.Info, Dictionary cctSeverities = null, IssueTextRange textRange = null, - DateTimeOffset created = default, DateTimeOffset lastUpdate = default, bool resolved = true, string contextKey = null, params IssueFlow[] flows) => - new(issueKey, filePath, hash, message, null, rule, resolved, severity, created, lastUpdate, textRange, flows.ToList(), contextKey, defaultImpacts: cctSeverities); + [TestMethod] + public void MqrSeverity_EmptySeverities_Throws() + { + var taintVulnerabilityDto = CreateDefaultTaintDto(new MQRModeDetails(default, [])); - private static IssueLocation CreateServerLocation(string filePath = "test.cpp", string message = "message", - IssueTextRange textRange = null) => new(filePath, null, textRange, message); + var act = () => testSubject.Convert(taintVulnerabilityDto, "C:\\root"); - private static IssueFlow CreateServerFlow(params IssueLocation[] locations) => new(locations.ToList()); + act.Should().Throw(); + issueVizConverter.DidNotReceiveWithAnyArgs().Convert(default); + } - private static IAnalysisIssueVisualization CreateIssueViz(string serverFilePath = null, params IAnalysisIssueLocationVisualization[] locationVizs) - { - var issueViz = new Mock(); + [DataTestMethod] + [DataRow(ImpactSeverity.BLOCKER, SoftwareQualitySeverity.Blocker)] + [DataRow(ImpactSeverity.HIGH, SoftwareQualitySeverity.High)] + [DataRow(ImpactSeverity.MEDIUM, SoftwareQualitySeverity.Medium)] + [DataRow(ImpactSeverity.LOW, SoftwareQualitySeverity.Low)] + [DataRow(ImpactSeverity.INFO, SoftwareQualitySeverity.Info)] + public void MqrSeverity_SingleSeverity_ReturnsIt( + ImpactSeverity slCoreSeverity, + SoftwareQualitySeverity expectedSeverity) + { + var taintVulnerabilityDto = CreateDefaultTaintDto(new MQRModeDetails(default, [new ImpactDto(SoftwareQuality.SECURITY, slCoreSeverity)])); - var flowViz = new Mock(); - flowViz.Setup(x => x.Locations).Returns(locationVizs); + testSubject.Convert(taintVulnerabilityDto, "C:\\root"); - issueViz.Setup(x => x.Flows).Returns(new[] { flowViz.Object }); - issueViz.SetupGet(x => x.Location.FilePath).Returns(serverFilePath); - issueViz.SetupProperty(x => x.CurrentFilePath); - issueViz.SetupProperty(x => x.IsSuppressed); + issueVizConverter.Received() + .Convert(Arg.Is(x => x.HighestSoftwareQualitySeverity == expectedSeverity && x.Severity == null)); + } - return issueViz.Object; - } + [TestMethod] + public void MqrSeverity_InvalidSeverity_Throws() + { + var taintVulnerabilityDto = CreateDefaultTaintDto(new MQRModeDetails(default, [new ImpactDto(SoftwareQuality.SECURITY, (ImpactSeverity)999)])); - private static IAnalysisIssueLocationVisualization CreateLocationViz(string serverFilePath) - { - var locationViz = new Mock(); - locationViz.SetupGet(x => x.Location.FilePath).Returns(serverFilePath); - locationViz.SetupProperty(x => x.CurrentFilePath); + var act = () => testSubject.Convert(taintVulnerabilityDto, "C:\\root"); - return locationViz.Object; - } + act.Should().Throw(); + } - private SonarQubeIssue CreateDummySonarQubeIssue() - { - return CreateServerIssue(textRange: new IssueTextRange(1, 2, 3, 4)); - } + [DataTestMethod] + [DataRow(new[] { ImpactSeverity.LOW, ImpactSeverity.LOW, ImpactSeverity.LOW }, SoftwareQualitySeverity.Low)] + [DataRow(new[] { ImpactSeverity.LOW, ImpactSeverity.LOW, ImpactSeverity.INFO }, SoftwareQualitySeverity.Low)] + [DataRow(new[] { ImpactSeverity.LOW, ImpactSeverity.MEDIUM, ImpactSeverity.LOW }, SoftwareQualitySeverity.Medium)] + [DataRow(new[] { ImpactSeverity.HIGH, ImpactSeverity.MEDIUM, ImpactSeverity.LOW }, SoftwareQualitySeverity.High)] + [DataRow(new[] { ImpactSeverity.MEDIUM, ImpactSeverity.BLOCKER, ImpactSeverity.HIGH }, SoftwareQualitySeverity.Blocker)] + public void MqrSeverity_MultipleSeverities_ReturnsHighest(ImpactSeverity[] slCoreSeverities, SoftwareQualitySeverity expectedSeverity) + { + var qualities = Enum.GetValues(typeof(SoftwareQuality)).Cast().ToArray(); - private ITaintIssue CreateDummyTaintSonarQubeIssue() + if (slCoreSeverities.Length != qualities.Length) { - return CreateTaintServerIssue("key", "rule", DateTimeOffset.UtcNow, SonarQubeIssueSeverity.Blocker, - mainLocation: CreateTaintServerLocation(serverFilePath: "path", message: "blah", - textRange: CreateTaintTextRange(1, 2, 3, 4, "hash"))); + Assert.Fail("Wrong length of the list"); } - private static ITaintIssue CreateTaintServerIssue(string issueKey = "issue1", - string ruleKey = "rule1", - DateTimeOffset creationDate = default, - SonarQubeIssueSeverity severity = SonarQubeIssueSeverity.Blocker, - Dictionary defaultImpacts = null, - ILocation mainLocation = null, - params IFlow[] flows) - { - var issue = new Mock(); + var taintVulnerabilityDto = CreateDefaultTaintDto(new MQRModeDetails(default, qualities.Zip(slCoreSeverities, (x, y) => new ImpactDto(x, y)).ToList())); - issue.SetupGet(x => x.Key).Returns(issueKey); - issue.SetupGet(x => x.RuleKey).Returns(ruleKey); - issue.SetupGet(x => x.Severity).Returns(severity); - issue.SetupGet(x => x.Flows).Returns(flows); - issue.SetupGet(x => x.CreationDate).Returns(creationDate); - issue.SetupGet(x => x.MainLocation).Returns(mainLocation); - issue.SetupGet(x => x.DefaultImpacts).Returns(defaultImpacts); + testSubject.Convert(taintVulnerabilityDto, "C:\\root"); - return issue.Object; - } - - private static IFlow CreateTaintServerFlow(params ILocation[] locations) - { - var flow = new Mock(); + issueVizConverter.Received() + .Convert(Arg.Is(x => x.HighestSoftwareQualitySeverity == expectedSeverity && x.Severity == null)); + } - flow.SetupGet(x => x.Locations).Returns(locations); + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void Convert_IssueVizIsCorrectlyMarkedAsSuppressed(bool isIssueSuppressed) + { + var taintVulnerabilityDto = CreateDefaultTaintDto(resolved: isIssueSuppressed); + var expectedConvertedIssueViz = CreateIssueViz(); + issueVizConverter.Convert(Arg.Any()) + .Returns(expectedConvertedIssueViz); - return flow.Object; - } + testSubject.Convert(taintVulnerabilityDto, "C:\\root"); - private static ILocation CreateTaintServerLocation(string serverFilePath = "file.cpp", string message = "message", ITextRange textRange = null) - { - var location = new Mock(); + expectedConvertedIssueViz.Received().IsSuppressed = isIssueSuppressed; + } - location.SetupGet(x => x.FilePath).Returns(serverFilePath); - location.SetupGet(x => x.Message).Returns(message); - location.SetupGet(x => x.TextRange).Returns(textRange); + private static TaintVulnerabilityDto CreateDefaultTaintDto(Either severity = null, bool resolved = true) => + new( + Guid.Parse("efa697a2-9cfd-4faf-ba21-71b378667a81"), + "serverkey", + resolved, + "rulekey:S123", + "message1", + "file\\path\\1", + DateTimeOffset.Now, + severity ?? new StandardModeDetails(IssueSeverity.MINOR, RuleType.VULNERABILITY), + [], + new TextRangeWithHashDto(1, 2, 3, 4, "hash1"), + "rulecontext", + false); + + private static IAnalysisIssueVisualization CreateIssueViz(string serverFilePath = null, params IAnalysisIssueLocationVisualization[] locationVizs) + { + var issueViz = Substitute.For(); - return location.Object; - } + var flowViz = Substitute.For(); + flowViz.Locations.Returns(locationVizs); - private static ITextRange CreateTaintTextRange(int startLine, int endLine, int startLineOffset, int endLineOffset, string hash) - { - var textRange = new Mock(); + issueViz.Flows.Returns([flowViz]); - textRange.SetupGet(x => x.StartLine).Returns(startLine); - textRange.SetupGet(x => x.EndLine).Returns(endLine); - textRange.SetupGet(x => x.StartLineOffset).Returns(startLineOffset); - textRange.SetupGet(x => x.EndLineOffset).Returns(endLineOffset); - textRange.SetupGet(x => x.Hash).Returns(hash); + var location = Substitute.For(); + issueViz.Location.Returns(location); + location.FilePath.Returns(serverFilePath); - return textRange.Object; - } + return issueViz; } } diff --git a/src/IssueViz.Security.UnitTests/Taint/TaintIssuesBindingMonitorTests.cs b/src/IssueViz.Security.UnitTests/Taint/TaintIssuesBindingMonitorTests.cs deleted file mode 100644 index aed1f4acc5..0000000000 --- a/src/IssueViz.Security.UnitTests/Taint/TaintIssuesBindingMonitorTests.cs +++ /dev/null @@ -1,107 +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 FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarLint.VisualStudio.IssueVisualization.Security.Taint; - -namespace SonarLint.VisualStudio.IssueVisualization.Security.UnitTests.Taint -{ - [TestClass] - public class TaintIssuesBindingMonitorTests - { - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void Ctor_SubscribeToSolutionBindingUpdated() - { - var synchronizer = new Mock(); - var activeSolutionBoundTracker = new Mock(); - var folderWorkspaceInitialized = new Mock(); - - new TaintIssuesBindingMonitor(activeSolutionBoundTracker.Object, folderWorkspaceInitialized.Object, synchronizer.Object); - synchronizer.Invocations.Clear(); - - activeSolutionBoundTracker.Raise(x=> x.SolutionBindingUpdated += null, EventArgs.Empty); - - synchronizer.Verify(x=> x.SynchronizeWithServer(), Times.Once); - } - - [TestMethod] - public void Ctor_SubscribeToSolutionBindingChanged() - { - var synchronizer = new Mock(); - var activeSolutionBoundTracker = new Mock(); - var folderWorkspaceInitialized = new Mock(); - - new TaintIssuesBindingMonitor(activeSolutionBoundTracker.Object, folderWorkspaceInitialized.Object, synchronizer.Object); - synchronizer.Invocations.Clear(); - - activeSolutionBoundTracker.Raise(x => x.SolutionBindingChanged += null, new ActiveSolutionBindingEventArgs(BindingConfiguration.Standalone)); - - synchronizer.Verify(x => x.SynchronizeWithServer(), Times.Once); - } - - [TestMethod] - public void Ctor_SubscribeToFolderInitialized() - { - var synchronizer = new Mock(); - var activeSolutionBoundTracker = new Mock(); - var folderWorkspaceInitialized = new Mock(); - - new TaintIssuesBindingMonitor(activeSolutionBoundTracker.Object, folderWorkspaceInitialized.Object, synchronizer.Object); - synchronizer.Invocations.Clear(); - - folderWorkspaceInitialized.Raise(x => x.FolderWorkspaceInitialized += null, EventArgs.Empty); - - synchronizer.Verify(x => x.SynchronizeWithServer(), Times.Once); - } - - [TestMethod] - public void Dispose_UnsubscribeFromEvents() - { - var synchronizer = new Mock(); - var activeSolutionBoundTracker = new Mock(); - var folderWorkspaceInitialized = new Mock(); - - var testSubject = new TaintIssuesBindingMonitor(activeSolutionBoundTracker.Object, folderWorkspaceInitialized.Object, synchronizer.Object); - testSubject.Dispose(); - synchronizer.Invocations.Clear(); - - folderWorkspaceInitialized.Raise(x => x.FolderWorkspaceInitialized += null, EventArgs.Empty); - activeSolutionBoundTracker.Raise(x => x.SolutionBindingUpdated += null, EventArgs.Empty); - activeSolutionBoundTracker.Raise(x => x.SolutionBindingChanged += null, new ActiveSolutionBindingEventArgs(BindingConfiguration.Standalone)); - - synchronizer.Invocations.Count.Should().Be(0); - } - } -} diff --git a/src/IssueViz.Security.UnitTests/Taint/TaintIssuesConfigurationScopeMonitorTests.cs b/src/IssueViz.Security.UnitTests/Taint/TaintIssuesConfigurationScopeMonitorTests.cs new file mode 100644 index 0000000000..04b0d5fe97 --- /dev/null +++ b/src/IssueViz.Security.UnitTests/Taint/TaintIssuesConfigurationScopeMonitorTests.cs @@ -0,0 +1,63 @@ +/* + * 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.IssueVisualization.Security.Taint; +using SonarLint.VisualStudio.SLCore.State; + +namespace SonarLint.VisualStudio.IssueVisualization.Security.UnitTests.Taint; + +[TestClass] +public class TaintIssuesConfigurationScopeMonitorTests +{ + private TaintIssuesConfigurationScopeMonitor testSubject; + private IActiveConfigScopeTracker activeConfigScopeTracker; + private ITaintIssuesSynchronizer taintIssuesSynchronizer; + + [TestInitialize] + public void TestInitialize() + { + activeConfigScopeTracker = Substitute.For(); + taintIssuesSynchronizer = Substitute.For(); + testSubject = new TaintIssuesConfigurationScopeMonitor(activeConfigScopeTracker, taintIssuesSynchronizer); + } + + [TestMethod] + public void Ctor_SubscribesToConfigurationScopeEvents() => + activeConfigScopeTracker.Received().CurrentConfigurationScopeChanged += Arg.Any(); + + [TestMethod] + public void Dispose_UnsubscribesToConfigurationScopeEvents() + { + testSubject.Dispose(); + + activeConfigScopeTracker.Received().CurrentConfigurationScopeChanged -= Arg.Any(); + } + + [TestMethod] + public void ConfigScopeChangedEvent_CallsTaintSynchronizer() + { + var configurationScope = new ConfigurationScope("config scope"); + activeConfigScopeTracker.Current.Returns(configurationScope); + + activeConfigScopeTracker.CurrentConfigurationScopeChanged += Raise.Event(); + + taintIssuesSynchronizer.Received(1).UpdateTaintVulnerabilitiesAsync(configurationScope); + } +} diff --git a/src/IssueViz.Security.UnitTests/Taint/TaintIssuesSynchronizerTests.cs b/src/IssueViz.Security.UnitTests/Taint/TaintIssuesSynchronizerTests.cs index a71686cddd..0db01427a7 100644 --- a/src/IssueViz.Security.UnitTests/Taint/TaintIssuesSynchronizerTests.cs +++ b/src/IssueViz.Security.UnitTests/Taint/TaintIssuesSynchronizerTests.cs @@ -18,570 +18,330 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Linq; -using System.Threading; -using FluentAssertions; -using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio; using Microsoft.VisualStudio.Shell.Interop; -using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; +using NSubstitute.ClearExtensions; +using NSubstitute.Core; +using NSubstitute.ExceptionExtensions; +using NSubstitute.ReceivedExtensions; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; +using SonarLint.VisualStudio.Core.Synchronization; using SonarLint.VisualStudio.Infrastructure.VS; using SonarLint.VisualStudio.IssueVisualization.Models; using SonarLint.VisualStudio.IssueVisualization.Security.Taint; using SonarLint.VisualStudio.IssueVisualization.Security.Taint.TaintList; +using SonarLint.VisualStudio.SLCore.Common.Models; +using SonarLint.VisualStudio.SLCore.Core; +using SonarLint.VisualStudio.SLCore.Protocol; +using SonarLint.VisualStudio.SLCore.Service.Rules.Models; +using SonarLint.VisualStudio.SLCore.Service.Taint; +using SonarLint.VisualStudio.SLCore.State; using SonarLint.VisualStudio.TestInfrastructure; using SonarQube.Client; -using SonarQube.Client.Models; -using Task = System.Threading.Tasks.Task; -using VSShellInterop = Microsoft.VisualStudio.Shell.Interop; +namespace SonarLint.VisualStudio.IssueVisualization.Security.UnitTests.Taint; -namespace SonarLint.VisualStudio.IssueVisualization.Security.UnitTests.Taint +[TestClass] +public class TaintIssuesSynchronizerTests { - [TestClass] - public class TaintIssuesSynchronizerTests + private ITaintStore taintStore; + private ISLCoreServiceProvider slCoreServiceProvider; + private ITaintVulnerabilityTrackingSlCoreService taintService; + private ITaintIssueToIssueVisualizationConverter converter; + private IToolWindowService toolWindowService; + private IVsUIServiceOperation vsUiServiceOperation; + private IVsMonitorSelection vsMonitorSelection; + private IThreadHandling threadHandling; + private IAsyncLockFactory asyncLockFactory; + private IAsyncLock asyncLock; + private TestLogger logger; + private TaintIssuesSynchronizer testSubject; + private IReleaseAsyncLock asyncLockReleaser; + private static readonly ConfigurationScope None = null; + private static readonly ConfigurationScope Standalone = new("some id 1", null, null, "some path"); + private static readonly ConfigurationScope ConnectedWithUninitializedRoot = new("some id 2", "some connection", "some project"); + private static readonly ConfigurationScope ConnectedReady = new("some id 3", "some connection", "some project", "some path"); + + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + + [TestMethod] + public void Ctor_DoesNotCallAnyServices() { - private static readonly BindingConfiguration BindingConfig_Standalone = BindingConfiguration.Standalone; - private static readonly BindingConfiguration BindingConfig_Connected = CreateBindingConfig(SonarLintMode.Connected, "any project key"); - - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - // The constructor calls the service provider so we need to pass a correctly-configured one - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void MefCtor_DoesNotCallAnyServices() - { - var taintStore = new Mock(); - var sonarQubeService = new Mock(); - var taintIssueToIssueVisualizationConverter = new Mock(); - var configurationProvider = new Mock(); - var statefulServerBranchProvider = new Mock(); - var vsUIServiceOperation = new Mock(); - var toolWindowService = new Mock(); - var logger = new Mock(); - - _ = new TaintIssuesSynchronizer(taintStore.Object, sonarQubeService.Object, taintIssueToIssueVisualizationConverter.Object, configurationProvider.Object, - toolWindowService.Object, statefulServerBranchProvider.Object, vsUIServiceOperation.Object, logger.Object); - - // The MEF constructor should be free-threaded, which it will be if - // it doesn't make any external calls. - taintStore.Invocations.Should().BeEmpty(); - sonarQubeService.Invocations.Should().BeEmpty(); - taintIssueToIssueVisualizationConverter.Invocations.Should().BeEmpty(); - configurationProvider.Invocations.Should().BeEmpty(); - toolWindowService.Invocations.Should().BeEmpty(); - statefulServerBranchProvider.Invocations.Should().BeEmpty(); - vsUIServiceOperation.Invocations.Should().BeEmpty(); - logger.Invocations.Should().BeEmpty(); - } - - [TestMethod] - public async Task SynchronizeWithServer_NonCriticalException_UIContextAndStoreCleared() - { - var logger = new TestLogger(); - - var sonarServer = CreateSonarService(); - sonarServer.Setup(x => x.GetTaintVulnerabilitiesAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Throws(new Exception("this is a test")); - - var taintStore = new Mock(); - const uint cookie = 123; - var monitor = CreateMonitorSelectionMock(cookie); - - var testSubject = CreateTestSubject( - bindingConfig: BindingConfig_Connected, - sonarService: sonarServer.Object, - taintStore: taintStore.Object, - vsMonitor: monitor.Object, - logger: logger); - - Func act = testSubject.SynchronizeWithServer; - await act.Should().NotThrowAsync(); - - CheckStoreIsCleared(taintStore); - CheckUIContextIsCleared(monitor, cookie); - logger.AssertPartialOutputStringExists("this is a test"); - } - - [TestMethod] - public async Task SynchronizeWithServer_CriticalException_ExceptionNotCaught() - { - var sonarServer = CreateSonarService(); - sonarServer.Setup(x => x.GetTaintVulnerabilitiesAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Throws(new StackOverflowException()); - - var testSubject = CreateTestSubject(sonarService: sonarServer.Object); - - Func act = testSubject.SynchronizeWithServer; - await act.Should().ThrowAsync(); - } - - [TestMethod] - [Description("Regression test for https://github.com/SonarSource/sonarlint-visualstudio/issues/3152")] - public void SynchronizeWithServer_DisconnectedInTheMiddle_ServerInfoIsReusedAndNoExceptions() - { - var sonarQubeServer = new Mock(); - sonarQubeServer - .SetupSequence(x => x.GetServerInfo()) - .Returns(new ServerInfo(new Version(1, 1), ServerType.SonarQube)) - .Returns((ServerInfo)null); - - var logger = new TestLogger(); - - var testSubject = CreateTestSubject( - bindingConfig: BindingConfig_Connected, - sonarService: sonarQubeServer.Object, - logger: logger); - - Func act = testSubject.SynchronizeWithServer; - - act.Should().NotThrow(); - - logger.AssertPartialOutputStringDoesNotExist("NullReferenceException"); - } - - [TestMethod] - public async Task SynchronizeWithServer_StandaloneMode_StoreAndUIContextCleared() - { - var sonarQubeServer = new Mock(); - var converter = new Mock(); - var taintStore = new Mock(); - var serverBranchProvider = new Mock(); - var logger = new TestLogger(); - - const uint cookie = 123; - var monitor = CreateMonitorSelectionMock(cookie); - var toolWindowService = new Mock(); - - var testSubject = CreateTestSubject( - bindingConfig: BindingConfig_Standalone, - taintStore: taintStore.Object, - sonarService: sonarQubeServer.Object, - converter: converter.Object, - serverBranchProvider: serverBranchProvider.Object, - vsMonitor: monitor.Object, - toolWindowService: toolWindowService.Object, - logger: logger); - - await testSubject.SynchronizeWithServer(); - - CheckStoreIsCleared(taintStore); - CheckUIContextIsCleared(monitor, cookie); - logger.AssertPartialOutputStringExists("not in connected mode"); - - // Server components should not be called - sonarQubeServer.Invocations.Should().HaveCount(0); - converter.Invocations.Should().HaveCount(0); - serverBranchProvider.Invocations.Should().HaveCount(0); - toolWindowService.Invocations.Should().HaveCount(0); - } + asyncLockFactory.ClearSubstitute(); + var loggerMock = Substitute.For(); + // The MEF constructor should be free-threaded, which it will be if + // it doesn't make any external calls. AsyncLockFactory is free-threaded, calling it is allowed + testSubject = new TaintIssuesSynchronizer(taintStore, + slCoreServiceProvider, + converter, + toolWindowService, + vsUiServiceOperation, + threadHandling, + asyncLockFactory, + loggerMock); + taintStore.ReceivedCalls().Should().BeEmpty(); + slCoreServiceProvider.ReceivedCalls().Should().BeEmpty(); + converter.ReceivedCalls().Should().BeEmpty(); + toolWindowService.ReceivedCalls().Should().BeEmpty(); + vsUiServiceOperation.ReceivedCalls().Should().BeEmpty(); + threadHandling.ReceivedCalls().Should().BeEmpty(); + asyncLockFactory.Received(1).Create(); + loggerMock.ReceivedCalls().Should().BeEmpty(); + } - [TestMethod] - public async Task SynchronizeWithServer_SonarQubeServerNotYetConnected_StoreAndUIContextCleared() - { - var sonarService = CreateSonarService(isConnected: false); - var converter = new Mock(); - var taintStore = new Mock(); - var logger = new TestLogger(); - - const uint cookie = 999; - var monitor = CreateMonitorSelectionMock(cookie); - var toolWindowService = new Mock(); - - var testSubject = CreateTestSubject( - bindingConfig: BindingConfig_Connected, - taintStore: taintStore.Object, - converter: converter.Object, - sonarService: sonarService.Object, - vsMonitor: monitor.Object, - toolWindowService: toolWindowService.Object, - logger: logger); - - await testSubject.SynchronizeWithServer(); - - logger.AssertPartialOutputStringExists("not yet established"); - CheckConnectedStatusIsChecked(sonarService); - CheckIssuesAreNotFetched(sonarService); - - CheckStoreIsCleared(taintStore); - CheckUIContextIsCleared(monitor, cookie); - - // Should be nothing to convert or display in the tool window - converter.Invocations.Should().HaveCount(0); - toolWindowService.Invocations.Should().HaveCount(0); - } + [TestInitialize] + public void TestInitialize() + { + taintStore = Substitute.For(); + taintService = Substitute.For(); + slCoreServiceProvider = CreateDefaultServiceProvider(taintService); + converter = Substitute.For(); + toolWindowService = Substitute.For(); + vsMonitorSelection = Substitute.For(); + vsUiServiceOperation = CreateDefaultServiceOperation(vsMonitorSelection); + threadHandling = CreateDefaultThreadHandling(); + asyncLock = Substitute.For(); + asyncLockReleaser = Substitute.For(); + asyncLockFactory = CreateDefaultAsyncLockFactory(asyncLock, asyncLockReleaser); + logger = new TestLogger(); + testSubject = new TaintIssuesSynchronizer(taintStore, + slCoreServiceProvider, + converter, + toolWindowService, + vsUiServiceOperation, + threadHandling, + asyncLockFactory, + logger); + } - [TestMethod] - [DataRow("7.9")] - [DataRow("8.5.9.9")] - public async Task SynchronizeWithServer_UnsupportedServerVersion_StoreAndUIContextCleared(string versionString) - { - var sonarQubeServer = CreateSonarService(isConnected: true, serverType: ServerType.SonarQube, versionString); - var logger = new TestLogger(); + [TestMethod] + public async Task UpdateTaintVulnerabilitiesAsync_NonCriticalException_UIContextAndStoreCleared() + { + slCoreServiceProvider.TryGetTransientService(out Arg.Any()).Throws(new Exception("this is a test")); - const uint cookie = 999; - var monitor = CreateMonitorSelectionMock(cookie); - var taintStore = new Mock(); + const uint cookie = 123; + SetUpMonitorSelectionMock(cookie); - var testSubject = CreateTestSubject( - bindingConfig: BindingConfig_Connected, - taintStore: taintStore.Object, - sonarService: sonarQubeServer.Object, - vsMonitor: monitor.Object, - logger: logger); + var act = () => testSubject.UpdateTaintVulnerabilitiesAsync(ConnectedReady); - await testSubject.SynchronizeWithServer(); + await act.Should().NotThrowAsync(); + CheckStoreIsCleared(); + CheckUIContextIsCleared(cookie); + logger.AssertPartialOutputStringExists("this is a test"); + asyncLockReleaser.Received().Dispose(); + } - logger.AssertPartialOutputStringExists("requires SonarQube v8.6 or later"); - logger.AssertPartialOutputStringExists($"Connected SonarQube version: v{versionString}"); + [TestMethod] + public async Task UpdateTaintVulnerabilitiesAsync_CriticalException_ExceptionNotCaught() + { + slCoreServiceProvider.TryGetTransientService(out Arg.Any()).Throws(new DivideByZeroException()); - CheckIssuesAreNotFetched(sonarQubeServer); - CheckStoreIsCleared(taintStore); - CheckUIContextIsCleared(monitor, cookie); - } + var act = async () => await testSubject.UpdateTaintVulnerabilitiesAsync(ConnectedReady); - [TestMethod] - [DataRow(ServerType.SonarCloud, "0.1")] - [DataRow(ServerType.SonarQube, "8.6.0.0")] - [DataRow(ServerType.SonarQube, "9.9")] - public async Task SynchronizeWithServer_SupportedServer_IssuesFetched(ServerType serverType, string serverVersion) - { - var logger = new TestLogger(); - var sonarServer = CreateSonarService(isConnected: true, serverType, serverVersion); + await act.Should().ThrowAsync(); + asyncLockReleaser.Received().Dispose(); + } - var bindingConfig = CreateBindingConfig(SonarLintMode.Connected, "keyXXX"); - var serverBranchProvider = CreateServerBranchProvider("branchXXX"); - SetupTaintIssues(sonarServer, "keyXXX", "branchXXX"); + [TestMethod] + public async Task UpdateTaintVulnerabilitiesAsync_NoConfigurationScope_StoreAndUIContextCleared() + { + const uint cookie = 123; + SetUpMonitorSelectionMock(cookie); - var testSubject = CreateTestSubject( - bindingConfig: bindingConfig, - serverBranchProvider: serverBranchProvider.Object, - sonarService: sonarServer.Object, - logger: logger); + await testSubject.UpdateTaintVulnerabilitiesAsync(None); - await testSubject.SynchronizeWithServer(); + CheckStoreIsCleared(); + CheckUIContextIsCleared(cookie); + logger.AssertPartialOutputStringExists("not in connected mode"); + slCoreServiceProvider.ReceivedCalls().Should().BeEmpty(); + converter.ReceivedCalls().Should().BeEmpty(); + toolWindowService.ReceivedCalls().Should().BeEmpty(); + } - logger.AssertPartialOutputStringDoesNotExist("requires SonarQube v8.6 or later"); - CheckIssuesAreFetched(sonarServer, "keyXXX", "branchXXX"); - } + [TestMethod] + public async Task UpdateTaintVulnerabilitiesAsync_StandaloneMode_StoreAndUIContextCleared() + { + const uint cookie = 123; + SetUpMonitorSelectionMock(cookie); - [TestMethod] - [DataRow(SonarLintMode.Connected)] - [DataRow(SonarLintMode.LegacyConnected)] - public async Task SynchronizeWithServer_ConnectedModeWithNoIssues_StoreIsSetAndUIContextCleared(SonarLintMode sonarLintMode) - { - var sonarService = CreateSonarService(isConnected: true); - var bindingConfig = CreateBindingConfig(sonarLintMode, "my-project-key"); - var serverBranchProvider = CreateServerBranchProvider("my-branch"); - var analysisInformation = new AnalysisInformation("my-branch", DateTimeOffset.Now); - - SetupTaintIssues(sonarService, "my-project-key", "my-branch" /* no issues */); - SetupAnalysisInformation(sonarService, "my-project-key", analysisInformation); - - var taintStore = new Mock(); - var converter = new Mock(); - - const uint cookie = 999; - var monitor = CreateMonitorSelectionMock(cookie); - var toolWindowService = new Mock(); - - var testSubject = CreateTestSubject( - bindingConfig: bindingConfig, - taintStore: taintStore.Object, - converter: converter.Object, - sonarService: sonarService.Object, - serverBranchProvider: serverBranchProvider.Object, - vsMonitor: monitor.Object, - toolWindowService: toolWindowService.Object); - - await testSubject.SynchronizeWithServer(); - - CheckConnectedStatusIsChecked(sonarService); - CheckIssuesAreFetched(sonarService, "my-project-key", "my-branch"); - CheckUIContextIsCleared(monitor, cookie); - - taintStore.Verify(x => x.Set(Enumerable.Empty(), - It.Is((AnalysisInformation a) => - a.AnalysisTimestamp == analysisInformation.AnalysisTimestamp && - a.BranchName == analysisInformation.BranchName)), Times.Once); - - // Should be nothing to display in the tool window - toolWindowService.Invocations.Should().HaveCount(0); - } + await testSubject.UpdateTaintVulnerabilitiesAsync(Standalone); - [TestMethod] - [DataRow(SonarLintMode.Connected)] - [DataRow(SonarLintMode.LegacyConnected)] - public async Task SynchronizeWithServer_ConnectedMode_UsesExpectedBranch(SonarLintMode sonarLintMode) - { - var bindingConfig = CreateBindingConfig(sonarLintMode, "xxx_project-key"); + CheckStoreIsCleared(); + CheckUIContextIsCleared(cookie); + logger.AssertPartialOutputStringExists("not in connected mode"); + slCoreServiceProvider.ReceivedCalls().Should().BeEmpty(); + toolWindowService.ReceivedCalls().Should().BeEmpty(); + } - var serverBranchProvider = CreateServerBranchProvider("branch-XYZ"); + [TestMethod] + public async Task UpdateTaintVulnerabilitiesAsync_ConnectedModeConfigScope_NotReady_StoreAndUIContextCleared() + { + const uint cookie = 123; + SetUpMonitorSelectionMock(cookie); - var sonarQubeService = CreateSonarService(isConnected: true); - SetupTaintIssues(sonarQubeService, "xxx_project-key", "branch-XYZ"); + await testSubject.UpdateTaintVulnerabilitiesAsync(ConnectedWithUninitializedRoot); - var testSubject = CreateTestSubject( - bindingConfig: bindingConfig, - sonarService: sonarQubeService.Object, - serverBranchProvider: serverBranchProvider.Object); + CheckStoreIsCleared(); + CheckUIContextIsCleared(cookie); + slCoreServiceProvider.ReceivedCalls().Should().BeEmpty(); + toolWindowService.ReceivedCalls().Should().BeEmpty(); + } - await testSubject.SynchronizeWithServer(); + [TestMethod] + public async Task UpdateTaintVulnerabilitiesAsync_SLCoreNotInitialized_StoreAndUIContextCleared() + { + slCoreServiceProvider.TryGetTransientService(out ITaintVulnerabilityTrackingSlCoreService _).Returns(false); + const uint cookie = 123; + SetUpMonitorSelectionMock(cookie); - serverBranchProvider.VerifyAll(); - sonarQubeService.Verify(x => x.GetTaintVulnerabilitiesAsync("xxx_project-key", "branch-XYZ", It.IsAny()), - Times.Once()); - } + await testSubject.UpdateTaintVulnerabilitiesAsync(ConnectedReady); - [TestMethod] - [DataRow(SonarLintMode.Connected)] - [DataRow(SonarLintMode.LegacyConnected)] - public async Task SynchronizeWithServer_ConnectedModeWithIssues_IssuesAddedToStore(SonarLintMode sonarLintMode) + CheckStoreIsCleared(); + CheckUIContextIsCleared(cookie); + Received.InOrder(() => { - var serverIssue1 = new TestSonarQubeIssue(); - var serverIssue2 = new TestSonarQubeIssue(); - var issueViz1 = Mock.Of(); - var issueViz2 = Mock.Of(); - - var converter = new Mock(); - converter.Setup(x => x.Convert(serverIssue1)).Returns(issueViz1); - converter.Setup(x => x.Convert(serverIssue2)).Returns(issueViz2); - - var taintStore = new Mock(); - - var analysisInformation = new AnalysisInformation("a branch", DateTimeOffset.Now); - - var sonarServer = CreateSonarService(); + threadHandling.RunOnBackgroundThread(Arg.Any>>()); + asyncLock.AcquireAsync(); + slCoreServiceProvider.TryGetTransientService(out Arg.Any()); + asyncLockReleaser.Dispose(); + }); + taintService.ReceivedCalls().Should().BeEmpty(); + toolWindowService.ReceivedCalls().Should().BeEmpty(); + } - var serverBranchProvider = CreateServerBranchProvider("a branch"); - var bindingConfig = CreateBindingConfig(sonarLintMode, "projectKey123"); - SetupTaintIssues(sonarServer, "projectKey123", "a branch", serverIssue1, serverIssue2); - SetupAnalysisInformation(sonarServer, "projectKey123", analysisInformation); + [TestMethod] + public async Task UpdateTaintVulnerabilitiesAsync_TaintStoreAlreadyInitialized_Ignored() + { + taintStore.ConfigurationScope.Returns(ConnectedReady.Id); - var testSubject = CreateTestSubject( - bindingConfig: bindingConfig, - taintStore: taintStore.Object, - converter: converter.Object, - sonarService: sonarServer.Object, - serverBranchProvider: serverBranchProvider.Object); + await testSubject.UpdateTaintVulnerabilitiesAsync(ConnectedReady); - await testSubject.SynchronizeWithServer(); + taintStore.DidNotReceiveWithAnyArgs().Set(default, default); + taintService.ReceivedCalls().Should().BeEmpty(); + } - taintStore.Verify(x => x.Set(new[] { issueViz1, issueViz2 }, - It.Is((AnalysisInformation a) => - a.AnalysisTimestamp == analysisInformation.AnalysisTimestamp && - a.BranchName == analysisInformation.BranchName)), Times.Once); - } + [TestMethod] + public async Task UpdateTaintVulnerabilitiesAsync_NoIssuesForConfigScope_SetsStoreAndClearsUIContext() + { + const uint cookie = 123; + SetUpMonitorSelectionMock(cookie); + taintService.ListAllAsync(Arg.Is(x => x.shouldRefresh && x.configurationScopeId == ConnectedReady.Id)).Returns(new ListAllTaintsResponse([])); - [TestMethod] - [DataRow(SonarLintMode.Connected)] - [DataRow(SonarLintMode.LegacyConnected)] - public async Task SynchronizeWithServer_ConnectedModeWithIssues_UIContextIsSetAndToolWindowCalled(SonarLintMode sonarLintMode) - { - var sonarService = CreateSonarService(); - - var bindingConfig = CreateBindingConfig(sonarLintMode, "myProjectKey___"); - var serverBranchProvider = CreateServerBranchProvider("branchYYY"); - SetupTaintIssues(sonarService, "myProjectKey___", "branchYYY", new TestSonarQubeIssue()); - SetupAnalysisInformation(sonarService, "myProjectKey___", new AnalysisInformation("branchYYY", DateTimeOffset.Now)); - - const uint cookie = 212; - var monitor = CreateMonitorSelectionMock(cookie); - var toolWindowService = new Mock(); - - var testSubject = CreateTestSubject( - bindingConfig: bindingConfig, - serverBranchProvider: serverBranchProvider.Object, - sonarService: sonarService.Object, - vsMonitor: monitor.Object, - toolWindowService: toolWindowService.Object); - - await testSubject.SynchronizeWithServer(); - - CheckConnectedStatusIsChecked(sonarService); - CheckIssuesAreFetched(sonarService, "myProjectKey___", "branchYYY"); - CheckUIContextIsSet(monitor, cookie); - CheckToolWindowServiceIsCalled(toolWindowService); - } + await testSubject.UpdateTaintVulnerabilitiesAsync(ConnectedReady); - [TestMethod] - [DataRow(null)] - [DataRow("")] - [DataRow("unknown local branch")] - public async Task SynchronizeWithServer_NoMatchingServerBranch_UIContextAndStoreCleared(string localBranch) - { - var sonarService = CreateSonarService(); - var bindingConfig = CreateBindingConfig(SonarLintMode.Connected, "my proj"); - var serverBranchProvider = CreateServerBranchProvider(localBranch); - SetupTaintIssues(sonarService, "my proj", localBranch, new TestSonarQubeIssue()); - - var analysisInformation = new AnalysisInformation("some main branch", DateTimeOffset.Now); - SetupAnalysisInformation(sonarService, "my proj", analysisInformation); - - const uint cookie = 212; - var monitor = CreateMonitorSelectionMock(cookie); - var toolWindowService = new Mock(); - var taintStore = new Mock(); - - var testSubject = CreateTestSubject( - taintStore: taintStore.Object, - bindingConfig: bindingConfig, - serverBranchProvider: serverBranchProvider.Object, - sonarService: sonarService.Object, - vsMonitor: monitor.Object, - toolWindowService: toolWindowService.Object); - - using (new AssertIgnoreScope()) - { - await testSubject.SynchronizeWithServer(); - } - - CheckIssuesAreFetched(sonarService, "my proj", localBranch); - CheckUIContextIsCleared(monitor, cookie); - CheckStoreIsCleared(taintStore); - } + CheckUIContextIsCleared(cookie); + converter.DidNotReceiveWithAnyArgs().Convert(default, default); + taintStore.Received(1).Set(Arg.Is>(x => x.SequenceEqual(Array.Empty())), ConnectedReady.Id); + toolWindowService.ReceivedCalls().Should().BeEmpty(); + logger.AssertPartialOutputStringExists(string.Format(TaintResources.Synchronizer_NumberOfServerIssues, 0)); + } - private static BindingConfiguration CreateBindingConfig(SonarLintMode mode = SonarLintMode.Connected, string projectKey = "any") - => new(new BoundServerProject("solution", projectKey, new ServerConnection.SonarQube(new Uri("http://bound"))), mode, "any dir"); - - private static TaintIssuesSynchronizer CreateTestSubject( - BindingConfiguration bindingConfig = null, - ITaintStore taintStore = null, - ITaintIssueToIssueVisualizationConverter converter = null, - ILogger logger = null, - ISonarQubeService sonarService = null, - IStatefulServerBranchProvider serverBranchProvider = null, - IVsMonitorSelection vsMonitor = null, - IToolWindowService toolWindowService = null) + [TestMethod] + public async Task UpdateTaintVulnerabilitiesAsync_MultipleIssues_SetsStoreAndUIContextAndCallsToolWindow() + { + const uint cookie = 123; + SetUpMonitorSelectionMock(cookie); + List taints = [CreateDefaultTaintDto(), CreateDefaultTaintDto(), CreateDefaultTaintDto()]; + List taintVisualizations = [Substitute.For(), Substitute.For(), Substitute.For()]; + taintService.ListAllAsync(Arg.Is(x => x.shouldRefresh && x.configurationScopeId == ConnectedReady.Id)).Returns(new ListAllTaintsResponse(taints)); + for (var i = 0; i < taints.Count; i++) { - taintStore ??= Mock.Of(); - converter ??= Mock.Of(); - - var serviceOperation = CreateServiceOperation(vsMonitor); - - bindingConfig ??= CreateBindingConfig(SonarLintMode.Connected, "any branch"); - - var configurationProvider = new Mock(); - configurationProvider - .Setup(x => x.GetConfiguration()) - .Returns(bindingConfig); - - sonarService ??= CreateSonarService().Object; - serverBranchProvider ??= Mock.Of(); - toolWindowService ??= Mock.Of(); - - logger ??= Mock.Of(); - - return new TaintIssuesSynchronizer(taintStore, sonarService, converter, configurationProvider.Object, - toolWindowService, serverBranchProvider, serviceOperation, logger); + converter.Convert(taints[i], ConnectedReady.RootPath).Returns(taintVisualizations[i]); } - private static IVsUIServiceOperation CreateServiceOperation(IVsMonitorSelection svcToPassToCallback) - { - svcToPassToCallback ??= Mock.Of(); - - var serviceOp = new Mock(); - - // Set up the mock to invoke the operation with the supplied VS service - serviceOp.Setup(x => x.Execute(It.IsAny>())) - .Callback>(op => op(svcToPassToCallback)); + await testSubject.UpdateTaintVulnerabilitiesAsync(ConnectedReady); - return serviceOp.Object; - } - - private static Mock CreateServerBranchProvider(string branchName) - { - var serverBranchProvider = new Mock(); - serverBranchProvider.Setup(x => x.GetServerBranchNameAsync(It.IsAny())).ReturnsAsync(branchName); - return serverBranchProvider; - } + CheckUIContextIsSet(cookie); + converter.ReceivedWithAnyArgs(3).Convert(default, default); + taintStore.Received(1).Set(Arg.Is>(x => x.SequenceEqual(taintVisualizations)), ConnectedReady.Id); + CheckToolWindowServiceIsCalled(); + logger.AssertPartialOutputStringExists(string.Format(TaintResources.Synchronizer_NumberOfServerIssues, 3)); + } - private static Mock CreateSonarService(bool isConnected = true, - ServerType serverType = ServerType.SonarQube, - string versionString = "9.9") + private void SetUpMonitorSelectionMock(uint cookie) + { + var localGuid = TaintIssuesExistUIContext.Guid; + vsMonitorSelection.GetCmdUIContextCookie(ref localGuid, out Arg.Any()).Returns(call => { - var serverInfo = isConnected ? new ServerInfo(new Version(versionString), serverType) : null; - - var sonarQubeService = new Mock(); - sonarQubeService.Setup(x => x.GetServerInfo()).Returns(serverInfo); - - return sonarQubeService; - } + call[1] = cookie; + return VSConstants.S_OK; + }); + } - private static Mock CreateMonitorSelectionMock(uint cookie) + private static ISLCoreServiceProvider CreateDefaultServiceProvider(ITaintVulnerabilityTrackingSlCoreService taintService) + { + var slCoreServiceProvider = Substitute.For(); + slCoreServiceProvider.TryGetTransientService(out ITaintVulnerabilityTrackingSlCoreService _).Returns(call => { - var monitor = new Mock(); - var localGuid = TaintIssuesExistUIContext.Guid; - monitor.Setup(x => x.GetCmdUIContextCookie(ref localGuid, out cookie)); + call[0] = taintService; + return true; + }); - return monitor; - } + return slCoreServiceProvider; + } - private static void CheckUIContextIsCleared(Mock monitorMock, uint expectedCookie) => - CheckUIContextUpdated(monitorMock, expectedCookie, 0); + private static IAsyncLockFactory CreateDefaultAsyncLockFactory(IAsyncLock asyncLock, IReleaseAsyncLock release) + { + var factory = Substitute.For(); + factory.Create().Returns(asyncLock); + asyncLock.AcquireAsync().Returns(release); + return factory; + } - private static void CheckUIContextIsSet(Mock monitorMock, uint expectedCookie) => - CheckUIContextUpdated(monitorMock, expectedCookie, 1); + private static IVsUIServiceOperation CreateDefaultServiceOperation(IVsMonitorSelection svcToPassToCallback) + { + var serviceOp = Substitute.For(); - private static void CheckUIContextUpdated(Mock monitorMock, uint expectedCookie, int expectedState) => - monitorMock.Verify(x => x.SetCmdUIContext(expectedCookie, expectedState), Times.Once); + // Set up the mock to invoke the operation with the supplied VS service + serviceOp.When(x => x.Execute(Arg.Any>())) + .Do(call => call.Arg>().Invoke(svcToPassToCallback)); - private static void CheckConnectedStatusIsChecked(Mock serviceMock) => - serviceMock.Verify(x => x.GetServerInfo(), Times.Once); + return serviceOp; + } - private static void CheckIssuesAreFetched(Mock serviceMock, string projectKey, string branch) => - serviceMock.Verify(x => x.GetTaintVulnerabilitiesAsync(projectKey, branch, It.IsAny()), Times.Once); + private static IThreadHandling CreateDefaultThreadHandling() + { + var mock = Substitute.For(); + mock.RunOnBackgroundThread(Arg.Any>>()).Returns(call => call.Arg>>()()); + return mock; + } - private static void CheckIssuesAreNotFetched(Mock serviceMock) => - serviceMock.Verify(x => x.GetTaintVulnerabilitiesAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + private static TaintVulnerabilityDto CreateDefaultTaintDto() => + new( + Guid.Parse("efa697a2-9cfd-4faf-ba21-71b378667a81"), + "serverkey", + true, + "rulekey:S123", + "message1", + "file\\path\\1", + DateTimeOffset.Now, + new StandardModeDetails(IssueSeverity.MINOR, RuleType.VULNERABILITY), + [], + new TextRangeWithHashDto(1, 2, 3, 4, "hash1"), + "rulecontext", + false); - private static void CheckToolWindowServiceIsCalled(Mock toolWindowServiceMock) => - toolWindowServiceMock.Verify(x => x.EnsureToolWindowExists(TaintToolWindow.ToolWindowId), Times.Once); + private void CheckUIContextIsCleared(uint expectedCookie) => CheckUIContextUpdated(expectedCookie, 0); - private void CheckStoreIsCleared(Mock taintStore) => - taintStore.Verify(x => x.Set(Enumerable.Empty(), null), Times.Once()); + private void CheckUIContextIsSet(uint expectedCookie) => CheckUIContextUpdated(expectedCookie, 1); - private class TestSonarQubeIssue : SonarQubeIssue - { - public TestSonarQubeIssue() - : base("test", "test", "test", "test", "test", "test", true, SonarQubeIssueSeverity.Info, - DateTimeOffset.MinValue, DateTimeOffset.MinValue, null, null) - { - } - } + private void CheckUIContextUpdated(uint expectedCookie, int expectedState) => + vsMonitorSelection.Received(1).SetCmdUIContext(expectedCookie, expectedState); - private void SetupAnalysisInformation(Mock sonarQubeService, string projectKey, AnalysisInformation mainBranchInformation) - { - var projectBranches = new[] - { - new SonarQubeProjectBranch(Guid.NewGuid().ToString(), false, DateTimeOffset.MaxValue, "BRANCH"), - new SonarQubeProjectBranch(mainBranchInformation.BranchName, true, mainBranchInformation.AnalysisTimestamp, "BRANCH"), - new SonarQubeProjectBranch(Guid.NewGuid().ToString(), false, DateTimeOffset.MinValue, "BRANCH") - }; - - sonarQubeService.Setup(x => x.GetProjectBranchesAsync(projectKey, CancellationToken.None)) - .ReturnsAsync(projectBranches); - } + private void CheckToolWindowServiceIsCalled() => + toolWindowService.Received().EnsureToolWindowExists(TaintToolWindow.ToolWindowId); - private void SetupTaintIssues(Mock sonarQubeService, string projectKey, string branch, params SonarQubeIssue[] issues) - { - sonarQubeService - .Setup(x => x.GetTaintVulnerabilitiesAsync(projectKey, branch, CancellationToken.None)) - .ReturnsAsync(issues); - } - } + private void CheckStoreIsCleared() => taintStore.Received(1).Set([], null); } diff --git a/src/IssueViz.Security.UnitTests/Taint/TaintList/TaintIssuesControlViewModelTests.Caption.cs b/src/IssueViz.Security.UnitTests/Taint/TaintList/TaintIssuesControlViewModelTests.Caption.cs index f458b4c5f5..ff3fd84cf1 100644 --- a/src/IssueViz.Security.UnitTests/Taint/TaintList/TaintIssuesControlViewModelTests.Caption.cs +++ b/src/IssueViz.Security.UnitTests/Taint/TaintList/TaintIssuesControlViewModelTests.Caption.cs @@ -27,6 +27,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.Text; using Moq; +using SonarLint.VisualStudio.Core.Analysis; using SonarLint.VisualStudio.Core.Telemetry; using SonarLint.VisualStudio.Infrastructure.VS; using SonarLint.VisualStudio.Infrastructure.VS.DocumentEvents; @@ -259,6 +260,7 @@ private static IAnalysisIssueVisualization CreateIssueViz(string filePath = "tes var issue = new Mock(); var issueViz = new Mock(); + issue.Setup(x => x.Severity).Returns(AnalysisIssueSeverity.Major); issueViz.Setup(x => x.CurrentFilePath).Returns(filePath); issueViz.Setup(x => x.Issue).Returns(issue.Object); issueViz.Setup(x => x.Flows).Returns(Array.Empty()); diff --git a/src/IssueViz.Security.UnitTests/Taint/TaintList/TaintIssuesControlViewModelTests.cs b/src/IssueViz.Security.UnitTests/Taint/TaintList/TaintIssuesControlViewModelTests.cs index 9af9baaf54..a32de3b9aa 100644 --- a/src/IssueViz.Security.UnitTests/Taint/TaintList/TaintIssuesControlViewModelTests.cs +++ b/src/IssueViz.Security.UnitTests/Taint/TaintList/TaintIssuesControlViewModelTests.cs @@ -1,4 +1,4 @@ -/* +/* * SonarLint for Visual Studio * Copyright (C) 2016-2024 SonarSource SA * mailto:info AT sonarsource DOT com @@ -30,6 +30,7 @@ using Microsoft.VisualStudio.Text; using Moq; using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Analysis; using SonarLint.VisualStudio.Core.Telemetry; using SonarLint.VisualStudio.Infrastructure.VS; using SonarLint.VisualStudio.Infrastructure.VS.DocumentEvents; @@ -693,48 +694,6 @@ public void SetSelectedIssue_ValueIsTheSame_SelectionServiceNotCalled() selectionService.VerifyNoOtherCalls(); } - [TestMethod] - public void AnalysisInformation_NoAnalysisInformation_Null() - { - var store = new Mock(); - SetupAnalysisInformation(store, null); - - var testSubject = CreateTestSubject(store: store); - - testSubject.AnalysisInformation.Should().BeNull(); - } - - [TestMethod] - public void AnalysisInformation_HasAnalysisInformation_PropertySet() - { - var store = new Mock(); - var analysisInformation = new AnalysisInformation("some branch", default); - SetupAnalysisInformation(store, analysisInformation); - - var testSubject = CreateTestSubject(store: store); - - testSubject.AnalysisInformation.Should().BeSameAs(analysisInformation); - } - - [TestMethod] - public void AnalysisInformation_IssuesChanged_RaisesPropertyChanged() - { - var store = new Mock(); - var testSubject = CreateTestSubject(store: store); - - var eventHandler = new Mock(); - testSubject.PropertyChanged += eventHandler.Object; - - var analysisInformation = new AnalysisInformation("some branch", default); - - SetupAnalysisInformation(store, analysisInformation); - RaiseStoreIssuesChangedEvent(store); - - VerifyPropertyChangedWasRaised(eventHandler, nameof(testSubject.AnalysisInformation)); - - testSubject.AnalysisInformation.Should().BeSameAs(analysisInformation); - } - [TestMethod] [DataRow(null, ServerType.SonarCloud, nameof(ServerType.SonarCloud), true)] [DataRow(null, null, "", false)] @@ -928,6 +887,7 @@ private IAnalysisIssueVisualization CreateIssueViz(string filePath = "test.cpp", DateTimeOffset created = default, bool isSuppressed = false, params IAnalysisIssueLocationVisualization[] locations) { var issue = new Mock(); + issue.Setup(x => x.Severity).Returns(AnalysisIssueSeverity.Major); issue.Setup(x => x.IssueKey).Returns(issueKey); issue.Setup(x => x.CreationTimestamp).Returns(created); @@ -1009,10 +969,5 @@ private void VerifyPropertyChangedWasRaised(Mock ev It.Is((PropertyChangedEventArgs e) => e.PropertyName == expectedProperty)), Times.Once); } - - private void SetupAnalysisInformation(Mock store, AnalysisInformation analysisInformation) - { - store.Setup(x => x.GetAnalysisInformation()).Returns(analysisInformation); - } } } diff --git a/src/IssueViz.Security.UnitTests/Taint/TaintStoreTests.cs b/src/IssueViz.Security.UnitTests/Taint/TaintStoreTests.cs index 67502ed20b..bec2492be3 100644 --- a/src/IssueViz.Security.UnitTests/Taint/TaintStoreTests.cs +++ b/src/IssueViz.Security.UnitTests/Taint/TaintStoreTests.cs @@ -18,374 +18,344 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting; -using System.Linq; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; using SonarLint.VisualStudio.TestInfrastructure; using SonarLint.VisualStudio.IssueVisualization.Models; using SonarLint.VisualStudio.IssueVisualization.Security.IssuesStore; using SonarLint.VisualStudio.IssueVisualization.Security.Taint; using SonarLint.VisualStudio.IssueVisualization.Security.Taint.Models; -namespace SonarLint.VisualStudio.IssueVisualization.Security.UnitTests.Taint +namespace SonarLint.VisualStudio.IssueVisualization.Security.UnitTests.Taint; + +[TestClass] +public class TaintStoreTests { - [TestClass] - public class TaintStoreTests + private ITaintStore testSubject; + + [TestMethod] + public void MefCtor_CheckExports() + { + var batch = new CompositionBatch(); + + var storeImport = new SingleObjectImporter(); + var issuesStoreImport = new SingleObjectImporter(); + batch.AddPart(storeImport); + batch.AddPart(issuesStoreImport); + + var catalog = new TypeCatalog(typeof(TaintStore)); + using var container = new CompositionContainer(catalog); + container.Compose(batch); + + storeImport.Import.Should().NotBeNull(); + issuesStoreImport.Import.Should().NotBeNull(); + + storeImport.Import.Should().BeSameAs(issuesStoreImport.Import); + } + + [TestInitialize] + public void TestInitialize() + { + testSubject = new TaintStore(); + } + + [TestMethod] + public void GetAll_ReturnsImmutableInstance() + { + var oldItems = new[] { SetupIssueViz(), SetupIssueViz() }; + testSubject.Set(oldItems, "config scope"); + + var issuesList1 = testSubject.GetAll(); + testSubject.Add(SetupIssueViz()); + var issuesList2 = testSubject.GetAll(); + + issuesList1.Count.Should().Be(2); + issuesList2.Count.Should().Be(3); + } + + [TestMethod] + public void Set_NullCollection_ArgumentNullException() + { + Action act = () => testSubject.Set(null, null); + + act.Should().Throw().And.ParamName.Should().Be("issueVisualizations"); + } + + [TestMethod] + public void Set_NoSubscribersToIssuesChangedEvent_NoException() + { + Action act = () => testSubject.Set(new[] { SetupIssueViz() }, "some config scope"); + + act.Should().NotThrow(); + } + + [TestMethod] + public void Set_NoPreviousItems_NoNewItems_CollectionChangedAndEventRaised() + { + var eventHandlerMock = Substitute.For>(); + testSubject.IssuesChanged += eventHandlerMock; + + testSubject.Set([], null); + + testSubject.GetAll().Should().BeEmpty(); + eventHandlerMock.ReceivedWithAnyArgs(1).Invoke(default, default); + } + + [TestMethod] + public void Set_NoPreviousItems_HasNewItems_CollectionChangedAndEventRaised() + { + var eventHandlerMock = Substitute.For>(); + testSubject.IssuesChanged += eventHandlerMock; + + var newItems = new[] { SetupIssueViz(), SetupIssueViz() }; + testSubject.Set(newItems, "some config scope"); + + testSubject.GetAll().Should().BeEquivalentTo(newItems); + eventHandlerMock.ReceivedWithAnyArgs(1).Invoke(default, default); + var eventArgs = (IssuesChangedEventArgs)eventHandlerMock.ReceivedCalls().Single().GetArguments()[1]!; + eventArgs.RemovedIssues.Should().BeEmpty(); + eventArgs.AddedIssues.Should().BeEquivalentTo(newItems); + } + + [TestMethod] + public void Set_HasPreviousItems_NoNewItems_CollectionChangedAndEventRaised() + { + var oldItems = new[] { SetupIssueViz(), SetupIssueViz() }; + testSubject.Set(oldItems, "some config scope"); + + var eventHandlerMock = Substitute.For>(); + testSubject.IssuesChanged += eventHandlerMock; + + testSubject.Set([], "some config scope"); + + testSubject.GetAll().Should().BeEmpty(); + eventHandlerMock.ReceivedWithAnyArgs(1).Invoke(default, default); + var eventArgs = (IssuesChangedEventArgs)eventHandlerMock.ReceivedCalls().Single().GetArguments()[1]!; + eventArgs.RemovedIssues.Should().BeEquivalentTo(oldItems); + eventArgs.AddedIssues.Should().BeEmpty(); + } + + [TestMethod] + public void Set_HasPreviousItems_HasNewItems_CollectionChangedAndEventRaised() { - [TestMethod] - public void MefCtor_CheckExports() - { - var batch = new CompositionBatch(); - - var storeImport = new SingleObjectImporter(); - var issuesStoreImport = new SingleObjectImporter(); - batch.AddPart(storeImport); - batch.AddPart(issuesStoreImport); - - var catalog = new TypeCatalog(typeof(TaintStore)); - using var container = new CompositionContainer(catalog); - container.Compose(batch); - - storeImport.Import.Should().NotBeNull(); - issuesStoreImport.Import.Should().NotBeNull(); - - storeImport.Import.Should().BeSameAs(issuesStoreImport.Import); - } - - [TestMethod] - public void GetAll_ReturnsImmutableInstance() - { - var testSubject = CreateTestSubject(); - var oldItems = new[] { SetupIssueViz(), SetupIssueViz() }; - testSubject.Set(oldItems, new AnalysisInformation("some branch", DateTimeOffset.Now)); - - var issuesList1 = testSubject.GetAll(); - testSubject.Add(SetupIssueViz()); - var issuesList2 = testSubject.GetAll(); - - issuesList1.Count.Should().Be(2); - issuesList2.Count.Should().Be(3); - } - - [TestMethod] - public void Set_NullCollection_ArgumentNullException() - { - var testSubject = CreateTestSubject(); - - Action act = () => testSubject.Set(null, null); - - act.Should().Throw().And.ParamName.Should().Be("issueVisualizations"); - } - - [TestMethod] - public void Set_NoSubscribersToIssuesChangedEvent_NoException() - { - var testSubject = CreateTestSubject(); - - Action act = () => testSubject.Set(new[] { SetupIssueViz() }, null); - - act.Should().NotThrow(); - } - - [TestMethod] - public void Set_NoPreviousItems_NoNewItems_CollectionChangedAndEventRaised() - { - var testSubject = CreateTestSubject(); - - var callCount = 0; - testSubject.IssuesChanged += (sender, args) => { callCount++; }; - - testSubject.Set(Enumerable.Empty(), null); - - testSubject.GetAll().Should().BeEmpty(); - callCount.Should().Be(1); - } - - [TestMethod] - public void Set_NoPreviousItems_HasNewItems_CollectionChangedAndEventRaised() - { - var testSubject = CreateTestSubject(); - - var callCount = 0; - IssuesChangedEventArgs suppliedArgs = null; - testSubject.IssuesChanged += (sender, args) => { callCount++; suppliedArgs = args; }; - - var newItems = new[] { SetupIssueViz(), SetupIssueViz() }; - testSubject.Set(newItems, null); - - testSubject.GetAll().Should().BeEquivalentTo(newItems); - callCount.Should().Be(1); - suppliedArgs.RemovedIssues.Should().BeEmpty(); - suppliedArgs.AddedIssues.Should().BeEquivalentTo(newItems); - } - - [TestMethod] - public void Set_HasPreviousItems_NoNewItems_CollectionChangedAndEventRaised() - { - var testSubject = CreateTestSubject(); - - var oldItems = new[] { SetupIssueViz(), SetupIssueViz() }; - testSubject.Set(oldItems, null); - - var callCount = 0; - IssuesChangedEventArgs suppliedArgs = null; - testSubject.IssuesChanged += (sender, args) => { callCount++; suppliedArgs = args; }; - - testSubject.Set(Enumerable.Empty(), null); - - testSubject.GetAll().Should().BeEmpty(); - callCount.Should().Be(1); - suppliedArgs.RemovedIssues.Should().BeEquivalentTo(oldItems); - suppliedArgs.AddedIssues.Should().BeEmpty(); - } - - [TestMethod] - public void Set_HasPreviousItems_HasNewItems_CollectionChangedAndEventRaised() - { - var testSubject = CreateTestSubject(); - - var oldItems = new[] { SetupIssueViz(), SetupIssueViz() }; - testSubject.Set(oldItems, null); + var oldItems = new[] { SetupIssueViz(), SetupIssueViz() }; + testSubject.Set(oldItems, "some config scope"); + + var eventHandlerMock = Substitute.For>(); + testSubject.IssuesChanged += eventHandlerMock; + + var newItems = new[] { SetupIssueViz(), SetupIssueViz() }; + testSubject.Set(newItems, "some config scope"); - var callCount = 0; - IssuesChangedEventArgs suppliedArgs = null; - testSubject.IssuesChanged += (sender, args) => { callCount++; suppliedArgs = args; }; + testSubject.GetAll().Should().BeEquivalentTo(newItems); + eventHandlerMock.ReceivedWithAnyArgs(1).Invoke(default, default); + var eventArgs = (IssuesChangedEventArgs)eventHandlerMock.ReceivedCalls().Single().GetArguments()[1]!; + eventArgs.RemovedIssues.Should().BeEquivalentTo(oldItems); + eventArgs.AddedIssues.Should().BeEquivalentTo(newItems); + } - var newItems = new[] { SetupIssueViz(), SetupIssueViz() }; - testSubject.Set(newItems, null); + [TestMethod] + public void Set_HasPreviousItems_HasSomeNewItems_CollectionChangedAndEventRaised() + { + var issueViz1 = SetupIssueViz("key1"); + var issueViz2 = SetupIssueViz("key2"); + var issueViz2NewObject = SetupIssueViz("key2"); + var issueViz3 = SetupIssueViz("key3"); - testSubject.GetAll().Should().BeEquivalentTo(newItems); - callCount.Should().Be(1); - suppliedArgs.RemovedIssues.Should().BeEquivalentTo(oldItems); - suppliedArgs.AddedIssues.Should().BeEquivalentTo(newItems); - } + var oldItems = new[] { issueViz1, issueViz2 }; + testSubject.Set(oldItems, "some config scope"); - [TestMethod] - public void Set_HasPreviousItems_HasSomeNewItems_CollectionChangedAndEventRaised() - { - var testSubject = CreateTestSubject(); + var eventHandlerMock = Substitute.For>(); + testSubject.IssuesChanged += eventHandlerMock; - var issueViz1 = SetupIssueViz("key1"); - var issueViz2 = SetupIssueViz("key2"); - var issueViz2NewObject = SetupIssueViz("key2"); - var issueViz3 = SetupIssueViz("key3"); + var newItems = new[] { issueViz2NewObject, issueViz3}; + testSubject.Set(newItems, "some config scope"); - var oldItems = new[] { issueViz1, issueViz2 }; - testSubject.Set(oldItems, null); + testSubject.GetAll().Should().BeEquivalentTo(newItems); + eventHandlerMock.ReceivedWithAnyArgs(1).Invoke(default, default); + var eventArgs = (IssuesChangedEventArgs)eventHandlerMock.ReceivedCalls().Single().GetArguments()[1]!; + eventArgs.RemovedIssues.Should().BeEquivalentTo(issueViz1); + eventArgs.AddedIssues.Should().BeEquivalentTo(issueViz3); + } - var callCount = 0; - IssuesChangedEventArgs suppliedArgs = null; - testSubject.IssuesChanged += (sender, args) => { callCount++; suppliedArgs = args; }; + [TestMethod] + public void Set_HasItems_NoConfigScope_Throws() + { + var issueViz1 = SetupIssueViz("key1"); - var newItems = new[] { issueViz2NewObject, issueViz3}; - testSubject.Set(newItems, null); + var act = () => testSubject.Set([issueViz1], null); - testSubject.GetAll().Should().BeEquivalentTo(newItems); - callCount.Should().Be(1); - suppliedArgs.RemovedIssues.Should().BeEquivalentTo(issueViz1); - suppliedArgs.AddedIssues.Should().BeEquivalentTo(issueViz3); - } - - [TestMethod] - public void GetAnalysisInformation_NoInformation_ReturnsNull() - { - var testSubject = CreateTestSubject(); - testSubject.Set(Enumerable.Empty(), null); + act.Should().Throw().And.ParamName.Should().Be("newConfigurationScope"); + } + + [TestMethod] + public void ConfigScope_NoInformation_ReturnsNull() + { + testSubject.Set([], null); + + var result = testSubject.ConfigurationScope; + result.Should().BeNull(); + } + + [TestMethod] + public void ConfigScope_HasInformation_ReturnsInformation() + { + const string newConfigurationScope = "some config scope"; + + testSubject.Set([], newConfigurationScope); + + var result = testSubject.ConfigurationScope; + result.Should().BeSameAs(newConfigurationScope); + } - var result = testSubject.GetAnalysisInformation(); - result.Should().BeNull(); - } - - [TestMethod] - public void GetAnalysisInformation_HasInformation_ReturnsInformation() - { - var analysisInformation = new AnalysisInformation("some branch", DateTimeOffset.Now); - - var testSubject = CreateTestSubject(); - testSubject.Set(Enumerable.Empty(), analysisInformation); - - var result = testSubject.GetAnalysisInformation(); - result.Should().BeSameAs(analysisInformation); - } - - [TestMethod] - public void Remove_IssueKeyIsNull_ArgumentNullException() - { - var testSubject = CreateTestSubject(); + [TestMethod] + public void Remove_IssueKeyIsNull_ArgumentNullException() + { + Action act = () => testSubject.Remove(null); - Action act = () => testSubject.Remove(null); - - act.Should().Throw().And.ParamName.Should().Be("issueKey"); - } + act.Should().Throw().And.ParamName.Should().Be("issueKey"); + } - [TestMethod] - public void Remove_IssueNotFound_NoIssuesInList_NoEventIsRaised() - { - var testSubject = CreateTestSubject(); + [TestMethod] + public void Remove_IssueNotFound_NoIssuesInList_NoEventIsRaised() + { + var eventHandlerMock = Substitute.For>(); + testSubject.IssuesChanged += eventHandlerMock; - var callCount = 0; - IssuesChangedEventArgs suppliedArgs = null; - testSubject.IssuesChanged += (_, args) => { callCount++; suppliedArgs = args; }; + testSubject.Remove("some unknown key"); - testSubject.Remove("some unknown key"); + eventHandlerMock.DidNotReceiveWithAnyArgs().Invoke(default, default); + testSubject.GetAll().Should().BeEmpty(); + } - callCount.Should().Be(0); - testSubject.GetAll().Should().BeEmpty(); - } + [TestMethod] + public void Remove_IssueNotFound_NoIssueWithThisId_NoEventIsRaised() + { + var existingIssue = SetupIssueViz("key1"); - [TestMethod] - public void Remove_IssueNotFound_NoIssueWithThisId_NoEventIsRaised() - { - var existingIssue = SetupIssueViz("key1"); + testSubject.Set(new[] { existingIssue }, "some config scope"); - var testSubject = CreateTestSubject(); - testSubject.Set(new[] { existingIssue }, null); + var eventHandlerMock = Substitute.For>(); + testSubject.IssuesChanged += eventHandlerMock; - var callCount = 0; - IssuesChangedEventArgs suppliedArgs = null; - testSubject.IssuesChanged += (_, args) => { callCount++; suppliedArgs = args; }; + testSubject.Remove("some unknown key"); - testSubject.Remove("some unknown key"); + eventHandlerMock.DidNotReceiveWithAnyArgs().Invoke(default, default); + testSubject.GetAll().Should().BeEquivalentTo(existingIssue); + } - callCount.Should().Be(0); - testSubject.GetAll().Should().BeEquivalentTo(existingIssue); - } - - [TestMethod] - public void Remove_IssueFound_IssueIsRemovedAndEventIsRaised() - { - var existingIssue1 = SetupIssueViz("key1"); - var existingIssue2 = SetupIssueViz("key2"); - var existingIssue3 = SetupIssueViz("key3"); - - var testSubject = CreateTestSubject(); - testSubject.Set(new[] {existingIssue1, existingIssue2, existingIssue3}, null); - - var callCount = 0; - IssuesChangedEventArgs suppliedArgs = null; - testSubject.IssuesChanged += (_, args) => { callCount++; suppliedArgs = args; }; + [TestMethod] + public void Remove_IssueFound_IssueIsRemovedAndEventIsRaised() + { + var existingIssue1 = SetupIssueViz("key1"); + var existingIssue2 = SetupIssueViz("key2"); + var existingIssue3 = SetupIssueViz("key3"); - testSubject.Remove("key2"); + testSubject.Set([existingIssue1, existingIssue2, existingIssue3], "some config scope"); - callCount.Should().Be(1); - suppliedArgs.RemovedIssues.Should().BeEquivalentTo(existingIssue2); - suppliedArgs.AddedIssues.Should().BeEmpty(); - testSubject.GetAll().Should().BeEquivalentTo(existingIssue1, existingIssue3); - } + var eventHandlerMock = Substitute.For>(); + testSubject.IssuesChanged += eventHandlerMock; - [TestMethod] - public void Remove_MultipleIssuesFoundWithSameId_FirstIssueIsRemovedAndEventIsRaised() - { - var existingIssue1 = SetupIssueViz("key1"); - var existingIssue2 = SetupIssueViz("key1"); - var existingIssue3 = SetupIssueViz("key1"); + testSubject.Remove("key2"); - var testSubject = CreateTestSubject(); - testSubject.Set(new[] { existingIssue1, existingIssue2, existingIssue3 }, null); + eventHandlerMock.ReceivedWithAnyArgs(1).Invoke(default, default); + var eventArgs = (IssuesChangedEventArgs)eventHandlerMock.ReceivedCalls().Single().GetArguments()[1]!; + eventArgs.RemovedIssues.Should().BeEquivalentTo(existingIssue2); + eventArgs.AddedIssues.Should().BeEmpty(); + testSubject.GetAll().Should().BeEquivalentTo(existingIssue1, existingIssue3); + } - var callCount = 0; - IssuesChangedEventArgs suppliedArgs = null; - testSubject.IssuesChanged += (_, args) => { callCount++; suppliedArgs = args; }; + [TestMethod] + public void Remove_MultipleIssuesFoundWithSameId_FirstIssueIsRemovedAndEventIsRaised() + { + var existingIssue1 = SetupIssueViz("key1"); + var existingIssue2 = SetupIssueViz("key1"); + var existingIssue3 = SetupIssueViz("key1"); - testSubject.Remove("key1"); + testSubject.Set([existingIssue1, existingIssue2, existingIssue3], "some config scope"); - callCount.Should().Be(1); - suppliedArgs.RemovedIssues.Should().BeEquivalentTo(existingIssue1); - suppliedArgs.AddedIssues.Should().BeEmpty(); - testSubject.GetAll().Should().BeEquivalentTo(existingIssue2, existingIssue3); - } + var eventHandlerMock = Substitute.For>(); + testSubject.IssuesChanged += eventHandlerMock; - [TestMethod] - public void Add_IssueIsNull_ArgumentNullException() - { - var testSubject = CreateTestSubject(); + testSubject.Remove("key1"); - Action act = () => testSubject.Add(null); + eventHandlerMock.ReceivedWithAnyArgs(1).Invoke(default, default); + var eventArgs = (IssuesChangedEventArgs)eventHandlerMock.ReceivedCalls().Single().GetArguments()[1]!; + eventArgs.RemovedIssues.Should().BeEquivalentTo(existingIssue1); + eventArgs.AddedIssues.Should().BeEmpty(); + testSubject.GetAll().Should().BeEquivalentTo(existingIssue2, existingIssue3); + } - act.Should().Throw().And.ParamName.Should().Be("issueVisualization"); - } + [TestMethod] + public void Add_IssueIsNull_ArgumentNullException() + { + Action act = () => testSubject.Add(null); - [TestMethod] - public void Add_NoAnalysisInformation_IssueIgnoredAndNoEventIsRaised() - { - var existingIssue = SetupIssueViz("key1"); + act.Should().Throw().And.ParamName.Should().Be("issueVisualization"); + } - var testSubject = CreateTestSubject(); - testSubject.Set(new[] { existingIssue}, null); + [TestMethod] + public void Add_NoConfigScope_IssueIgnoredAndNoEventIsRaised() + { + testSubject.Set([], null); - var callCount = 0; - IssuesChangedEventArgs suppliedArgs = null; - testSubject.IssuesChanged += (_, args) => { callCount++; suppliedArgs = args; }; + var eventHandlerMock = Substitute.For>(); + testSubject.IssuesChanged += eventHandlerMock; - testSubject.Add(SetupIssueViz()); + testSubject.Add(SetupIssueViz()); - callCount.Should().Be(0); - testSubject.GetAll().Should().BeEquivalentTo(existingIssue); - } + eventHandlerMock.DidNotReceiveWithAnyArgs().Invoke(default, default); + testSubject.GetAll().Should().BeEmpty(); + } - [TestMethod] - public void Add_HasAnalysisInformation_IssueAddedAndEventIsRaised() - { - var analysisInformation = new AnalysisInformation("some branch", DateTimeOffset.Now); - var existingIssue = SetupIssueViz("key1"); + [TestMethod] + public void Add_HasConfigScope_IssueAddedAndEventIsRaised() + { + var existingIssue = SetupIssueViz("key1"); - var testSubject = CreateTestSubject(); - testSubject.Set(new[] { existingIssue }, analysisInformation); + testSubject.Set([existingIssue], "some config scope"); - var callCount = 0; - IssuesChangedEventArgs suppliedArgs = null; - testSubject.IssuesChanged += (_, args) => { callCount++; suppliedArgs = args; }; + var eventHandlerMock = Substitute.For>(); + testSubject.IssuesChanged += eventHandlerMock; - var newIssue = SetupIssueViz(); - testSubject.Add(newIssue); + var newIssue = SetupIssueViz(); + testSubject.Add(newIssue); - callCount.Should().Be(1); - suppliedArgs.RemovedIssues.Should().BeEmpty(); - suppliedArgs.AddedIssues.Should().BeEquivalentTo(newIssue); - testSubject.GetAll().Should().BeEquivalentTo(existingIssue, newIssue); - } + eventHandlerMock.ReceivedWithAnyArgs(1).Invoke(default, default); + var eventArgs = (IssuesChangedEventArgs)eventHandlerMock.ReceivedCalls().Single().GetArguments()[1]!; + eventArgs.RemovedIssues.Should().BeEmpty(); + eventArgs.AddedIssues.Should().BeEquivalentTo(newIssue); + testSubject.GetAll().Should().BeEquivalentTo(existingIssue, newIssue); + } - [TestMethod] - public void Add_DuplicateIssue_IssueIgnoredAndNoEventIsRaised() - { - var analysisInformation = new AnalysisInformation("some branch", DateTimeOffset.Now); - var issueKey = "key1"; - var existingIssue = SetupIssueViz(issueKey); - - var testSubject = CreateTestSubject(); - testSubject.Set(new[] { existingIssue }, analysisInformation); + [TestMethod] + public void Add_DuplicateIssue_IssueIgnoredAndNoEventIsRaised() + { + var issueKey = "key1"; + var existingIssue = SetupIssueViz(issueKey); - var callCount = 0; - IssuesChangedEventArgs suppliedArgs = null; - testSubject.IssuesChanged += (_, args) => { callCount++; suppliedArgs = args; }; + testSubject.Set([existingIssue], "some config scope"); - var newIssue = SetupIssueViz(issueKey); - testSubject.Add(newIssue); + var eventHandlerMock = Substitute.For>(); + testSubject.IssuesChanged += eventHandlerMock; - callCount.Should().Be(0); - testSubject.GetAll().Should().BeEquivalentTo(existingIssue); - } + var newIssue = SetupIssueViz(issueKey); + testSubject.Add(newIssue); - private IAnalysisIssueVisualization SetupIssueViz(string issueKey = null) - { - issueKey ??= Guid.NewGuid().ToString(); + eventHandlerMock.DidNotReceiveWithAnyArgs().Invoke(default, default); + testSubject.GetAll().Should().BeEquivalentTo(existingIssue); + } - var taintIssue = new Mock(); - taintIssue.Setup(x => x.IssueKey).Returns(issueKey); + private IAnalysisIssueVisualization SetupIssueViz(string issueKey = null) + { + issueKey ??= Guid.NewGuid().ToString(); - var issueViz = new Mock(); - issueViz.Setup(x => x.Issue).Returns(taintIssue.Object); + var taintIssue = Substitute.For(); + taintIssue.IssueKey.Returns(issueKey); - return issueViz.Object; - } + var issueViz = Substitute.For(); + issueViz.Issue.Returns(taintIssue); - private ITaintStore CreateTestSubject() - { - return new TaintStore(); - } + return issueViz; } } diff --git a/src/IssueViz.Security/Hotspots/HotspotsList/HotspotsControl.xaml b/src/IssueViz.Security/Hotspots/HotspotsList/HotspotsControl.xaml index 66f02160c2..fe0e54411a 100644 --- a/src/IssueViz.Security/Hotspots/HotspotsList/HotspotsControl.xaml +++ b/src/IssueViz.Security/Hotspots/HotspotsList/HotspotsControl.xaml @@ -99,6 +99,7 @@ + diff --git a/src/IssueViz.Security/Hotspots/Models/IHotspot.cs b/src/IssueViz.Security/Hotspots/Models/IHotspot.cs index f4b2b36d2f..77d6d8ce8e 100644 --- a/src/IssueViz.Security/Hotspots/Models/IHotspot.cs +++ b/src/IssueViz.Security/Hotspots/Models/IHotspot.cs @@ -40,13 +40,15 @@ internal class Hotspot : IHotspot { private static readonly IReadOnlyList EmptyFlows = Array.Empty(); - public Hotspot(string hotspotKey, + public Hotspot(Guid? id, + string hotspotKey, string serverFilePath, IAnalysisIssueLocation primaryLocation, IHotspotRule rule, IReadOnlyList flows, string context = null) { + Id = id; HotspotKey = hotspotKey; ServerFilePath = serverFilePath; PrimaryLocation = primaryLocation ?? throw new ArgumentNullException(nameof(primaryLocation)); @@ -56,6 +58,7 @@ public Hotspot(string hotspotKey, } public string HotspotKey { get; } + public Guid? Id { get; } public string RuleKey => Rule.RuleKey; public IHotspotRule Rule { get; } public IReadOnlyList Flows { get; } diff --git a/src/IssueViz.Security/OpenInIdeHotspots/HotspotDetailsDtoToHotspotConverter.cs b/src/IssueViz.Security/OpenInIdeHotspots/HotspotDetailsDtoToHotspotConverter.cs index c18a5e872f..cdcd7c7385 100644 --- a/src/IssueViz.Security/OpenInIdeHotspots/HotspotDetailsDtoToHotspotConverter.cs +++ b/src/IssueViz.Security/OpenInIdeHotspots/HotspotDetailsDtoToHotspotConverter.cs @@ -47,7 +47,8 @@ public HotspotDetailsDtoToHotspotConverter(IChecksumCalculator checksumCalculato public IAnalysisIssueBase Convert(HotspotDetailsDto hotspotDetailsDto, string rootPath) { - return new Hotspot(hotspotDetailsDto.key, + return new Hotspot(id: null, + hotspotDetailsDto.key, hotspotDetailsDto.ideFilePath, new AnalysisIssueLocation(hotspotDetailsDto.message, Path.Combine(rootPath, hotspotDetailsDto.ideFilePath), diff --git a/src/IssueViz.Security/OpenInIdeHotspots_List/HotspotsList/OpenInIDEHotspotsControl.xaml b/src/IssueViz.Security/OpenInIdeHotspots_List/HotspotsList/OpenInIDEHotspotsControl.xaml index ff53980ea3..c06d875ca5 100644 --- a/src/IssueViz.Security/OpenInIdeHotspots_List/HotspotsList/OpenInIDEHotspotsControl.xaml +++ b/src/IssueViz.Security/OpenInIdeHotspots_List/HotspotsList/OpenInIDEHotspotsControl.xaml @@ -116,6 +116,7 @@ + diff --git a/src/IssueViz.Security/Taint/Models/ITaintIssue.cs b/src/IssueViz.Security/Taint/Models/ITaintIssue.cs index 2f2a28dfd4..0fa26184fd 100644 --- a/src/IssueViz.Security/Taint/Models/ITaintIssue.cs +++ b/src/IssueViz.Security/Taint/Models/ITaintIssue.cs @@ -18,56 +18,58 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Collections.Generic; using SonarLint.VisualStudio.Core.Analysis; namespace SonarLint.VisualStudio.IssueVisualization.Security.Taint.Models { - internal interface ITaintIssue : IAnalysisIssueBase + public interface ITaintIssue : IAnalysisIssueBase { string IssueKey { get; } - AnalysisIssueSeverity Severity { get; } - + AnalysisIssueSeverity? Severity { get; } + SoftwareQualitySeverity? HighestSoftwareQualitySeverity { get; } DateTimeOffset CreationTimestamp { get; } - - DateTimeOffset LastUpdateTimestamp { get; } } - internal class TaintIssue : ITaintIssue + public class TaintIssue : ITaintIssue { private static readonly IReadOnlyList EmptyFlows = Array.Empty(); - public TaintIssue(string issueKey, + public TaintIssue( + Guid? id, + string issueKey, string ruleKey, IAnalysisIssueLocation primaryLocation, - AnalysisIssueSeverity severity, + AnalysisIssueSeverity? severity, SoftwareQualitySeverity? highestSoftwareQualitySeverity, DateTimeOffset creationTimestamp, - DateTimeOffset lastUpdateTimestamp, IReadOnlyList flows, string ruleDescriptionContextKey) { + Id = id; IssueKey = issueKey; RuleKey = ruleKey; PrimaryLocation = primaryLocation ?? throw new ArgumentNullException(nameof(primaryLocation)); Severity = severity; CreationTimestamp = creationTimestamp; - LastUpdateTimestamp = lastUpdateTimestamp; Flows = flows ?? EmptyFlows; RuleDescriptionContextKey = ruleDescriptionContextKey; - this.HighestSoftwareQualitySeverity = highestSoftwareQualitySeverity; + HighestSoftwareQualitySeverity = highestSoftwareQualitySeverity; + + if (!severity.HasValue && !highestSoftwareQualitySeverity.HasValue) + { + throw new ArgumentException(string.Format(TaintResources.TaintIssue_SeverityUndefined, IssueKey)); + } } + public Guid? Id { get; } public string IssueKey { get; } public string RuleKey { get; } - public AnalysisIssueSeverity Severity { get; } + public AnalysisIssueSeverity? Severity { get; } public SoftwareQualitySeverity? HighestSoftwareQualitySeverity { get; } public DateTimeOffset CreationTimestamp { get; } - public DateTimeOffset LastUpdateTimestamp { get; } public IReadOnlyList Flows { get; } public IAnalysisIssueLocation PrimaryLocation { get; } public string RuleDescriptionContextKey { get; } diff --git a/src/IssueViz.Security/Taint/ServerSentEvents/TaintServerEventsListener.cs b/src/IssueViz.Security/Taint/ServerSentEvents/TaintServerEventsListener.cs deleted file mode 100644 index 6903c95711..0000000000 --- a/src/IssueViz.Security/Taint/ServerSentEvents/TaintServerEventsListener.cs +++ /dev/null @@ -1,135 +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.Threading.Tasks; -using SonarLint.VisualStudio.Core; -using System.Threading; -using System.ComponentModel.Composition; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; -using System; - -namespace SonarLint.VisualStudio.IssueVisualization.Security.Taint.ServerSentEvents -{ - /// - /// Consumes and handles coming from the server. - /// - internal interface ITaintServerEventsListener : IDisposable - { - Task ListenAsync(); - } - - [Export(typeof(ITaintServerEventsListener))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal sealed class TaintServerEventsListener : ITaintServerEventsListener - { - private readonly IStatefulServerBranchProvider serverBranchProvider; - private readonly ITaintServerEventSource taintServerEventSource; - private readonly ITaintStore taintStore; - private readonly IThreadHandling threadHandling; - private readonly ITaintIssueToIssueVisualizationConverter taintToIssueVizConverter; - private readonly ILogger logger; - private readonly CancellationTokenSource cancellationTokenSource; - - [ImportingConstructor] - public TaintServerEventsListener( - IStatefulServerBranchProvider serverBranchProvider, - ITaintServerEventSource taintServerEventSource, - ITaintStore taintStore, - IThreadHandling threadHandling, - ITaintIssueToIssueVisualizationConverter taintToIssueVizConverter, - ILogger logger) - { - this.serverBranchProvider = serverBranchProvider; - this.taintServerEventSource = taintServerEventSource; - this.taintStore = taintStore; - this.threadHandling = threadHandling; - this.taintToIssueVizConverter = taintToIssueVizConverter; - this.logger = logger; - - cancellationTokenSource = new CancellationTokenSource(); - } - - public async Task ListenAsync() - { - await threadHandling.SwitchToBackgroundThread(); - - while (!cancellationTokenSource.IsCancellationRequested) - { - try - { - var taintServerEvent = await taintServerEventSource.GetNextEventOrNullAsync(); - - switch (taintServerEvent) - { - case null: - { - // Will return null when taintServerEventSource is disposed - return; - } - case ITaintVulnerabilityClosedServerEvent taintClosedEvent: - { - taintStore.Remove(taintClosedEvent.Key); - break; - } - case ITaintVulnerabilityRaisedServerEvent taintRaisedEvent: - { - await AddToStoreIfOnTheRightBranchAsync(taintRaisedEvent); - break; - } - default: - { - logger.LogVerbose($"[TaintServerEventsListener] Unrecognized taint event type: {taintServerEvent.GetType()}"); - break; - } - } - } - catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) - { - logger.LogVerbose($"[TaintServerEventsListener] Failed to handle taint event: {ex}"); - } - } - } - - private async Task AddToStoreIfOnTheRightBranchAsync(ITaintVulnerabilityRaisedServerEvent taintRaisedEvent) - { - var serverBranch = await serverBranchProvider.GetServerBranchNameAsync(cancellationTokenSource.Token); - - if (taintRaisedEvent.Branch.Equals(serverBranch)) - { - var taintIssue = taintToIssueVizConverter.Convert(taintRaisedEvent.Issue); - taintStore.Add(taintIssue); - } - } - - private bool disposed; - - public void Dispose() - { - if (disposed) - { - return; - } - - cancellationTokenSource.Cancel(); - cancellationTokenSource.Dispose(); - disposed = true; - } - } -} diff --git a/src/IssueViz.Security/Taint/TaintIssueToIssueVisualizationConverter.cs b/src/IssueViz.Security/Taint/TaintIssueToIssueVisualizationConverter.cs index 7424e95719..0b25185ad1 100644 --- a/src/IssueViz.Security/Taint/TaintIssueToIssueVisualizationConverter.cs +++ b/src/IssueViz.Security/Taint/TaintIssueToIssueVisualizationConverter.cs @@ -18,224 +18,75 @@ * 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.IO; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Analysis; using SonarLint.VisualStudio.IssueVisualization.Models; using SonarLint.VisualStudio.IssueVisualization.Security.Taint.Models; -using SonarQube.Client.Models; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; -using ITaintIssue = SonarQube.Client.Models.ServerSentEvents.ClientContract.ITaintIssue; +using SonarLint.VisualStudio.SLCore.Common.Helpers; +using SonarLint.VisualStudio.SLCore.Common.Models; -namespace SonarLint.VisualStudio.IssueVisualization.Security.Taint -{ - internal interface ITaintIssueToIssueVisualizationConverter - { - IAnalysisIssueVisualization Convert(SonarQubeIssue sonarQubeIssue); +namespace SonarLint.VisualStudio.IssueVisualization.Security.Taint; - IAnalysisIssueVisualization Convert(ITaintIssue sonarQubeTaintIssue); - } +internal interface ITaintIssueToIssueVisualizationConverter +{ + IAnalysisIssueVisualization Convert(TaintVulnerabilityDto slcoreTaintIssue, string configScopeRoot); +} - [Export(typeof(ITaintIssueToIssueVisualizationConverter))] - internal class TaintIssueToIssueVisualizationConverter : ITaintIssueToIssueVisualizationConverter +[Export(typeof(ITaintIssueToIssueVisualizationConverter))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +internal class TaintIssueToIssueVisualizationConverter(IAnalysisIssueVisualizationConverter issueVisualizationConverter) + : ITaintIssueToIssueVisualizationConverter +{ + public IAnalysisIssueVisualization Convert(TaintVulnerabilityDto slcoreTaintIssue, string configScopeRoot) { - private readonly IAnalysisIssueVisualizationConverter issueVisualizationConverter; - private readonly IAbsoluteFilePathLocator absoluteFilePathLocator; - - [ImportingConstructor] - public TaintIssueToIssueVisualizationConverter(IAnalysisIssueVisualizationConverter issueVisualizationConverter, IAbsoluteFilePathLocator absoluteFilePathLocator) - { - this.issueVisualizationConverter = issueVisualizationConverter; - this.absoluteFilePathLocator = absoluteFilePathLocator; - } - - public IAnalysisIssueVisualization Convert(SonarQubeIssue sonarQubeIssue) - { - var analysisIssue = ConvertToAnalysisIssue(sonarQubeIssue); - var issueViz = CreateAnalysisIssueVisualization(analysisIssue); - issueViz.IsSuppressed = sonarQubeIssue.IsResolved; - - return issueViz; - } - - public IAnalysisIssueVisualization Convert(ITaintIssue sonarQubeTaintIssue) - { - var analysisIssue = ConvertToAnalysisIssue(sonarQubeTaintIssue); - - return CreateAnalysisIssueVisualization(analysisIssue); - } - - private IAnalysisIssueVisualization CreateAnalysisIssueVisualization(IAnalysisIssueBase analysisIssue) - { - var issueViz = issueVisualizationConverter.Convert(analysisIssue); - - CalculateLocalFilePaths(issueViz); - - return issueViz; - } - - private void CalculateLocalFilePaths(IAnalysisIssueVisualization issueViz) - { - var allLocations = issueViz.GetAllLocations(); - - foreach (var location in allLocations) - { - location.CurrentFilePath = absoluteFilePathLocator.Locate(location.Location.FilePath); - } - } - - private static IAnalysisIssueBase ConvertToAnalysisIssue(SonarQubeIssue sonarQubeIssue) - { - if (sonarQubeIssue.TextRange == null) - { - throw new ArgumentNullException(nameof(sonarQubeIssue.TextRange)); - } + var analysisIssue = ConvertToAnalysisIssue(slcoreTaintIssue, configScopeRoot); + var issueViz = CreateAnalysisIssueVisualization(analysisIssue); + issueViz.IsSuppressed = slcoreTaintIssue.resolved; - return new TaintIssue( - sonarQubeIssue.IssueKey, - sonarQubeIssue.RuleId, - primaryLocation: new AnalysisIssueLocation( - sonarQubeIssue.Message, - sonarQubeIssue.FilePath, - textRange: new TextRange( - sonarQubeIssue.TextRange.StartLine, - sonarQubeIssue.TextRange.EndLine, - sonarQubeIssue.TextRange.StartOffset, - sonarQubeIssue.TextRange.EndOffset, - sonarQubeIssue.Hash)), - Convert(sonarQubeIssue.Severity), - ConvertToHighestSeverity(sonarQubeIssue.DefaultImpacts), - sonarQubeIssue.CreationTimestamp, - sonarQubeIssue.LastUpdateTimestamp, - Convert(sonarQubeIssue.Flows), - sonarQubeIssue.Context - ); - } - - private static IAnalysisIssueBase ConvertToAnalysisIssue(ITaintIssue sonarQubeTaintIssue) - { - return new TaintIssue( - sonarQubeTaintIssue.Key, - sonarQubeTaintIssue.RuleKey, - primaryLocation: new AnalysisIssueLocation( - sonarQubeTaintIssue.MainLocation.Message, - sonarQubeTaintIssue.MainLocation.FilePath, - textRange: new TextRange( - sonarQubeTaintIssue.MainLocation.TextRange.StartLine, - sonarQubeTaintIssue.MainLocation.TextRange.EndLine, - sonarQubeTaintIssue.MainLocation.TextRange.StartLineOffset, - sonarQubeTaintIssue.MainLocation.TextRange.EndLineOffset, - sonarQubeTaintIssue.MainLocation.TextRange.Hash)), - Convert(sonarQubeTaintIssue.Severity), - ConvertToHighestSeverity(sonarQubeTaintIssue.DefaultImpacts), - sonarQubeTaintIssue.CreationDate, - default, - Convert(sonarQubeTaintIssue.Flows), - sonarQubeTaintIssue.Context - ); - } - - private static IReadOnlyList Convert(IEnumerable flows) => - flows.Select(x => new AnalysisIssueFlow(Convert(x.Locations))).ToArray(); - - private static IReadOnlyList Convert(IEnumerable flows) => - flows.Select(x => new AnalysisIssueFlow(Convert(x.Locations))).ToArray(); - - private static IReadOnlyList Convert(IEnumerable locations) => - locations.Reverse().Select(location => - { - if (location.TextRange == null) - { - throw new ArgumentNullException(nameof(location.TextRange)); - } - - return new AnalysisIssueLocation(location.Message, - location.FilePath, - textRange: new TextRange( - location.TextRange.StartLine, - location.TextRange.EndLine, - location.TextRange.StartOffset, - location.TextRange.EndOffset, - null)); - }).ToArray(); - - private static IReadOnlyList Convert(IEnumerable locations) => - locations.Reverse().Select(location => - { - if (location.TextRange == null) - { - throw new ArgumentNullException(nameof(location.TextRange)); - } - - return new AnalysisIssueLocation(location.Message, - location.FilePath, - textRange: new TextRange( - location.TextRange.StartLine, - location.TextRange.EndLine, - location.TextRange.StartLineOffset, - location.TextRange.EndLineOffset, - location.TextRange.Hash)); - }).ToArray(); - - /// - /// Converts from the sonarqube issue severity enum to the standard AnalysisIssueSeverity - /// - internal /* for testing */ static AnalysisIssueSeverity Convert(SonarQubeIssueSeverity issueSeverity) - { - switch (issueSeverity) - { - case SonarQubeIssueSeverity.Blocker: - return AnalysisIssueSeverity.Blocker; - - case SonarQubeIssueSeverity.Critical: - return AnalysisIssueSeverity.Critical; - - case SonarQubeIssueSeverity.Info: - return AnalysisIssueSeverity.Info; - - case SonarQubeIssueSeverity.Major: - return AnalysisIssueSeverity.Major; - - case SonarQubeIssueSeverity.Minor: - return AnalysisIssueSeverity.Minor; - - default: - throw new ArgumentOutOfRangeException(nameof(issueSeverity)); - } - } - - internal /* for testing */ static SoftwareQualitySeverity? ConvertToHighestSeverity( - Dictionary sonarQubeSoftwareQualitySeverities) - { - if (sonarQubeSoftwareQualitySeverities == null || sonarQubeSoftwareQualitySeverities.Count == 0) - { - return null; - } - - return sonarQubeSoftwareQualitySeverities - .Select(kvp => kvp.Value) - .Select(sqSeverity => - { - switch (sqSeverity) - { - case SonarQubeSoftwareQualitySeverity.Info: - return SoftwareQualitySeverity.Info; - case SonarQubeSoftwareQualitySeverity.Low: - return SoftwareQualitySeverity.Low; - case SonarQubeSoftwareQualitySeverity.Medium: - return SoftwareQualitySeverity.Medium; - case SonarQubeSoftwareQualitySeverity.High: - return SoftwareQualitySeverity.High; - case SonarQubeSoftwareQualitySeverity.Blocker: - return SoftwareQualitySeverity.Blocker; - default: - throw new ArgumentOutOfRangeException(nameof(sqSeverity)); - } - }) - .Max(); - } + return issueViz; } + + private static IAnalysisIssueBase ConvertToAnalysisIssue(TaintVulnerabilityDto slcoreTaintIssue, string configScopeRoot) => + new TaintIssue( + slcoreTaintIssue.id, + slcoreTaintIssue.sonarServerKey, + slcoreTaintIssue.ruleKey, + CreateLocation(slcoreTaintIssue.message, slcoreTaintIssue.ideFilePath, configScopeRoot, slcoreTaintIssue.textRange), + slcoreTaintIssue.severityMode.Left?.severity.ToAnalysisIssueSeverity(), + slcoreTaintIssue.severityMode.Right?.impacts.Select(x => x?.impactSeverity.ToSoftwareQualitySeverity()).Max(), + slcoreTaintIssue.introductionDate, + slcoreTaintIssue + .flows + .Select(taintFlow => + new AnalysisIssueFlow( + taintFlow + .locations + .Select(taintLocation => + CreateLocation( + taintLocation.message, + taintLocation.filePath, + configScopeRoot, + taintLocation.textRange)) + .ToArray())) + .ToArray(), + slcoreTaintIssue.ruleDescriptionContextKey); + + private static AnalysisIssueLocation CreateLocation( + string message, + string filePath, + string configScopeRoot, + TextRangeWithHashDto textRange) => + new(message, + Path.Combine(configScopeRoot, filePath), + new TextRange(textRange.startLine, + textRange.endLine, + textRange.startLineOffset, + textRange.endLineOffset, + textRange.hash)); + + private IAnalysisIssueVisualization CreateAnalysisIssueVisualization(IAnalysisIssueBase analysisIssue) => + issueVisualizationConverter.Convert(analysisIssue); } diff --git a/src/IssueViz.Security/Taint/TaintIssuesBindingMonitor.cs b/src/IssueViz.Security/Taint/TaintIssuesBindingMonitor.cs deleted file mode 100644 index 6cfc54395b..0000000000 --- a/src/IssueViz.Security/Taint/TaintIssuesBindingMonitor.cs +++ /dev/null @@ -1,86 +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 System.Threading.Tasks; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; - -namespace SonarLint.VisualStudio.IssueVisualization.Security.Taint -{ - /// - /// Listens to binding changes and triggers fetching of taint vulnerabilities from the connected server. - /// Doesn't do initial sync - only triggers the fetch when the binding changes. - /// - internal interface ITaintIssuesBindingMonitor : IDisposable - { - } - - [Export(typeof(ITaintIssuesBindingMonitor))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal sealed class TaintIssuesBindingMonitor : ITaintIssuesBindingMonitor - { - private readonly IActiveSolutionBoundTracker activeSolutionBoundTracker; - private readonly IFolderWorkspaceMonitor folderWorkspaceMonitor; - private readonly ITaintIssuesSynchronizer taintIssuesSynchronizer; - - [ImportingConstructor] - public TaintIssuesBindingMonitor(IActiveSolutionBoundTracker activeSolutionBoundTracker, - IFolderWorkspaceMonitor folderWorkspaceMonitor, - ITaintIssuesSynchronizer taintIssuesSynchronizer) - { - this.activeSolutionBoundTracker = activeSolutionBoundTracker; - this.folderWorkspaceMonitor = folderWorkspaceMonitor; - this.taintIssuesSynchronizer = taintIssuesSynchronizer; - - folderWorkspaceMonitor.FolderWorkspaceInitialized += FolderWorkspaceInitializedEvent_FolderWorkspaceInitialized; - activeSolutionBoundTracker.SolutionBindingChanged += ActiveSolutionBoundTracker_SolutionBindingChanged; - activeSolutionBoundTracker.SolutionBindingUpdated += ActiveSolutionBoundTracker_SolutionBindingUpdated; - } - - private async void FolderWorkspaceInitializedEvent_FolderWorkspaceInitialized(object sender, EventArgs e) - { - await Sync(); - } - - private async void ActiveSolutionBoundTracker_SolutionBindingUpdated(object sender, EventArgs e) - { - await Sync(); - } - - private async void ActiveSolutionBoundTracker_SolutionBindingChanged(object sender, ActiveSolutionBindingEventArgs e) - { - await Sync(); - } - - private async Task Sync() - { - await taintIssuesSynchronizer.SynchronizeWithServer(); - } - - public void Dispose() - { - folderWorkspaceMonitor.FolderWorkspaceInitialized -= FolderWorkspaceInitializedEvent_FolderWorkspaceInitialized; - activeSolutionBoundTracker.SolutionBindingChanged -= ActiveSolutionBoundTracker_SolutionBindingChanged; - activeSolutionBoundTracker.SolutionBindingUpdated -= ActiveSolutionBoundTracker_SolutionBindingUpdated; - } - } -} diff --git a/src/IssueViz.Security/Taint/TaintIssuesConfigurationScopeMonitor.cs b/src/IssueViz.Security/Taint/TaintIssuesConfigurationScopeMonitor.cs new file mode 100644 index 0000000000..c1eb071c4e --- /dev/null +++ b/src/IssueViz.Security/Taint/TaintIssuesConfigurationScopeMonitor.cs @@ -0,0 +1,56 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using Microsoft.VisualStudio.Threading; +using SonarLint.VisualStudio.SLCore.State; + +namespace SonarLint.VisualStudio.IssueVisualization.Security.Taint +{ + /// + /// Listens to binding changes and triggers fetching of taint vulnerabilities from the connected server. + /// Doesn't do initial sync - only triggers the fetch when the binding changes. + /// + internal interface ITaintIssuesBindingMonitor : IDisposable; + + [Export(typeof(ITaintIssuesBindingMonitor))] + [PartCreationPolicy(CreationPolicy.Shared)] + internal sealed class TaintIssuesConfigurationScopeMonitor : ITaintIssuesBindingMonitor + { + private readonly IActiveConfigScopeTracker activeConfigScopeTracker; + private readonly ITaintIssuesSynchronizer taintIssuesSynchronizer; + + [ImportingConstructor] + public TaintIssuesConfigurationScopeMonitor(IActiveConfigScopeTracker activeConfigScopeTracker, + ITaintIssuesSynchronizer taintIssuesSynchronizer) + { + this.activeConfigScopeTracker = activeConfigScopeTracker; + this.taintIssuesSynchronizer = taintIssuesSynchronizer; + + this.activeConfigScopeTracker.CurrentConfigurationScopeChanged += ActiveConfigScopeTrackerOnCurrentConfigurationScopeChanged; + } + + private void ActiveConfigScopeTrackerOnCurrentConfigurationScopeChanged(object sender, EventArgs e) => + taintIssuesSynchronizer.UpdateTaintVulnerabilitiesAsync(activeConfigScopeTracker.Current).Forget(); + + public void Dispose() => + activeConfigScopeTracker.CurrentConfigurationScopeChanged -= ActiveConfigScopeTrackerOnCurrentConfigurationScopeChanged; + } +} diff --git a/src/IssueViz.Security/Taint/TaintIssuesSynchronizer.cs b/src/IssueViz.Security/Taint/TaintIssuesSynchronizer.cs index c8eafbcad0..5dccbe2476 100644 --- a/src/IssueViz.Security/Taint/TaintIssuesSynchronizer.cs +++ b/src/IssueViz.Security/Taint/TaintIssuesSynchronizer.cs @@ -18,183 +18,173 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; using System.ComponentModel.Composition; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; +using SonarLint.VisualStudio.Core.Synchronization; using SonarLint.VisualStudio.Infrastructure.VS; -using SonarLint.VisualStudio.IssueVisualization.Models; using SonarLint.VisualStudio.IssueVisualization.Security.Taint.TaintList; -using SonarQube.Client; +using SonarLint.VisualStudio.SLCore.Core; +using SonarLint.VisualStudio.SLCore.Service.Taint; +using SonarLint.VisualStudio.SLCore.State; using VSShellInterop = Microsoft.VisualStudio.Shell.Interop; -namespace SonarLint.VisualStudio.IssueVisualization.Security.Taint +namespace SonarLint.VisualStudio.IssueVisualization.Security.Taint; + +internal interface ITaintIssuesSynchronizer { - internal interface ITaintIssuesSynchronizer - { - /// - /// Fetches taint vulnerabilities from the server, converts them into visualizations and populates . - /// - Task SynchronizeWithServer(); - } + /// + /// Fetches taint vulnerabilities from the server, converts them into visualizations and populates . + /// + Task UpdateTaintVulnerabilitiesAsync(ConfigurationScope configurationScope); +} - [Export(typeof(ITaintIssuesSynchronizer))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal sealed class TaintIssuesSynchronizer : ITaintIssuesSynchronizer +[Export(typeof(ITaintIssuesSynchronizer))] +[PartCreationPolicy(CreationPolicy.Shared)] +internal sealed class TaintIssuesSynchronizer : ITaintIssuesSynchronizer +{ + private readonly IAsyncLock asyncLock; + private readonly ITaintIssueToIssueVisualizationConverter converter; + private readonly ILogger logger; + private readonly ISLCoreServiceProvider slCoreServiceProvider; + private readonly ITaintStore taintStore; + private readonly IThreadHandling threadHandling; + private readonly IToolWindowService toolWindowService; + private readonly IVsUIServiceOperation vSServiceOperation; + + [ImportingConstructor] + public TaintIssuesSynchronizer( + ITaintStore taintStore, + ISLCoreServiceProvider slCoreServiceProvider, + ITaintIssueToIssueVisualizationConverter converter, + IToolWindowService toolWindowService, + IVsUIServiceOperation vSServiceOperation, + IThreadHandling threadHandling, + IAsyncLockFactory asyncLockFactory, + ILogger logger) { - private static readonly Version MinimumRequiredSonarQubeVersion = new Version(8, 6); - - private readonly ITaintStore taintStore; - private readonly ISonarQubeService sonarQubeService; - private readonly ITaintIssueToIssueVisualizationConverter converter; - private readonly IConfigurationProvider configurationProvider; - private readonly IToolWindowService toolWindowService; - private readonly IStatefulServerBranchProvider serverBranchProvider; - private readonly IVsUIServiceOperation vSServiceOperation; - private readonly ILogger logger; - - [ImportingConstructor] - public TaintIssuesSynchronizer(ITaintStore taintStore, - ISonarQubeService sonarQubeService, - ITaintIssueToIssueVisualizationConverter converter, - IConfigurationProvider configurationProvider, - IToolWindowService toolWindowService, - IStatefulServerBranchProvider serverBranchProvider, - IVsUIServiceOperation vSServiceOperation, - ILogger logger) - { - this.taintStore = taintStore; - this.sonarQubeService = sonarQubeService; - this.converter = converter; - this.configurationProvider = configurationProvider; - this.toolWindowService = toolWindowService; - this.serverBranchProvider = serverBranchProvider; - this.vSServiceOperation = vSServiceOperation; - this.logger = logger; - } + this.taintStore = taintStore; + this.slCoreServiceProvider = slCoreServiceProvider; + this.converter = converter; + this.toolWindowService = toolWindowService; + this.vSServiceOperation = vSServiceOperation; + asyncLock = asyncLockFactory.Create(); + this.threadHandling = threadHandling; + this.logger = logger; + } - public async Task SynchronizeWithServer() + public Task UpdateTaintVulnerabilitiesAsync(ConfigurationScope configurationScope) => + threadHandling.RunOnBackgroundThread(async () => { - try + using (await asyncLock.AcquireAsync()) { - var bindingConfiguration = configurationProvider.GetConfiguration(); - - if (IsStandalone(bindingConfiguration) || !IsConnected(out var serverInfo) || !IsFeatureSupported(serverInfo)) - { - HandleNoTaintIssues(); - return; - } - - var projectKey = bindingConfiguration.Project.ServerProjectKey; - var serverBranch = await serverBranchProvider.GetServerBranchNameAsync(CancellationToken.None); - - var taintVulnerabilities = await sonarQubeService.GetTaintVulnerabilitiesAsync(projectKey, - serverBranch, - CancellationToken.None); - - logger.WriteLine(TaintResources.Synchronizer_NumberOfServerIssues, taintVulnerabilities.Count); - - var analysisInformation = await GetAnalysisInformation(projectKey, serverBranch); - var taintIssueVizs = taintVulnerabilities.Select(converter.Convert).ToArray(); - taintStore.Set(taintIssueVizs, analysisInformation); - - var hasTaintIssues = taintVulnerabilities.Count > 0; - - if (!hasTaintIssues) - { - UpdateTaintIssuesUIContext(false); - } - else - { - UpdateTaintIssuesUIContext(true); - - // We need the tool window content to exist so the issues are filtered and the - // tool window caption is updated. See the "EnsureToolWindowExists" method comment - // for more information. - toolWindowService.EnsureToolWindowExists(TaintToolWindow.ToolWindowId); - } + await PerformSynchronizationInternalAsync(configurationScope); } - catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) + }); + + private async Task PerformSynchronizationInternalAsync(ConfigurationScope configurationScope) + { + try + { + if (!IsConnectedModeConfigScope(configurationScope) + || !IsConfigScopeReady(configurationScope) + || !TryGetSLCoreService(out var taintService)) { - logger.WriteLine(TaintResources.Synchronizer_Failure, ex); HandleNoTaintIssues(); + return; } - } - private bool IsStandalone(BindingConfiguration bindingConfiguration) - { - if (bindingConfiguration.Mode == SonarLintMode.Standalone) + if (IsAlreadyInitializedForConfigScope(configurationScope)) { - logger.WriteLine(TaintResources.Synchronizer_NotInConnectedMode); - return true; + return; } - return false; - } - - private bool IsConnected(out ServerInfo serverInfo) - { - serverInfo = sonarQubeService.GetServerInfo(); + var taintsResponse = await taintService.ListAllAsync(new ListAllTaintsParams(configurationScope.Id, true)); + logger.WriteLine(TaintResources.Synchronizer_NumberOfServerIssues, taintsResponse.taintVulnerabilities.Count); - if (serverInfo != null) - { - return true; - } + taintStore.Set(taintsResponse.taintVulnerabilities.Select(x => converter.Convert(x, configurationScope.RootPath)).ToArray(), configurationScope.Id); - logger.WriteLine(TaintResources.Synchronizer_ServerNotConnected); - return false; + HandleUIContextUpdate(taintsResponse.taintVulnerabilities.Count); } - - private bool IsFeatureSupported(ServerInfo serverInfo) + catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) { - if (serverInfo.ServerType == ServerType.SonarCloud || - serverInfo.Version >= MinimumRequiredSonarQubeVersion) - { - return true; - } - - logger.WriteLine(TaintResources.Synchronizer_UnsupportedSQVersion, serverInfo.Version, DocumentationLinks.TaintVulnerabilities); - return false; + logger.WriteLine(TaintResources.Synchronizer_Failure, ex); + HandleNoTaintIssues(); } + } - private async Task GetAnalysisInformation(string projectKey, string branchName) + private bool TryGetSLCoreService(out ITaintVulnerabilityTrackingSlCoreService taintService) + { + var result = slCoreServiceProvider.TryGetTransientService(out taintService); + if (!result) { - Debug.Assert(branchName != null, "BranchName should not be null when in Connected Mode"); + logger.WriteLine(TaintResources.Synchronizer_SLCoreNotReady); + } + return result; + } - var branches = await sonarQubeService.GetProjectBranchesAsync(projectKey, CancellationToken.None); + private bool IsConfigScopeReady(ConfigurationScope configurationScope) + { + var isReady = configurationScope.RootPath is not null; + if (!isReady) + { + logger.LogVerbose(TaintResources.Synchronizer_Verbose_ConfigScopeNotReady); + } + return isReady; + } - var issuesBranch = branches.FirstOrDefault(x => x.Name.Equals(branchName)); + private bool IsAlreadyInitializedForConfigScope(ConfigurationScope configurationScope) + { + var isAlreadyInitialized = taintStore.ConfigurationScope == configurationScope.Id; + if (!isAlreadyInitialized) + { + logger.LogVerbose(TaintResources.Synchronizer_Verbose_AlreadyInitialized); + } + return isAlreadyInitialized; + } - Debug.Assert(issuesBranch != null, "Should always find a matching branch"); + private void HandleUIContextUpdate(int taintsCount) + { + if (taintsCount > 0) + { + UpdateTaintIssuesUIContext(true); - return new AnalysisInformation(issuesBranch.Name, issuesBranch.LastAnalysisTimestamp); + // We need the tool window content to exist so the issues are filtered and the + // tool window caption is updated. See the "EnsureToolWindowExists" method comment + // for more information. + toolWindowService.EnsureToolWindowExists(TaintToolWindow.ToolWindowId); } - - private void HandleNoTaintIssues() + else { - ClearStore(); UpdateTaintIssuesUIContext(false); } + } - private void ClearStore() + private bool IsConnectedModeConfigScope(ConfigurationScope configurationScope) + { + if (configurationScope is { SonarProjectId: not null }) { - taintStore.Set(Enumerable.Empty(), null); + return true; } - private void UpdateTaintIssuesUIContext(bool hasTaintIssues) - { - vSServiceOperation.Execute( - monitorSelection => - { - Guid localGuid = TaintIssuesExistUIContext.Guid; - - monitorSelection.GetCmdUIContextCookie(ref localGuid, out var cookie); - monitorSelection.SetCmdUIContext(cookie, hasTaintIssues ? 1 : 0); - }); - } + logger.WriteLine(TaintResources.Synchronizer_NotInConnectedMode); + return false; + } + + private void HandleNoTaintIssues() + { + ClearStore(); + UpdateTaintIssuesUIContext(false); } + + private void ClearStore() => taintStore.Set([], null); + + private void UpdateTaintIssuesUIContext(bool hasTaintIssues) => + vSServiceOperation.Execute( + monitorSelection => + { + var localGuid = TaintIssuesExistUIContext.Guid; + + monitorSelection.GetCmdUIContextCookie(ref localGuid, out var cookie); + monitorSelection.SetCmdUIContext(cookie, hasTaintIssues ? 1 : 0); + }); } diff --git a/src/IssueViz.Security/Taint/TaintList/TaintIssuesControl.xaml b/src/IssueViz.Security/Taint/TaintList/TaintIssuesControl.xaml index 1b11b1a9e9..bff83519e4 100644 --- a/src/IssueViz.Security/Taint/TaintList/TaintIssuesControl.xaml +++ b/src/IssueViz.Security/Taint/TaintList/TaintIssuesControl.xaml @@ -117,14 +117,7 @@ - - - - - - - - + @@ -174,6 +167,7 @@ + diff --git a/src/IssueViz.Security/Taint/TaintList/ViewModels/TaintIssuesControlViewModel.cs b/src/IssueViz.Security/Taint/TaintList/ViewModels/TaintIssuesControlViewModel.cs index ecdf4726cb..3d2ecafe46 100644 --- a/src/IssueViz.Security/Taint/TaintList/ViewModels/TaintIssuesControlViewModel.cs +++ b/src/IssueViz.Security/Taint/TaintList/ViewModels/TaintIssuesControlViewModel.cs @@ -65,8 +65,6 @@ internal interface ITaintIssuesControlViewModel : INotifyPropertyChanged, IDispo string WindowCaption { get; } string ServerType { get; } - - AnalysisInformation AnalysisInformation { get; } } /// @@ -132,8 +130,6 @@ public ITaintIssueViewModel SelectedIssue } } - public AnalysisInformation AnalysisInformation { get; private set; } - public string ServerType => serverType.ToString(); public TaintIssuesControlViewModel(ITaintStore store, @@ -164,7 +160,7 @@ INavigateToRuleDescriptionCommand navigateToRuleDescriptionCommand ISonarQubeService sonarQubeService, INavigateToRuleDescriptionCommand navigateToRuleDescriptionCommand, IThreadHandling threadHandling) - { + { this.threadHandling = threadHandling; unfilteredIssues = new ObservableCollection(); AllowMultiThreadedAccessToIssuesCollection(); @@ -301,10 +297,7 @@ private void UpdateIssues() taintIssueViewModel.TaintIssueViz.PropertyChanged += OnTaintIssuePropertyChanged; } - AnalysisInformation = store.GetAnalysisInformation(); - NotifyPropertyChanged(nameof(HasServerIssues)); - NotifyPropertyChanged(nameof(AnalysisInformation)); } diff --git a/src/IssueViz.Security/Taint/TaintResources.Designer.cs b/src/IssueViz.Security/Taint/TaintResources.Designer.cs index 9e593c89c8..f848b1871c 100644 --- a/src/IssueViz.Security/Taint/TaintResources.Designer.cs +++ b/src/IssueViz.Security/Taint/TaintResources.Designer.cs @@ -1,7 +1,6 @@ //------------------------------------------------------------------------------ // // 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. @@ -88,21 +87,29 @@ internal static string Synchronizer_NumberOfServerIssues { } /// - /// Looks up a localized string similar to [Taint] Unable to fetch taint vulnerabilities: a connection to the server is not yet established.. + /// Looks up a localized string similar to [Taint] Unable to fetch taint vulnerabilities: SLCore backend is not available.. /// - internal static string Synchronizer_ServerNotConnected { + internal static string Synchronizer_SLCoreNotReady { get { - return ResourceManager.GetString("Synchronizer_ServerNotConnected", resourceCulture); + return ResourceManager.GetString("Synchronizer_SLCoreNotReady", resourceCulture); } } /// - /// Looks up a localized string similar to [Taint] Displaying taint vulnerabilities in the IDE requires SonarQube v8.6 or later, or SonarCloud. Connected SonarQube version: v{0} - /// Visit {1} to find out more about this and other SonarLint features.. + /// Looks up a localized string similar to [Taint Sync] Taint storage has already been initialized for current configuration scope. /// - internal static string Synchronizer_UnsupportedSQVersion { + internal static string Synchronizer_Verbose_AlreadyInitialized { get { - return ResourceManager.GetString("Synchronizer_UnsupportedSQVersion", resourceCulture); + return ResourceManager.GetString("Synchronizer_Verbose_AlreadyInitialized", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [Taint Sync] Configuration scope root hasn't been initialized.... + /// + internal static string Synchronizer_Verbose_ConfigScopeNotReady { + get { + return ResourceManager.GetString("Synchronizer_Verbose_ConfigScopeNotReady", resourceCulture); } } @@ -123,5 +130,14 @@ internal static string SyncPackage_Initializing { return ResourceManager.GetString("SyncPackage_Initializing", resourceCulture); } } + + /// + /// Looks up a localized string similar to Taint issue with id: {0} has no defined severity. + /// + internal static string TaintIssue_SeverityUndefined { + get { + return ResourceManager.GetString("TaintIssue_SeverityUndefined", resourceCulture); + } + } } } diff --git a/src/IssueViz.Security/Taint/TaintResources.resx b/src/IssueViz.Security/Taint/TaintResources.resx index 4f77c80341..fcd41169fc 100644 --- a/src/IssueViz.Security/Taint/TaintResources.resx +++ b/src/IssueViz.Security/Taint/TaintResources.resx @@ -123,8 +123,8 @@ [Taint] Unable to fetch taint vulnerabilities: not in connected mode. - - [Taint] Unable to fetch taint vulnerabilities: a connection to the server is not yet established. + + [Taint] Unable to fetch taint vulnerabilities: SLCore backend is not available. [Taint] Fetched {0} taint vulnerabilities. @@ -135,8 +135,13 @@ [Taint] Initializing taint issues synchronization package... - - [Taint] Displaying taint vulnerabilities in the IDE requires SonarQube v8.6 or later, or SonarCloud. Connected SonarQube version: v{0} - Visit {1} to find out more about this and other SonarLint features. + + Taint issue with id: {0} has no defined severity + + + [Taint Sync] Configuration scope root hasn't been initialized... + + + [Taint Sync] Taint storage has already been initialized for current configuration scope \ No newline at end of file diff --git a/src/IssueViz.Security/Taint/TaintStore.cs b/src/IssueViz.Security/Taint/TaintStore.cs index 0fd72a7f0d..35db00dd10 100644 --- a/src/IssueViz.Security/Taint/TaintStore.cs +++ b/src/IssueViz.Security/Taint/TaintStore.cs @@ -18,10 +18,7 @@ * 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 SonarLint.VisualStudio.IssueVisualization.Models; using SonarLint.VisualStudio.IssueVisualization.Security.IssuesStore; using SonarLint.VisualStudio.IssueVisualization.Security.Taint.Models; @@ -34,12 +31,9 @@ internal interface ITaintStore : IIssuesStore /// Removes all existing visualizations and initializes the store to the given collection. /// Can be called multiple times. /// - void Set(IEnumerable issueVisualizations, AnalysisInformation analysisInformation); + void Set(IReadOnlyCollection issueVisualizations, string newConfigurationScope); - /// - /// Returns additional analysis information for the existing visualizations in the store. - /// - AnalysisInformation GetAnalysisInformation(); + string ConfigurationScope { get; } /// /// Add the given issue to the existing list of visualizations. @@ -61,21 +55,19 @@ internal sealed class TaintStore : ITaintStore { public event EventHandler IssuesChanged; - private static readonly object Locker = new object(); + private readonly object locker = new object(); + private string configurationScope; private List taintVulnerabilities = new List(); - private AnalysisInformation analysisInformation; public IReadOnlyCollection GetAll() { - lock (Locker) + lock (locker) { return taintVulnerabilities.ToList(); } } - public AnalysisInformation GetAnalysisInformation() => analysisInformation; - public void Add(IAnalysisIssueVisualization issueVisualization) { if (issueVisualization == null) @@ -83,9 +75,9 @@ public void Add(IAnalysisIssueVisualization issueVisualization) throw new ArgumentNullException(nameof(issueVisualization)); } - lock (Locker) + lock (locker) { - if (analysisInformation == null) + if (configurationScope == null) { return; } @@ -97,8 +89,9 @@ public void Add(IAnalysisIssueVisualization issueVisualization) taintVulnerabilities.Add(issueVisualization); - NotifyIssuesChanged(Array.Empty(), new[] { issueVisualization }); } + + NotifyIssuesChanged([], [issueVisualization]); } public void Remove(string issueKey) @@ -108,8 +101,15 @@ public void Remove(string issueKey) throw new ArgumentNullException(nameof(issueKey)); } - lock (Locker) + IAnalysisIssueVisualization valueToRemove; + + lock (locker) { + if (configurationScope == null) + { + return; + } + var indexToRemove = taintVulnerabilities.FindIndex(issueViz => ((ITaintIssue)issueViz.Issue).IssueKey.Equals(issueKey)); @@ -118,31 +118,51 @@ public void Remove(string issueKey) return; } - var valueToRemove = taintVulnerabilities[indexToRemove]; + valueToRemove = taintVulnerabilities[indexToRemove]; taintVulnerabilities.RemoveAt(indexToRemove); - NotifyIssuesChanged(new[] { valueToRemove }, Array.Empty()); } + + NotifyIssuesChanged([valueToRemove], []); } - public void Set(IEnumerable issueVisualizations, AnalysisInformation analysisInformation) + public void Set(IReadOnlyCollection issueVisualizations, string newConfigurationScope) { if (issueVisualizations == null) { throw new ArgumentNullException(nameof(issueVisualizations)); } - lock (Locker) + if (issueVisualizations.Count > 0 && newConfigurationScope == null) { - this.analysisInformation = analysisInformation; + throw new ArgumentNullException(nameof(newConfigurationScope)); + } + IAnalysisIssueVisualization[] removedIssues; + IAnalysisIssueVisualization[] addedIssues; + + lock (locker) + { var oldIssues = taintVulnerabilities; taintVulnerabilities = issueVisualizations.ToList(); + configurationScope = newConfigurationScope; - var removedIssues = oldIssues.Except(taintVulnerabilities, TaintAnalysisIssueVisualizationByIssueKeyEqualityComparer.Instance).ToArray(); - var addedIssues = taintVulnerabilities.Except(oldIssues, TaintAnalysisIssueVisualizationByIssueKeyEqualityComparer.Instance).ToArray(); - NotifyIssuesChanged(removedIssues, addedIssues); + removedIssues = oldIssues.Except(taintVulnerabilities, TaintAnalysisIssueVisualizationByIssueKeyEqualityComparer.Instance).ToArray(); + addedIssues = taintVulnerabilities.Except(oldIssues, TaintAnalysisIssueVisualizationByIssueKeyEqualityComparer.Instance).ToArray(); + } + + NotifyIssuesChanged(removedIssues, addedIssues); + } + + public string ConfigurationScope + { + get + { + lock (locker) + { + return configurationScope; + } } } @@ -180,7 +200,7 @@ public bool Equals(IAnalysisIssueVisualization first, IAnalysisIssueVisualizatio return firstTaintIssue.IssueKey.Equals(secondTaintIssue.IssueKey); } - + public int GetHashCode(IAnalysisIssueVisualization obj) { diff --git a/src/IssueViz.Security/Taint/TaintSyncPackage.cs b/src/IssueViz.Security/Taint/TaintSyncPackage.cs index c58e673612..6801b8b57b 100644 --- a/src/IssueViz.Security/Taint/TaintSyncPackage.cs +++ b/src/IssueViz.Security/Taint/TaintSyncPackage.cs @@ -18,17 +18,14 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; -using System.Threading; using Microsoft.VisualStudio.ComponentModelHost; using Microsoft.VisualStudio.Shell; -using Microsoft.VisualStudio.Threading; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; using SonarLint.VisualStudio.Infrastructure.VS; -using SonarLint.VisualStudio.IssueVisualization.Security.Taint.ServerSentEvents; +using SonarLint.VisualStudio.SLCore.State; using Task = System.Threading.Tasks.Task; namespace SonarLint.VisualStudio.IssueVisualization.Security.Taint @@ -52,7 +49,6 @@ to avoid threading issues on package initialization (if the sync process and tai public sealed class TaintSyncPackage : AsyncPackage { private ITaintIssuesBindingMonitor bindingMonitor; - private ITaintServerEventsListener taintServerEventsListener; protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) { @@ -64,13 +60,11 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke logger.WriteLine(TaintResources.SyncPackage_Initializing); bindingMonitor = componentModel.GetService(); - taintServerEventsListener = componentModel.GetService(); var taintIssuesSynchronizer = componentModel.GetService(); await ThreadHandling.Instance.SwitchToBackgroundThread(); - await taintIssuesSynchronizer.SynchronizeWithServer(); - taintServerEventsListener.ListenAsync().Forget(); + await taintIssuesSynchronizer.UpdateTaintVulnerabilitiesAsync(componentModel.GetService().Current); logger.WriteLine(TaintResources.SyncPackage_Initialized); } @@ -80,7 +74,6 @@ protected override void Dispose(bool disposing) if (disposing) { bindingMonitor?.Dispose(); - taintServerEventsListener?.Dispose(); } base.Dispose(disposing); diff --git a/src/IssueViz.UnitTests/AnalysisIssueVisualizationConverterTests.cs b/src/IssueViz.UnitTests/AnalysisIssueVisualizationConverterTests.cs index 8426babfa2..77a6dffd88 100644 --- a/src/IssueViz.UnitTests/AnalysisIssueVisualizationConverterTests.cs +++ b/src/IssueViz.UnitTests/AnalysisIssueVisualizationConverterTests.cs @@ -268,10 +268,11 @@ private void AssertConversion(IAnalysisIssueVisualization expectedIssueVisualiza private IAnalysisIssue CreateIssue(params IQuickFix[] quickFixes) { var issue = new AnalysisIssue( + Guid.NewGuid(), Guid.NewGuid().ToString(), AnalysisIssueSeverity.Blocker, AnalysisIssueType.Bug, - SoftwareQualitySeverity.High, + new Impact(SoftwareQuality.Maintainability, SoftwareQualitySeverity.High), CreateLocation(Guid.NewGuid().ToString()), null, quickFixes @@ -283,10 +284,11 @@ private IAnalysisIssue CreateIssue(params IQuickFix[] quickFixes) private IAnalysisIssue CreateIssue(string filePath, params IAnalysisIssueFlow[] flows) { var issue = new AnalysisIssue( + Guid.NewGuid(), Guid.NewGuid().ToString(), AnalysisIssueSeverity.Blocker, AnalysisIssueType.Bug, - SoftwareQualitySeverity.High, + new Impact(SoftwareQuality.Maintainability, SoftwareQualitySeverity.High), CreateLocation(filePath), flows ); diff --git a/src/IssueViz.UnitTests/Helpers/AnalysisSeverityToVsSeverityConverterTests.cs b/src/IssueViz.UnitTests/Helpers/AnalysisSeverityToVsSeverityConverterTests.cs index 194f9f4d27..4199d46702 100644 --- a/src/IssueViz.UnitTests/Helpers/AnalysisSeverityToVsSeverityConverterTests.cs +++ b/src/IssueViz.UnitTests/Helpers/AnalysisSeverityToVsSeverityConverterTests.cs @@ -90,7 +90,7 @@ public void GetVsSeverity_IssueWithNewCct_UsesNewCctConverter() converter.Object.GetVsSeverity(new DummyAnalysisIssue { - Severity = AnalysisIssueSeverity.Major, HighestSoftwareQualitySeverity = SoftwareQualitySeverity.High + Severity = AnalysisIssueSeverity.Major, HighestImpact = new Impact(SoftwareQuality.Maintainability, SoftwareQualitySeverity.High) }); converter.Verify(x => x.ConvertFromCct(SoftwareQualitySeverity.High), Times.Once); diff --git a/src/IssueViz.UnitTests/IssueVisualizationControl/NavigateToRuleDescriptionCommandConverterTests.cs b/src/IssueViz.UnitTests/IssueVisualizationControl/NavigateToRuleDescriptionCommandConverterTests.cs index b5637c1565..2c2d648c18 100644 --- a/src/IssueViz.UnitTests/IssueVisualizationControl/NavigateToRuleDescriptionCommandConverterTests.cs +++ b/src/IssueViz.UnitTests/IssueVisualizationControl/NavigateToRuleDescriptionCommandConverterTests.cs @@ -18,8 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; using SonarLint.VisualStudio.IssueVisualization.IssueVisualizationControl.ViewModels.Commands; using SonarLint.VisualStudio.TestInfrastructure; @@ -43,13 +41,15 @@ public void Convert_CorrectFormat_Converts() } [TestMethod] - public void Convert_WrongNumberOfParams_ReturnsNull() + public void Convert_WrongNumberOfParams_IgnoresAdditionalParameter() { - var values = new object[] { "RuleKey", "context", "third param" }; + var values = new object[] { "RuleKey", "context", "third param", "fourth param" }; var command = testSubject.Convert(values, default, default, default); - command.Should().BeNull(); + (command is NavigateToRuleDescriptionCommandParam).Should().BeTrue(); + ((NavigateToRuleDescriptionCommandParam)command).FullRuleKey.Should().Be("RuleKey"); + ((NavigateToRuleDescriptionCommandParam)command).Context.Should().Be("context"); } [DataRow("str", 3)] @@ -90,5 +90,34 @@ public void ConvertBack_WrongType_ReturnsValues() values.Should().BeNull(); } } + + [TestMethod] + public void Convert_ThirdOptionalValueProvided_SetsIssueId() + { + var issuedId = Guid.NewGuid(); + var values = new object[] { "RuleKey", "context", issuedId }; + + var command = testSubject.Convert(values, default, default, default); + + var navigateToRuleDescriptionParam = command as NavigateToRuleDescriptionCommandParam; + navigateToRuleDescriptionParam.Should().NotBeNull(); + navigateToRuleDescriptionParam.FullRuleKey.Should().Be("RuleKey"); + navigateToRuleDescriptionParam.Context.Should().Be("context"); + navigateToRuleDescriptionParam.IssueId.Should().Be(issuedId); + } + + [TestMethod] + public void Convert_ThirdOptionalValueIsNull_SetsIssueIdToNull() + { + var values = new object[] { "RuleKey", "context", null }; + + var command = testSubject.Convert(values, default, default, default); + + var navigateToRuleDescriptionParam = command as NavigateToRuleDescriptionCommandParam; + navigateToRuleDescriptionParam.Should().NotBeNull(); + navigateToRuleDescriptionParam.FullRuleKey.Should().Be("RuleKey"); + navigateToRuleDescriptionParam.Context.Should().Be("context"); + navigateToRuleDescriptionParam.IssueId.Should().BeNull(); + } } } diff --git a/src/IssueViz.UnitTests/IssueVisualizationControl/ViewModelCommands/NavigateToRuleDescriptionCommandTests.cs b/src/IssueViz.UnitTests/IssueVisualizationControl/ViewModelCommands/NavigateToRuleDescriptionCommandTests.cs index e3baeedbc0..d732bbf95f 100644 --- a/src/IssueViz.UnitTests/IssueVisualizationControl/ViewModelCommands/NavigateToRuleDescriptionCommandTests.cs +++ b/src/IssueViz.UnitTests/IssueVisualizationControl/ViewModelCommands/NavigateToRuleDescriptionCommandTests.cs @@ -18,8 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.IssueVisualization.IssueVisualizationControl.ViewModels.Commands; @@ -82,7 +80,7 @@ public void Execute_RuleDocumentationShown(string fullRuleKey) testSubject.Execute(executeParam); - educationService.Verify(x => x.ShowRuleHelp(It.IsAny(), /* todo */ null), Times.Once); + educationService.Verify(x => x.ShowRuleHelp(It.IsAny(),null, /* todo by SLVS-1630 */null), Times.Once); educationService.VerifyNoOtherCalls(); var actualRuleId = (SonarCompositeRuleId)educationService.Invocations[0].Arguments[0]; @@ -103,6 +101,20 @@ public void Execute_WrongTypeParameter_DoesNotCrash() educationService.VerifyNoOtherCalls(); } + [TestMethod] + public void Execute_IssueIdProvided_RuleDocumentationShownForIssue() + { + var issueId = Guid.NewGuid(); + var educationService = new Mock(); + var testSubject = CreateTestSubject(educationService.Object); + var executeParam = new NavigateToRuleDescriptionCommandParam { FullRuleKey = "csharp:S100", IssueId = issueId}; + + testSubject.Execute(executeParam); + + educationService.Verify(x => x.ShowRuleHelp(It.IsAny(), issueId, null), Times.Once); + educationService.VerifyNoOtherCalls(); + } + private NavigateToRuleDescriptionCommand CreateTestSubject(IEducation educationService = null) { educationService ??= Mock.Of(); diff --git a/src/IssueViz.UnitTests/Models/AnalysisIssueVisualizationTests.cs b/src/IssueViz.UnitTests/Models/AnalysisIssueVisualizationTests.cs index c380357fea..93dab60f90 100644 --- a/src/IssueViz.UnitTests/Models/AnalysisIssueVisualizationTests.cs +++ b/src/IssueViz.UnitTests/Models/AnalysisIssueVisualizationTests.cs @@ -211,7 +211,9 @@ public void SetIsSuppressed_HasSubscribers_VerifyRaised() [TestMethod] public void IsFilterable() { + var id = Guid.NewGuid(); var issueMock = new Mock(); + issueMock.SetupGet(x => x.Id).Returns(id); issueMock.SetupGet(x => x.RuleKey).Returns("my key"); issueMock.SetupGet(x => x.PrimaryLocation.FilePath).Returns("x:\\aaa.foo"); issueMock.SetupGet(x => x.PrimaryLocation.TextRange.StartLine).Returns(999); @@ -223,6 +225,7 @@ public void IsFilterable() var filterable = (IFilterableIssue)testSubject; + filterable.IssueId.Should().Be(id); filterable.RuleId.Should().Be(issueMock.Object.RuleKey); filterable.FilePath.Should().Be(issueMock.Object.PrimaryLocation.FilePath); filterable.StartLine.Should().Be(issueMock.Object.PrimaryLocation.TextRange.StartLine); diff --git a/src/IssueViz/Helpers/IAnalysisSeverityToVsSeverityConverter.cs b/src/IssueViz/Helpers/IAnalysisSeverityToVsSeverityConverter.cs index 68baeaff65..19966c575f 100644 --- a/src/IssueViz/Helpers/IAnalysisSeverityToVsSeverityConverter.cs +++ b/src/IssueViz/Helpers/IAnalysisSeverityToVsSeverityConverter.cs @@ -26,7 +26,7 @@ namespace SonarLint.VisualStudio.IssueVisualization.Helpers { public interface IAnalysisSeverityToVsSeverityConverter { - __VSERRORCATEGORY Convert(AnalysisIssueSeverity severity); + __VSERRORCATEGORY Convert(AnalysisIssueSeverity? severity); __VSERRORCATEGORY ConvertFromCct(SoftwareQualitySeverity severity); } @@ -65,7 +65,7 @@ public __VSERRORCATEGORY ConvertFromCct(SoftwareQualitySeverity severity) } } - public __VSERRORCATEGORY Convert(AnalysisIssueSeverity severity) + public __VSERRORCATEGORY Convert(AnalysisIssueSeverity? severity) { switch (severity) { @@ -94,8 +94,8 @@ public static class AnalysisSeverityToVsSeverityConverterExtensions public static __VSERRORCATEGORY GetVsSeverity( this IAnalysisSeverityToVsSeverityConverter converter, IAnalysisIssue issue) => - issue.HighestSoftwareQualitySeverity.HasValue - ? converter.ConvertFromCct(issue.HighestSoftwareQualitySeverity.Value) + issue.HighestImpact?.Severity != null + ? converter.ConvertFromCct(issue.HighestImpact.Severity) : converter.Convert(issue.Severity); } } diff --git a/src/IssueViz/IssueVisualizationControl/IssueVisualizationControl.xaml b/src/IssueViz/IssueVisualizationControl/IssueVisualizationControl.xaml index f250640fd0..84a7df41b3 100644 --- a/src/IssueViz/IssueVisualizationControl/IssueVisualizationControl.xaml +++ b/src/IssueViz/IssueVisualizationControl/IssueVisualizationControl.xaml @@ -322,6 +322,7 @@ + diff --git a/src/IssueViz/IssueVisualizationControl/ViewModels/Commands/NavigateToRuleDescriptionCommand.cs b/src/IssueViz/IssueVisualizationControl/ViewModels/Commands/NavigateToRuleDescriptionCommand.cs index f9de58f3ca..a094284bd3 100644 --- a/src/IssueViz/IssueVisualizationControl/ViewModels/Commands/NavigateToRuleDescriptionCommand.cs +++ b/src/IssueViz/IssueVisualizationControl/ViewModels/Commands/NavigateToRuleDescriptionCommand.cs @@ -44,7 +44,7 @@ public NavigateToRuleDescriptionCommand(IEducation educationService) var paramObject = parameter as NavigateToRuleDescriptionCommandParam; if (SonarCompositeRuleId.TryParse(paramObject?.FullRuleKey, out var ruleId)) { - educationService.ShowRuleHelp(ruleId, paramObject?.Context); + educationService.ShowRuleHelp(ruleId, paramObject?.IssueId, paramObject?.Context); } }, parameter => parameter is NavigateToRuleDescriptionCommandParam s && @@ -56,6 +56,10 @@ public NavigateToRuleDescriptionCommand(IEducation educationService) internal class NavigateToRuleDescriptionCommandParam { + /// + /// The id of the issue that comes from SlCore + /// + public Guid? IssueId { get; set; } public string FullRuleKey { get; set; } public string Context { get; set; } } @@ -64,9 +68,14 @@ public class NavigateToRuleDescriptionCommandConverter : IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { - if (values.Length == 2 && values[0] is string && (values[1] is string || values[1] == null)) + if (values.Length >= 2 && values[0] is string && (values[1] is string || values[1] == null)) { - return new NavigateToRuleDescriptionCommandParam { FullRuleKey = (string)values[0], Context = (string)values[1] }; + var parameters = new NavigateToRuleDescriptionCommandParam { FullRuleKey = (string)values[0], Context = (string)values[1] }; + if (values.Length == 3 && values[2] is Guid) + { + parameters.IssueId = (Guid)values[2]; + } + return parameters; } return null; } diff --git a/src/IssueViz/Models/AnalysisIssueVisualization.cs b/src/IssueViz/Models/AnalysisIssueVisualization.cs index 15cdfd9dae..7facf0c2e7 100644 --- a/src/IssueViz/Models/AnalysisIssueVisualization.cs +++ b/src/IssueViz/Models/AnalysisIssueVisualization.cs @@ -107,6 +107,7 @@ protected virtual void NotifyPropertyChanged([CallerMemberName] string propertyN PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } + public Guid? IssueId => Issue.Id; string IFilterableIssue.RuleId => Issue.RuleKey; string IFilterableIssue.FilePath => CurrentFilePath; diff --git a/src/IssueViz/OpenInIde/IssueDetailDtoToAnalysisIssueConverter.cs b/src/IssueViz/OpenInIde/IssueDetailDtoToAnalysisIssueConverter.cs index 5c3de6458e..6927f68088 100644 --- a/src/IssueViz/OpenInIde/IssueDetailDtoToAnalysisIssueConverter.cs +++ b/src/IssueViz/OpenInIde/IssueDetailDtoToAnalysisIssueConverter.cs @@ -47,7 +47,7 @@ public IssueDetailDtoToAnalysisIssueConverter(IChecksumCalculator checksumCalcul public IAnalysisIssueBase Convert(IssueDetailDto issueDetailDto, string rootPath) { - return new ServerIssue( + return new ServerIssue(Id: null, issueDetailDto.ruleKey, new AnalysisIssueLocation(issueDetailDto.message, Path.Combine(rootPath, issueDetailDto.ideFilePath), @@ -73,9 +73,12 @@ public IAnalysisIssueBase Convert(IssueDetailDto issueDetailDto, string rootPath } private sealed record ServerIssue( + Guid? Id, string RuleKey, IAnalysisIssueLocation PrimaryLocation, string RuleDescriptionContextKey, IReadOnlyList Flows) - : IAnalysisIssueBase; + : IAnalysisIssueBase + { + } } diff --git a/src/SLCore.IntegrationTests/FileAnalysisTestsRunner.cs b/src/SLCore.IntegrationTests/FileAnalysisTestsRunner.cs index 9acb967933..8575f8ef6a 100644 --- a/src/SLCore.IntegrationTests/FileAnalysisTestsRunner.cs +++ b/src/SLCore.IntegrationTests/FileAnalysisTestsRunner.cs @@ -180,7 +180,7 @@ internal interface ITestingFile List ExpectedIssues { get; } } -internal record ExpectedTestIssue(string ruleKey, TextRangeDto textRange, RuleType type, int expectedFlows); +internal record ExpectedTestIssue(string ruleKey, TextRangeDto textRange, CleanCodeAttribute cleanCodeAttribute, int expectedFlows); internal class JavaScriptIssuesFile : ITestingFile { @@ -188,8 +188,8 @@ internal class JavaScriptIssuesFile : ITestingFile public List ExpectedIssues => [ - new ExpectedTestIssue("javascript:S1135", new TextRangeDto(1, 3, 1, 7), RuleType.CODE_SMELL, 0), - new ExpectedTestIssue("javascript:S3504", new TextRangeDto(2, 0, 2, 5), RuleType.CODE_SMELL, 0) + new ExpectedTestIssue("javascript:S1135", new TextRangeDto(1, 3, 1, 7), CleanCodeAttribute.COMPLETE, 0), + new ExpectedTestIssue("javascript:S3504", new TextRangeDto(2, 0, 2, 5), CleanCodeAttribute.CLEAR, 0) ]; } @@ -209,9 +209,9 @@ internal class TypeScriptIssuesFile : ITestingFile public List ExpectedIssues => [ - new ExpectedTestIssue("typescript:S2737", new TextRangeDto(3, 2, 3, 7), RuleType.CODE_SMELL, 0), - new ExpectedTestIssue("typescript:S1186", new TextRangeDto(7, 16, 7, 19), RuleType.CODE_SMELL, 0), - new ExpectedTestIssue("typescript:S3776", new TextRangeDto(30, 9, 30, 18), RuleType.CODE_SMELL, 21) + new ExpectedTestIssue("typescript:S2737", new TextRangeDto(3, 2, 3, 7), CleanCodeAttribute.CLEAR, 0), + new ExpectedTestIssue("typescript:S1186", new TextRangeDto(7, 16, 7, 19), CleanCodeAttribute.COMPLETE, 0), + new ExpectedTestIssue("typescript:S3776", new TextRangeDto(30, 9, 30, 18), CleanCodeAttribute.FOCUSED, 21) ]; } @@ -221,8 +221,8 @@ internal class CssIssuesFile : ITestingFile public List ExpectedIssues => [ - new ExpectedTestIssue("css:S4666", new TextRangeDto(20, 0, 20, 77), RuleType.CODE_SMELL, 0), - new ExpectedTestIssue("css:S4655", new TextRangeDto(12, 0, 12, 38), RuleType.BUG, 0), + new ExpectedTestIssue("css:S4666", new TextRangeDto(20, 0, 20, 77), CleanCodeAttribute.LOGICAL, 0), + new ExpectedTestIssue("css:S4655", new TextRangeDto(12, 0, 12, 38), CleanCodeAttribute.LOGICAL, 0), ]; } @@ -232,8 +232,8 @@ internal class VueIssuesFile : ITestingFile public List ExpectedIssues => [ - new ExpectedTestIssue("css:S4661", new TextRangeDto(12, 0, 12, 43), RuleType.BUG, 0), - new ExpectedTestIssue("css:S4658", new TextRangeDto(12, 0, 12, 43), RuleType.CODE_SMELL, 0), + new ExpectedTestIssue("css:S4661", new TextRangeDto(12, 0, 12, 43), CleanCodeAttribute.LOGICAL, 0), + new ExpectedTestIssue("css:S4658", new TextRangeDto(12, 0, 12, 43), CleanCodeAttribute.CLEAR, 0), ]; } @@ -245,9 +245,9 @@ internal class SecretsIssuesFile : ITestingFile public List ExpectedIssues => [ - new ExpectedTestIssue(CloudSecretsRuleKey, new TextRangeDto(9, 24, 9, 40), RuleType.VULNERABILITY, 0), - new ExpectedTestIssue(CloudSecretsRuleKey, new TextRangeDto(14, 24, 14, 40), RuleType.VULNERABILITY, 0), - new ExpectedTestIssue("secrets:S6337", new TextRangeDto(20, 12, 20, 56), RuleType.VULNERABILITY, 0), + new ExpectedTestIssue(CloudSecretsRuleKey, new TextRangeDto(9, 24, 9, 40), CleanCodeAttribute.TRUSTWORTHY, 0), + new ExpectedTestIssue(CloudSecretsRuleKey, new TextRangeDto(14, 24, 14, 40), CleanCodeAttribute.TRUSTWORTHY, 0), + new ExpectedTestIssue("secrets:S6337", new TextRangeDto(20, 12, 20, 56), CleanCodeAttribute.TRUSTWORTHY, 0), ]; } diff --git a/src/SLCore.IntegrationTests/RuleDescriptionConversionSmokeTest.cs b/src/SLCore.IntegrationTests/RuleDescriptionConversionSmokeTest.cs index 5568601d39..bdfb64a2e7 100644 --- a/src/SLCore.IntegrationTests/RuleDescriptionConversionSmokeTest.cs +++ b/src/SLCore.IntegrationTests/RuleDescriptionConversionSmokeTest.cs @@ -132,6 +132,7 @@ private static SLCoreRuleMetaDataProvider CreateSlCoreRuleMetaDataProvider(SLCor IActiveConfigScopeTracker activeConfigScopeTracker, ILogger testLogger) => new(slCoreTestRunner.SLCoreServiceProvider, activeConfigScopeTracker, + new RuleInfoConverter(), testLogger); private static ActiveConfigScopeTracker CreateActiveConfigScopeTracker(SLCoreTestRunner slCoreTestRunner) => diff --git a/src/SLCore.IntegrationTests/SimpleAnalysisTests.cs b/src/SLCore.IntegrationTests/SimpleAnalysisTests.cs index 77ab2ed9f0..e8fdabb457 100644 --- a/src/SLCore.IntegrationTests/SimpleAnalysisTests.cs +++ b/src/SLCore.IntegrationTests/SimpleAnalysisTests.cs @@ -93,7 +93,8 @@ private async Task DefaultRuleConfig_AnalysisProducesExpectedIssuesInFile(ITesti { var receivedIssue = receivedIssues.SingleOrDefault(x => x.ruleKey == expectedIssue.ruleKey && x.textRange.Equals(expectedIssue.textRange)); receivedIssue.Should().NotBeNull(); - receivedIssue.type.Should().Be(expectedIssue.type); + receivedIssue.severityMode.Right.Should().NotBeNull(); + receivedIssue.severityMode.Right.cleanCodeAttribute.Should().Be(expectedIssue.cleanCodeAttribute); receivedIssue.flows.Count.Should().Be(expectedIssue.expectedFlows); } } diff --git a/src/SLCore.Listeners.UnitTests/Implementation/Analysis/RaisedFindingProcessorTests.cs b/src/SLCore.Listeners.UnitTests/Implementation/Analysis/RaisedFindingProcessorTests.cs index df756d6bd0..e854d379ad 100644 --- a/src/SLCore.Listeners.UnitTests/Implementation/Analysis/RaisedFindingProcessorTests.cs +++ b/src/SLCore.Listeners.UnitTests/Implementation/Analysis/RaisedFindingProcessorTests.cs @@ -26,6 +26,8 @@ using SonarLint.VisualStudio.SLCore.Listener.Analysis; using SonarLint.VisualStudio.SLCore.Listener.Analysis.Models; using SonarLint.VisualStudio.SLCore.Listeners.Implementation.Analysis; +using SonarLint.VisualStudio.SLCore.Protocol; +using SonarLint.VisualStudio.SLCore.Service.Rules.Models; using CleanCodeAttribute = SonarLint.VisualStudio.SLCore.Common.Models.CleanCodeAttribute; using IssueSeverity = SonarLint.VisualStudio.SLCore.Common.Models.IssueSeverity; using SloopLanguage = SonarLint.VisualStudio.SLCore.Common.Models.Language; @@ -299,7 +301,7 @@ private IAnalysisStatusNotifierFactory CreateAnalysisStatusNotifierFactory(out I private TestFinding CreateTestFinding(string ruleKey) { - return new TestFinding(default, default, ruleKey, default, default, default, default, default, default, default, default, default, default, default, default); + return new TestFinding(default, default, ruleKey, default, default, default, default, default, default, default, default, default); } private static IAnalysisIssue CreateAnalysisIssue(string ruleKey) @@ -314,30 +316,24 @@ private record TestFinding( string serverKey, string ruleKey, string primaryMessage, - IssueSeverity severity, - RuleType type, - CleanCodeAttribute cleanCodeAttribute, - List impacts, DateTimeOffset introductionDate, bool isOnNewCode, bool resolved, TextRangeDto textRange, List flows, List quickFixes, - string ruleDescriptionContextKey) : + string ruleDescriptionContextKey, + Either severityMode) : RaisedFindingDto(id, serverKey, ruleKey, primaryMessage, - severity, - type, - cleanCodeAttribute, - impacts, introductionDate, isOnNewCode, resolved, textRange, flows, quickFixes, - ruleDescriptionContextKey); + ruleDescriptionContextKey, + severityMode); } diff --git a/src/SLCore.Listeners.UnitTests/Implementation/TaintVulnerabilityListenerTests.cs b/src/SLCore.Listeners.UnitTests/Implementation/TaintVulnerabilityListenerTests.cs new file mode 100644 index 0000000000..4b00ae86ef --- /dev/null +++ b/src/SLCore.Listeners.UnitTests/Implementation/TaintVulnerabilityListenerTests.cs @@ -0,0 +1,34 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.SLCore.Core; +using SonarLint.VisualStudio.SLCore.Listener.Taint; + +namespace SonarLint.VisualStudio.SLCore.Listeners.UnitTests.Implementation; + +[TestClass] +public class TaintVulnerabilityListenerTests +{ + [TestMethod] + public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported(); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); +} diff --git a/src/IssueViz.Security/Taint/ServerSentEvents/TaintServerEventChannel.cs b/src/SLCore.Listeners/Implementation/TaintVulnerabilityListener.cs similarity index 60% rename from src/IssueViz.Security/Taint/ServerSentEvents/TaintServerEventChannel.cs rename to src/SLCore.Listeners/Implementation/TaintVulnerabilityListener.cs index 76ce20c5e9..f983ae055f 100644 --- a/src/IssueViz.Security/Taint/ServerSentEvents/TaintServerEventChannel.cs +++ b/src/SLCore.Listeners/Implementation/TaintVulnerabilityListener.cs @@ -19,16 +19,25 @@ */ using System.ComponentModel.Composition; -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Taint; -using SonarLint.VisualStudio.Core.ServerSentEvents; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; +using Newtonsoft.Json; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.SLCore.Core; +using SonarLint.VisualStudio.SLCore.Listener.Taint; -namespace SonarLint.VisualStudio.IssueVisualization.Security.Taint.ServerSentEvents +namespace SonarLint.VisualStudio.SLCore.Listeners.Implementation; + +[Export(typeof(ISLCoreListener))] +[PartCreationPolicy(CreationPolicy.Shared)] +public class TaintVulnerabilityListener : ITaintVulnerabilityListener { - [Export(typeof(ITaintServerEventSource))] - [Export(typeof(ITaintServerEventSourcePublisher))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal class TaintServerEventChannel : ServerEventChannel, ITaintServerEventSource, ITaintServerEventSourcePublisher + + [ImportingConstructor] + public TaintVulnerabilityListener() + { + } + + public void DidChangeTaintVulnerabilities(DidChangeTaintVulnerabilitiesParams parameters) { + // todo https://sonarsource.atlassian.net/browse/SLVS-1592 } } diff --git a/src/SLCore.UnitTests/Common/Helpers/ModelConversionExtensionsTests.cs b/src/SLCore.UnitTests/Common/Helpers/ModelConversionExtensionsTests.cs index 35163d10b4..a329799485 100644 --- a/src/SLCore.UnitTests/Common/Helpers/ModelConversionExtensionsTests.cs +++ b/src/SLCore.UnitTests/Common/Helpers/ModelConversionExtensionsTests.cs @@ -21,125 +21,176 @@ using SonarLint.VisualStudio.Core.Analysis; using SonarLint.VisualStudio.SLCore.Common.Helpers; using SonarLint.VisualStudio.SLCore.Common.Models; +using SoftwareQuality = SonarLint.VisualStudio.Core.Analysis.SoftwareQuality; -namespace SonarLint.VisualStudio.SLCore.UnitTests.Common.Helpers +namespace SonarLint.VisualStudio.SLCore.UnitTests.Common.Helpers; + +[TestClass] +public class ModelConversionExtensionsTests { - [TestClass] - public class ModelConversionExtensionsTests + [DataRow(IssueSeverity.BLOCKER, AnalysisIssueSeverity.Blocker)] + [DataRow(IssueSeverity.CRITICAL, AnalysisIssueSeverity.Critical)] + [DataRow(IssueSeverity.MAJOR, AnalysisIssueSeverity.Major)] + [DataRow(IssueSeverity.MINOR, AnalysisIssueSeverity.Minor)] + [DataRow(IssueSeverity.INFO, AnalysisIssueSeverity.Info)] + [TestMethod] + public void ToAnalysisIssueSeverity_ConvertsCorrectly(IssueSeverity issueSeverity, AnalysisIssueSeverity excpectedAnalysisIssueSeverity) => + issueSeverity.ToAnalysisIssueSeverity().Should().Be(excpectedAnalysisIssueSeverity); + + [TestMethod] + public void ToAnalysisIssueSeverity_DoesNotThrow() { - [DataRow(IssueSeverity.BLOCKER, AnalysisIssueSeverity.Blocker)] - [DataRow(IssueSeverity.CRITICAL, AnalysisIssueSeverity.Critical)] - [DataRow(IssueSeverity.MAJOR, AnalysisIssueSeverity.Major)] - [DataRow(IssueSeverity.MINOR, AnalysisIssueSeverity.Minor)] - [DataRow(IssueSeverity.INFO, AnalysisIssueSeverity.Info)] - [TestMethod] - public void ToAnalysisIssueSeverity_ConvertsCorrectly(IssueSeverity issueSeverity, AnalysisIssueSeverity excpectedAnalysisIssueSeverity) + foreach (var issueSeverity in Enum.GetValues(typeof(IssueSeverity))) { - issueSeverity.ToAnalysisIssueSeverity().Should().Be(excpectedAnalysisIssueSeverity); + var act = () => ((IssueSeverity)issueSeverity).ToAnalysisIssueSeverity(); + act.Should().NotThrow(); } + } - [TestMethod] - public void ToAnalysisIssueSeverity_DoesNotThrow() - { - foreach (var issueSeverity in Enum.GetValues(typeof(IssueSeverity))) - { - var act = () => ((IssueSeverity)issueSeverity).ToAnalysisIssueSeverity(); - act.Should().NotThrow(); - } - } - - [TestMethod] - public void ToAnalysisIssueSeverity_ValueOutOfRange_Throws() - { - var act = () => ((IssueSeverity)1000).ToAnalysisIssueSeverity(); - act.Should().Throw().WithMessage(""" - Unexpected enum value - Parameter name: issueSeverity - Actual value was 1000. - """); - } + [TestMethod] + public void ToAnalysisIssueSeverity_ValueOutOfRange_Throws() + { + var act = () => ((IssueSeverity)1000).ToAnalysisIssueSeverity(); + act.Should().Throw().WithMessage(""" + Unexpected enum value + Parameter name: issueSeverity + Actual value was 1000. + """); + } - [DataRow(RuleType.BUG, AnalysisIssueType.Bug)] - [DataRow(RuleType.CODE_SMELL, AnalysisIssueType.CodeSmell)] - [DataRow(RuleType.SECURITY_HOTSPOT, AnalysisIssueType.SecurityHotspot)] - [DataRow(RuleType.VULNERABILITY, AnalysisIssueType.Vulnerability)] - [TestMethod] - public void ToAnalysisIssueType_ConvertsCorrectly(RuleType ruleType, AnalysisIssueType excpectedAnalysisIssueType) - { - ruleType.ToAnalysisIssueType().Should().Be(excpectedAnalysisIssueType); - } + [DataRow(RuleType.BUG, AnalysisIssueType.Bug)] + [DataRow(RuleType.CODE_SMELL, AnalysisIssueType.CodeSmell)] + [DataRow(RuleType.SECURITY_HOTSPOT, AnalysisIssueType.SecurityHotspot)] + [DataRow(RuleType.VULNERABILITY, AnalysisIssueType.Vulnerability)] + [TestMethod] + public void ToAnalysisIssueType_ConvertsCorrectly(RuleType ruleType, AnalysisIssueType excpectedAnalysisIssueType) => ruleType.ToAnalysisIssueType().Should().Be(excpectedAnalysisIssueType); - [TestMethod] - public void ToAnalysisIssueType_DoesNotThrow() - { - foreach (var ruleType in Enum.GetValues(typeof(RuleType))) - { - var act = () => ((RuleType)ruleType).ToAnalysisIssueType(); - act.Should().NotThrow(); - } - } - - [TestMethod] - public void ToAnalysisIssueType_ValueOutOfRange_Throws() + [TestMethod] + public void ToAnalysisIssueType_DoesNotThrow() + { + foreach (var ruleType in Enum.GetValues(typeof(RuleType))) { - var act = () => ((RuleType)1000).ToAnalysisIssueType(); - act.Should().Throw().WithMessage(""" - Unexpected enum value - Parameter name: ruleType - Actual value was 1000. - """); + var act = () => ((RuleType)ruleType).ToAnalysisIssueType(); + act.Should().NotThrow(); } + } - [DataRow(ImpactSeverity.LOW, SoftwareQualitySeverity.Low)] - [DataRow(ImpactSeverity.MEDIUM, SoftwareQualitySeverity.Medium)] - [DataRow(ImpactSeverity.HIGH, SoftwareQualitySeverity.High)] - [TestMethod] - public void ToSoftwareQualitySeverity_ConvertsCorrectly(ImpactSeverity impactSeverity, SoftwareQualitySeverity excpectedSoftwareQualitySeverity) - { - impactSeverity.ToSoftwareQualitySeverity().Should().Be(excpectedSoftwareQualitySeverity); - } + [TestMethod] + public void ToAnalysisIssueType_ValueOutOfRange_Throws() + { + var act = () => ((RuleType)1000).ToAnalysisIssueType(); + act.Should().Throw().WithMessage(""" + Unexpected enum value + Parameter name: ruleType + Actual value was 1000. + """); + } - [TestMethod] - public void ToSoftwareQualitySeverity_DoesNotThrow() - { - foreach (var impactSeverity in Enum.GetValues(typeof(ImpactSeverity))) - { - var act = () => ((ImpactSeverity)impactSeverity).ToSoftwareQualitySeverity(); - act.Should().NotThrow(); - } - } - - [TestMethod] - public void ToSoftwareQualitySeverity_ValueOutOfRange_Throws() + [DataRow(ImpactSeverity.LOW, SoftwareQualitySeverity.Low)] + [DataRow(ImpactSeverity.MEDIUM, SoftwareQualitySeverity.Medium)] + [DataRow(ImpactSeverity.HIGH, SoftwareQualitySeverity.High)] + [TestMethod] + public void ToSoftwareQualitySeverity_ConvertsCorrectly(ImpactSeverity impactSeverity, SoftwareQualitySeverity excpectedSoftwareQualitySeverity) => + impactSeverity.ToSoftwareQualitySeverity().Should().Be(excpectedSoftwareQualitySeverity); + + [TestMethod] + public void ToSoftwareQualitySeverity_DoesNotThrow() + { + foreach (var impactSeverity in Enum.GetValues(typeof(ImpactSeverity))) { - var act = () => ((ImpactSeverity)1000).ToSoftwareQualitySeverity(); - act.Should().Throw().WithMessage(""" - Unexpected enum value - Parameter name: impactSeverity - Actual value was 1000. - """); + var act = () => ((ImpactSeverity)impactSeverity).ToSoftwareQualitySeverity(); + act.Should().NotThrow(); } + } - [TestMethod] - [DataRow(null, null)] - [DataRow(VulnerabilityProbability.HIGH, HotspotPriority.High)] - [DataRow(VulnerabilityProbability.MEDIUM, HotspotPriority.Medium)] - [DataRow(VulnerabilityProbability.LOW, HotspotPriority.Low)] - public void GetHotspotPriority_HotspotHasVulnerabilityProbability_ConvertsCorrectly(VulnerabilityProbability? vulnerabilityProbability, HotspotPriority? expectedHotspotPriority) - { - var result = vulnerabilityProbability.GetHotspotPriority(); + [TestMethod] + public void ToSoftwareQualitySeverity_ValueOutOfRange_Throws() + { + var act = () => ((ImpactSeverity)1000).ToSoftwareQualitySeverity(); + act.Should().Throw().WithMessage(""" + Unexpected enum value + Parameter name: impactSeverity + Actual value was 1000. + """); + } - result.Should().Be(expectedHotspotPriority); - } + [TestMethod] + [DataRow(null, null)] + [DataRow(VulnerabilityProbability.HIGH, HotspotPriority.High)] + [DataRow(VulnerabilityProbability.MEDIUM, HotspotPriority.Medium)] + [DataRow(VulnerabilityProbability.LOW, HotspotPriority.Low)] + public void GetHotspotPriority_HotspotHasVulnerabilityProbability_ConvertsCorrectly(VulnerabilityProbability? vulnerabilityProbability, HotspotPriority? expectedHotspotPriority) + { + var result = vulnerabilityProbability.GetHotspotPriority(); - [TestMethod] - public void GetHotspotPriority_ValueOutOfRange_Throws() - { - var vulnerabilityProbability = ((VulnerabilityProbability?)1000); + result.Should().Be(expectedHotspotPriority); + } - var act = () => vulnerabilityProbability.GetHotspotPriority(); + [TestMethod] + public void GetHotspotPriority_ValueOutOfRange_Throws() + { + var vulnerabilityProbability = (VulnerabilityProbability?)1000; - act.Should().Throw(); - } + var act = () => vulnerabilityProbability.GetHotspotPriority(); + + act.Should().Throw(); + } + + [TestMethod] + [DataRow(SLCore.Common.Models.SoftwareQuality.MAINTAINABILITY, SoftwareQuality.Maintainability)] + [DataRow(SLCore.Common.Models.SoftwareQuality.RELIABILITY, SoftwareQuality.Reliability)] + [DataRow(SLCore.Common.Models.SoftwareQuality.SECURITY, SoftwareQuality.Security)] + public void ToSoftwareQuality_ConvertsCorrectly(SLCore.Common.Models.SoftwareQuality softwareQuality, SoftwareQuality expectedSoftwareQuality) => + softwareQuality.ToSoftwareQuality().Should().Be(expectedSoftwareQuality); + + [TestMethod] + public void ToSoftwareQuality_ValueOutOfRange_Throws() + { + var act = () => ((SLCore.Common.Models.SoftwareQuality)1000).ToSoftwareQuality(); + + act.Should().Throw().WithMessage(""" + Unexpected enum value + Parameter name: softwareQuality + Actual value was 1000. + """); + } + + [TestMethod] + [DataRow(SLCore.Common.Models.SoftwareQuality.MAINTAINABILITY, SoftwareQuality.Maintainability, ImpactSeverity.LOW, SoftwareQualitySeverity.Low)] + [DataRow(SLCore.Common.Models.SoftwareQuality.RELIABILITY, SoftwareQuality.Reliability, ImpactSeverity.MEDIUM, SoftwareQualitySeverity.Medium)] + [DataRow(SLCore.Common.Models.SoftwareQuality.SECURITY, SoftwareQuality.Security, ImpactSeverity.HIGH, SoftwareQualitySeverity.High)] + public void ToImpact_ConvertsCorrectly( + SLCore.Common.Models.SoftwareQuality softwareQuality, + SoftwareQuality expectedSoftwareQuality, + ImpactSeverity severity, + SoftwareQualitySeverity expectedSeverity) + { + var impactDto = new ImpactDto(softwareQuality, severity); + + impactDto.ToImpact().Should().BeEquivalentTo(new Impact(expectedSoftwareQuality, expectedSeverity)); + } + + [TestMethod] + public void ToImpact_SeverityValueOutOfRange_Throws() + { + var act = () => new ImpactDto(SLCore.Common.Models.SoftwareQuality.MAINTAINABILITY, (ImpactSeverity)1000).ToImpact(); + + act.Should().Throw().WithMessage(""" + Unexpected enum value + Parameter name: impactSeverity + Actual value was 1000. + """); + } + + [TestMethod] + public void ToImpact_SoftwareQualityValueOutOfRange_Throws() + { + var act = () => new ImpactDto((SLCore.Common.Models.SoftwareQuality)1000, ImpactSeverity.LOW).ToImpact(); + + act.Should().Throw().WithMessage(""" + Unexpected enum value + Parameter name: softwareQuality + Actual value was 1000. + """); } } diff --git a/src/SLCore.UnitTests/Common/Models/TaintVulnerabilityDtoTests.cs b/src/SLCore.UnitTests/Common/Models/TaintVulnerabilityDtoTests.cs new file mode 100644 index 0000000000..f3a8a91f32 --- /dev/null +++ b/src/SLCore.UnitTests/Common/Models/TaintVulnerabilityDtoTests.cs @@ -0,0 +1,119 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Newtonsoft.Json; +using SonarLint.VisualStudio.SLCore.Common.Models; +using SonarLint.VisualStudio.SLCore.Service.Rules.Models; + +namespace SonarLint.VisualStudio.SLCore.UnitTests.Common.Models; + +[TestClass] +public class TaintVulnerabilityDtoTests +{ + [TestMethod] + public void Deserialized_AsExpected() + { + var expected = new TaintVulnerabilityDto( + Guid.Parse("f1276bb9-54a4-4cbd-b4ac-41d2541302ee"), + "AXgSTUZl007Zyo8hMhT-", + false, + "roslyn.sonaranalyzer.security.cs:S5135", + "Change this code to not deserialize user-controlled data.", + "sonarlint-visualstudio-sampleprojects\\bound\\sonarcloud\\SLVS_Samples_Bound_VS2019\\Taint_CSharp_NetCore_WebAppReact\\Taint\\XmlSerializerInjectionController.cs", + DateTimeOffset.FromUnixTimeMilliseconds(1615214736000), + new MQRModeDetails(CleanCodeAttribute.COMPLETE, [new ImpactDto(SoftwareQuality.SECURITY, ImpactSeverity.HIGH)]), + [ + new TaintFlowDto([ + new TaintFlowLocationDto(new TextRangeWithHashDto(20, 32, 20, 58, "f677236678ac4b2ab451d66d4b251e8f"), + "Sink: this invocation is not safe; a malicious value can be injected into the caller", + "sonarlint-visualstudio-sampleprojects\\bound\\sonarcloud\\SLVS_Samples_Bound_VS2019\\Taint_CSharp_NetCore_WebAppReact\\Taint\\XmlSerializerInjectionController.cs") + ]) + ], + new TextRangeWithHashDto(20, 32, 20, 58, "f677236678ac4b2ab451d66d4b251e8f"), + null, + false); + + const string serialized = + """ + { + "id": "f1276bb9-54a4-4cbd-b4ac-41d2541302ee", + "sonarServerKey": "AXgSTUZl007Zyo8hMhT-", + "resolved": false, + "ruleKey": "roslyn.sonaranalyzer.security.cs:S5135", + "message": "Change this code to not deserialize user-controlled data.", + "ideFilePath": "sonarlint-visualstudio-sampleprojects\\bound\\sonarcloud\\SLVS_Samples_Bound_VS2019\\Taint_CSharp_NetCore_WebAppReact\\Taint\\XmlSerializerInjectionController.cs", + "introductionDate": 1615214736000, + "severityMode": { + "cleanCodeAttribute": "COMPLETE", + "impacts": [ + { + "softwareQuality": "SECURITY", + "impactSeverity": "HIGH" + } + ] + }, + "severity": "BLOCKER", + "type": "VULNERABILITY", + "flows": [ + { + "locations": [ + { + "textRange": { + "startLine": 20, + "startLineOffset": 32, + "endLine": 20, + "endLineOffset": 58, + "hash": "f677236678ac4b2ab451d66d4b251e8f" + }, + "message": "Sink: this invocation is not safe; a malicious value can be injected into the caller", + "filePath": "sonarlint-visualstudio-sampleprojects\\bound\\sonarcloud\\SLVS_Samples_Bound_VS2019\\Taint_CSharp_NetCore_WebAppReact\\Taint\\XmlSerializerInjectionController.cs" + } + ] + } + ], + "textRange": { + "startLine": 20, + "startLineOffset": 32, + "endLine": 20, + "endLineOffset": 58, + "hash": "f677236678ac4b2ab451d66d4b251e8f" + }, + "ruleDescriptionContextKey": null, + "cleanCodeAttribute": "COMPLETE", + "impacts": { + "SECURITY": "HIGH" + }, + "isOnNewCode": false + } + """; + + var actual = JsonConvert.DeserializeObject(serialized); + + actual + .Should() + .BeEquivalentTo(expected, + options => + options + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers()); + } +} diff --git a/src/SLCore.UnitTests/Listener/Analysis/RaiseFindingToAnalysisIssueConverterTests.cs b/src/SLCore.UnitTests/Listener/Analysis/RaiseFindingToAnalysisIssueConverterTests.cs index f94e426474..3a676f5319 100644 --- a/src/SLCore.UnitTests/Listener/Analysis/RaiseFindingToAnalysisIssueConverterTests.cs +++ b/src/SLCore.UnitTests/Listener/Analysis/RaiseFindingToAnalysisIssueConverterTests.cs @@ -22,6 +22,7 @@ using SonarLint.VisualStudio.SLCore.Common.Models; using SonarLint.VisualStudio.SLCore.Listener.Analysis; using SonarLint.VisualStudio.SLCore.Listener.Analysis.Models; +using SonarLint.VisualStudio.SLCore.Service.Rules.Models; using CleanCodeAttribute = SonarLint.VisualStudio.SLCore.Common.Models.CleanCodeAttribute; using SoftwareQuality = SonarLint.VisualStudio.SLCore.Common.Models.SoftwareQuality; @@ -30,26 +31,17 @@ namespace SonarLint.VisualStudio.SLCore.UnitTests.Listener.Analysis; [TestClass] public class RaiseFindingToAnalysisIssueConverterTests { - private RaiseFindingToAnalysisIssueConverter testSubject; private readonly FileUri fileUri = new(@"C:\file"); + private RaiseFindingToAnalysisIssueConverter testSubject; [TestInitialize] - public void TestInitialize() - { - testSubject = new RaiseFindingToAnalysisIssueConverter(); - } + public void TestInitialize() => testSubject = new RaiseFindingToAnalysisIssueConverter(); [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported(); - } + public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported(); [TestMethod] - public void MefCtor_CheckIsSingleton() - { - MefTestHelpers.CheckIsSingletonMefComponent(); - } + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); [TestMethod] public void GetAnalysisIssues_HasNoIssues_ReturnsEmpty() @@ -64,36 +56,30 @@ public void GetAnalysisIssues_HasIssues_ConvertsCorrectly() { var dateTimeOffset = DateTimeOffset.Now; var issue1 = new RaisedIssueDto( - Guid.NewGuid(), - "serverKey1", - "ruleKey1", - "PrimaryMessage1", - IssueSeverity.MAJOR, - RuleType.CODE_SMELL, - CleanCodeAttribute.EFFICIENT, - [], - dateTimeOffset, - true, - false, - new TextRangeDto(1, 2, 3, 4), - null, - null, - "context1"); - var issue2 = new RaisedIssueDto(Guid.NewGuid(), + IssueWithFlowsAndQuickFixesUseCase.Issue1Id, + "serverKey1", + "ruleKey1", + "PrimaryMessage1", + dateTimeOffset, + true, + false, + new TextRangeDto(1, 2, 3, 4), + null, + null, + "context1", + new StandardModeDetails(IssueSeverity.MAJOR, RuleType.CODE_SMELL)); + var issue2 = new RaisedIssueDto(IssueWithFlowsAndQuickFixesUseCase.Issue2Id, "serverKey2", "ruleKey2", "PrimaryMessage2", - IssueSeverity.CRITICAL, - RuleType.BUG, - CleanCodeAttribute.LOGICAL, - IssueWithFlowsAndQuickFixesUseCase.Issue2Impacts, dateTimeOffset, true, false, new TextRangeDto(61, 62, 63, 64), [IssueWithFlowsAndQuickFixesUseCase.Issue2Flow1, IssueWithFlowsAndQuickFixesUseCase.Issue2Flow2], [IssueWithFlowsAndQuickFixesUseCase.Issue2Fix1, IssueWithFlowsAndQuickFixesUseCase.Issue2Fix2], - "context2"); + "context2", + new MQRModeDetails(CleanCodeAttribute.COMPLETE, IssueWithFlowsAndQuickFixesUseCase.Issue2Impacts)); var result = testSubject.GetAnalysisIssues(new FileUri("C:\\IssueFile.cs"), new List { issue1, issue2 }).ToList(); @@ -104,31 +90,24 @@ public void GetAnalysisIssues_HasIssues_ConvertsCorrectly() public void GetAnalysisIssues_HasHotspot_ConvertsCorrectly() { var dateTimeOffset = DateTimeOffset.Now; - var issue1 = new RaisedHotspotDto(Guid.NewGuid(), + var issue1 = new RaisedHotspotDto(IssueWithFlowsAndQuickFixesUseCase.Issue1Id, "serverKey1", "ruleKey1", - "PrimaryMessage1", - IssueSeverity.MAJOR, - RuleType.CODE_SMELL, - CleanCodeAttribute.EFFICIENT, - [], - dateTimeOffset, - true, - false, - new TextRangeDto(1, 2, 3, 4), - null, - null, - "context1", - VulnerabilityProbability.HIGH, - HotspotStatus.FIXED); - var issue2 = new RaisedHotspotDto(Guid.NewGuid(), + "PrimaryMessage1", + dateTimeOffset, + true, + false, + new TextRangeDto(1, 2, 3, 4), + null, + null, + "context1", + VulnerabilityProbability.HIGH, + HotspotStatus.FIXED, + new StandardModeDetails(IssueSeverity.MAJOR, RuleType.CODE_SMELL)); + var issue2 = new RaisedHotspotDto(IssueWithFlowsAndQuickFixesUseCase.Issue2Id, "serverKey2", "ruleKey2", "PrimaryMessage2", - IssueSeverity.CRITICAL, - RuleType.BUG, - CleanCodeAttribute.LOGICAL, - IssueWithFlowsAndQuickFixesUseCase.Issue2Impacts, dateTimeOffset, true, false, @@ -136,7 +115,8 @@ public void GetAnalysisIssues_HasHotspot_ConvertsCorrectly() [IssueWithFlowsAndQuickFixesUseCase.Issue2Flow1, IssueWithFlowsAndQuickFixesUseCase.Issue2Flow2], [IssueWithFlowsAndQuickFixesUseCase.Issue2Fix1, IssueWithFlowsAndQuickFixesUseCase.Issue2Fix2], "context2", VulnerabilityProbability.HIGH, - HotspotStatus.FIXED); + HotspotStatus.FIXED, + new MQRModeDetails(CleanCodeAttribute.COMPLETE, IssueWithFlowsAndQuickFixesUseCase.Issue2Impacts)); var result = testSubject.GetAnalysisIssues(new FileUri("C:\\IssueFile.cs"), new List { issue1, issue2 }).ToList(); @@ -155,17 +135,14 @@ public void GetAnalysisIssues_IssueHasUnflattenedFlows_FlattensIntoSingleFlow() default, default, default, - default, - default, - default, - default, new TextRangeDto(1, 2, 3, 4), UnflattenedFlowsUseCase.UnflattenedFlows, default, - default) + default, + new StandardModeDetails(default, default)) }); UnflattenedFlowsUseCase.VerifyFlattenedFlow(analysisIssues); @@ -183,10 +160,6 @@ public void GetAnalysisIssues_HotspotHasUnflattenedFlows_FlattensIntoSingleFlow( default, default, default, - default, - default, - default, - default, new TextRangeDto(1, 2, 3, @@ -195,7 +168,8 @@ public void GetAnalysisIssues_HotspotHasUnflattenedFlows_FlattensIntoSingleFlow( default, default, VulnerabilityProbability.HIGH, - HotspotStatus.SAFE) + HotspotStatus.SAFE, + new StandardModeDetails(default, default)) }); UnflattenedFlowsUseCase.VerifyFlattenedFlow(analysisIssues); @@ -205,15 +179,13 @@ public void GetAnalysisIssues_HotspotHasUnflattenedFlows_FlattensIntoSingleFlow( [DataRow(VulnerabilityProbability.HIGH, HotspotPriority.High)] [DataRow(VulnerabilityProbability.MEDIUM, HotspotPriority.Medium)] [DataRow(VulnerabilityProbability.LOW, HotspotPriority.Low)] - public void GetAnalysisIssues_HotspotHasVulnerabilityProbability_AnalysisHotspotIssueIsCreatedWithCorrectHotspotPriority(VulnerabilityProbability vulnerabilityProbability, HotspotPriority expectedHotspotPriority) + public void GetAnalysisIssues_HotspotHasVulnerabilityProbability_AnalysisHotspotIssueIsCreatedWithCorrectHotspotPriority( + VulnerabilityProbability vulnerabilityProbability, + HotspotPriority expectedHotspotPriority) { var analysisIssues = testSubject.GetAnalysisIssues(fileUri, new List { - new(Guid.Empty, - default, - default, - default, - default, + new(Guid.Empty, default, default, default, @@ -228,7 +200,8 @@ public void GetAnalysisIssues_HotspotHasVulnerabilityProbability_AnalysisHotspot default, default, vulnerabilityProbability, - HotspotStatus.SAFE) + HotspotStatus.SAFE, + new StandardModeDetails(default, default)) }); analysisIssues.Single().Should().BeOfType().Which.HotspotPriority.Should().Be(expectedHotspotPriority); @@ -244,6 +217,34 @@ public void GetAnalysisIssues_HotspotHasNoVulnerabilityProbability_AnalysisHotsp default, default, default, + default, + default, + new TextRangeDto(1, + 2, + 3, + 4), + [], + default, + default, + null, + HotspotStatus.SAFE, + new StandardModeDetails(default, default)) + }); + + analysisIssues.Single().Should().BeOfType().Which.HotspotPriority.Should().BeNull(); + } + + [TestMethod] + [DataRow(ImpactSeverity.BLOCKER, SoftwareQualitySeverity.Blocker)] + [DataRow(ImpactSeverity.HIGH, SoftwareQualitySeverity.High)] + [DataRow(ImpactSeverity.MEDIUM, SoftwareQualitySeverity.Medium)] + [DataRow(ImpactSeverity.LOW, SoftwareQualitySeverity.Low)] + [DataRow(ImpactSeverity.INFO, SoftwareQualitySeverity.Info)] + public void GetAnalysisIssues_HotspotWithTwoHighImpactsForDifferentQualities_GetsTheHighestSoftwareQuality(ImpactSeverity severity, SoftwareQualitySeverity expectedSoftwareQualitySeverity) + { + var analysisIssues = testSubject.GetAnalysisIssues(fileUri, new List + { + new(Guid.Empty, default, default, default, @@ -258,39 +259,83 @@ public void GetAnalysisIssues_HotspotHasNoVulnerabilityProbability_AnalysisHotsp default, default, null, - HotspotStatus.SAFE) + HotspotStatus.SAFE, + new MQRModeDetails(default, + [ + new ImpactDto(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.INFO), + new ImpactDto(SoftwareQuality.RELIABILITY, severity), + new ImpactDto(SoftwareQuality.SECURITY, severity) + ])) }); - analysisIssues.Single().Should().BeOfType().Which.HotspotPriority.Should().BeNull(); + var hotspotIssue = analysisIssues.SingleOrDefault() as AnalysisHotspotIssue; + hotspotIssue.Should().NotBeNull(); + hotspotIssue.HighestImpact.Should().NotBeNull(); + hotspotIssue.HighestImpact.Quality.Should().Be(VisualStudio.Core.Analysis.SoftwareQuality.Security); + hotspotIssue.HighestImpact.Severity.Should().Be(expectedSoftwareQualitySeverity); + } + + [TestMethod] + [DataRow(ImpactSeverity.BLOCKER, SoftwareQualitySeverity.Blocker)] + [DataRow(ImpactSeverity.HIGH, SoftwareQualitySeverity.High)] + [DataRow(ImpactSeverity.MEDIUM, SoftwareQualitySeverity.Medium)] + [DataRow(ImpactSeverity.LOW, SoftwareQualitySeverity.Low)] + [DataRow(ImpactSeverity.INFO, SoftwareQualitySeverity.Info)] + public void GetAnalysisIssues_IssueWithTwoHighImpactsForDifferentQualities_GetsTheHighestSoftwareQuality(ImpactSeverity severity, SoftwareQualitySeverity expectedSoftwareQualitySeverity) + { + var analysisIssues = testSubject.GetAnalysisIssues(fileUri, new List + { + new(Guid.Empty, + default, + default, + default, + default, + default, + default, + new TextRangeDto(1, + 2, + 3, + 4), + [], + default, + default, + new MQRModeDetails(default, + [ + new ImpactDto(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.INFO), + new ImpactDto(SoftwareQuality.RELIABILITY, severity), + new ImpactDto(SoftwareQuality.SECURITY, severity) + ])) + }); + + var issue = analysisIssues.SingleOrDefault() as AnalysisIssue; + issue.Should().NotBeNull(); + issue.HighestImpact.Should().NotBeNull(); + issue.HighestImpact.Quality.Should().Be(VisualStudio.Core.Analysis.SoftwareQuality.Security); + issue.HighestImpact.Severity.Should().Be(expectedSoftwareQualitySeverity); } private static class UnflattenedFlowsUseCase { internal static FileUri FileUri => new("C:\\IssueFile.cs"); - internal static List UnflattenedFlows => [ - new IssueFlowDto([new IssueLocationDto(new TextRangeDto(1, 1, 1, 1), "1", FileUri)]), - new IssueFlowDto([new IssueLocationDto(new TextRangeDto(2, 2, 2, 2), "2", FileUri)]), - new IssueFlowDto([new IssueLocationDto(new TextRangeDto(3, 3, 3, 3), "3", FileUri)]), - new IssueFlowDto([new IssueLocationDto(new TextRangeDto(4, 4, 4, 4), "4", FileUri)]), - new IssueFlowDto([new IssueLocationDto(new TextRangeDto(5, 5, 5, 5), "5", FileUri)]), + internal static List UnflattenedFlows => + [ + new([new IssueLocationDto(new TextRangeDto(1, 1, 1, 1), "1", FileUri)]), + new([new IssueLocationDto(new TextRangeDto(2, 2, 2, 2), "2", FileUri)]), + new([new IssueLocationDto(new TextRangeDto(3, 3, 3, 3), "3", FileUri)]), + new([new IssueLocationDto(new TextRangeDto(4, 4, 4, 4), "4", FileUri)]), + new([new IssueLocationDto(new TextRangeDto(5, 5, 5, 5), "5", FileUri)]) ]; - internal static void VerifyFlattenedFlow(IEnumerable analysisIssues) - { + internal static void VerifyFlattenedFlow(IEnumerable analysisIssues) => analysisIssues.Single() .Flows.Should() .ContainSingle() .Which.Locations.Should() .HaveCount(5) - .And.BeEquivalentTo([ - new AnalysisIssueLocation("1", FileUri.LocalPath, new TextRange(1, 1, 1, 1, null)), - new AnalysisIssueLocation("2", FileUri.LocalPath, new TextRange(2, 2, 2, 2, null)), - new AnalysisIssueLocation("3", FileUri.LocalPath, new TextRange(3, 3, 3, 3, null)), - new AnalysisIssueLocation("4", FileUri.LocalPath, new TextRange(4, 4, 4, 4, null)), - new AnalysisIssueLocation("5", FileUri.LocalPath, new TextRange(5, 5, 5, 5, null)), - ]); - } + .And.BeEquivalentTo(new AnalysisIssueLocation("1", FileUri.LocalPath, new TextRange(1, 1, 1, 1, null)), + new AnalysisIssueLocation("2", FileUri.LocalPath, new TextRange(2, 2, 2, 2, null)), new AnalysisIssueLocation("3", FileUri.LocalPath, new TextRange(3, 3, 3, 3, null)), + new AnalysisIssueLocation("4", FileUri.LocalPath, new TextRange(4, 4, 4, 4, null)), new AnalysisIssueLocation("5", FileUri.LocalPath, new TextRange(5, 5, 5, 5, null))); } private static class IssueWithFlowsAndQuickFixesUseCase @@ -301,7 +346,7 @@ private static class IssueWithFlowsAndQuickFixesUseCase internal static List Issue2Impacts => [Issue2Impact1, Issue2Impact2, Issue2Impact3]; private static IssueLocationDto Issue2Flow1Location1 => new(new TextRangeDto(11, 12, 13, 14), "Flow1Location1Message", new FileUri("C:\\flowFile1.cs")); private static IssueLocationDto Issue2Flow1Location2 => new(new TextRangeDto(21, 22, 23, 24), "Flow1Location2Message", new FileUri("C:\\flowFile1.cs")); - internal static IssueFlowDto Issue2Flow1 => new ([Issue2Flow1Location1, Issue2Flow1Location2]); + internal static IssueFlowDto Issue2Flow1 => new([Issue2Flow1Location1, Issue2Flow1Location2]); private static IssueLocationDto Issue2Flow2Location1 => new(new TextRangeDto(31, 32, 33, 34), "Flow2Location1Message", new FileUri("C:\\flowFile2.cs")); private static IssueLocationDto Issue2Flow2Location2 => new(new TextRangeDto(41, 42, 43, 44), "Flow2Location2Message", new FileUri("C:\\flowFile2.cs")); internal static IssueFlowDto Issue2Flow2 => new([Issue2Flow2Location1, Issue2Flow2Location2]); @@ -310,17 +355,19 @@ private static class IssueWithFlowsAndQuickFixesUseCase private static TextEditDto Issue2Fix2FileEdit1Textedit1 => new(new TextRangeDto(51, 52, 53, 54), "new text"); private static FileEditDto Issue2Fix2FileEdit1 => new(new FileUri("C:\\IssueFile.cs"), [Issue2Fix2FileEdit1Textedit1]); internal static QuickFixDto Issue2Fix2 => new([Issue2Fix2FileEdit1], "issue 2 fix 2"); - + internal static Guid Issue1Id { get; } = Guid.NewGuid(); + internal static Guid Issue2Id { get; } = Guid.NewGuid(); internal static void VerifyDtosConvertedCorrectly(List result) { result.Should().NotBeNull(); result.Should().HaveCount(2); + result[0].Id.Should().Be(Issue1Id); result[0].RuleKey.Should().Be("ruleKey1"); result[0].Severity.Should().Be(AnalysisIssueSeverity.Major); result[0].Type.Should().Be(AnalysisIssueType.CodeSmell); - result[0].HighestSoftwareQualitySeverity.Should().BeNull(); + result[0].HighestImpact.Should().BeNull(); result[0].RuleDescriptionContextKey.Should().Be("context1"); result[0].PrimaryLocation.FilePath.Should().Be("C:\\IssueFile.cs"); @@ -334,10 +381,11 @@ internal static void VerifyDtosConvertedCorrectly(List result) result[0].Flows.Should().BeEmpty(); result[0].Fixes.Should().BeEmpty(); + result[1].Id.Should().Be(Issue2Id); result[1].RuleKey.Should().Be("ruleKey2"); - result[1].Severity.Should().Be(AnalysisIssueSeverity.Critical); - result[1].Type.Should().Be(AnalysisIssueType.Bug); - result[1].HighestSoftwareQualitySeverity.Should().Be(SoftwareQualitySeverity.High); + result[1].Severity.Should().BeNull(); + result[1].Type.Should().BeNull(); + result[1].HighestImpact.Severity.Should().Be(SoftwareQualitySeverity.High); result[1].RuleDescriptionContextKey.Should().Be("context2"); result[1].PrimaryLocation.FilePath.Should().Be("C:\\IssueFile.cs"); @@ -390,6 +438,5 @@ internal static void VerifyDtosConvertedCorrectly(List result) result[1].Fixes[0].Edits[0].RangeToReplace.EndLineOffset.Should().Be(54); result[1].Fixes[0].Edits[0].RangeToReplace.LineHash.Should().BeNull(); } - } } diff --git a/src/SLCore.UnitTests/Listener/Analysis/RaiseIssuesParamsTests.cs b/src/SLCore.UnitTests/Listener/Analysis/RaiseIssuesParamsTests.cs index 6ff693ff29..798fd3318d 100644 --- a/src/SLCore.UnitTests/Listener/Analysis/RaiseIssuesParamsTests.cs +++ b/src/SLCore.UnitTests/Listener/Analysis/RaiseIssuesParamsTests.cs @@ -22,41 +22,115 @@ using SonarLint.VisualStudio.SLCore.Common.Models; using SonarLint.VisualStudio.SLCore.Listener.Analysis; using SonarLint.VisualStudio.SLCore.Listener.Analysis.Models; +using SonarLint.VisualStudio.SLCore.Service.Rules.Models; namespace SonarLint.VisualStudio.SLCore.UnitTests.Listener.Analysis; [TestClass] public class RaiseIssuesParamsTests { + [TestMethod] + public void RaisedIssueDto_MqrMode_DeserializedCorrectly() + { + var expected = new RaiseFindingParams("SLVS_Bound_VS2019", + new Dictionary> + { + { + new FileUri( + "file:///C:/Users/developer/Documents/Repos/sonarlint-visualstudio-sampleprojects%20AAA%20ЖЖЖЖ/bound/sonarcloud/SLVS_Samples_Bound_VS2019/Secrets/ShouldExclude/Excluded.yml"), + [ + new RaisedIssueDto(Guid.Parse("10bd4422-7d55-402f-889c-e080dbe4c781"), + null, + "secrets:S6336", + "Make sure this Alibaba Cloud Access Key Secret gets revoked, changed, and removed from the code.", + DateTimeOffset.FromUnixTimeMilliseconds(1718182975467), + true, + false, + new TextRangeDto(14, 24, 14, 54), + [], + [], + null, + new MQRModeDetails(CleanCodeAttribute.COMPLETE, [new ImpactDto(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.LOW)])) + ] + } + }, + false, + Guid.Parse("11ec4b5a-8ff6-4211-ab95-8c16eb8c7f0a")); + + var serialized = + """ + { + "configurationScopeId": "SLVS_Bound_VS2019", + "issuesByFileUri": { + "file:///C:/Users/developer/Documents/Repos/sonarlint-visualstudio-sampleprojects%20AAA%20ЖЖЖЖ/bound/sonarcloud/SLVS_Samples_Bound_VS2019/Secrets/ShouldExclude/Excluded.yml": [ + { + "id": "10bd4422-7d55-402f-889c-e080dbe4c781", + "serverKey": null, + "ruleKey": "secrets:S6336", + "primaryMessage": "Make sure this Alibaba Cloud Access Key Secret gets revoked, changed, and removed from the code.", + "severityMode": { + "cleanCodeAttribute": "COMPLETE", + "impacts": [ + { + "softwareQuality": "MAINTAINABILITY", + "impactSeverity": "LOW" + } + ] + }, + "introductionDate": 1718182975467, + "isOnNewCode": true, + "resolved": false, + "textRange": { + "startLine": 14, + "startLineOffset": 24, + "endLine": 14, + "endLineOffset": 54 + }, + "flows": [], + "quickFixes": [], + "ruleDescriptionContextKey": null + } + ] + }, + "isIntermediatePublication": false, + "analysisId": "11ec4b5a-8ff6-4211-ab95-8c16eb8c7f0a" + } + """; + + var deserialized = JsonConvert.DeserializeObject>(serialized); + + deserialized.Should().BeEquivalentTo(expected, options => + options.ComparingByMembers>().ComparingByMembers().ComparingByMembers()); + } [TestMethod] - public void DeserializedCorrectly() + public void RaisedIssueDto_StandardMode_DeserializedCorrectly() { var expected = new RaiseFindingParams("SLVS_Bound_VS2019", new Dictionary> { { - new FileUri("file:///C:/Users/developer/Documents/Repos/sonarlint-visualstudio-sampleprojects%20AAA%20ЖЖЖЖ/bound/sonarcloud/SLVS_Samples_Bound_VS2019/Secrets/ShouldExclude/Excluded.yml"), - [new RaisedIssueDto(Guid.Parse("10bd4422-7d55-402f-889c-e080dbe4c781"), - null, - "secrets:S6336", - "Make sure this Alibaba Cloud Access Key Secret gets revoked, changed, and removed from the code.", - IssueSeverity.BLOCKER, - RuleType.VULNERABILITY, - CleanCodeAttribute.TRUSTWORTHY, - [new ImpactDto(SoftwareQuality.SECURITY, ImpactSeverity.HIGH)], - DateTimeOffset.FromUnixTimeMilliseconds(1718182975467), - true, - false, - new TextRangeDto(14, 24, 14, 54), - [], - [], - null)] + new FileUri( + "file:///C:/Users/developer/Documents/Repos/sonarlint-visualstudio-sampleprojects%20AAA%20ЖЖЖЖ/bound/sonarcloud/SLVS_Samples_Bound_VS2019/Secrets/ShouldExclude/Excluded.yml"), + [ + new RaisedIssueDto(Guid.Parse("10bd4422-7d55-402f-889c-e080dbe4c781"), + null, + "secrets:S6336", + "Make sure this Alibaba Cloud Access Key Secret gets revoked, changed, and removed from the code.", + DateTimeOffset.FromUnixTimeMilliseconds(1718182975467), + true, + false, + new TextRangeDto(14, 24, 14, 54), + [], + [], + null, + new StandardModeDetails(IssueSeverity.BLOCKER, RuleType.BUG)) + ] } }, false, Guid.Parse("11ec4b5a-8ff6-4211-ab95-8c16eb8c7f0a")); - + var serialized = """ { @@ -68,15 +142,10 @@ [new ImpactDto(SoftwareQuality.SECURITY, ImpactSeverity.HIGH)], "serverKey": null, "ruleKey": "secrets:S6336", "primaryMessage": "Make sure this Alibaba Cloud Access Key Secret gets revoked, changed, and removed from the code.", - "severity": "BLOCKER", - "type": "VULNERABILITY", - "cleanCodeAttribute": "TRUSTWORTHY", - "impacts": [ - { - "softwareQuality": "SECURITY", - "impactSeverity": "HIGH" - } - ], + "severityMode": { + "severity": "BLOCKER", + "type": "BUG" + }, "introductionDate": 1718182975467, "isOnNewCode": true, "resolved": false, @@ -98,36 +167,36 @@ [new ImpactDto(SoftwareQuality.SECURITY, ImpactSeverity.HIGH)], """; var deserialized = JsonConvert.DeserializeObject>(serialized); - - deserialized.Should().BeEquivalentTo(expected, options => - options.ComparingByMembers>().ComparingByMembers()); + + deserialized.Should().BeEquivalentTo(expected, options => + options.ComparingByMembers>().ComparingByMembers().ComparingByMembers()); } [TestMethod] - public void RaiseHotspotParams_DeserializedCorrectly() + public void RaiseHotspotParams_MqrMode_DeserializedCorrectly() { var expected = new RaiseHotspotParams("SLVS_Bound_VS2019", new Dictionary> { { - new FileUri("file:///C:/Users/developer/Documents/Repos/sonarlint-visualstudio-sampleprojects%20AAA%20ЖЖЖЖ/bound/sonarcloud/SLVS_Samples_Bound_VS2019/Secrets/ShouldExclude/Excluded.yml"), - [new RaisedHotspotDto(Guid.Parse("10bd4422-7d55-402f-889c-e080dbe4c781"), - null, - "secrets:S6336", - "Make sure this Alibaba Cloud Access Key Secret gets revoked, changed, and removed from the code.", - IssueSeverity.BLOCKER, - RuleType.VULNERABILITY, - CleanCodeAttribute.TRUSTWORTHY, - [new ImpactDto(SoftwareQuality.SECURITY, ImpactSeverity.HIGH)], - DateTimeOffset.FromUnixTimeMilliseconds(1718182975467), - true, - false, - new TextRangeDto(14, 24, 14, 54), - [], - [], - null, - VulnerabilityProbability.HIGH, - HotspotStatus.TO_REVIEW)] + new FileUri( + "file:///C:/Users/developer/Documents/Repos/sonarlint-visualstudio-sampleprojects%20AAA%20ЖЖЖЖ/bound/sonarcloud/SLVS_Samples_Bound_VS2019/Secrets/ShouldExclude/Excluded.yml"), + [ + new RaisedHotspotDto(Guid.Parse("10bd4422-7d55-402f-889c-e080dbe4c781"), + null, + "secrets:S6336", + "Make sure this Alibaba Cloud Access Key Secret gets revoked, changed, and removed from the code.", + DateTimeOffset.FromUnixTimeMilliseconds(1718182975467), + true, + false, + new TextRangeDto(14, 24, 14, 54), + [], + [], + null, + VulnerabilityProbability.HIGH, + HotspotStatus.TO_REVIEW, + new MQRModeDetails(CleanCodeAttribute.CLEAR, [new ImpactDto(SoftwareQuality.SECURITY, ImpactSeverity.HIGH)])) + ] } }, false, @@ -144,8 +213,165 @@ [new ImpactDto(SoftwareQuality.SECURITY, ImpactSeverity.HIGH)], "serverKey": null, "ruleKey": "secrets:S6336", "primaryMessage": "Make sure this Alibaba Cloud Access Key Secret gets revoked, changed, and removed from the code.", - "severity": "BLOCKER", - "type": "VULNERABILITY", + "severityMode": { + "cleanCodeAttribute": "CLEAR", + "impacts": [ + { + "softwareQuality": "SECURITY", + "impactSeverity": "HIGH" + } + ] + }, + "introductionDate": 1718182975467, + "isOnNewCode": true, + "resolved": false, + "textRange": { + "startLine": 14, + "startLineOffset": 24, + "endLine": 14, + "endLineOffset": 54 + }, + "flows": [], + "quickFixes": [], + "ruleDescriptionContextKey": null, + "vulnerabilityProbability": "HIGH", + "status": "TO_REVIEW" + } + ] + }, + "isIntermediatePublication": false, + "analysisId": "11ec4b5a-8ff6-4211-ab95-8c16eb8c7f0a" + } + """; + + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.Should().BeEquivalentTo(expected, options => + options.ComparingByMembers().ComparingByMembers().ComparingByMembers()); + } + + [TestMethod] + public void RaiseHotspotParams_StandardMode_DeserializedCorrectly() + { + var expected = new RaiseHotspotParams("SLVS_Bound_VS2019", + new Dictionary> + { + { + new FileUri( + "file:///C:/Users/developer/Documents/Repos/sonarlint-visualstudio-sampleprojects%20AAA%20ЖЖЖЖ/bound/sonarcloud/SLVS_Samples_Bound_VS2019/Secrets/ShouldExclude/Excluded.yml"), + [ + new RaisedHotspotDto(Guid.Parse("10bd4422-7d55-402f-889c-e080dbe4c781"), + null, + "secrets:S6336", + "Make sure this Alibaba Cloud Access Key Secret gets revoked, changed, and removed from the code.", + DateTimeOffset.FromUnixTimeMilliseconds(1718182975467), + true, + false, + new TextRangeDto(14, 24, 14, 54), + [], + [], + null, + VulnerabilityProbability.HIGH, + HotspotStatus.TO_REVIEW, + new StandardModeDetails(IssueSeverity.MINOR, RuleType.VULNERABILITY)) + ] + } + }, + false, + Guid.Parse("11ec4b5a-8ff6-4211-ab95-8c16eb8c7f0a")); + + var serialized = + """ + { + "configurationScopeId": "SLVS_Bound_VS2019", + "hotspotsByFileUri": { + "file:///C:/Users/developer/Documents/Repos/sonarlint-visualstudio-sampleprojects%20AAA%20ЖЖЖЖ/bound/sonarcloud/SLVS_Samples_Bound_VS2019/Secrets/ShouldExclude/Excluded.yml": [ + { + "id": "10bd4422-7d55-402f-889c-e080dbe4c781", + "serverKey": null, + "ruleKey": "secrets:S6336", + "primaryMessage": "Make sure this Alibaba Cloud Access Key Secret gets revoked, changed, and removed from the code.", + "severityMode": { + "severity": "MINOR", + "type": "VULNERABILITY" + }, + "introductionDate": 1718182975467, + "isOnNewCode": true, + "resolved": false, + "textRange": { + "startLine": 14, + "startLineOffset": 24, + "endLine": 14, + "endLineOffset": 54 + }, + "flows": [], + "quickFixes": [], + "ruleDescriptionContextKey": null, + "vulnerabilityProbability": "HIGH", + "status": "TO_REVIEW" + } + ] + }, + "isIntermediatePublication": false, + "analysisId": "11ec4b5a-8ff6-4211-ab95-8c16eb8c7f0a" + } + """; + + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.Should().BeEquivalentTo(expected, options => + options.ComparingByMembers().ComparingByMembers().ComparingByMembers()); + } + + /// + /// The fields severity, type are still set by SlCore, but they are deprecated and should be ignored. + /// Instead, the values from of should be used + /// + [TestMethod] + public void RaisedIssueDto_StandardMode_IgnoresDeprecatedFields() + { + var expected = new RaiseFindingParams("SLVS_Bound_VS2019", + new Dictionary> + { + { + new FileUri( + "file:///C:/Users/developer/Documents/Repos/sonarlint-visualstudio-sampleprojects%20AAA%20ЖЖЖЖ/bound/sonarcloud/SLVS_Samples_Bound_VS2019/Secrets/ShouldExclude/Excluded.yml"), + [ + new RaisedIssueDto(Guid.Parse("10bd4422-7d55-402f-889c-e080dbe4c781"), + null, + "secrets:S6336", + "Make sure this Alibaba Cloud Access Key Secret gets revoked, changed, and removed from the code.", + DateTimeOffset.FromUnixTimeMilliseconds(1718182975467), + true, + false, + new TextRangeDto(14, 24, 14, 54), + [], + [], + null, + new StandardModeDetails(IssueSeverity.BLOCKER, RuleType.BUG)) + ] + } + }, + false, + Guid.Parse("11ec4b5a-8ff6-4211-ab95-8c16eb8c7f0a")); + + var serialized = + """ + { + "configurationScopeId": "SLVS_Bound_VS2019", + "issuesByFileUri": { + "file:///C:/Users/developer/Documents/Repos/sonarlint-visualstudio-sampleprojects%20AAA%20ЖЖЖЖ/bound/sonarcloud/SLVS_Samples_Bound_VS2019/Secrets/ShouldExclude/Excluded.yml": [ + { + "id": "10bd4422-7d55-402f-889c-e080dbe4c781", + "serverKey": null, + "ruleKey": "secrets:S6336", + "primaryMessage": "Make sure this Alibaba Cloud Access Key Secret gets revoked, changed, and removed from the code.", + "severityMode": { + "severity": "BLOCKER", + "type": "BUG" + }, + "severity": "MINOR", + "type": "CODE_SMELL", "cleanCodeAttribute": "TRUSTWORTHY", "impacts": [ { @@ -164,6 +390,268 @@ [new ImpactDto(SoftwareQuality.SECURITY, ImpactSeverity.HIGH)], }, "flows": [], "quickFixes": [], + "ruleDescriptionContextKey": null + } + ] + }, + "isIntermediatePublication": false, + "analysisId": "11ec4b5a-8ff6-4211-ab95-8c16eb8c7f0a" + } + """; + + var deserialized = JsonConvert.DeserializeObject>(serialized); + + deserialized.Should().BeEquivalentTo(expected, options => + options.ComparingByMembers>().ComparingByMembers().ComparingByMembers()); + } + + /// + /// The fields cleanCodeAttribute, impacts are still set by SlCore, but they are deprecated and should be ignored. + /// Instead, the values from of should be used + /// + [TestMethod] + public void RaisedIssueDto_MqrMode_IgnoresDeprecatedFields() + { + var expected = new RaiseFindingParams("SLVS_Bound_VS2019", + new Dictionary> + { + { + new FileUri( + "file:///C:/Users/developer/Documents/Repos/sonarlint-visualstudio-sampleprojects%20AAA%20ЖЖЖЖ/bound/sonarcloud/SLVS_Samples_Bound_VS2019/Secrets/ShouldExclude/Excluded.yml"), + [ + new RaisedIssueDto(Guid.Parse("10bd4422-7d55-402f-889c-e080dbe4c781"), + null, + "secrets:S6336", + "Make sure this Alibaba Cloud Access Key Secret gets revoked, changed, and removed from the code.", + DateTimeOffset.FromUnixTimeMilliseconds(1718182975467), + true, + false, + new TextRangeDto(14, 24, 14, 54), + [], + [], + null, + new MQRModeDetails(CleanCodeAttribute.COMPLETE, [new ImpactDto(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.LOW)])) + ] + } + }, + false, + Guid.Parse("11ec4b5a-8ff6-4211-ab95-8c16eb8c7f0a")); + + var serialized = + """ + { + "configurationScopeId": "SLVS_Bound_VS2019", + "issuesByFileUri": { + "file:///C:/Users/developer/Documents/Repos/sonarlint-visualstudio-sampleprojects%20AAA%20ЖЖЖЖ/bound/sonarcloud/SLVS_Samples_Bound_VS2019/Secrets/ShouldExclude/Excluded.yml": [ + { + "id": "10bd4422-7d55-402f-889c-e080dbe4c781", + "serverKey": null, + "ruleKey": "secrets:S6336", + "primaryMessage": "Make sure this Alibaba Cloud Access Key Secret gets revoked, changed, and removed from the code.", + "severityMode": { + "cleanCodeAttribute": "COMPLETE", + "impacts": [ + { + "softwareQuality": "MAINTAINABILITY", + "impactSeverity": "LOW" + } + ] + }, + "severity": "MINOR", + "type": "CODE_SMELL", + "cleanCodeAttribute": "TRUSTWORTHY", + "impacts": [ + { + "softwareQuality": "SECURITY", + "impactSeverity": "HIGH" + } + ], + "introductionDate": 1718182975467, + "isOnNewCode": true, + "resolved": false, + "textRange": { + "startLine": 14, + "startLineOffset": 24, + "endLine": 14, + "endLineOffset": 54 + }, + "flows": [], + "quickFixes": [], + "ruleDescriptionContextKey": null + } + ] + }, + "isIntermediatePublication": false, + "analysisId": "11ec4b5a-8ff6-4211-ab95-8c16eb8c7f0a" + } + """; + + var deserialized = JsonConvert.DeserializeObject>(serialized); + + deserialized.Should().BeEquivalentTo(expected, options => + options.ComparingByMembers>().ComparingByMembers().ComparingByMembers()); + } + + /// + /// The fields cleanCodeAttribute, impacts are still set by SlCore, but they are deprecated and should be ignored. + /// Instead, the values from of should be used + /// + [TestMethod] + public void RaiseHotspotParams_MqrMode_IgnoresDeprecatedFields() + { + var expected = new RaiseHotspotParams("SLVS_Bound_VS2019", + new Dictionary> + { + { + new FileUri( + "file:///C:/Users/developer/Documents/Repos/sonarlint-visualstudio-sampleprojects%20AAA%20ЖЖЖЖ/bound/sonarcloud/SLVS_Samples_Bound_VS2019/Secrets/ShouldExclude/Excluded.yml"), + [ + new RaisedHotspotDto(Guid.Parse("10bd4422-7d55-402f-889c-e080dbe4c781"), + null, + "secrets:S6336", + "Make sure this Alibaba Cloud Access Key Secret gets revoked, changed, and removed from the code.", + DateTimeOffset.FromUnixTimeMilliseconds(1718182975467), + true, + false, + new TextRangeDto(14, 24, 14, 54), + [], + [], + null, + VulnerabilityProbability.HIGH, + HotspotStatus.TO_REVIEW, + new MQRModeDetails(CleanCodeAttribute.CLEAR, [new ImpactDto(SoftwareQuality.SECURITY, ImpactSeverity.HIGH)])) + ] + } + }, + false, + Guid.Parse("11ec4b5a-8ff6-4211-ab95-8c16eb8c7f0a")); + + var serialized = + """ + { + "configurationScopeId": "SLVS_Bound_VS2019", + "hotspotsByFileUri": { + "file:///C:/Users/developer/Documents/Repos/sonarlint-visualstudio-sampleprojects%20AAA%20ЖЖЖЖ/bound/sonarcloud/SLVS_Samples_Bound_VS2019/Secrets/ShouldExclude/Excluded.yml": [ + { + "id": "10bd4422-7d55-402f-889c-e080dbe4c781", + "serverKey": null, + "ruleKey": "secrets:S6336", + "primaryMessage": "Make sure this Alibaba Cloud Access Key Secret gets revoked, changed, and removed from the code.", + "severityMode": { + "cleanCodeAttribute": "CLEAR", + "impacts": [ + { + "softwareQuality": "SECURITY", + "impactSeverity": "HIGH" + } + ] + }, + "severity": "MINOR", + "type": "CODE_SMELL", + "cleanCodeAttribute": "TRUSTWORTHY", + "impacts": [ + { + "softwareQuality": "RELIABILITY", + "impactSeverity": "LOW" + } + ], + "introductionDate": 1718182975467, + "isOnNewCode": true, + "resolved": false, + "textRange": { + "startLine": 14, + "startLineOffset": 24, + "endLine": 14, + "endLineOffset": 54 + }, + "flows": [], + "quickFixes": [], + "ruleDescriptionContextKey": null, + "vulnerabilityProbability": "HIGH", + "status": "TO_REVIEW" + } + ] + }, + "isIntermediatePublication": false, + "analysisId": "11ec4b5a-8ff6-4211-ab95-8c16eb8c7f0a" + } + """; + + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.Should().BeEquivalentTo(expected, options => + options.ComparingByMembers().ComparingByMembers().ComparingByMembers()); + } + + /// + /// The fields severity, type are still set by SlCore, but they are deprecated and should be ignored. + /// Instead, the values from of should be used + /// + [TestMethod] + public void RaiseHotspotParams_StandardMode_IgnoresDeprecatedFields() + { + var expected = new RaiseHotspotParams("SLVS_Bound_VS2019", + new Dictionary> + { + { + new FileUri( + "file:///C:/Users/developer/Documents/Repos/sonarlint-visualstudio-sampleprojects%20AAA%20ЖЖЖЖ/bound/sonarcloud/SLVS_Samples_Bound_VS2019/Secrets/ShouldExclude/Excluded.yml"), + [ + new RaisedHotspotDto(Guid.Parse("10bd4422-7d55-402f-889c-e080dbe4c781"), + null, + "secrets:S6336", + "Make sure this Alibaba Cloud Access Key Secret gets revoked, changed, and removed from the code.", + DateTimeOffset.FromUnixTimeMilliseconds(1718182975467), + true, + false, + new TextRangeDto(14, 24, 14, 54), + [], + [], + null, + VulnerabilityProbability.HIGH, + HotspotStatus.TO_REVIEW, + new StandardModeDetails(IssueSeverity.MINOR, RuleType.VULNERABILITY)) + ] + } + }, + false, + Guid.Parse("11ec4b5a-8ff6-4211-ab95-8c16eb8c7f0a")); + + var serialized = + """ + { + "configurationScopeId": "SLVS_Bound_VS2019", + "hotspotsByFileUri": { + "file:///C:/Users/developer/Documents/Repos/sonarlint-visualstudio-sampleprojects%20AAA%20ЖЖЖЖ/bound/sonarcloud/SLVS_Samples_Bound_VS2019/Secrets/ShouldExclude/Excluded.yml": [ + { + "id": "10bd4422-7d55-402f-889c-e080dbe4c781", + "serverKey": null, + "ruleKey": "secrets:S6336", + "primaryMessage": "Make sure this Alibaba Cloud Access Key Secret gets revoked, changed, and removed from the code.", + "severityMode": { + "severity": "MINOR", + "type": "VULNERABILITY" + }, + "severity": "INFO", + "type": "SECURITY_HOTSPOT", + "cleanCodeAttribute": "TRUSTWORTHY", + "impacts": [ + { + "softwareQuality": "RELIABILITY", + "impactSeverity": "LOW" + } + ], + "introductionDate": 1718182975467, + "isOnNewCode": true, + "resolved": false, + "textRange": { + "startLine": 14, + "startLineOffset": 24, + "endLine": 14, + "endLineOffset": 54 + }, + "flows": [], + "quickFixes": [], "ruleDescriptionContextKey": null, "vulnerabilityProbability": "HIGH", "status": "TO_REVIEW" @@ -178,6 +666,6 @@ [new ImpactDto(SoftwareQuality.SECURITY, ImpactSeverity.HIGH)], var deserialized = JsonConvert.DeserializeObject(serialized); deserialized.Should().BeEquivalentTo(expected, options => - options.ComparingByMembers().ComparingByMembers()); + options.ComparingByMembers().ComparingByMembers().ComparingByMembers()); } } diff --git a/src/SLCore.UnitTests/Listener/Taint/DidChangeTaintVulnerabilitiesParamsTests.cs b/src/SLCore.UnitTests/Listener/Taint/DidChangeTaintVulnerabilitiesParamsTests.cs new file mode 100644 index 0000000000..8c3636e282 --- /dev/null +++ b/src/SLCore.UnitTests/Listener/Taint/DidChangeTaintVulnerabilitiesParamsTests.cs @@ -0,0 +1,177 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Newtonsoft.Json; +using SonarLint.VisualStudio.SLCore.Listener.Taint; + +namespace SonarLint.VisualStudio.SLCore.UnitTests.Listener.Taint; + +[TestClass] +public class DidChangeTaintVulnerabilitiesParamsTests +{ + [TestMethod] + public void Deserialized_AsExpected_SmokeTest() + { + const string serialized = + """ + { + "configurationScopeId": "SLVS_Bound_VS2019", + "closedTaintVulnerabilityIds": [ "62294585-d219-4d07-8e40-6d28d2f2f90e", "62294585-d219-4d07-8e40-6d28d2f2f90e", "62294585-d219-4d07-8e40-6d28d2f2f90e" ], + "addedTaintVulnerabilities": [ + { + "id": "62294585-d219-4d07-8e40-6d28d2f2f90e", + "sonarServerKey": "AXgSTUbU007Zyo8hMhUK", + "resolved": false, + "ruleKey": "roslyn.sonaranalyzer.security.cs:S2091", + "message": "Change this code to not construct this XPath expression from user-controlled data.", + "ideFilePath": "sonarlint-visualstudio-sampleprojects\\bound\\sonarcloud\\SLVS_Samples_Bound_VS2019\\Taint_CSharp_NetCore_WebAppReact\\Taint\\XPathInjectionController.cs", + "introductionDate": 1615214736000, + "severityMode": { + "cleanCodeAttribute": "COMPLETE", + "impacts": [ + { + "softwareQuality": "SECURITY", + "impactSeverity": "HIGH" + } + ] + }, + "severity": "BLOCKER", + "type": "VULNERABILITY", + "flows": [ + { + "locations": [] + }, + { + "locations": [] + } + ], + "textRange": { + "startLine": 23, + "startLineOffset": 27, + "endLine": 23, + "endLineOffset": 59, + "hash": "4fdeebd4a19fd4b1c4c5b9b43ea9f71e" + }, + "ruleDescriptionContextKey": null, + "cleanCodeAttribute": "COMPLETE", + "impacts": { + "SECURITY": "HIGH" + }, + "isOnNewCode": false + }, + { + "id": "62294585-d219-4d07-8e40-6d28d2f2f90e", + "sonarServerKey": "AXgSTUbU007Zyo8hMhUK", + "resolved": false, + "ruleKey": "roslyn.sonaranalyzer.security.cs:S2091", + "message": "Change this code to not construct this XPath expression from user-controlled data.", + "ideFilePath": "sonarlint-visualstudio-sampleprojects\\bound\\sonarcloud\\SLVS_Samples_Bound_VS2019\\Taint_CSharp_NetCore_WebAppReact\\Taint\\XPathInjectionController.cs", + "introductionDate": 1615214736000, + "severityMode": { + "cleanCodeAttribute": "COMPLETE", + "impacts": [ + { + "softwareQuality": "SECURITY", + "impactSeverity": "HIGH" + } + ] + }, + "severity": "BLOCKER", + "type": "VULNERABILITY", + "flows": [ + { + "locations": [] + }, + { + "locations": [] + } + ], + "textRange": { + "startLine": 23, + "startLineOffset": 27, + "endLine": 23, + "endLineOffset": 59, + "hash": "4fdeebd4a19fd4b1c4c5b9b43ea9f71e" + }, + "ruleDescriptionContextKey": null, + "cleanCodeAttribute": "COMPLETE", + "impacts": { + "SECURITY": "HIGH" + }, + "isOnNewCode": false + } + ], + "updatedTaintVulnerabilities": [ + { + "id": "62294585-d219-4d07-8e40-6d28d2f2f90e", + "sonarServerKey": "AXgSTUbU007Zyo8hMhUK", + "resolved": false, + "ruleKey": "roslyn.sonaranalyzer.security.cs:S2091", + "message": "Change this code to not construct this XPath expression from user-controlled data.", + "ideFilePath": "sonarlint-visualstudio-sampleprojects\\bound\\sonarcloud\\SLVS_Samples_Bound_VS2019\\Taint_CSharp_NetCore_WebAppReact\\Taint\\XPathInjectionController.cs", + "introductionDate": 1615214736000, + "severityMode": { + "cleanCodeAttribute": "COMPLETE", + "impacts": [ + { + "softwareQuality": "SECURITY", + "impactSeverity": "HIGH" + } + ] + }, + "severity": "BLOCKER", + "type": "VULNERABILITY", + "flows": [ + { + "locations": [] + }, + { + "locations": [] + } + ], + "textRange": { + "startLine": 23, + "startLineOffset": 27, + "endLine": 23, + "endLineOffset": 59, + "hash": "4fdeebd4a19fd4b1c4c5b9b43ea9f71e" + }, + "ruleDescriptionContextKey": null, + "cleanCodeAttribute": "COMPLETE", + "impacts": { + "SECURITY": "HIGH" + }, + "isOnNewCode": false + } + ] + } + """; + + var actual = JsonConvert.DeserializeObject(serialized); + + actual.configurationScopeId.Should().Be("SLVS_Bound_VS2019"); + actual.closedTaintVulnerabilityIds.Should().HaveCount(3); + actual.closedTaintVulnerabilityIds.Should().NotContain(Guid.Empty); + actual.addedTaintVulnerabilities.Should().HaveCount(2); + actual.addedTaintVulnerabilities.Should().NotContainNulls(); + actual.updatedTaintVulnerabilities.Should().HaveCount(1); + actual.updatedTaintVulnerabilities.Should().NotContainNulls(); + } +} diff --git a/src/SLCore.UnitTests/Protocol/EitherJsonConverterTests.cs b/src/SLCore.UnitTests/Protocol/EitherJsonConverterTests.cs index d375a93e46..7ecf2bd9e1 100644 --- a/src/SLCore.UnitTests/Protocol/EitherJsonConverterTests.cs +++ b/src/SLCore.UnitTests/Protocol/EitherJsonConverterTests.cs @@ -37,7 +37,7 @@ public void CanConvert_TypeHasToMatch(Type typeToCheck, bool isSupported) testSubject.CanConvert(typeToCheck).Should().Be(isSupported); } - + [TestMethod] public void SerializeObject_SerializesEitherAsSingleObject() { @@ -64,7 +64,7 @@ public void DeserializeObject_PrimitiveNotAnObject_Throws() var str = """ { "Property" : "ThisIsExpectedToBeAnObjectButItIsAString" - } + } """; Action act = () => JsonConvert.DeserializeObject(str); @@ -78,7 +78,7 @@ public void DeserializeObject_CollectionNotAnObject_Throws() var str = """ { "Property" : [1, 2, 3] - } + } """; Action act = () => JsonConvert.DeserializeObject(str); diff --git a/src/SonarQube.Client/Api/V10_2/GetTaintVulnerabilitiesWithCCTRequest.cs b/src/SLCore.UnitTests/Protocol/EitherTests.cs similarity index 53% rename from src/SonarQube.Client/Api/V10_2/GetTaintVulnerabilitiesWithCCTRequest.cs rename to src/SLCore.UnitTests/Protocol/EitherTests.cs index 97d049b14f..6244e50e0a 100644 --- a/src/SonarQube.Client/Api/V10_2/GetTaintVulnerabilitiesWithCCTRequest.cs +++ b/src/SLCore.UnitTests/Protocol/EitherTests.cs @@ -18,21 +18,35 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using SonarQube.Client.Api.V8_6; -using SonarQube.Client.Models; +using SonarLint.VisualStudio.SLCore.Protocol; -namespace SonarQube.Client.Api.V10_2 +namespace SonarLint.VisualStudio.SLCore.UnitTests.Protocol; + +[TestClass] +public class EitherTests { - internal class GetTaintVulnerabilitiesWithCCTRequest : GetTaintVulnerabilitiesRequest + [TestMethod] + public void ImplicitConvertFrom_LeftValue_ConvertsToEquivalentEither() + { + LeftType value = new LeftType(); + + Either either = value; + + either.Left.Should().BeSameAs(value); + either.Right.Should().BeNull(); + } + + [TestMethod] + public void ImplicitConvertFrom_RightValue_ConvertsToEquivalentEither() { - public override async Task InvokeAsync(HttpClient httpClient, CancellationToken token) - { - getIssuesRequest = new GetIssuesWithCCTRequest(); + RightType value = new RightType(); - return await base.InvokeAsync(httpClient, token); - } + Either either = value; + + either.Right.Should().BeSameAs(value); + either.Left.Should().BeNull(); } + + private class LeftType; + private class RightType; } diff --git a/src/SonarQube.Client/Api/V9_6/GetTaintVulnerabilitiesWithContextRequest.cs b/src/SLCore.UnitTests/Service/Issue/GetEffectiveIssueDetailsParamsTests.cs similarity index 53% rename from src/SonarQube.Client/Api/V9_6/GetTaintVulnerabilitiesWithContextRequest.cs rename to src/SLCore.UnitTests/Service/Issue/GetEffectiveIssueDetailsParamsTests.cs index ebecb3993d..9ae9bc678b 100644 --- a/src/SonarQube.Client/Api/V9_6/GetTaintVulnerabilitiesWithContextRequest.cs +++ b/src/SLCore.UnitTests/Service/Issue/GetEffectiveIssueDetailsParamsTests.cs @@ -18,21 +18,27 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using SonarQube.Client.Api.V8_6; -using SonarQube.Client.Models; +using Newtonsoft.Json; +using SonarLint.VisualStudio.SLCore.Service.Issue; -namespace SonarQube.Client.Api.V9_6 +namespace SonarLint.VisualStudio.SLCore.UnitTests.Service.Issue; + +[TestClass] +public class GetEffectiveIssueDetailsParamsTests { - internal class GetTaintVulnerabilitiesWithContextRequest : GetTaintVulnerabilitiesRequest + [TestMethod] + public void Serialized_AsExpected() { - public override async Task InvokeAsync(HttpClient httpClient, CancellationToken token) - { - getIssuesRequest = new GetIssuesWithContextRequest(); + var guid = Guid.NewGuid(); + var expected = $$""" + { + "configurationScopeId": "CONFIG_SCOPE_ID", + "issueId": "{{guid.ToString()}}" + } + """; + + var getEffectiveIssueDetailsParams = new GetEffectiveIssueDetailsParams("CONFIG_SCOPE_ID", guid); - return await base.InvokeAsync(httpClient, token); - } + JsonConvert.SerializeObject(getEffectiveIssueDetailsParams, Formatting.Indented).Should().Be(expected); } } diff --git a/src/SLCore.UnitTests/Service/Issue/GetEffectiveIssueDetailsResponseTests.cs b/src/SLCore.UnitTests/Service/Issue/GetEffectiveIssueDetailsResponseTests.cs new file mode 100644 index 0000000000..a8d77dfe85 --- /dev/null +++ b/src/SLCore.UnitTests/Service/Issue/GetEffectiveIssueDetailsResponseTests.cs @@ -0,0 +1,86 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Newtonsoft.Json; +using SonarLint.VisualStudio.SLCore.Common.Models; +using SonarLint.VisualStudio.SLCore.Service.Issue; +using SonarLint.VisualStudio.SLCore.Service.Issue.Models; +using SonarLint.VisualStudio.SLCore.Service.Rules.Models; + +namespace SonarLint.VisualStudio.SLCore.UnitTests.Service.Issue; + +[TestClass] +public class GetEffectiveIssueDetailsResponseTests +{ + [TestMethod] + public void Deserialized_AsExpected() + { + var expected = new GetEffectiveIssueDetailsResponse( + details: new EffectiveIssueDetailsDto( + key: "S3776", + name: "Cognitive Complexity of methods should not be too high", + language: Language.CS, + vulnerabilityProbability: VulnerabilityProbability.HIGH, + description: new RuleMonolithicDescriptionDto("

Cognitive Complexity is a measure of how hard it is to understand the control flow of a unit of code.

"), + parameters: [new EffectiveRuleParamDto("max", "Maximum cognitive complexity", "15", "15")], + severityDetails: new StandardModeDetails(IssueSeverity.CRITICAL, RuleType.CODE_SMELL), + ruleDescriptionContextKey: "key")); + + const string serialized = """ + { + details: { + "ruleKey": "S3776", + "name": "Cognitive Complexity of methods should not be too high", + "language": "cs", + "vulnerabilityProbability": "HIGH", + "description": { + "htmlContent": "

Cognitive Complexity is a measure of how hard it is to understand the control flow of a unit of code.

" + }, + "params": [ + { + "name": "max", + "description": "Maximum cognitive complexity", + "value": "15", + "defaultValue": "15" + } + ], + "severityDetails": { + "severity": "CRITICAL", + "type": "CODE_SMELL" + }, + "ruleDescriptionContextKey": "key" + } + } + """; + + var actual = JsonConvert.DeserializeObject(serialized); + + actual + .Should() + .BeEquivalentTo(expected, + options => + options + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers()); + } +} diff --git a/src/SLCore.UnitTests/Service/Rules/GetEffectiveRuleDetailsResponseTests.cs b/src/SLCore.UnitTests/Service/Rules/GetEffectiveRuleDetailsResponseTests.cs index f5ef98eba3..0ec72dc98d 100644 --- a/src/SLCore.UnitTests/Service/Rules/GetEffectiveRuleDetailsResponseTests.cs +++ b/src/SLCore.UnitTests/Service/Rules/GetEffectiveRuleDetailsResponseTests.cs @@ -18,8 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.Collections.Generic; -using FluentAssertions.Equivalency; using Newtonsoft.Json; using SonarLint.VisualStudio.SLCore.Common.Models; using SonarLint.VisualStudio.SLCore.Protocol; @@ -32,65 +30,130 @@ namespace SonarLint.VisualStudio.SLCore.UnitTests.Service.Rules; public class GetEffectiveRuleDetailsResponseTests { [TestMethod] - public void DeserializeSlcoreResponse_AsExpected() + public void DeserializeSLCoreResponse_Standard_Mode_AsExpected() { const string slcoreResponse = """ { "details": { + "key": "cpp:S3776", + "name": "Cognitive Complexity of functions should not be too high", + "severityDetails": { + "severity": "CRITICAL", + "type": "CODE_SMELL" + }, + "language": "CPP", + "vulnerabilityProbability": null, "description": { "introductionHtmlContent": "

This rule raises an issue when the code cognitive complexity of a function is above a certain threshold.

", "tabs": [ { "title": "Why is this an issue?", "content": { - "htmlContent": "

Cognitive Complexity is a measure of how hard it is to understand the control flow of a unit of code. Code with high cognitive complexity is hard\nto read, understand, test, and modify.

\n

As a rule of thumb, high cognitive complexity is a sign that the code should be refactored into smaller, easier-to-manage pieces.

\n

Which syntax in code does impact cognitive complexity score?

\n

Here are the core concepts:

\n
    \n
  • Cognitive complexity is incremented each time the code breaks the normal linear reading flow.
    This concerns, for example:\n Loop structures, Conditionals, Catches, Switches, Jumps to label and mixed operators in condition.
  • \n
  • Each nesting level adds a malus to the breaking call.
    During code reading, the deeper you go through nested layers, the\n harder it becomes to keep the context in mind.
  • \n
  • Method calls are free
    A well-picked method name is a summary of multiple lines of code. A reader can first explore a\n high-level view of what the code is performing then go deeper and deeper by looking at called functions content.
    Note: This does not\n apply to recursive calls, those will increment cognitive score.
  • \n
\n

The method of computation is fully detailed in the pdf linked in the resources.

\n

What is the potential impact?

\n

Developers spend more time reading and understanding code than writing it. High cognitive complexity slows down changes and increases the cost of\nmaintenance.

" + "htmlContent": "

Cognitive Complexity is a measure of how hard it is to understand the control flow of a unit of code. Code with high cognitive complexity is hard\nto read, understand, test, and modify.

\n

As a rule of thumb, high cognitive complexity is a sign that the code should be refactored into smaller, easier-to-manage pieces.

\n

Which syntax in code does impact cognitive complexity score?

\n

Here are the core concepts:

\n
    \n
  • Cognitive complexity is incremented each time the code breaks the normal linear reading flow.
    This concerns, for example,\n loop structures, conditionals, catches, switches, jumps to labels, and conditions mixing multiple operators.
  • \n
  • Each nesting level increases complexity.
    During code reading, the deeper you go through nested layers, the harder it\n becomes to keep the context in mind.
  • \n
  • Method calls are free
    A well-picked method name is a summary of multiple lines of code. A reader can first explore a\n high-level view of what the code is performing then go deeper and deeper by looking at called functions content.
    Note: This does not\n apply to recursive calls, those will increment cognitive score.
  • \n
\n

The method of computation is fully detailed in the pdf linked in the resources.

\n

What is the potential impact?

\n

Developers spend more time reading and understanding code than writing it. High cognitive complexity slows down changes and increases the cost of\nmaintenance.

" } }, { "title": "How can I fix it?", "content": { - "htmlContent": "

Reducing cognitive complexity can be challenging.
Here are a few suggestions:

\n
    \n
  • Extract complex conditions in a new function.
    Mixed operators in condition will increase complexity. Extracting the\n condition in a new function with an appropriate name will reduce cognitive load.
  • \n
  • Break down large functions.
    Large functions can be hard to understand and maintain. If a function is doing too many\n things, consider breaking it down into smaller, more manageable functions. Each function should have a single responsibility.
  • \n
  • Avoid deep nesting by returning early.
    To avoid the nesting of conditions, process exceptional cases first and return\n early.
  • \n
  • Use null-safe operations (if available in the language).
    When available the .? or ?? operator\n replaces multiple tests and simplifies the flow.
  • \n
\n\n

Extraction of a complex condition in a new function.

\n

Noncompliant code example

\n

The code is using a complex condition and has a cognitive cost of 3.

\n
\nfunction calculateFinalPrice(user, cart) {\n  let total = calculateTotal(cart);\n  if (user.hasMembership                       // +1 (if)\n    && user.orders > 10                        // +1 (more than one condition)\n    && user.accountActive\n    && !user.hasDiscount\n    || user.orders === 1) {                    // +1 (change of operator in condition)\n      total = applyDiscount(user, total);\n  }\n  return total;\n}\n
\n

Compliant solution

\n

Even if the cognitive complexity of the whole program did not change, it is easier for a reader to understand the code of the\ncalculateFinalPrice function, which now only has a cognitive cost of 1.

\n
\nfunction calculateFinalPrice(user, cart) {\n  let total = calculateTotal(cart);\n  if (isEligibleForDiscount(user)) {       // +1 (if)\n    total = applyDiscount(user, total);\n  }\n  return total;\n}\n\nfunction isEligibleForDiscount(user) {\n  return user.hasMembership\n    && user.orders > 10                     // +1 (more than one condition)\n    && user.accountActive\n    && !user.hasDiscount\n    || user.orders === 1                    // +1 (change of operator in condition)\n}\n
\n

Break down large functions.

\n

Noncompliant code example

\n

For example, consider a function that calculates the total price of a shopping cart, including sales tax and shipping.
Note: The code\nis simplified here, to illustrate the purpose. Please imagine there is more happening in the for loops.

\n
\nfunction calculateTotal(cart) {\n  let total = 0;\n  for (let i = 0; i < cart.length; i++) {       // +1 (for)\n    total += cart[i].price;\n  }\n\n  // calculateSalesTax\n  for (let i = 0; i < cart.length; i++) {       // +1 (for)\n    total += 0.2 * cart[i].price;\n  }\n\n  //calculateShipping\n  total += 5 * cart.length;\n\n  return total;\n}\n
\n

This function could be refactored into smaller functions: The complexity is spread over multiple functions and the complex\ncalculateTotal has now a complexity score of zero.

\n

Compliant solution

\n
\nfunction calculateTotal(cart) {\n  let total = calculateSubtotal(cart);\n  total += calculateSalesTax(cart);\n  total += calculateShipping(cart);\n  return total;\n}\n\nfunction calculateSubtotal(cart) {\n  let subTotal = 0;\n  for (const item of cart) {        // +1 (for)\n    subTotal += item.price;\n  }\n  return subTotal;\n}\n\nfunction calculateSalesTax(cart) {\n  let salesTax = 0;\n  for (const item of cart) {        // +1 (for)\n    salesTax += 0.2 * item.price;\n  }\n  return salesTax;\n}\n\nfunction calculateShipping(cart) {\n  return 5 * cart.length;\n}\n
\n

Avoid deep nesting by returning early.

\n

Noncompliant code example

\n

The below code has a cognitive complexity of 6.

\n
\nfunction calculateDiscount(price, user) {\n  if (isEligibleForDiscount(user)) {  // +1 ( if )\n    if (user?.hasMembership) {        // +2 ( nested if )\n      return price * 0.9;\n  } else if (user?.orders === 1 ) {   // +1 ( else )\n          return price * 0.95;\n    } else {                          // +1 ( else )\n      return price;\n    }\n  } else {                            // +1 ( else )\n    return price;\n  }\n}\n
\n

Compliant solution

\n

Checking for the edge case first flattens the if statements and reduces the cognitive complexity to 3.

\n
\nfunction calculateDiscount(price, user) {\n    if (!isEligibleForDiscount(user)) {  // +1 ( if )\n      return price;\n    }\n    if (user?.hasMembership) {           // +1 ( if )\n      return price * 0.9;\n    }\n    if (user?.orders === 1) {            // +1 ( if )\n      return price * 0.95;\n    }\n    return price;\n}\n
\n

Use the optional chaining operator to access data.

\n

In the below code, the cognitive complexity is increased due to the multiple checks required to access the manufacturer’s name. This can be\nsimplified using the optional chaining operator.

\n

Noncompliant code example

\n
\nlet manufacturerName = null;\n\nif (product && product.details && product.details.manufacturer) { // +1 (if) +1 (multiple condition)\n    manufacturerName = product.details.manufacturer.name;\n}\nif (manufacturerName) { // +1 (if)\n  console.log(manufacturerName);\n} else {\n  console.log('Manufacturer name not found');\n}\n
\n

Compliant solution

\n

The optional chaining operator will return undefined if any reference in the chain is undefined or null,\navoiding multiple checks:

\n
\nlet manufacturerName = product?.details?.manufacturer?.name;\n\nif (manufacturerName) { // +1 (if)\n  console.log(manufacturerName);\n} else {\n  console.log('Manufacturer name not found');\n}\n
\n

Pitfalls

\n

As this code is complex, ensure that you have unit tests that cover the code before refactoring.

" + "htmlContent": "

Reducing cognitive complexity can be challenging.
Here are a few suggestions:

\n
    \n
  • Extract complex conditions in a new function.
    Mixed operators in condition will increase complexity. Extracting the\n condition in a new function with an appropriate name will reduce cognitive load.
  • \n
  • Break down large functions.
    Large functions can be hard to understand and maintain. If a function is doing too many\n things, consider breaking it down into smaller, more manageable functions. Each function should have a single responsibility.
  • \n
  • Avoid deep nesting by returning early.
    To avoid the nesting of conditions, process exceptional cases first and return\n early.
  • \n
\n\n

Extraction of a complex condition in a new function.

\n

Noncompliant code example

\n

The code is using a complex condition and has a cognitive cost of 3.

\n
\nvoid processEligibleUser(User user) {\n  if ((user.isActive() && user.hasProfile()) // +1 (if) +1 (multiple conditions)\n     || (user.getAge() > 18)) {              // +1 (mixing operators)\n    // process the user\n  }\n}\n
\n

Compliant solution

\n

Even if the cognitive complexity of the whole program did not change, it is easier for a reader to understand the code of the\nprocessEligibleUser function, which now only has a cognitive score of 1.

\n
\nvoid processEligibleUser(User user) {\n  if (isEligibleUser(user)) {  // +1 (if)\n    // process the user\n  }\n}\n\nbool isEligibleUser(User user) {\n  return (user.isActive() && user.hasProfile()) // +1 (multiple conditions)\n      || (user.getAge() > 18));                 // +1 (mixing operators)\n}\n
\n

Break down large functions.

\n

Noncompliant code example

\n

The code is simplified here to illustrate the purpose. Please imagine there is more happening in the process.
The overall complexity of\nprocessUser is 8.

\n
\nvoid processUser(User user) {\n  if (user.isActive()) {      // +1 (if)\n    if (user.hasProfile()) {  // +1 (if) +1 (nested)\n      // process active user with profile\n    } else {                  // +1 (else)\n      // process active user without profile\n    }\n  } else {                    // +1 (else)\n    if (user.hasProfile()) {  // +1 (if) +1 (nested)\n      // process inactive user with profile\n    } else {                  // +1 (else)\n      // process inactive user without profile\n    }\n  }\n}\n
\n

This function could be refactored into smaller functions: The complexity is spread over multiple functions, and the breaks in flow are no longer\nnested.
The processUser now has a complexity score of two.

\n

Compliant solution

\n
\nvoid processUser(User user) {\n  if (user.isActive()) {      // +1 (if)\n    processActiveUser(user);\n  } else {                    // +1 (else)\n    processInactiveUser(user);\n  }\n}\n\nvoid processActiveUser(User user) {\n  if (user.hasProfile()) {    // +1 (if)\n      // process active user with profile\n  } else {                    // +1 (else)\n      // process active user without profile\n  }\n}\n\nvoid processInactiveUser(User user) {\n  if (user.hasProfile()) {    // +1 (if)\n    // process inactive user with profile\n  } else {                    // +1 (else)\n    // process inactive user without profile\n  }\n}\n
\n

Avoid deep nesting by returning early.

\n

Noncompliant code example

\n

The below code has a cognitive complexity of 3.

\n
\nvoid checkUser(User user) {\n  if (user.isActive()) {     // +1 (if)\n    if (user.hasProfile()) { // +1 (if) +1 (nested)\n      // do something\n    }\n  }\n}\n
\n

Compliant solution

\n

Checking for the edge case first flattens the if statements and reduces the cognitive complexity to 2.

\n
\nvoid checkUser(User user) {\n  if (!user.isActive()) {\n    return;\n  }\n  if (!user.hasProfile()) {\n    return;\n  }\n  // do something\n}\n
\n

Pitfalls

\n

As this code is complex, ensure that you have unit tests that cover the code before refactoring.

" } }, { "title": "More Info", "content": { - "htmlContent": "

Documentation

\n\n

Articles & blog posts

\nBLABLA

bla bla


dsfsfsd" + "htmlContent": "

Documentation

\n\n

Articles & blog posts

\n" } } ] }, - "params": [], - "key": "javascript:S3776", + "params": [] + } + } + """; + var ruleDetailsResponse = JsonConvert.DeserializeObject(slcoreResponse); + + var expectedRuleDetails = new GetEffectiveRuleDetailsResponse(new EffectiveRuleDetailsDto( + key:"cpp:S3776", + name:"Cognitive Complexity of functions should not be too high", + Language.CPP, + new StandardModeDetails(IssueSeverity.CRITICAL, RuleType.CODE_SMELL), + null, + Either.CreateRight( + new RuleSplitDescriptionDto( + "

This rule raises an issue when the code cognitive complexity of a function is above a certain threshold.

", + new List + { + new("Why is this an issue?", Either.CreateLeft(new RuleNonContextualSectionDto(""))), + new("How can I fix it?", Either.CreateLeft(new RuleNonContextualSectionDto(""))), + new("More Info", Either.CreateLeft(new RuleNonContextualSectionDto(""))) + })), + new List())); + + ruleDetailsResponse + .Should() + .BeEquivalentTo(expectedRuleDetails, + options => options + .ComparingByMembers() + .ComparingByMembers() + .ComparingByMembers() + .WithStrictOrdering() + .RespectingDeclaredTypes() + .Excluding(info => info.RuntimeType == typeof(string) && info.SelectedMemberPath.EndsWith(".content.Left.htmlContent"))); + } + + [TestMethod] + public void DeserializeSLCoreResponse_MQR_Mode_AsExpected() + { + const string slcoreResponse = """ + { + "details": { + "key": "cpp:S3776", "name": "Cognitive Complexity of functions should not be too high", - "severity": "CRITICAL", - "type": "CODE_SMELL", - "cleanCodeAttribute": "FOCUSED", - "cleanCodeAttributeCategory": "ADAPTABLE", - "defaultImpacts": [ - { - "softwareQuality": "MAINTAINABILITY", - "impactSeverity": "HIGH" - } - ], - "language": "JS" + "severityDetails": { + "cleanCodeAttribute": "FOCUSED", + "impacts": [ + { + "softwareQuality": "MAINTAINABILITY", + "impactSeverity": "HIGH" + } + ] + }, + "language": "CPP", + "vulnerabilityProbability": null, + "description": { + "introductionHtmlContent": "

This rule raises an issue when the code cognitive complexity of a function is above a certain threshold.

", + "tabs": [ + { + "title": "Why is this an issue?", + "content": { + "htmlContent": "

Cognitive Complexity is a measure of how hard it is to understand the control flow of a unit of code. Code with high cognitive complexity is hard\nto read, understand, test, and modify.

\n

As a rule of thumb, high cognitive complexity is a sign that the code should be refactored into smaller, easier-to-manage pieces.

\n

Which syntax in code does impact cognitive complexity score?

\n

Here are the core concepts:

\n
    \n
  • Cognitive complexity is incremented each time the code breaks the normal linear reading flow.
    This concerns, for example,\n loop structures, conditionals, catches, switches, jumps to labels, and conditions mixing multiple operators.
  • \n
  • Each nesting level increases complexity.
    During code reading, the deeper you go through nested layers, the harder it\n becomes to keep the context in mind.
  • \n
  • Method calls are free
    A well-picked method name is a summary of multiple lines of code. A reader can first explore a\n high-level view of what the code is performing then go deeper and deeper by looking at called functions content.
    Note: This does not\n apply to recursive calls, those will increment cognitive score.
  • \n
\n

The method of computation is fully detailed in the pdf linked in the resources.

\n

What is the potential impact?

\n

Developers spend more time reading and understanding code than writing it. High cognitive complexity slows down changes and increases the cost of\nmaintenance.

" + } + }, + { + "title": "How can I fix it?", + "content": { + "htmlContent": "

Reducing cognitive complexity can be challenging.
Here are a few suggestions:

\n
    \n
  • Extract complex conditions in a new function.
    Mixed operators in condition will increase complexity. Extracting the\n condition in a new function with an appropriate name will reduce cognitive load.
  • \n
  • Break down large functions.
    Large functions can be hard to understand and maintain. If a function is doing too many\n things, consider breaking it down into smaller, more manageable functions. Each function should have a single responsibility.
  • \n
  • Avoid deep nesting by returning early.
    To avoid the nesting of conditions, process exceptional cases first and return\n early.
  • \n
\n\n

Extraction of a complex condition in a new function.

\n

Noncompliant code example

\n

The code is using a complex condition and has a cognitive cost of 3.

\n
\nvoid processEligibleUser(User user) {\n  if ((user.isActive() && user.hasProfile()) // +1 (if) +1 (multiple conditions)\n     || (user.getAge() > 18)) {              // +1 (mixing operators)\n    // process the user\n  }\n}\n
\n

Compliant solution

\n

Even if the cognitive complexity of the whole program did not change, it is easier for a reader to understand the code of the\nprocessEligibleUser function, which now only has a cognitive score of 1.

\n
\nvoid processEligibleUser(User user) {\n  if (isEligibleUser(user)) {  // +1 (if)\n    // process the user\n  }\n}\n\nbool isEligibleUser(User user) {\n  return (user.isActive() && user.hasProfile()) // +1 (multiple conditions)\n      || (user.getAge() > 18));                 // +1 (mixing operators)\n}\n
\n

Break down large functions.

\n

Noncompliant code example

\n

The code is simplified here to illustrate the purpose. Please imagine there is more happening in the process.
The overall complexity of\nprocessUser is 8.

\n
\nvoid processUser(User user) {\n  if (user.isActive()) {      // +1 (if)\n    if (user.hasProfile()) {  // +1 (if) +1 (nested)\n      // process active user with profile\n    } else {                  // +1 (else)\n      // process active user without profile\n    }\n  } else {                    // +1 (else)\n    if (user.hasProfile()) {  // +1 (if) +1 (nested)\n      // process inactive user with profile\n    } else {                  // +1 (else)\n      // process inactive user without profile\n    }\n  }\n}\n
\n

This function could be refactored into smaller functions: The complexity is spread over multiple functions, and the breaks in flow are no longer\nnested.
The processUser now has a complexity score of two.

\n

Compliant solution

\n
\nvoid processUser(User user) {\n  if (user.isActive()) {      // +1 (if)\n    processActiveUser(user);\n  } else {                    // +1 (else)\n    processInactiveUser(user);\n  }\n}\n\nvoid processActiveUser(User user) {\n  if (user.hasProfile()) {    // +1 (if)\n      // process active user with profile\n  } else {                    // +1 (else)\n      // process active user without profile\n  }\n}\n\nvoid processInactiveUser(User user) {\n  if (user.hasProfile()) {    // +1 (if)\n    // process inactive user with profile\n  } else {                    // +1 (else)\n    // process inactive user without profile\n  }\n}\n
\n

Avoid deep nesting by returning early.

\n

Noncompliant code example

\n

The below code has a cognitive complexity of 3.

\n
\nvoid checkUser(User user) {\n  if (user.isActive()) {     // +1 (if)\n    if (user.hasProfile()) { // +1 (if) +1 (nested)\n      // do something\n    }\n  }\n}\n
\n

Compliant solution

\n

Checking for the edge case first flattens the if statements and reduces the cognitive complexity to 2.

\n
\nvoid checkUser(User user) {\n  if (!user.isActive()) {\n    return;\n  }\n  if (!user.hasProfile()) {\n    return;\n  }\n  // do something\n}\n
\n

Pitfalls

\n

As this code is complex, ensure that you have unit tests that cover the code before refactoring.

" + } + }, + { + "title": "More Info", + "content": { + "htmlContent": "

Documentation

\n\n

Articles & blog posts

\n" + } + } + ] + }, + "params": [] } } """; var ruleDetailsResponse = JsonConvert.DeserializeObject(slcoreResponse); var expectedRuleDetails = new GetEffectiveRuleDetailsResponse(new EffectiveRuleDetailsDto( - key:"javascript:S3776", - name:"Cognitive Complexity of functions should not be too high", - severity:IssueSeverity.CRITICAL, - type:RuleType.CODE_SMELL, - cleanCodeAttribute:CleanCodeAttribute.FOCUSED, - cleanCodeAttributeCategory:CleanCodeAttributeCategory.ADAPTABLE, - defaultImpacts:new List - { - new(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.HIGH) - }, - Language.JS, + key:"cpp:S3776", + name:"Cognitive Complexity of functions should not be too high", + Language.CPP, + new MQRModeDetails(CleanCodeAttribute.FOCUSED, [new ImpactDto(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.HIGH)]), null, Either.CreateRight( new RuleSplitDescriptionDto( @@ -102,15 +165,16 @@ public void DeserializeSlcoreResponse_AsExpected() new("More Info", Either.CreateLeft(new RuleNonContextualSectionDto(""))) })), new List())); - + ruleDetailsResponse .Should() .BeEquivalentTo(expectedRuleDetails, options => options .ComparingByMembers() .ComparingByMembers() + .ComparingByMembers() .WithStrictOrdering() .RespectingDeclaredTypes() - .Excluding((IMemberInfo info) => info.RuntimeType == typeof(string) && info.SelectedMemberPath.EndsWith(".content.Left.htmlContent"))); + .Excluding(info => info.RuntimeType == typeof(string) && info.SelectedMemberPath.EndsWith(".content.Left.htmlContent"))); } } diff --git a/src/SonarQube.Client/Api/IGetTaintVulnerabilitiesRequest.cs b/src/SLCore.UnitTests/Service/Taint/ListAllTaintsParamsTests.cs similarity index 56% rename from src/SonarQube.Client/Api/IGetTaintVulnerabilitiesRequest.cs rename to src/SLCore.UnitTests/Service/Taint/ListAllTaintsParamsTests.cs index 845f9ed8dc..b8570547df 100644 --- a/src/SonarQube.Client/Api/IGetTaintVulnerabilitiesRequest.cs +++ b/src/SLCore.UnitTests/Service/Taint/ListAllTaintsParamsTests.cs @@ -18,22 +18,23 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using SonarQube.Client.Models; -using SonarQube.Client.Requests; +using Newtonsoft.Json; +using SonarLint.VisualStudio.SLCore.Service.Taint; -namespace SonarQube.Client.Api +namespace SonarLint.VisualStudio.SLCore.UnitTests.Service.Taint; + +[TestClass] +public class ListAllTaintsParamsTests { - /// - /// Returns taint vulnerabilities that are in status OPEN, CONFIRMED, or REOPENED. - /// - public interface IGetTaintVulnerabilitiesRequest : IRequest + [DataTestMethod] + [DataRow("scope 1", false)] + [DataRow("other scope 2", true)] + public void Serialized_AsExpected(string configurationScopeId, bool shouldRefresh) { - string ProjectKey { get; set; } + var expected = $$"""{"configurationScopeId":"{{configurationScopeId}}","shouldRefresh":{{shouldRefresh.ToString().ToLower()}}}"""; + + var listAllTaintsParams = new ListAllTaintsParams(configurationScopeId, shouldRefresh); - /// - /// The branch name to fetch. - /// - /// If the value is null/empty, the main branch will be fetched - string Branch { get; set; } + JsonConvert.SerializeObject(listAllTaintsParams).Should().Be(expected); } } diff --git a/src/SLCore.UnitTests/Service/Taint/ListAllTaintsResponseTests.cs b/src/SLCore.UnitTests/Service/Taint/ListAllTaintsResponseTests.cs new file mode 100644 index 0000000000..192d48bcb6 --- /dev/null +++ b/src/SLCore.UnitTests/Service/Taint/ListAllTaintsResponseTests.cs @@ -0,0 +1,360 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Newtonsoft.Json; +using SonarLint.VisualStudio.SLCore.Service.Taint; + +namespace SonarLint.VisualStudio.SLCore.UnitTests.Service.Taint; + +[TestClass] +public class ListAllTaintsResponseTests +{ + [TestMethod] + public void Deserialized_AsExpected_SmokeTest() + { + const string serialized = """ + { + "taintVulnerabilities": [ + { + "id": "f1276bb9-54a4-4cbd-b4ac-41d2541302ee", + "sonarServerKey": "AXgSTUZl007Zyo8hMhT-", + "resolved": false, + "ruleKey": "roslyn.sonaranalyzer.security.cs:S5135", + "message": "Change this code to not deserialize user-controlled data.", + "ideFilePath": "sonarlint-visualstudio-sampleprojects\\bound\\sonarcloud\\SLVS_Samples_Bound_VS2019\\Taint_CSharp_NetCore_WebAppReact\\Taint\\XmlSerializerInjectionController.cs", + "introductionDate": 1615214736000, + "severityMode": { + "cleanCodeAttribute": "COMPLETE", + "impacts": [ + { + "softwareQuality": "SECURITY", + "impactSeverity": "HIGH" + } + ] + }, + "severity": "BLOCKER", + "type": "VULNERABILITY", + "flows": [ + { + "locations": [] + } + ], + "textRange": { + "startLine": 20, + "startLineOffset": 32, + "endLine": 20, + "endLineOffset": 58, + "hash": "f677236678ac4b2ab451d66d4b251e8f" + }, + "ruleDescriptionContextKey": null, + "cleanCodeAttribute": "COMPLETE", + "impacts": { + "SECURITY": "HIGH" + }, + "isOnNewCode": false + }, + { + "id": "95c062cf-b30f-4cc4-88db-f9fee7344639", + "sonarServerKey": "AXgSTUbP007Zyo8hMhUG", + "resolved": false, + "ruleKey": "roslyn.sonaranalyzer.security.cs:S2083", + "message": "Change this code to not construct the path from user-controlled data.", + "ideFilePath": "sonarlint-visualstudio-sampleprojects\\bound\\sonarcloud\\SLVS_Samples_Bound_VS2019\\Taint_CSharp_NetCore_WebAppReact\\Taint\\MixedIssuesController.cs", + "introductionDate": 1615214736000, + "severityMode": { + "cleanCodeAttribute": "COMPLETE", + "impacts": [ + { + "softwareQuality": "SECURITY", + "impactSeverity": "HIGH" + } + ] + }, + "severity": "BLOCKER", + "type": "VULNERABILITY", + "flows": [ + { + "locations": [] + } + ], + "textRange": { + "startLine": 15, + "startLineOffset": 12, + "endLine": 15, + "endLineOffset": 43, + "hash": "75a2a40f1881db4654f6860a1114a0bf" + }, + "ruleDescriptionContextKey": null, + "cleanCodeAttribute": "COMPLETE", + "impacts": { + "SECURITY": "HIGH" + }, + "isOnNewCode": false + }, + { + "id": "a7ef1f6e-523b-49f5-a29a-9b549695b0e0", + "sonarServerKey": "AXgSTUbP007Zyo8hMhUH", + "resolved": true, + "ruleKey": "roslyn.sonaranalyzer.security.cs:S5135", + "message": "Change this code to not deserialize user-controlled data.", + "ideFilePath": "sonarlint-visualstudio-sampleprojects\\bound\\sonarcloud\\SLVS_Samples_Bound_VS2019\\Taint_CSharp_NetCore_WebAppReact\\Taint\\MixedIssuesController.cs", + "introductionDate": 1615214736000, + "severityMode": { + "cleanCodeAttribute": "COMPLETE", + "impacts": [ + { + "softwareQuality": "SECURITY", + "impactSeverity": "HIGH" + } + ] + }, + "severity": "BLOCKER", + "type": "VULNERABILITY", + "flows": [ + { + "locations": [] + } + ], + "textRange": { + "startLine": 34, + "startLineOffset": 32, + "endLine": 34, + "endLineOffset": 58, + "hash": "f677236678ac4b2ab451d66d4b251e8f" + }, + "ruleDescriptionContextKey": null, + "cleanCodeAttribute": "COMPLETE", + "impacts": { + "SECURITY": "HIGH" + }, + "isOnNewCode": false + }, + { + "id": "a7cbd2da-71e4-4a93-80fb-f0cd6ca9da89", + "sonarServerKey": "AXgSTUbP007Zyo8hMhUF", + "resolved": false, + "ruleKey": "roslyn.sonaranalyzer.security.cs:S2091", + "message": "Change this code to not construct this XPath expression from user-controlled data.", + "ideFilePath": "sonarlint-visualstudio-sampleprojects\\bound\\sonarcloud\\SLVS_Samples_Bound_VS2019\\Taint_CSharp_NetCore_WebAppReact\\Taint\\MixedIssuesController.cs", + "introductionDate": 1615214736000, + "severityMode": { + "cleanCodeAttribute": "COMPLETE", + "impacts": [ + { + "softwareQuality": "SECURITY", + "impactSeverity": "HIGH" + } + ] + }, + "severity": "BLOCKER", + "type": "VULNERABILITY", + "flows": [ + { + "locations": [] + }, + { + "locations": [] + } + ], + "textRange": { + "startLine": 58, + "startLineOffset": 27, + "endLine": 58, + "endLineOffset": 59, + "hash": "4fdeebd4a19fd4b1c4c5b9b43ea9f71e" + }, + "ruleDescriptionContextKey": null, + "cleanCodeAttribute": "COMPLETE", + "impacts": { + "SECURITY": "HIGH" + }, + "isOnNewCode": false + }, + { + "id": "352ea2a3-e77f-49a8-880b-9549504be448", + "sonarServerKey": "AYfh2w3VueSGJHh8vWDj", + "resolved": false, + "ruleKey": "roslyn.sonaranalyzer.security.cs:S5146", + "message": "Change this code to not perform redirects based on user-controlled data.", + "ideFilePath": "sonarlint-visualstudio-sampleprojects\\bound\\sonarcloud\\SLVS_Samples_Bound_VS2019\\Taint_CSharp_NetCore_WebAppReact\\Controllers\\WeatherForecastController.cs", + "introductionDate": 1681210777000, + "severityMode": { + "cleanCodeAttribute": "COMPLETE", + "impacts": [ + { + "softwareQuality": "SECURITY", + "impactSeverity": "HIGH" + } + ] + }, + "severity": "BLOCKER", + "type": "VULNERABILITY", + "flows": [ + { + "locations": [] + } + ], + "textRange": { + "startLine": 43, + "startLineOffset": 12, + "endLine": 43, + "endLineOffset": 34, + "hash": "9e3e6f8af5838423c1b97b7d423cebab" + }, + "ruleDescriptionContextKey": null, + "cleanCodeAttribute": "COMPLETE", + "impacts": { + "SECURITY": "HIGH" + }, + "isOnNewCode": true + }, + { + "id": "abca89aa-c7a7-44c1-92e6-41b7a639e51c", + "sonarServerKey": "AXgSV_UkF9imBvjh6CmG", + "resolved": false, + "ruleKey": "roslyn.sonaranalyzer.security.cs:S2083", + "message": "Change this code to not construct the path from user-controlled data.", + "ideFilePath": "sonarlint-visualstudio-sampleprojects\\bound\\sonarcloud\\SLVS_Samples_Bound_VS2019\\Taint_CSharp_NetCore_WebAppReact\\Taint\\MultiFlow_IOPathInjectionController.cs", + "introductionDate": 1615215436000, + "severityMode": { + "cleanCodeAttribute": "COMPLETE", + "impacts": [ + { + "softwareQuality": "SECURITY", + "impactSeverity": "HIGH" + } + ] + }, + "severity": "BLOCKER", + "type": "VULNERABILITY", + "flows": [ + { + "locations": [] + } + ], + "textRange": { + "startLine": 17, + "startLineOffset": 12, + "endLine": 17, + "endLineOffset": 43, + "hash": "75a2a40f1881db4654f6860a1114a0bf" + }, + "ruleDescriptionContextKey": null, + "cleanCodeAttribute": "COMPLETE", + "impacts": { + "SECURITY": "HIGH" + }, + "isOnNewCode": false + }, + { + "id": "2f91bf04-f867-413c-8453-8f36d9756001", + "sonarServerKey": "AXgSV_UkF9imBvjh6CmF", + "resolved": false, + "ruleKey": "roslyn.sonaranalyzer.security.cs:S2083", + "message": "Change this code to not construct the path from user-controlled data.", + "ideFilePath": "sonarlint-visualstudio-sampleprojects\\bound\\sonarcloud\\SLVS_Samples_Bound_VS2019\\Taint_CSharp_NetCore_WebAppReact\\Taint\\MultiFlow_IOPathInjectionController.cs", + "introductionDate": 1615215436000, + "severityMode": { + "cleanCodeAttribute": "COMPLETE", + "impacts": [ + { + "softwareQuality": "SECURITY", + "impactSeverity": "HIGH" + } + ] + }, + "severity": "BLOCKER", + "type": "VULNERABILITY", + "flows": [ + { + "locations": [] + }, + { + "locations": [] + }, + { + "locations": [] + } + ], + "textRange": { + "startLine": 53, + "startLineOffset": 12, + "endLine": 53, + "endLineOffset": 43, + "hash": "75a2a40f1881db4654f6860a1114a0bf" + }, + "ruleDescriptionContextKey": null, + "cleanCodeAttribute": "COMPLETE", + "impacts": { + "SECURITY": "HIGH" + }, + "isOnNewCode": false + }, + { + "id": "62294585-d219-4d07-8e40-6d28d2f2f90e", + "sonarServerKey": "AXgSTUbU007Zyo8hMhUK", + "resolved": false, + "ruleKey": "roslyn.sonaranalyzer.security.cs:S2091", + "message": "Change this code to not construct this XPath expression from user-controlled data.", + "ideFilePath": "sonarlint-visualstudio-sampleprojects\\bound\\sonarcloud\\SLVS_Samples_Bound_VS2019\\Taint_CSharp_NetCore_WebAppReact\\Taint\\XPathInjectionController.cs", + "introductionDate": 1615214736000, + "severityMode": { + "cleanCodeAttribute": "COMPLETE", + "impacts": [ + { + "softwareQuality": "SECURITY", + "impactSeverity": "HIGH" + } + ] + }, + "severity": "BLOCKER", + "type": "VULNERABILITY", + "flows": [ + { + "locations": [] + }, + { + "locations": [] + } + ], + "textRange": { + "startLine": 23, + "startLineOffset": 27, + "endLine": 23, + "endLineOffset": 59, + "hash": "4fdeebd4a19fd4b1c4c5b9b43ea9f71e" + }, + "ruleDescriptionContextKey": null, + "cleanCodeAttribute": "COMPLETE", + "impacts": { + "SECURITY": "HIGH" + }, + "isOnNewCode": false + } + ] + } + """; + + var actual = JsonConvert.DeserializeObject(serialized); + + actual.taintVulnerabilities.Count.Should().Be(8); + actual.taintVulnerabilities.Should().NotContainNulls(); + } +} diff --git a/src/SLCore.UnitTests/State/ActiveConfigScopeTrackerTests.cs b/src/SLCore.UnitTests/State/ActiveConfigScopeTrackerTests.cs index e5b57782c0..f5cb309621 100644 --- a/src/SLCore.UnitTests/State/ActiveConfigScopeTrackerTests.cs +++ b/src/SLCore.UnitTests/State/ActiveConfigScopeTrackerTests.cs @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using System.ComponentModel; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Synchronization; using SonarLint.VisualStudio.SLCore.Core; @@ -31,6 +32,32 @@ namespace SonarLint.VisualStudio.SLCore.UnitTests.State; [TestClass] public class ActiveConfigScopeTrackerTests { + private ActiveConfigScopeTracker testSubject; + private IConfigurationScopeSLCoreService configScopeService; + private IAsyncLock asyncLock; + private IReleaseAsyncLock lockRelease; + private ISLCoreServiceProvider serviceProvider; + private IAsyncLockFactory asyncLockFactory; + private IThreadHandling threadHandling; + private EventHandler currentConfigScopeChangedEventHandler; + + [TestInitialize] + public void TestInitialize() + { + configScopeService = Substitute.For(); + asyncLock = Substitute.For(); + lockRelease = Substitute.For(); + serviceProvider = Substitute.For(); + asyncLockFactory = Substitute.For(); + threadHandling = Substitute.For(); + ConfigureServiceProvider(isServiceAvailable:true); + ConfigureAsyncLockFactory(); + currentConfigScopeChangedEventHandler = Substitute.For(); + + testSubject = new ActiveConfigScopeTracker(serviceProvider, asyncLockFactory, threadHandling); + testSubject.CurrentConfigurationScopeChanged += currentConfigScopeChangedEventHandler; + } + [TestMethod] public void MefCtor_CheckIsExported() { @@ -50,17 +77,14 @@ public void MefCtor_CheckIsSingleton() public void SetCurrentConfigScope_SetsUnboundScope() { const string configScopeId = "myid"; - var threadHandling = new Mock(); - ConfigureServiceProvider(out var serviceProvider, out var configScopeService); - ConfigureAsyncLockFactory(out var lockFactory, out var asyncLock, out var lockRelease); - var testSubject = CreateTestSubject(serviceProvider.Object, threadHandling.Object, lockFactory.Object); testSubject.SetCurrentConfigScope(configScopeId); testSubject.currentConfigScope.Should().BeEquivalentTo(new ConfigurationScope(configScopeId)); - VerifyThreadHandling(threadHandling); - VerifyServiceAddCall(configScopeService, testSubject); - VerifyLockTakenSynchronouslyAndReleased(asyncLock, lockRelease); + VerifyThreadHandling(); + VerifyServiceAddCall(); + VerifyLockTakenSynchronouslyAndReleased(); + VerifyCurrentConfigurationScopeChangedRaised(); } [TestMethod] @@ -70,16 +94,13 @@ public void TryUpdateRootOnCurrentConfigScope_ConfigScopeSame_Updates() const string connectionId = "connectionid"; const string sonarProjectKey = "projectkey"; const bool isReady = true; - var threadHandling = new Mock(); - ConfigureServiceProvider(out var serviceProvider, out _); - ConfigureAsyncLockFactory(out var lockFactory, out _, out _); - var testSubject = CreateTestSubject(serviceProvider.Object, threadHandling.Object, lockFactory.Object); testSubject.currentConfigScope = new ConfigurationScope(configScopeId, connectionId, sonarProjectKey, isReadyForAnalysis: isReady); var result = testSubject.TryUpdateRootOnCurrentConfigScope(configScopeId, "some root"); result.Should().BeTrue(); testSubject.currentConfigScope.Should().BeEquivalentTo(new ConfigurationScope(configScopeId, connectionId, sonarProjectKey, "some root", isReady)); + VerifyCurrentConfigurationScopeChangedRaised(); } [TestMethod] @@ -89,16 +110,13 @@ public void TryUpdateRootOnCurrentConfigScope_ConfigScopeDifferent_DoesNotUpdate const string connectionId = "connectionid"; const string sonarProjectKey = "projectkey"; const bool isReady = true; - var threadHandling = new Mock(); - ConfigureServiceProvider(out var serviceProvider, out _); - ConfigureAsyncLockFactory(out var lockFactory, out _, out _); - var testSubject = CreateTestSubject(serviceProvider.Object, threadHandling.Object, lockFactory.Object); testSubject.currentConfigScope = new ConfigurationScope(configScopeId, connectionId, sonarProjectKey, isReadyForAnalysis: isReady); var result = testSubject.TryUpdateRootOnCurrentConfigScope("some other id", "some root"); result.Should().BeFalse(); testSubject.currentConfigScope.Should().BeEquivalentTo(new ConfigurationScope(configScopeId, connectionId, sonarProjectKey, isReadyForAnalysis: isReady)); + VerifyCurrentConfigurationScopeChangedNotRaised(); } [TestMethod] @@ -108,16 +126,13 @@ public void TryUpdateAnalysisReadinessOnCurrentConfigScope_ConfigScopeSame_Updat const string connectionId = "connectionid"; const string sonarProjectKey = "projectkey"; const string root = "root"; - var threadHandling = new Mock(); - ConfigureServiceProvider(out var serviceProvider, out _); - ConfigureAsyncLockFactory(out var lockFactory, out _, out _); - var testSubject = CreateTestSubject(serviceProvider.Object, threadHandling.Object, lockFactory.Object); testSubject.currentConfigScope = new ConfigurationScope(configScopeId, connectionId, sonarProjectKey, root); var result = testSubject.TryUpdateAnalysisReadinessOnCurrentConfigScope(configScopeId, true); result.Should().BeTrue(); testSubject.currentConfigScope.Should().BeEquivalentTo(new ConfigurationScope(configScopeId, connectionId, sonarProjectKey, root, true)); + VerifyCurrentConfigurationScopeChangedRaised(); } [TestMethod] @@ -127,16 +142,13 @@ public void TryUpdateAnalysisReadinessOnCurrentConfigScope_ConfigScopeDifferent_ const string connectionId = "connectionid"; const string sonarProjectKey = "projectkey"; const string root = "root"; - var threadHandling = new Mock(); - ConfigureServiceProvider(out var serviceProvider, out _); - ConfigureAsyncLockFactory(out var lockFactory, out _, out _); - var testSubject = CreateTestSubject(serviceProvider.Object, threadHandling.Object, lockFactory.Object); testSubject.currentConfigScope = new ConfigurationScope(configScopeId, connectionId, sonarProjectKey, root); var result = testSubject.TryUpdateAnalysisReadinessOnCurrentConfigScope("some other id", true); result.Should().BeFalse(); testSubject.currentConfigScope.Should().BeEquivalentTo(new ConfigurationScope(configScopeId, connectionId, sonarProjectKey, root)); + VerifyCurrentConfigurationScopeChangedNotRaised(); } [TestMethod] @@ -145,17 +157,14 @@ public void SetCurrentConfigScope_SetsBoundScope() const string configScopeId = "myid"; const string connectionId = "myconid"; const string sonarProjectKey = "projectkey"; - var threadHandling = new Mock(); - ConfigureServiceProvider(out var serviceProvider, out var configScopeService); - ConfigureAsyncLockFactory(out var lockFactory, out var asyncLock, out var lockRelease); - var testSubject = CreateTestSubject(serviceProvider.Object, threadHandling.Object, lockFactory.Object); testSubject.SetCurrentConfigScope(configScopeId, connectionId, sonarProjectKey); testSubject.currentConfigScope.Should().BeEquivalentTo(new ConfigurationScope(configScopeId, connectionId, sonarProjectKey)); - VerifyThreadHandling(threadHandling); - VerifyServiceAddCall(configScopeService, testSubject); - VerifyLockTakenSynchronouslyAndReleased(asyncLock, lockRelease); + VerifyThreadHandling(); + VerifyServiceAddCall(); + VerifyLockTakenSynchronouslyAndReleased(); + VerifyCurrentConfigurationScopeChangedRaised(); } [TestMethod] @@ -165,10 +174,6 @@ public void SetCurrentConfigScope_CurrentScopeExists_UpdatesBoundScope() const string connectionId = "myconid"; const string sonarProjectKey = "projectkey"; const string rootPath = "somepath"; - var threadHandling = new Mock(); - ConfigureServiceProvider(out var serviceProvider, out var configScopeService); - ConfigureAsyncLockFactory(out var lockFactory, out var asyncLock, out var lockRelease); - var testSubject = CreateTestSubject(serviceProvider.Object, threadHandling.Object, lockFactory.Object); var existingConfigScope = new ConfigurationScope(configScopeId, RootPath: rootPath); testSubject.currentConfigScope = existingConfigScope; @@ -176,22 +181,22 @@ public void SetCurrentConfigScope_CurrentScopeExists_UpdatesBoundScope() testSubject.currentConfigScope.Should().BeEquivalentTo(new ConfigurationScope(configScopeId, connectionId, sonarProjectKey, rootPath)); testSubject.currentConfigScope.Should().NotBeSameAs(existingConfigScope); - VerifyThreadHandling(threadHandling); - VerifyServiceUpdateCall(configScopeService, testSubject); - VerifyLockTakenSynchronouslyAndReleased(asyncLock, lockRelease); + VerifyThreadHandling(); + VerifyServiceUpdateCall(); + VerifyLockTakenSynchronouslyAndReleased(); + VerifyCurrentConfigurationScopeChangedRaised(); } [TestMethod] public void SetCurrentConfigScope_ServiceUnavailable_Throws() { - var threadHandling = new Mock(); - ConfigureAsyncLockFactory(out var lockFactory, out _, out _); - var testSubject = CreateTestSubject(Mock.Of(), threadHandling.Object, lockFactory.Object); + ConfigureServiceProvider(isServiceAvailable: false); var act = () => testSubject.SetCurrentConfigScope("id"); act.Should().ThrowExactly().WithMessage(SLCoreStrings.ServiceProviderNotInitialized); - VerifyThreadHandling(threadHandling); + VerifyThreadHandling(); + VerifyCurrentConfigurationScopeChangedNotRaised(); } [TestMethod] @@ -199,80 +204,65 @@ public void SetCurrentConfigScope_UpdateConfigScopeWithDifferentId_Throws() { const string configScopeId = "myid"; const string anotherConfigScopeId = "anotherid"; - var threadHandling = new Mock(); - ConfigureServiceProvider(out var serviceProvider, out var configScopeService); - ConfigureAsyncLockFactory(out var lockFactory, out var asyncLock, out var lockRelease); - var testSubject = CreateTestSubject(serviceProvider.Object, threadHandling.Object, lockFactory.Object); var existingConfigScope = new ConfigurationScope(configScopeId); testSubject.currentConfigScope = existingConfigScope; var act = () => testSubject.SetCurrentConfigScope(anotherConfigScopeId); act.Should().ThrowExactly().WithMessage(SLCoreStrings.ConfigScopeConflict); - VerifyThreadHandling(threadHandling); + VerifyThreadHandling(); + VerifyCurrentConfigurationScopeChangedNotRaised(); } [TestMethod] public void RemoveCurrentConfigScope_RemovesScope() { const string configScopeId = "myid"; - var threadHandling = new Mock(); - ConfigureServiceProvider(out var serviceProvider, out var configScopeService); - ConfigureAsyncLockFactory(out var lockFactory, out var asyncLock, out var lockRelease); - var testSubject = CreateTestSubject(serviceProvider.Object, threadHandling.Object, lockFactory.Object); testSubject.currentConfigScope = new ConfigurationScope(configScopeId); testSubject.RemoveCurrentConfigScope(); - configScopeService.Verify(x => x.DidRemoveConfigurationScope(It.Is(p => p.removedId == configScopeId))); - VerifyThreadHandling(threadHandling); - VerifyLockTakenSynchronouslyAndReleased(asyncLock, lockRelease); + configScopeService.Received().DidRemoveConfigurationScope(Arg.Is(p => p.removedId == configScopeId)); + VerifyThreadHandling(); + VerifyLockTakenSynchronouslyAndReleased(); + VerifyCurrentConfigurationScopeChangedRaised(); } [TestMethod] public void RemoveCurrentConfigScope_NoCurrentScope_DoesNothing() { - var threadHandling = new Mock(); - ConfigureServiceProvider(out var serviceProvider, out var configScopeService); - ConfigureAsyncLockFactory(out var lockFactory, out var asyncLock, out var lockRelease); - var testSubject = CreateTestSubject(serviceProvider.Object, threadHandling.Object, lockFactory.Object); - testSubject.RemoveCurrentConfigScope(); - configScopeService.VerifyNoOtherCalls(); - VerifyThreadHandling(threadHandling); - VerifyLockTakenSynchronouslyAndReleased(asyncLock, lockRelease); + configScopeService.ReceivedCalls().Count().Should().Be(0); + VerifyThreadHandling(); + VerifyLockTakenSynchronouslyAndReleased(); + VerifyCurrentConfigurationScopeChangedNotRaised(); } [TestMethod] public void RemoveCurrentConfigScope_ServiceUnavailable_Throws() { - var threadHandling = new Mock(); - ConfigureAsyncLockFactory(out var lockFactory, out _, out _); - var testSubject = CreateTestSubject(Mock.Of(), threadHandling.Object, lockFactory.Object); testSubject.currentConfigScope = new ConfigurationScope("some Id", default, default, default); + ConfigureServiceProvider(isServiceAvailable: false); var act = () => testSubject.RemoveCurrentConfigScope(); act.Should().ThrowExactly().WithMessage(SLCoreStrings.ServiceProviderNotInitialized); - VerifyThreadHandling(threadHandling); + VerifyThreadHandling(); + VerifyCurrentConfigurationScopeChangedNotRaised(); } [TestMethod] public void GetCurrent_ReturnsUnboundScope() { const string configScopeId = "myid"; - var threadHandling = new Mock(); - ConfigureServiceProvider(out var serviceProvider, out _); - ConfigureAsyncLockFactory(out var lockFactory, out var asyncLock, out var lockRelease); - var testSubject = CreateTestSubject(serviceProvider.Object, threadHandling.Object, lockFactory.Object); testSubject.currentConfigScope = new ConfigurationScope(configScopeId); var currentScope = testSubject.Current; currentScope.Should().BeEquivalentTo(new ConfigurationScope(configScopeId)); - VerifyThreadHandling(threadHandling); - VerifyLockTakenSynchronouslyAndReleased(asyncLock, lockRelease); + VerifyThreadHandling(); + VerifyLockTakenSynchronouslyAndReleased(); } [TestMethod] @@ -281,18 +271,14 @@ public void GetCurrent_ReturnsBoundScope() const string configScopeId = "myid"; const string connectionId = "myconid"; const string sonarProjectKey = "projectkey"; - var threadHandling = new Mock(); - ConfigureServiceProvider(out var serviceProvider, out _); - ConfigureAsyncLockFactory(out var lockFactory, out var asyncLock, out var lockRelease); - var testSubject = CreateTestSubject(serviceProvider.Object, threadHandling.Object, lockFactory.Object); testSubject.currentConfigScope = new ConfigurationScope(configScopeId, connectionId, sonarProjectKey); var currentScope = testSubject.Current; currentScope.Should().BeEquivalentTo(new ConfigurationScope(configScopeId, connectionId, sonarProjectKey)); currentScope.RootPath.Should().Be(null); // not implemented - VerifyThreadHandling(threadHandling); - VerifyLockTakenSynchronouslyAndReleased(asyncLock, lockRelease); + VerifyThreadHandling(); + VerifyLockTakenSynchronouslyAndReleased(); } [TestMethod] @@ -301,92 +287,79 @@ public void Reset_SetsCurrentScopeToNull() const string configScopeId = "myid"; const string connectionId = "myconid"; const string sonarProjectKey = "projectkey"; - var threadHandling = new Mock(); - ConfigureServiceProvider(out var serviceProvider, out _); - ConfigureAsyncLockFactory(out var lockFactory, out var asyncLock, out var lockRelease); - var testSubject = CreateTestSubject(serviceProvider.Object, threadHandling.Object, lockFactory.Object); testSubject.currentConfigScope = new ConfigurationScope(configScopeId, configScopeId, connectionId, sonarProjectKey); testSubject.Reset(); testSubject.currentConfigScope.Should().BeNull(); - serviceProvider.VerifyNoOtherCalls(); - VerifyThreadHandling(threadHandling); - VerifyLockTakenSynchronouslyAndReleased(asyncLock, lockRelease); + serviceProvider.ReceivedCalls().Count().Should().Be(0); + VerifyThreadHandling(); + VerifyLockTakenSynchronouslyAndReleased(); + VerifyCurrentConfigurationScopeChangedRaised(); } [TestMethod] public void Dispose_DisposesLock() { - ConfigureServiceProvider(out var serviceProvider, out _); - ConfigureAsyncLockFactory(out var lockFactory, out var asyncLock, out _); - - var testSubject = CreateTestSubject(serviceProvider.Object, Mock.Of(), lockFactory.Object); - testSubject.Dispose(); - asyncLock.Verify(x => x.Dispose()); + + asyncLock.Received(1).Dispose(); } - private static void VerifyThreadHandling(Mock threadHandling) + private void VerifyThreadHandling() { - threadHandling.Verify(x => x.ThrowIfOnUIThread()); - threadHandling.VerifyNoOtherCalls(); + threadHandling.Received(1).ThrowIfOnUIThread(); + threadHandling.ReceivedCalls().Count().Should().Be(1); // verify no other calls } - private static void VerifyServiceAddCall(Mock configScopeService, ActiveConfigScopeTracker testSubject) + private void VerifyServiceAddCall() { var currentConfigScopeDto = new ConfigurationScopeDto(testSubject.currentConfigScope.Id, testSubject.currentConfigScope.Id, true, testSubject.currentConfigScope.ConnectionId is not null ? new BindingConfigurationDto(testSubject.currentConfigScope.ConnectionId, testSubject.currentConfigScope.SonarProjectId) : null); - configScopeService - .Verify(x => - x.DidAddConfigurationScopes(It.Is(p => - p.addedScopes.SequenceEqual(new[] { currentConfigScopeDto }, new ConfigurationScopeDtoComparer()))), - Times.Once); - configScopeService.VerifyNoOtherCalls(); + configScopeService.Received(1).DidAddConfigurationScopes(Arg.Is(p => + p.addedScopes.SequenceEqual(new[] { currentConfigScopeDto }, new ConfigurationScopeDtoComparer()))); + configScopeService.ReceivedCalls().Count().Should().Be(1); } - private static void VerifyServiceUpdateCall(Mock configScopeService, - ActiveConfigScopeTracker testSubject) + private void VerifyServiceUpdateCall() { - configScopeService - .Verify(x => x.DidUpdateBinding(It.Is(p => - p.configScopeId == testSubject.currentConfigScope.Id && new BindingConfigurationDtoComparer().Equals(p.updatedBinding, new BindingConfigurationDto(testSubject.currentConfigScope.ConnectionId, testSubject.currentConfigScope.SonarProjectId, true)))), Times.Once); - configScopeService.VerifyNoOtherCalls(); + configScopeService.Received(1).DidUpdateBinding(Arg.Is(p => + p.configScopeId == testSubject.currentConfigScope.Id && new BindingConfigurationDtoComparer().Equals(p.updatedBinding, new BindingConfigurationDto(testSubject.currentConfigScope.ConnectionId, testSubject.currentConfigScope.SonarProjectId, true)))); + configScopeService.ReceivedCalls().Count().Should().Be(1); } - private static void VerifyLockTakenSynchronouslyAndReleased(Mock asyncLock, Mock lockRelease) + private void VerifyLockTakenSynchronouslyAndReleased() { - asyncLock.Verify(x => x.Acquire(), Times.Once); - lockRelease.Verify(x => x.Dispose(), Times.Once); + asyncLock.Received(1).Acquire(); + lockRelease.Received(1).Dispose(); } - private static void ConfigureAsyncLockFactory(out Mock asyncLockFactory, - out Mock asyncLock, out Mock asyncLockRelease) + private void ConfigureAsyncLockFactory() { - asyncLockRelease = new Mock(); - asyncLock = new Mock(); - asyncLock.Setup(x => x.AcquireAsync()).ReturnsAsync(asyncLockRelease.Object); - asyncLock.Setup(x => x.Acquire()).Returns(asyncLockRelease.Object); - asyncLockFactory = new Mock(); - asyncLockFactory.Setup(x => x.Create()).Returns(asyncLock.Object); + asyncLock.AcquireAsync().Returns(lockRelease); + asyncLock.Acquire().Returns(lockRelease); + asyncLockFactory.Create().Returns(asyncLock); + } + + private void ConfigureServiceProvider(bool isServiceAvailable) + { + serviceProvider.TryGetTransientService(out IConfigurationScopeSLCoreService _).Returns(x => + { + x[0] = configScopeService; + return isServiceAvailable; + }); } - private static void ConfigureServiceProvider(out Mock serviceProvider, - out Mock configScopeService) + private void VerifyCurrentConfigurationScopeChangedRaised() { - serviceProvider = new Mock(); - configScopeService = new Mock(); - var service = configScopeService.Object; - serviceProvider.Setup(x => x.TryGetTransientService(out service)).Returns(true); + currentConfigScopeChangedEventHandler.Received(1).Invoke(testSubject, Arg.Any()); } - private static ActiveConfigScopeTracker CreateTestSubject(ISLCoreServiceProvider slCoreServiceProvider, - IThreadHandling threadHandling, - IAsyncLockFactory asyncLockFactory) + private void VerifyCurrentConfigurationScopeChangedNotRaised() { - return new ActiveConfigScopeTracker(slCoreServiceProvider, asyncLockFactory, threadHandling); + currentConfigScopeChangedEventHandler.DidNotReceive().Invoke(testSubject, Arg.Any()); } private class ConfigurationScopeDtoComparer : IEqualityComparer diff --git a/src/SLCore/Common/Helpers/ModelConversionExtensions.cs b/src/SLCore/Common/Helpers/ModelConversionExtensions.cs index dfc476d7ba..75bf5ac193 100644 --- a/src/SLCore/Common/Helpers/ModelConversionExtensions.cs +++ b/src/SLCore/Common/Helpers/ModelConversionExtensions.cs @@ -20,59 +20,62 @@ using SonarLint.VisualStudio.Core.Analysis; using SonarLint.VisualStudio.SLCore.Common.Models; +using SoftwareQuality = SonarLint.VisualStudio.Core.Analysis.SoftwareQuality; -namespace SonarLint.VisualStudio.SLCore.Common.Helpers +namespace SonarLint.VisualStudio.SLCore.Common.Helpers; + +public static class ModelConversionExtensions { - public static class ModelConversionExtensions - { - public static AnalysisIssueSeverity ToAnalysisIssueSeverity(this IssueSeverity issueSeverity) + public static AnalysisIssueSeverity ToAnalysisIssueSeverity(this IssueSeverity issueSeverity) => + issueSeverity switch + { + IssueSeverity.BLOCKER => AnalysisIssueSeverity.Blocker, + IssueSeverity.CRITICAL => AnalysisIssueSeverity.Critical, + IssueSeverity.MAJOR => AnalysisIssueSeverity.Major, + IssueSeverity.MINOR => AnalysisIssueSeverity.Minor, + IssueSeverity.INFO => AnalysisIssueSeverity.Info, + _ => throw new ArgumentOutOfRangeException(nameof(issueSeverity), issueSeverity, SLCoreStrings.ModelExtensions_UnexpectedValue) + }; + + public static AnalysisIssueType ToAnalysisIssueType(this RuleType ruleType) => + ruleType switch { - return issueSeverity switch - { - IssueSeverity.BLOCKER => AnalysisIssueSeverity.Blocker, - IssueSeverity.CRITICAL => AnalysisIssueSeverity.Critical, - IssueSeverity.MAJOR => AnalysisIssueSeverity.Major, - IssueSeverity.MINOR => AnalysisIssueSeverity.Minor, - IssueSeverity.INFO => AnalysisIssueSeverity.Info, - _ => throw new ArgumentOutOfRangeException(nameof(issueSeverity), issueSeverity, SLCoreStrings.ModelExtensions_UnexpectedValue), - }; - } + RuleType.CODE_SMELL => AnalysisIssueType.CodeSmell, + RuleType.BUG => AnalysisIssueType.Bug, + RuleType.VULNERABILITY => AnalysisIssueType.Vulnerability, + RuleType.SECURITY_HOTSPOT => AnalysisIssueType.SecurityHotspot, + _ => throw new ArgumentOutOfRangeException(nameof(ruleType), ruleType, SLCoreStrings.ModelExtensions_UnexpectedValue) + }; - public static AnalysisIssueType ToAnalysisIssueType(this RuleType ruleType) + public static SoftwareQualitySeverity ToSoftwareQualitySeverity(this ImpactSeverity impactSeverity) => + impactSeverity switch { - return ruleType switch - { - RuleType.CODE_SMELL => AnalysisIssueType.CodeSmell, - RuleType.BUG => AnalysisIssueType.Bug, - RuleType.VULNERABILITY => AnalysisIssueType.Vulnerability, - RuleType.SECURITY_HOTSPOT => AnalysisIssueType.SecurityHotspot, - _ => throw new ArgumentOutOfRangeException(nameof(ruleType), ruleType, SLCoreStrings.ModelExtensions_UnexpectedValue), - }; - } + ImpactSeverity.INFO => SoftwareQualitySeverity.Info, + ImpactSeverity.LOW => SoftwareQualitySeverity.Low, + ImpactSeverity.MEDIUM => SoftwareQualitySeverity.Medium, + ImpactSeverity.HIGH => SoftwareQualitySeverity.High, + ImpactSeverity.BLOCKER => SoftwareQualitySeverity.Blocker, + _ => throw new ArgumentOutOfRangeException(nameof(impactSeverity), impactSeverity, SLCoreStrings.ModelExtensions_UnexpectedValue) + }; - public static SoftwareQualitySeverity ToSoftwareQualitySeverity(this ImpactSeverity impactSeverity) + public static HotspotPriority? GetHotspotPriority(this VulnerabilityProbability? vulnerabilityProbability) => + vulnerabilityProbability switch { - return impactSeverity switch - { - ImpactSeverity.INFO => SoftwareQualitySeverity.Info, - ImpactSeverity.LOW => SoftwareQualitySeverity.Low, - ImpactSeverity.MEDIUM => SoftwareQualitySeverity.Medium, - ImpactSeverity.HIGH => SoftwareQualitySeverity.High, - ImpactSeverity.BLOCKER => SoftwareQualitySeverity.Blocker, - _ => throw new ArgumentOutOfRangeException(nameof(impactSeverity), impactSeverity, SLCoreStrings.ModelExtensions_UnexpectedValue), - }; - } + null => null, + VulnerabilityProbability.HIGH => HotspotPriority.High, + VulnerabilityProbability.MEDIUM => HotspotPriority.Medium, + VulnerabilityProbability.LOW => HotspotPriority.Low, + _ => throw new ArgumentOutOfRangeException(nameof(vulnerabilityProbability), vulnerabilityProbability, SLCoreStrings.ModelExtensions_UnexpectedValue) + }; - public static HotspotPriority? GetHotspotPriority(this VulnerabilityProbability? vulnerabilityProbability) + public static SoftwareQuality ToSoftwareQuality(this Models.SoftwareQuality softwareQuality) => + softwareQuality switch { - return vulnerabilityProbability switch - { - null => null, - VulnerabilityProbability.HIGH => HotspotPriority.High, - VulnerabilityProbability.MEDIUM => HotspotPriority.Medium, - VulnerabilityProbability.LOW => HotspotPriority.Low, - _ => throw new ArgumentOutOfRangeException(nameof(vulnerabilityProbability), vulnerabilityProbability, SLCoreStrings.ModelExtensions_UnexpectedValue), - }; - } - } + Models.SoftwareQuality.MAINTAINABILITY => SoftwareQuality.Maintainability, + Models.SoftwareQuality.RELIABILITY => SoftwareQuality.Reliability, + Models.SoftwareQuality.SECURITY => SoftwareQuality.Security, + _ => throw new ArgumentOutOfRangeException(nameof(softwareQuality), softwareQuality, SLCoreStrings.ModelExtensions_UnexpectedValue) + }; + + public static Impact ToImpact(this ImpactDto impact) => new(impact.softwareQuality.ToSoftwareQuality(), impact.impactSeverity.ToSoftwareQualitySeverity()); } diff --git a/src/SLCore/Common/Models/TaintVulnerabilityDto.cs b/src/SLCore/Common/Models/TaintVulnerabilityDto.cs new file mode 100644 index 0000000000..4f0ab3a9f4 --- /dev/null +++ b/src/SLCore/Common/Models/TaintVulnerabilityDto.cs @@ -0,0 +1,44 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Newtonsoft.Json; +using SonarLint.VisualStudio.SLCore.Common.Helpers; +using SonarLint.VisualStudio.SLCore.Protocol; +using SonarLint.VisualStudio.SLCore.Service.Rules.Models; + +namespace SonarLint.VisualStudio.SLCore.Common.Models; + +public record TaintVulnerabilityDto( + Guid id, + string sonarServerKey, + bool resolved, + string ruleKey, + string message, + string ideFilePath, + [JsonConverter(typeof(MillisecondUnixTimestampDateTimeOffsetConverter))]DateTimeOffset introductionDate, + [JsonConverter(typeof(EitherJsonConverter))]Either severityMode, + List flows, + TextRangeWithHashDto textRange, + string ruleDescriptionContextKey, + bool isOnNewCode); + +public record TaintFlowDto(List locations); + +public record TaintFlowLocationDto(TextRangeWithHashDto textRange, string message, string filePath); diff --git a/src/SLCore/Common/Models/TextRangeWithHashDto.cs b/src/SLCore/Common/Models/TextRangeWithHashDto.cs new file mode 100644 index 0000000000..f45eb9b443 --- /dev/null +++ b/src/SLCore/Common/Models/TextRangeWithHashDto.cs @@ -0,0 +1,23 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.SLCore.Common.Models; + +public record TextRangeWithHashDto(int startLine, int startLineOffset, int endLine, int endLineOffset, string hash); diff --git a/src/SLCore/Listener/Analysis/Models/RaisedFindingDto.cs b/src/SLCore/Listener/Analysis/Models/RaisedFindingDto.cs index c3175cc43d..11e6aff8a0 100644 --- a/src/SLCore/Listener/Analysis/Models/RaisedFindingDto.cs +++ b/src/SLCore/Listener/Analysis/Models/RaisedFindingDto.cs @@ -19,7 +19,10 @@ */ using System.Diagnostics.CodeAnalysis; +using Newtonsoft.Json; using SonarLint.VisualStudio.SLCore.Common.Models; +using SonarLint.VisualStudio.SLCore.Protocol; +using SonarLint.VisualStudio.SLCore.Service.Rules.Models; namespace SonarLint.VisualStudio.SLCore.Listener.Analysis.Models; @@ -29,14 +32,12 @@ public abstract record RaisedFindingDto( string serverKey, string ruleKey, string primaryMessage, - IssueSeverity severity, - RuleType type, - CleanCodeAttribute cleanCodeAttribute, - List impacts, DateTimeOffset introductionDate, bool isOnNewCode, bool resolved, TextRangeDto textRange, List flows, List quickFixes, - string ruleDescriptionContextKey); + string ruleDescriptionContextKey, + [property: JsonConverter(typeof(EitherJsonConverter))] + Either severityMode); diff --git a/src/SLCore/Listener/Analysis/Models/RaisedHotspotDto.cs b/src/SLCore/Listener/Analysis/Models/RaisedHotspotDto.cs index b1f69fe30c..2726142a76 100644 --- a/src/SLCore/Listener/Analysis/Models/RaisedHotspotDto.cs +++ b/src/SLCore/Listener/Analysis/Models/RaisedHotspotDto.cs @@ -22,6 +22,8 @@ using Newtonsoft.Json; using SonarLint.VisualStudio.SLCore.Common.Helpers; using SonarLint.VisualStudio.SLCore.Common.Models; +using SonarLint.VisualStudio.SLCore.Protocol; +using SonarLint.VisualStudio.SLCore.Service.Rules.Models; namespace SonarLint.VisualStudio.SLCore.Listener.Analysis.Models; @@ -31,11 +33,8 @@ public record RaisedHotspotDto( string serverKey, string ruleKey, string primaryMessage, - IssueSeverity severity, - RuleType type, - CleanCodeAttribute cleanCodeAttribute, - List impacts, - [JsonConverter(typeof(MillisecondUnixTimestampDateTimeOffsetConverter))] DateTimeOffset introductionDate, + [JsonConverter(typeof(MillisecondUnixTimestampDateTimeOffsetConverter))] + DateTimeOffset introductionDate, bool isOnNewCode, bool resolved, TextRangeDto textRange, @@ -43,5 +42,6 @@ public record RaisedHotspotDto( List quickFixes, string ruleDescriptionContextKey, VulnerabilityProbability? vulnerabilityProbability, - HotspotStatus status) : RaisedFindingDto(id, serverKey, ruleKey, primaryMessage, severity, type, cleanCodeAttribute, - impacts, introductionDate, isOnNewCode, resolved, textRange, flows, quickFixes, ruleDescriptionContextKey); + HotspotStatus status, + Either severityMode) : RaisedFindingDto(id, serverKey, ruleKey, primaryMessage, introductionDate, isOnNewCode, resolved, textRange, flows, quickFixes, + ruleDescriptionContextKey, severityMode); diff --git a/src/SLCore/Listener/Analysis/Models/RaisedIssueDto.cs b/src/SLCore/Listener/Analysis/Models/RaisedIssueDto.cs index 1331278879..6029feaa27 100644 --- a/src/SLCore/Listener/Analysis/Models/RaisedIssueDto.cs +++ b/src/SLCore/Listener/Analysis/Models/RaisedIssueDto.cs @@ -21,6 +21,8 @@ using Newtonsoft.Json; using SonarLint.VisualStudio.SLCore.Common.Helpers; using SonarLint.VisualStudio.SLCore.Common.Models; +using SonarLint.VisualStudio.SLCore.Protocol; +using SonarLint.VisualStudio.SLCore.Service.Rules.Models; namespace SonarLint.VisualStudio.SLCore.Listener.Analysis.Models; @@ -29,16 +31,13 @@ public record RaisedIssueDto( string serverKey, string ruleKey, string primaryMessage, - IssueSeverity severity, - RuleType type, - CleanCodeAttribute cleanCodeAttribute, - List impacts, - [JsonConverter(typeof(MillisecondUnixTimestampDateTimeOffsetConverter))] DateTimeOffset introductionDate, + [JsonConverter(typeof(MillisecondUnixTimestampDateTimeOffsetConverter))] + DateTimeOffset introductionDate, bool isOnNewCode, bool resolved, TextRangeDto textRange, List flows, List quickFixes, - string ruleDescriptionContextKey) : RaisedFindingDto(id, serverKey, ruleKey, primaryMessage, severity, type, - cleanCodeAttribute, impacts, introductionDate, isOnNewCode, resolved, textRange, flows, quickFixes, - ruleDescriptionContextKey); + string ruleDescriptionContextKey, + Either severityMode) : RaisedFindingDto(id, serverKey, ruleKey, primaryMessage, introductionDate, isOnNewCode, resolved, textRange, flows, quickFixes, + ruleDescriptionContextKey, severityMode); diff --git a/src/SLCore/Listener/Analysis/RaiseFindingToAnalysisIssueConverter.cs b/src/SLCore/Listener/Analysis/RaiseFindingToAnalysisIssueConverter.cs index 0349793731..6b0f9786e1 100644 --- a/src/SLCore/Listener/Analysis/RaiseFindingToAnalysisIssueConverter.cs +++ b/src/SLCore/Listener/Analysis/RaiseFindingToAnalysisIssueConverter.cs @@ -37,10 +37,11 @@ public IEnumerable GetAnalysisIssues(FileUri fileUri, IEnumer private static AnalysisIssue CreateAnalysisIssue(FileUri fileUri, T item) where T : RaisedFindingDto { + var id = item.id; var itemRuleKey = item.ruleKey; - var analysisIssueSeverity = item.severity.ToAnalysisIssueSeverity(); - var analysisIssueType = item.type.ToAnalysisIssueType(); - var highestSoftwareQualitySeverity = GetHighestSoftwareQualitySeverity(item.impacts); + var analysisIssueSeverity = item.severityMode.Left?.severity.ToAnalysisIssueSeverity(); + var analysisIssueType = item.severityMode.Left?.type.ToAnalysisIssueType(); + var highestSoftwareQualitySeverity = GetHighestImpact(item.severityMode.Right?.impacts); var analysisIssueLocation = GetAnalysisIssueLocation(fileUri.LocalPath, item.primaryMessage, item.textRange); var analysisIssueFlows = GetFlows(item.flows); var readOnlyList = item.quickFixes?.Select(qf => GetQuickFix(fileUri, qf)).Where(qf => qf is not null).ToList(); @@ -48,7 +49,8 @@ private static AnalysisIssue CreateAnalysisIssue(FileUri fileUri, T item) whe if (item is RaisedHotspotDto raisedHotspotDto) { - return new AnalysisHotspotIssue(itemRuleKey, + return new AnalysisHotspotIssue(id, + itemRuleKey, analysisIssueSeverity, analysisIssueType, highestSoftwareQualitySeverity, @@ -59,7 +61,8 @@ private static AnalysisIssue CreateAnalysisIssue(FileUri fileUri, T item) whe raisedHotspotDto.vulnerabilityProbability.GetHotspotPriority()); } - return new AnalysisIssue(itemRuleKey, + return new AnalysisIssue(id, + itemRuleKey, analysisIssueSeverity, analysisIssueType, highestSoftwareQualitySeverity, @@ -69,10 +72,14 @@ private static AnalysisIssue CreateAnalysisIssue(FileUri fileUri, T item) whe itemRuleDescriptionContextKey); } - private static SoftwareQualitySeverity? GetHighestSoftwareQualitySeverity(List impacts) => - impacts is not null && impacts.Any() - ? impacts.Max(i => i.impactSeverity).ToSoftwareQualitySeverity() - : null; + private static Impact GetHighestImpact(List impacts) + { + if(impacts is null || impacts.Count == 0) + { + return null; + } + return impacts.OrderByDescending(i => i.impactSeverity).ThenByDescending(i => i.softwareQuality).First().ToImpact(); + } private static IAnalysisIssueLocation GetAnalysisIssueLocation(string filePath, string message, TextRangeDto textRangeDto) => new AnalysisIssueLocation(message, diff --git a/src/SLCore/Listener/Taint/DidChangeTaintVulnerabilitiesParams.cs b/src/SLCore/Listener/Taint/DidChangeTaintVulnerabilitiesParams.cs new file mode 100644 index 0000000000..65ff7d11fc --- /dev/null +++ b/src/SLCore/Listener/Taint/DidChangeTaintVulnerabilitiesParams.cs @@ -0,0 +1,29 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.SLCore.Common.Models; + +namespace SonarLint.VisualStudio.SLCore.Listener.Taint; + +public record DidChangeTaintVulnerabilitiesParams( + string configurationScopeId, + List closedTaintVulnerabilityIds, + List addedTaintVulnerabilities, + List updatedTaintVulnerabilities); diff --git a/src/ConnectedMode/ServerSentEvents/Taint/ITaintServerEventSourcePublisher.cs b/src/SLCore/Listener/Taint/ITaintVulnerabilityListener.cs similarity index 70% rename from src/ConnectedMode/ServerSentEvents/Taint/ITaintServerEventSourcePublisher.cs rename to src/SLCore/Listener/Taint/ITaintVulnerabilityListener.cs index 474eba4bbb..a8fe71d32e 100644 --- a/src/ConnectedMode/ServerSentEvents/Taint/ITaintServerEventSourcePublisher.cs +++ b/src/SLCore/Listener/Taint/ITaintVulnerabilityListener.cs @@ -18,11 +18,11 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using SonarLint.VisualStudio.Core.ServerSentEvents; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; +using SonarLint.VisualStudio.SLCore.Core; -namespace SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Taint +namespace SonarLint.VisualStudio.SLCore.Listener.Taint; + +public interface ITaintVulnerabilityListener : ISLCoreListener { - /// - public interface ITaintServerEventSourcePublisher : IServerSentEventSourcePublisher { } + void DidChangeTaintVulnerabilities(DidChangeTaintVulnerabilitiesParams parameters); } diff --git a/src/SLCore/Protocol/Either.cs b/src/SLCore/Protocol/Either.cs index d20d7e027d..35ba425440 100644 --- a/src/SLCore/Protocol/Either.cs +++ b/src/SLCore/Protocol/Either.cs @@ -18,25 +18,24 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; - namespace SonarLint.VisualStudio.SLCore.Protocol { /// /// Represents an option class where only one of the properties or is not null /// - public sealed class Either + public class Either where TLeft : class where TRight : class { - private Either() - { - } + private Either(){} + + public TLeft Left { get; private init; } + public TRight Right { get; private init; } - public TLeft Left { get; private set; } - public TRight Right { get; private set; } + public static Either CreateLeft(TLeft left) => new() {Left = left ?? throw new ArgumentNullException(nameof(left))}; + public static Either CreateRight(TRight right) => new() {Right = right ?? throw new ArgumentNullException(nameof(right))}; - public static Either CreateLeft(TLeft left) => new() { Left = left ?? throw new ArgumentNullException(nameof(left))}; - public static Either CreateRight(TRight right) => new() { Right = right ?? throw new ArgumentNullException(nameof(right))}; + public static implicit operator Either(TLeft left) => CreateLeft(left); + public static implicit operator Either(TRight right) => CreateRight(right); } } diff --git a/src/SLCore/Service/Issue/GetEffectiveIssueDetailsParams.cs b/src/SLCore/Service/Issue/GetEffectiveIssueDetailsParams.cs new file mode 100644 index 0000000000..ab7908f6c1 --- /dev/null +++ b/src/SLCore/Service/Issue/GetEffectiveIssueDetailsParams.cs @@ -0,0 +1,23 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.SLCore.Service.Issue; + +public record GetEffectiveIssueDetailsParams(string configurationScopeId, Guid issueId); diff --git a/src/SLCore/Service/Issue/GetEffectiveIssueDetailsResponse.cs b/src/SLCore/Service/Issue/GetEffectiveIssueDetailsResponse.cs new file mode 100644 index 0000000000..6b00cbd3a4 --- /dev/null +++ b/src/SLCore/Service/Issue/GetEffectiveIssueDetailsResponse.cs @@ -0,0 +1,25 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.SLCore.Service.Issue.Models; + +namespace SonarLint.VisualStudio.SLCore.Service.Issue; + +public record GetEffectiveIssueDetailsResponse(EffectiveIssueDetailsDto details); diff --git a/src/IssueViz.Security/Taint/ServerSentEvents/ITaintServerEventSource.cs b/src/SLCore/Service/Issue/IIssueSLCoreService.cs similarity index 70% rename from src/IssueViz.Security/Taint/ServerSentEvents/ITaintServerEventSource.cs rename to src/SLCore/Service/Issue/IIssueSLCoreService.cs index af869cb5eb..b6f3fe95d0 100644 --- a/src/IssueViz.Security/Taint/ServerSentEvents/ITaintServerEventSource.cs +++ b/src/SLCore/Service/Issue/IIssueSLCoreService.cs @@ -18,11 +18,13 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using SonarLint.VisualStudio.Core.ServerSentEvents; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; +using SonarLint.VisualStudio.SLCore.Core; +using SonarLint.VisualStudio.SLCore.Protocol; -namespace SonarLint.VisualStudio.IssueVisualization.Security.Taint.ServerSentEvents +namespace SonarLint.VisualStudio.SLCore.Service.Issue; + +[JsonRpcClass("issue")] +public interface IIssueSLCoreService : ISLCoreService { - /// - internal interface ITaintServerEventSource : IServerSentEventSource { } + Task GetEffectiveIssueDetailsAsync(GetEffectiveIssueDetailsParams parameters); } diff --git a/src/SLCore/Service/Rules/Models/RuleDefinitionDto.cs b/src/SLCore/Service/Issue/Models/EffectiveIssueDetailsDto.cs similarity index 58% rename from src/SLCore/Service/Rules/Models/RuleDefinitionDto.cs rename to src/SLCore/Service/Issue/Models/EffectiveIssueDetailsDto.cs index 5a12a56849..e86d9697e0 100644 --- a/src/SLCore/Service/Rules/Models/RuleDefinitionDto.cs +++ b/src/SLCore/Service/Issue/Models/EffectiveIssueDetailsDto.cs @@ -18,28 +18,19 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using Newtonsoft.Json; using SonarLint.VisualStudio.SLCore.Common.Models; +using SonarLint.VisualStudio.SLCore.Protocol; +using SonarLint.VisualStudio.SLCore.Service.Rules.Models; -namespace SonarLint.VisualStudio.SLCore.Service.Rules.Models; +namespace SonarLint.VisualStudio.SLCore.Service.Issue.Models; -public record RuleDefinitionDto( - string key, +public record EffectiveIssueDetailsDto( + [JsonProperty("ruleKey")] string key, string name, - IssueSeverity severity, - RuleType type, - CleanCodeAttribute? cleanCodeAttribute, - CleanCodeAttributeCategory? cleanCodeAttributeCategory, - List defaultImpacts, Language language, VulnerabilityProbability? vulnerabilityProbability, - Dictionary paramsByKey, // object because we ignore rule parameters at the moment - bool isActiveByDefault) - : AbstractRuleDto(key, - name, - severity, - type, - cleanCodeAttribute, - cleanCodeAttributeCategory, - defaultImpacts, - language, - vulnerabilityProbability); + [JsonConverter(typeof(EitherJsonConverter))] Either description, + [JsonProperty("params")] List parameters, + [JsonConverter(typeof(EitherJsonConverter))] Either severityDetails, + string ruleDescriptionContextKey) : IRuleDetails; diff --git a/src/SLCore/Service/Rules/ListAllStandaloneRulesDefinitionsResponse.cs b/src/SLCore/Service/Rules/ListAllStandaloneRulesDefinitionsResponse.cs index a05fd45eac..7d75f477af 100644 --- a/src/SLCore/Service/Rules/ListAllStandaloneRulesDefinitionsResponse.cs +++ b/src/SLCore/Service/Rules/ListAllStandaloneRulesDefinitionsResponse.cs @@ -18,8 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using SonarLint.VisualStudio.SLCore.Service.Rules.Models; - namespace SonarLint.VisualStudio.SLCore.Service.Rules; -public record ListAllStandaloneRulesDefinitionsResponse(Dictionary rulesByKey); +public record ListAllStandaloneRulesDefinitionsResponse(Dictionary rulesByKey); // we only care about rule ids, so we don't parse the value object diff --git a/src/SLCore/Service/Rules/Models/EffectiveRuleDetailsDto.cs b/src/SLCore/Service/Rules/Models/EffectiveRuleDetailsDto.cs index 4d214045b9..45b4798ecf 100644 --- a/src/SLCore/Service/Rules/Models/EffectiveRuleDetailsDto.cs +++ b/src/SLCore/Service/Rules/Models/EffectiveRuleDetailsDto.cs @@ -18,32 +18,23 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.Collections.Generic; using Newtonsoft.Json; using SonarLint.VisualStudio.SLCore.Common.Models; using SonarLint.VisualStudio.SLCore.Protocol; +using Language = SonarLint.VisualStudio.SLCore.Common.Models.Language; namespace SonarLint.VisualStudio.SLCore.Service.Rules.Models; public record EffectiveRuleDetailsDto( string key, string name, - IssueSeverity severity, - RuleType type, - CleanCodeAttribute? cleanCodeAttribute, - CleanCodeAttributeCategory? cleanCodeAttributeCategory, - List defaultImpacts, Language language, + [property: JsonConverter(typeof(EitherJsonConverter))] + Either severityDetails, VulnerabilityProbability? vulnerabilityProbability, [property: JsonConverter(typeof(EitherJsonConverter))] Either description, - List @params) - : AbstractRuleDto(key, - name, - severity, - type, - cleanCodeAttribute, - cleanCodeAttributeCategory, - defaultImpacts, - language, - vulnerabilityProbability); + [JsonProperty("params")] List parameters) : IRuleDetails; + +public record StandardModeDetails(IssueSeverity severity, RuleType type); +public record MQRModeDetails(CleanCodeAttribute cleanCodeAttribute, List impacts); diff --git a/src/SLCore/Service/Rules/Models/AbstractRuleDto.cs b/src/SLCore/Service/Rules/Models/IRuleDetails.cs similarity index 69% rename from src/SLCore/Service/Rules/Models/AbstractRuleDto.cs rename to src/SLCore/Service/Rules/Models/IRuleDetails.cs index 1ad3a3b9b5..1f41d4a522 100644 --- a/src/SLCore/Service/Rules/Models/AbstractRuleDto.cs +++ b/src/SLCore/Service/Rules/Models/IRuleDetails.cs @@ -18,18 +18,18 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.Collections.Generic; using SonarLint.VisualStudio.SLCore.Common.Models; +using SonarLint.VisualStudio.SLCore.Protocol; namespace SonarLint.VisualStudio.SLCore.Service.Rules.Models; -public abstract record AbstractRuleDto( - string key, - string name, - IssueSeverity severity, - RuleType type, - CleanCodeAttribute? cleanCodeAttribute, - CleanCodeAttributeCategory? cleanCodeAttributeCategory, - List defaultImpacts, - Language language, - VulnerabilityProbability? vulnerabilityProbability); +public interface IRuleDetails +{ + string key { get; } + string name { get; } + Language language { get; } + Either severityDetails { get; } + VulnerabilityProbability? vulnerabilityProbability { get; } + Either description { get; } + List parameters { get; } +} diff --git a/src/SLCore/Service/Taint/ListAllTaintsParams.cs b/src/SLCore/Service/Taint/ListAllTaintsParams.cs new file mode 100644 index 0000000000..940d808a56 --- /dev/null +++ b/src/SLCore/Service/Taint/ListAllTaintsParams.cs @@ -0,0 +1,23 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.SLCore.Service.Taint; + +public record ListAllTaintsParams(string configurationScopeId, bool shouldRefresh); diff --git a/src/SLCore/Service/Taint/ListAllTaintsResponse.cs b/src/SLCore/Service/Taint/ListAllTaintsResponse.cs new file mode 100644 index 0000000000..c52aacba4b --- /dev/null +++ b/src/SLCore/Service/Taint/ListAllTaintsResponse.cs @@ -0,0 +1,25 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.SLCore.Common.Models; + +namespace SonarLint.VisualStudio.SLCore.Service.Taint; + +public record ListAllTaintsResponse(List taintVulnerabilities); diff --git a/src/IssueViz.Security/Taint/AnalysisInformation.cs b/src/SLCore/Service/Taint/TaintVulnerabilityTrackingSLCoreService.cs similarity index 67% rename from src/IssueViz.Security/Taint/AnalysisInformation.cs rename to src/SLCore/Service/Taint/TaintVulnerabilityTrackingSLCoreService.cs index 024f210816..2e377c0882 100644 --- a/src/IssueViz.Security/Taint/AnalysisInformation.cs +++ b/src/SLCore/Service/Taint/TaintVulnerabilityTrackingSLCoreService.cs @@ -18,20 +18,13 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; +using SonarLint.VisualStudio.SLCore.Core; +using SonarLint.VisualStudio.SLCore.Protocol; -namespace SonarLint.VisualStudio.IssueVisualization.Security.Taint -{ - internal class AnalysisInformation - { - public AnalysisInformation(string branchName, DateTimeOffset analysisTimestamp) - { - BranchName = branchName; - AnalysisTimestamp = analysisTimestamp; - } - - public string BranchName { get; } +namespace SonarLint.VisualStudio.SLCore.Service.Taint; - public DateTimeOffset AnalysisTimestamp { get; } - } +[JsonRpcClass("taintVulnerability")] +public interface ITaintVulnerabilityTrackingSlCoreService : ISLCoreService +{ + Task ListAllAsync(ListAllTaintsParams parameters); } diff --git a/src/SLCore/State/ActiveConfigScopeTracker.cs b/src/SLCore/State/ActiveConfigScopeTracker.cs index c874c22717..e3c553a71f 100644 --- a/src/SLCore/State/ActiveConfigScopeTracker.cs +++ b/src/SLCore/State/ActiveConfigScopeTracker.cs @@ -18,9 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; using System.ComponentModel.Composition; -using System.Diagnostics; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Synchronization; using SonarLint.VisualStudio.SLCore.Core; @@ -43,6 +41,8 @@ public interface IActiveConfigScopeTracker : IDisposable bool TryUpdateRootOnCurrentConfigScope(string id, string root); bool TryUpdateAnalysisReadinessOnCurrentConfigScope(string id, bool isReady); + + event EventHandler CurrentConfigurationScopeChanged; } public record ConfigurationScope( @@ -107,23 +107,26 @@ public void SetCurrentConfigScope(string id, string connectionId = null, string { configurationScopeService.DidUpdateBinding(new DidUpdateBindingParams(id, GetBinding(connectionId, sonarProjectKey))); currentConfigScope = currentConfigScope with { ConnectionId = connectionId, SonarProjectId = sonarProjectKey }; - return; } - - configurationScopeService.DidAddConfigurationScopes(new DidAddConfigurationScopesParams([ - new ConfigurationScopeDto(id, id, true, GetBinding(connectionId, sonarProjectKey))])); - currentConfigScope = new ConfigurationScope(id, connectionId, sonarProjectKey); + else + { + configurationScopeService.DidAddConfigurationScopes(new DidAddConfigurationScopesParams([ + new ConfigurationScopeDto(id, id, true, GetBinding(connectionId, sonarProjectKey))])); + currentConfigScope = new ConfigurationScope(id, connectionId, sonarProjectKey); + } } + + OnCurrentConfigurationScopeChanged(); } public void Reset() { threadHandling.ThrowIfOnUIThread(); - using (asyncLock.Acquire()) { currentConfigScope = null; } + OnCurrentConfigurationScopeChanged(); } public void RemoveCurrentConfigScope() @@ -146,6 +149,8 @@ public void RemoveCurrentConfigScope() new DidRemoveConfigurationScopeParams(currentConfigScope.Id)); currentConfigScope = null; } + + OnCurrentConfigurationScopeChanged(); } public bool TryUpdateRootOnCurrentConfigScope(string id, string root) @@ -158,8 +163,9 @@ public bool TryUpdateRootOnCurrentConfigScope(string id, string root) } currentConfigScope = currentConfigScope with { RootPath = root }; - return true; } + OnCurrentConfigurationScopeChanged(); + return true; } public bool TryUpdateAnalysisReadinessOnCurrentConfigScope(string id, bool isReady) @@ -172,10 +178,13 @@ public bool TryUpdateAnalysisReadinessOnCurrentConfigScope(string id, bool isRea } currentConfigScope = currentConfigScope with { isReadyForAnalysis = isReady}; - return true; } + OnCurrentConfigurationScopeChanged(); + return true; } + public event EventHandler CurrentConfigurationScopeChanged; + public void Dispose() { asyncLock?.Dispose(); @@ -184,4 +193,9 @@ public void Dispose() private BindingConfigurationDto GetBinding(string connectionId, string sonarProjectKey) => connectionId is not null ? new BindingConfigurationDto(connectionId, sonarProjectKey) : null; + + private void OnCurrentConfigurationScopeChanged() + { + CurrentConfigurationScopeChanged?.Invoke(this, EventArgs.Empty); + } } diff --git a/src/SonarQube.Client.Tests/Models/ServerSentEvents/SSEStreamReaderTests.cs b/src/SonarQube.Client.Tests/Models/ServerSentEvents/SSEStreamReaderTests.cs index 1950095dbb..5f0377f179 100644 --- a/src/SonarQube.Client.Tests/Models/ServerSentEvents/SSEStreamReaderTests.cs +++ b/src/SonarQube.Client.Tests/Models/ServerSentEvents/SSEStreamReaderTests.cs @@ -18,15 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Newtonsoft.Json; -using SonarQube.Client.Api.Common; using SonarQube.Client.Logging; -using SonarQube.Client.Models; using SonarQube.Client.Models.ServerSentEvents; using SonarQube.Client.Models.ServerSentEvents.ClientContract; using SonarQube.Client.Models.ServerSentEvents.ServerContract; @@ -138,99 +132,6 @@ public async Task ReadAsync_IssueChangedEventType_DeserializedEvent() issues: new[] { new BranchAndIssueKey("key1", "master") })); } - [TestMethod] - public async Task ReadAsync_TaintVulnerabilityClosedEventType_DeserializedEvent() - { - const string serializedTaintVulnerabilityClosedEvent = - "{\"projectKey\": \"projectKey1\",\"key\": \"taintKey\"}"; - - var sqSSEStreamReader = CreateSqStreamReader(new SqServerEvent("TaintVulnerabilityClosed", serializedTaintVulnerabilityClosedEvent)); - - var testSubject = CreateTestSubject(sqSSEStreamReader); - - var result = await testSubject.ReadAsync(); - - result.Should().NotBeNull(); - result.Should().BeOfType(); - result.Should().BeEquivalentTo( - new TaintVulnerabilityClosedServerEvent( - projectKey: "projectKey1", - key: "taintKey")); - } - - [TestMethod] - public async Task ReadAsync_TaintVulnerabilityRaisedEventType_DeserializedEvent() - { - const string serializedTaintVulnerabilityRaisedEvent = @"{ - ""key"": ""taintKey"", - ""projectKey"": ""projectKey1"", - ""creationDate"": 1676390804000, - ""branch"": ""master"", - ""ruleKey"": ""javasecurity:S123"", - ""severity"": ""MAJOR"", - ""type"": ""VULNERABILITY"", - ""mainLocation"": { - ""filePath"": ""functions/taint.js"", - ""message"": ""blah blah"", - ""textRange"": { - ""startLine"": 17, - ""startLineOffset"": 10, - ""endLine"": 3, - ""endLineOffset"": 2, - ""hash"": ""hash"" - } - }, - ""flows"": [ - { - ""locations"": [ - { - ""filePath"": ""functions/taint.js"", - ""message"": ""sink"" - } - ] - } - ], - ""impacts"": [ - { - ""softwareQuality"": ""SECURITY"", - ""severity"": ""HIGH"" - } - ], - ""ruleDescriptionContextKey"": ""ContextKey"" -}"; - var sqSSEStreamReader = CreateSqStreamReader(new SqServerEvent("TaintVulnerabilityRaised", serializedTaintVulnerabilityRaisedEvent)); - - var testSubject = CreateTestSubject(sqSSEStreamReader); - - var result = await testSubject.ReadAsync(); - - result.Should().NotBeNull(); - result.Should().BeOfType(); - result.Should().BeEquivalentTo( - new TaintVulnerabilityRaisedServerEvent( - projectKey: "projectKey1", - key: "taintKey", - creationDate: DateTimeOffset.Parse("2023-02-14T16:06:44+00:00"), - branch: "master", - ruleKey: "javasecurity:S123", - severity: SonarQubeIssueSeverity.Major, - type: SonarQubeIssueType.Vulnerability, - impacts: new []{new ServerImpact{SoftwareQuality = "SECURITY", Severity = "HIGH"}}, - mainLocation: - new Location( - filePath: "functions/taint.js", - message: "blah blah", - textRange: new TextRange(17, 10, 3, 2, "hash")), - flows: new[] - { - new Flow(new[] - { - new Location(filePath: "functions/taint.js", message: "sink", textRange: null) - }) - }, - ruleDescriptionContextKey: "ContextKey")); - } - [TestMethod] public async Task ReadAsync_QualityProfileEventType_DeserializedEvent() { @@ -250,7 +151,7 @@ public async Task ReadAsync_QualityProfileEventType_DeserializedEvent() "deactivatedRules": [] } """; - + var sqSSEStreamReader = CreateSqStreamReader(new SqServerEvent("RuleSetChanged", serializedQualityProfileEvent)); var testSubject = CreateTestSubject(sqSSEStreamReader); diff --git a/src/SonarQube.Client.Tests/Models/ServerSentEvents/TaintVulnerabilityClosedServerEventTests.cs b/src/SonarQube.Client.Tests/Models/ServerSentEvents/TaintVulnerabilityClosedServerEventTests.cs deleted file mode 100644 index 7a8ac9b857..0000000000 --- a/src/SonarQube.Client.Tests/Models/ServerSentEvents/TaintVulnerabilityClosedServerEventTests.cs +++ /dev/null @@ -1,47 +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 FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarQube.Client.Tests.Models.ServerSentEvents -{ - [TestClass] - public class TaintVulnerabilityClosedServerEventTests - { - [TestMethod] - public void Ctor_InvalidProjectKey_Throws() - { - Action act = () => { new TaintVulnerabilityClosedServerEvent(null, "id1"); }; - - act.Should().ThrowExactly().And.ParamName.Should().Be("projectKey"); - } - - [TestMethod] - public void Ctor_InvalidKey_Throws() - { - Action act = () => { new TaintVulnerabilityClosedServerEvent("MyProject", null); }; - - act.Should().ThrowExactly().And.ParamName.Should().Be("key"); - } - } -} diff --git a/src/SonarQube.Client.Tests/Models/ServerSentEvents/TaintVulnerabilityRaisedServerEventTests.cs b/src/SonarQube.Client.Tests/Models/ServerSentEvents/TaintVulnerabilityRaisedServerEventTests.cs deleted file mode 100644 index 68a26a752a..0000000000 --- a/src/SonarQube.Client.Tests/Models/ServerSentEvents/TaintVulnerabilityRaisedServerEventTests.cs +++ /dev/null @@ -1,179 +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.Collections.Generic; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using Newtonsoft.Json; -using SonarQube.Client.Models; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarQube.Client.Tests.Models.ServerSentEvents -{ - [TestClass] - public class TaintVulnerabilityRaisedServerEventTests - { - [TestMethod] - public void Ctor_InvalidProjectKey_Throws() - { - Action act = () => { new TaintVulnerabilityRaisedServerEvent(null, "id1", "main", Mock.Of()); }; - - act.Should().ThrowExactly().And.ParamName.Should().Be("projectKey"); - } - - [TestMethod] - public void Ctor_InvalidKey_Throws() - { - Action act = () => { new TaintVulnerabilityRaisedServerEvent("MyProject", null, "main", Mock.Of()); }; - - act.Should().ThrowExactly().And.ParamName.Should().Be("key"); - } - - [TestMethod] - public void Ctor_InvalidBranch_Throws() - { - Action act = () => { new TaintVulnerabilityRaisedServerEvent("MyProject", "id1", null, Mock.Of()); }; - - act.Should().ThrowExactly().And.ParamName.Should().Be("branch"); - } - - [TestMethod] - public void Ctor_InvalidIssue_Throws() - { - Action act = () => { new TaintVulnerabilityRaisedServerEvent("MyProject", "id1", "branch", null); }; - - act.Should().ThrowExactly().And.ParamName.Should().Be("issue"); - } - - [TestMethod] - public void Deserialize_ServerEventDeserializedCorrectly() - { - const string serializedTaintVulnerabilityRaisedEvent = @"{ - ""key"": ""taintKey"", - ""projectKey"": ""projectKey1"", - ""branch"": ""master"", - ""creationDate"": 1676390804000, - ""ruleKey"": ""javasecurity:S123"", - ""severity"": ""MAJOR"", - ""type"": ""VULNERABILITY"", - ""mainLocation"": { - ""filePath"": ""functions/taint.js"", - ""message"": ""blah blah"", - ""textRange"": { - ""startLine"": 17, - ""startLineOffset"": 10, - ""endLine"": 3, - ""endLineOffset"": 2, - ""hash"": ""hash"" - } - }, - ""flows"": [ - { - ""locations"": [ - { - ""filePath"": ""functions/taint.js"", - ""message"": ""sink: tainted value is used to perform a security-sensitive operation"", - ""textRange"": { - ""startLine"": 15, - ""startLineOffset"": 14, - ""endLine"": 6, - ""endLineOffset"": 7, - ""hash"": ""hash1"" - } - }, - { - ""filePath"": ""functions/taint2.js"", - ""message"": ""sink: tainted value is used to perform a security-sensitive operation"", - ""textRange"": { - ""startLine"": 18, - ""startLineOffset"": 11, - ""endLine"": 4, - ""endLineOffset"": 3, - ""hash"": ""hash2"" - } - } - ] - } - ], - ""impacts"": [ - { - ""softwareQuality"": ""SECURITY"", - ""severity"": ""HIGH"" - } - ] -}"; - - var expectedDeserializedEvent = new TaintVulnerabilityRaisedServerEvent( - - projectKey: "projectKey1", - key: "taintKey", - branch: "master", - issue: new TaintIssue( - key: "taintKey", - ruleKey: "javasecurity:S123", - creationDate: DateTimeOffset.Parse("2023-02-14T16:06:44+00:00"), - severity: SonarQubeIssueSeverity.Major, - type: SonarQubeIssueType.Vulnerability, - defaultImpacts: new Dictionary{ - { SonarQubeSoftwareQuality.Security, SonarQubeSoftwareQualitySeverity.High}}, - flows: new[] - { - new Flow(locations: new[] - { - new Location( - message: "sink: tainted value is used to perform a security-sensitive operation", - filePath: "functions/taint.js", - textRange: new TextRange( - startLine: 15, - startLineOffset: 14, - endLine: 6, - endLineOffset: 7, - hash: "hash1")), - new Location( - message: "sink: tainted value is used to perform a security-sensitive operation", - filePath: "functions/taint2.js", - textRange: new TextRange( - startLine: 18, - startLineOffset: 11, - endLine: 4, - endLineOffset: 3, - hash: "hash2")) - }) - }, - mainLocation: new Location( - message: "blah blah", - filePath: "functions/taint.js", - textRange: new TextRange( - startLine: 17, - startLineOffset: 10, - endLine: 3, - endLineOffset: 2, - hash: "hash")), - context: null - )); - - var result = JsonConvert.DeserializeObject(serializedTaintVulnerabilityRaisedEvent); - - result.Should().BeEquivalentTo(expectedDeserializedEvent); - } - } -} diff --git a/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesRequestWrapperTests.cs b/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesRequestWrapperTests.cs index fb3465b55f..963b2cd670 100644 --- a/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesRequestWrapperTests.cs +++ b/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesRequestWrapperTests.cs @@ -18,142 +18,135 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using SonarQube.Client.Api; using SonarQube.Client.Api.V7_20; using SonarQube.Client.Tests.Infra; using static SonarQube.Client.Tests.Infra.MocksHelper; -namespace SonarQube.Client.Tests.Requests.Api.V7_20 +namespace SonarQube.Client.Tests.Requests.Api.V7_20; + +[TestClass] +public class GetIssuesRequestWrapperTests { - [TestClass] - public class GetIssuesRequestWrapperTests + private const string ComponentPropertyNameSonarQube = "components"; + private const string ComponentPropertyNameSonarCloud = "componentKeys"; + + [DataTestMethod] + [DataRow(ComponentPropertyNameSonarQube, DisplayName = "SonarQube")] + [DataRow(ComponentPropertyNameSonarCloud, DisplayName = "SonarCloud")] + public async Task InvokeAsync_NoIssueKeys_ExpectedPropertiesArePassedInMultipleRequests(string componentPropertyName) { - private const string ComponentPropertyNameSonarQube = "components"; - private const string ComponentPropertyNameSonarCloud = "componentKeys"; - - [DataTestMethod] - [DataRow(ComponentPropertyNameSonarQube, DisplayName = "SonarQube")] - [DataRow(ComponentPropertyNameSonarCloud, DisplayName = "SonarCloud")] - public async Task InvokeAsync_NoIssueKeys_ExpectedPropertiesArePassedInMultipleRequests(string componentPropertyName) + var testSubject = CreateTestSubject(componentPropertyName, "aaaProject", "xStatus", "yBranch", null, "rule1", "component1"); + + var handlerMock = new Mock(MockBehavior.Strict); + var httpClient = new HttpClient(handlerMock.Object) { - var testSubject = CreateTestSubject(componentPropertyName, "aaaProject", "xStatus", "yBranch", null, "rule1", "component1"); + BaseAddress = new Uri(ValidBaseAddress) + }; - var handlerMock = new Mock(MockBehavior.Strict); - var httpClient = new HttpClient(handlerMock.Object) - { - BaseAddress = new Uri(ValidBaseAddress) - }; + SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); + _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); + // The wrapper is expected to make three calls, for code smells, bugs, then vulnerabilities + handlerMock.Invocations.Count.Should().Be(2); + CheckExpectedQueryStringsParameters(componentPropertyName, handlerMock, 0, expectedTypes: "CODE_SMELL"); + CheckExpectedQueryStringsParameters(componentPropertyName, handlerMock, 1, expectedTypes: "BUG"); + } - // The wrapper is expected to make three calls, for code smells, bugs, then vulnerabilities - handlerMock.Invocations.Count.Should().Be(3); - CheckExpectedQueryStringsParameters(componentPropertyName, handlerMock, 0, expectedTypes: "CODE_SMELL"); - CheckExpectedQueryStringsParameters(componentPropertyName, handlerMock, 1, expectedTypes: "BUG"); - CheckExpectedQueryStringsParameters(componentPropertyName, handlerMock, 2, expectedTypes: "VULNERABILITY"); - } + [DataTestMethod] + [DataRow(ComponentPropertyNameSonarQube, DisplayName = "SonarQube")] + [DataRow(ComponentPropertyNameSonarCloud, DisplayName = "SonarCloud")] + public async Task InvokeAsync_HasIssueKeys_ExpectedPropertiesArePassedInASingleRequest(string componentPropertyName) + { + var issueKeys = new[] { "issue1", "issue2" }; + var testSubject = CreateTestSubject(componentPropertyName,"aaaProject", "xStatus", "yBranch", issueKeys, "rule1", "component1"); - [DataTestMethod] - [DataRow(ComponentPropertyNameSonarQube, DisplayName = "SonarQube")] - [DataRow(ComponentPropertyNameSonarCloud, DisplayName = "SonarCloud")] - public async Task InvokeAsync_HasIssueKeys_ExpectedPropertiesArePassedInASingleRequest(string componentPropertyName) + var handlerMock = new Mock(MockBehavior.Strict); + var httpClient = new HttpClient(handlerMock.Object) { - var issueKeys = new[] { "issue1", "issue2" }; - var testSubject = CreateTestSubject(componentPropertyName,"aaaProject", "xStatus", "yBranch", issueKeys, "rule1", "component1"); + BaseAddress = new Uri(ValidBaseAddress) + }; - var handlerMock = new Mock(MockBehavior.Strict); - var httpClient = new HttpClient(handlerMock.Object) - { - BaseAddress = new Uri(ValidBaseAddress) - }; + SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); + _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); + // The wrapper is expected to make one call with the given issueKeys + handlerMock.Invocations.Count.Should().Be(1); - // The wrapper is expected to make one call with the given issueKeys - handlerMock.Invocations.Count.Should().Be(1); - - CheckExpectedQueryStringsParameters(componentPropertyName, handlerMock, 0, expectedKeys: issueKeys); - } - - private static IGetIssuesRequest CreateTestSubject(string componentPropertyName, string projectKey, string statusesToRequest, string branch, string[] issueKeys, string ruleId, string componentKey) - { - return componentPropertyName switch - { - ComponentPropertyNameSonarQube => new GetIssuesRequestWrapper - { - Logger = new TestLogger(), - ProjectKey = projectKey, - Statuses = statusesToRequest, - Branch = branch, - IssueKeys = issueKeys, - RuleId = ruleId, - ComponentKey = componentKey - }, - ComponentPropertyNameSonarCloud => new GetIssuesRequestWrapper - { - Logger = new TestLogger(), - ProjectKey = projectKey, - Statuses = statusesToRequest, - Branch = branch, - IssueKeys = issueKeys, - RuleId = ruleId, - ComponentKey = componentKey - }, - _ => throw new ArgumentOutOfRangeException() - }; - } + CheckExpectedQueryStringsParameters(componentPropertyName, handlerMock, 0, expectedKeys: issueKeys); + } - private static void CheckExpectedQueryStringsParameters(string componentKeyName, - Mock handlerMock, - int invocationIndex, - string expectedTypes = null, - string[] expectedKeys = null) + private static IGetIssuesRequest CreateTestSubject(string componentPropertyName, string projectKey, string statusesToRequest, string branch, string[] issueKeys, string ruleId, string componentKey) + { + return componentPropertyName switch { - var actualQueryString = GetActualQueryStringForInvocation(handlerMock, invocationIndex); - - Console.WriteLine($"Invocation [{invocationIndex}]: {actualQueryString}"); - actualQueryString.Contains($"?{componentKeyName}=component1").Should().BeTrue(); - actualQueryString.Contains("&projects=aaaProject").Should().BeTrue(); - actualQueryString.Contains("&statuses=xStatus").Should().BeTrue(); - actualQueryString.Contains("&branch=yBranch").Should().BeTrue(); - actualQueryString.Contains("&rules=rule1").Should().BeTrue(); - - if (expectedTypes != null) + ComponentPropertyNameSonarQube => new GetIssuesRequestWrapper { - actualQueryString.Contains($"&types={expectedTypes}").Should().BeTrue(); - } - else + Logger = new TestLogger(), + ProjectKey = projectKey, + Statuses = statusesToRequest, + Branch = branch, + IssueKeys = issueKeys, + RuleId = ruleId, + ComponentKey = componentKey + }, + ComponentPropertyNameSonarCloud => new GetIssuesRequestWrapper { - actualQueryString.Contains("types").Should().BeFalse(); - } + Logger = new TestLogger(), + ProjectKey = projectKey, + Statuses = statusesToRequest, + Branch = branch, + IssueKeys = issueKeys, + RuleId = ruleId, + ComponentKey = componentKey + }, + _ => throw new ArgumentOutOfRangeException() + }; + } - if (expectedKeys != null) - { - var keys = string.Join("%2C", expectedKeys); - actualQueryString.Contains($"&issues={keys}").Should().BeTrue(); - } - else - { - actualQueryString.Contains("issues").Should().BeFalse(); - } + private static void CheckExpectedQueryStringsParameters(string componentKeyName, + Mock handlerMock, + int invocationIndex, + string expectedTypes = null, + string[] expectedKeys = null) + { + var actualQueryString = GetActualQueryStringForInvocation(handlerMock, invocationIndex); + + Console.WriteLine($"Invocation [{invocationIndex}]: {actualQueryString}"); + actualQueryString.Contains($"?{componentKeyName}=component1").Should().BeTrue(); + actualQueryString.Contains("&projects=aaaProject").Should().BeTrue(); + actualQueryString.Contains("&statuses=xStatus").Should().BeTrue(); + actualQueryString.Contains("&branch=yBranch").Should().BeTrue(); + actualQueryString.Contains("&rules=rule1").Should().BeTrue(); + if (expectedTypes != null) + { + actualQueryString.Contains($"&types={expectedTypes}").Should().BeTrue(); + } + else + { + actualQueryString.Contains("types").Should().BeFalse(); } - private static string GetActualQueryStringForInvocation(Mock handlerMock, int invocationIndex) + if (expectedKeys != null) + { + var keys = string.Join("%2C", expectedKeys); + actualQueryString.Contains($"&issues={keys}").Should().BeTrue(); + } + else { - var requestMessage = (HttpRequestMessage)handlerMock.Invocations[invocationIndex].Arguments[0]; - return requestMessage.RequestUri.Query; + actualQueryString.Contains("issues").Should().BeFalse(); } + + } + + private static string GetActualQueryStringForInvocation(Mock handlerMock, int invocationIndex) + { + var requestMessage = (HttpRequestMessage)handlerMock.Invocations[invocationIndex].Arguments[0]; + return requestMessage.RequestUri.Query; } } diff --git a/src/SonarQube.Client.Tests/Requests/DefaultConfiguration_Configure_Tests.cs b/src/SonarQube.Client.Tests/Requests/DefaultConfiguration_Configure_Tests.cs index 2405cbe246..59ade660a9 100644 --- a/src/SonarQube.Client.Tests/Requests/DefaultConfiguration_Configure_Tests.cs +++ b/src/SonarQube.Client.Tests/Requests/DefaultConfiguration_Configure_Tests.cs @@ -18,199 +18,188 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Diagnostics; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; using SonarQube.Client.Api; using SonarQube.Client.Requests; using SonarQube.Client.Tests.Infra; -namespace SonarQube.Client.Tests.Requests +namespace SonarQube.Client.Tests.Requests; + +[TestClass] +public class DefaultConfiguration_Configure_Tests { - [TestClass] - public class DefaultConfiguration_Configure_Tests + [TestMethod] + public void ConfigureSonarQube_Writes_Debug_Messages() { - [TestMethod] - public void ConfigureSonarQube_Writes_Debug_Messages() - { - var logger = new TestLogger(); - - var expected = new[] - { - "Registered SonarQube.Client.Api.V2_10.GetPluginsRequest for 2.1", - "Registered SonarQube.Client.Api.V2_10.GetProjectsRequest for 2.1", - "Registered SonarQube.Client.Api.V2_10.GetVersionRequest for 2.1", - "Registered SonarQube.Client.Api.V2_60.GetPropertiesRequest for 2.6", - "Registered SonarQube.Client.Api.V3_30.ValidateCredentialsRequest for 3.3", - "Registered SonarQube.Client.Api.V5_00.GetSourceCodeRequest for 5.0", - "Registered SonarQube.Client.Api.V5_10.GetIssuesRequest for 5.1", - "Registered SonarQube.Client.Api.V5_10.GetLanguagesRequest for 5.1", - "Registered SonarQube.Client.Api.V5_20.GetQualityProfileChangeLogRequest for 5.2", - "Registered SonarQube.Client.Api.V5_20.GetQualityProfilesRequest for 5.2", - "Registered SonarQube.Client.Api.V5_20.GetRoslynExportProfileRequest for 5.2", - "Registered SonarQube.Client.Api.V5_40.GetModulesRequest for 5.4", - "Registered SonarQube.Client.Api.V5_50.GetRulesRequest for 5.5", - "Registered SonarQube.Client.Api.V5_50.DownloadStaticFile for 5.5", - "Registered SonarQube.Client.Api.V6_20.GetOrganizationsRequest for 6.2", - "Registered SonarQube.Client.Api.V6_20.GetProjectsRequest for 6.2", - "Registered SonarQube.Client.Api.V6_30.GetPluginsRequest for 6.3", - "Registered SonarQube.Client.Api.V6_30.GetPropertiesRequest for 6.3", - "Registered SonarQube.Client.Api.V6_50.GetQualityProfileChangeLogRequest for 6.5", - "Registered SonarQube.Client.Api.V6_50.GetQualityProfilesRequest for 6.5", - "Registered SonarQube.Client.Api.V6_60.GetNotificationsRequest for 6.6", - "Registered SonarQube.Client.Api.V6_60.GetRoslynExportProfileRequest for 6.6", - "Registered SonarQube.Client.Api.V6_60.GetProjectBranchesRequest for 6.6", - "Registered SonarQube.Client.Api.V7_00.GetOrganizationsRequest for 7.0", - "Registered SonarQube.Client.Api.V7_20.GetIssuesRequestWrapper`1[SonarQube.Client.Api.V7_20.GetIssuesWithComponentSonarQubeRequest] for 7.2", - "Registered SonarQube.Client.Api.V8_6.GetHotspotRequest for 8.6", - "Registered SonarQube.Client.Api.V8_6.GetTaintVulnerabilitiesRequest for 8.6", - "Registered SonarQube.Client.Api.V7_20.GetExclusionsRequest for 7.2", - "Registered SonarQube.Client.Api.V9_4.GetSonarLintEventStream for 9.4", - "Registered SonarQube.Client.Api.V9_5.GetRulesWithDescriptionSectionsRequest for 9.5", - "Registered SonarQube.Client.Api.V9_6.GetRulesWithEducationPrinciplesRequest for 9.6", - "Registered SonarQube.Client.Api.V9_6.GetTaintVulnerabilitiesWithContextRequest for 9.6", - "Registered SonarQube.Client.Api.V9_7.SearchHotspotRequest for 9.7", - "Registered SonarQube.Client.Api.V10_2.SearchHotspotRequest for 10.2", - "Registered SonarQube.Client.Api.V10_2.GetTaintVulnerabilitiesWithCCTRequest for 10.2", - "Registered SonarQube.Client.Api.V10_2.GetRulesWithCCTRequest for 10.2", - "Registered SonarQube.Client.Api.V9_9.TransitionIssueRequestWithWontFix for 9.9", - "Registered SonarQube.Client.Api.V10_4.TransitionIssueRequestWithAccept for 10.4", - "Registered SonarQube.Client.Api.V9_9.CommentIssueRequest for 9.9", - "Registered SonarQube.Client.Api.V9_9.SearchFilesByNameRequest for 9.9" - }; - - DefaultConfiguration.ConfigureSonarQube(new RequestFactory(logger)); - - DumpDebugMessages(logger); - - logger.DebugMessages.Should().ContainInOrder(expected); - logger.DebugMessages.Count.Should().Be(expected.Length); - } + var logger = new TestLogger(); - [TestMethod] - public void ConfigureSonarCloud_Writes_Debug_Messages() + var expected = new[] { - var logger = new TestLogger(); - - var expected = new string[] - { - "Registered SonarQube.Client.Api.V2_10.GetVersionRequest", - "Registered SonarQube.Client.Api.V3_30.ValidateCredentialsRequest", - "Registered SonarQube.Client.Api.V5_00.GetSourceCodeRequest", - "Registered SonarQube.Client.Api.V5_10.GetLanguagesRequest", - "Registered SonarQube.Client.Api.V5_40.GetModulesRequest", - "Registered SonarQube.Client.Api.V10_2.GetRulesWithCCTRequest", - "Registered SonarQube.Client.Api.V5_50.DownloadStaticFile", - "Registered SonarQube.Client.Api.V6_20.GetProjectsRequest", - "Registered SonarQube.Client.Api.V6_30.GetPluginsRequest", - "Registered SonarQube.Client.Api.V6_30.GetPropertiesRequest", - "Registered SonarQube.Client.Api.V6_50.GetQualityProfileChangeLogRequest", - "Registered SonarQube.Client.Api.V6_50.GetQualityProfilesRequest", - "Registered SonarQube.Client.Api.V6_60.GetNotificationsRequest", - "Registered SonarQube.Client.Api.V6_60.GetRoslynExportProfileRequest", - "Registered SonarQube.Client.Api.V6_60.GetProjectBranchesRequest", - "Registered SonarQube.Client.Api.V7_00.GetOrganizationsRequest", - "Registered SonarQube.Client.Api.V7_20.GetIssuesRequestWrapper`1[SonarQube.Client.Api.V7_20.GetIssuesWithComponentSonarCloudRequest]", - "Registered SonarQube.Client.Api.V8_6.GetHotspotRequest", - "Registered SonarQube.Client.Api.V10_2.GetTaintVulnerabilitiesWithCCTRequest", - "Registered SonarQube.Client.Api.V7_20.GetExclusionsRequest", - "Registered SonarQube.Client.Api.V9_7.SearchHotspotRequest", - "Registered SonarQube.Client.Api.V10_4.TransitionIssueRequestWithAccept", - "Registered SonarQube.Client.Api.V9_9.CommentIssueRequest", - "Registered SonarQube.Client.Api.V9_9.SearchFilesByNameRequest" - }; - - DefaultConfiguration.ConfigureSonarCloud(new UnversionedRequestFactory(logger)); - - DumpDebugMessages(logger); - - logger.DebugMessages.Should().ContainInOrder(expected); - logger.DebugMessages.Count.Should().Be(expected.Length); - } + "Registered SonarQube.Client.Api.V2_10.GetPluginsRequest for 2.1", + "Registered SonarQube.Client.Api.V2_10.GetProjectsRequest for 2.1", + "Registered SonarQube.Client.Api.V2_10.GetVersionRequest for 2.1", + "Registered SonarQube.Client.Api.V2_60.GetPropertiesRequest for 2.6", + "Registered SonarQube.Client.Api.V3_30.ValidateCredentialsRequest for 3.3", + "Registered SonarQube.Client.Api.V5_00.GetSourceCodeRequest for 5.0", + "Registered SonarQube.Client.Api.V5_10.GetIssuesRequest for 5.1", + "Registered SonarQube.Client.Api.V5_10.GetLanguagesRequest for 5.1", + "Registered SonarQube.Client.Api.V5_20.GetQualityProfileChangeLogRequest for 5.2", + "Registered SonarQube.Client.Api.V5_20.GetQualityProfilesRequest for 5.2", + "Registered SonarQube.Client.Api.V5_20.GetRoslynExportProfileRequest for 5.2", + "Registered SonarQube.Client.Api.V5_40.GetModulesRequest for 5.4", + "Registered SonarQube.Client.Api.V5_50.GetRulesRequest for 5.5", + "Registered SonarQube.Client.Api.V5_50.DownloadStaticFile for 5.5", + "Registered SonarQube.Client.Api.V6_20.GetOrganizationsRequest for 6.2", + "Registered SonarQube.Client.Api.V6_20.GetProjectsRequest for 6.2", + "Registered SonarQube.Client.Api.V6_30.GetPluginsRequest for 6.3", + "Registered SonarQube.Client.Api.V6_30.GetPropertiesRequest for 6.3", + "Registered SonarQube.Client.Api.V6_50.GetQualityProfileChangeLogRequest for 6.5", + "Registered SonarQube.Client.Api.V6_50.GetQualityProfilesRequest for 6.5", + "Registered SonarQube.Client.Api.V6_60.GetNotificationsRequest for 6.6", + "Registered SonarQube.Client.Api.V6_60.GetRoslynExportProfileRequest for 6.6", + "Registered SonarQube.Client.Api.V6_60.GetProjectBranchesRequest for 6.6", + "Registered SonarQube.Client.Api.V7_00.GetOrganizationsRequest for 7.0", + "Registered SonarQube.Client.Api.V7_20.GetIssuesRequestWrapper`1[SonarQube.Client.Api.V7_20.GetIssuesWithComponentSonarQubeRequest] for 7.2", + "Registered SonarQube.Client.Api.V8_6.GetHotspotRequest for 8.6", + "Registered SonarQube.Client.Api.V7_20.GetExclusionsRequest for 7.2", + "Registered SonarQube.Client.Api.V9_4.GetSonarLintEventStream for 9.4", + "Registered SonarQube.Client.Api.V9_5.GetRulesWithDescriptionSectionsRequest for 9.5", + "Registered SonarQube.Client.Api.V9_6.GetRulesWithEducationPrinciplesRequest for 9.6", + "Registered SonarQube.Client.Api.V9_7.SearchHotspotRequest for 9.7", + "Registered SonarQube.Client.Api.V10_2.SearchHotspotRequest for 10.2", + "Registered SonarQube.Client.Api.V10_2.GetRulesWithCCTRequest for 10.2", + "Registered SonarQube.Client.Api.V9_9.TransitionIssueRequestWithWontFix for 9.9", + "Registered SonarQube.Client.Api.V10_4.TransitionIssueRequestWithAccept for 10.4", + "Registered SonarQube.Client.Api.V9_9.CommentIssueRequest for 9.9", + "Registered SonarQube.Client.Api.V9_9.SearchFilesByNameRequest for 9.9" + }; + + DefaultConfiguration.ConfigureSonarQube(new RequestFactory(logger)); + + DumpDebugMessages(logger); + + logger.DebugMessages.Should().ContainInOrder(expected); + logger.DebugMessages.Count.Should().Be(expected.Length); + } - [TestMethod] - public void ConfigureSonarQube_CheckAllRequestsImplemented() - { - var testSubject = DefaultConfiguration.ConfigureSonarQube(new RequestFactory(new TestLogger())); - var serverInfo = new ServerInfo(null /* latest */, ServerType.SonarQube); - - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - } + [TestMethod] + public void ConfigureSonarCloud_Writes_Debug_Messages() + { + var logger = new TestLogger(); - [TestMethod] - public void ConfigureSonarCloud_CheckAllRequestsImplemented() + var expected = new string[] { - var testSubject = DefaultConfiguration.ConfigureSonarCloud(new UnversionedRequestFactory(new TestLogger())); - var serverInfo = new ServerInfo(null /* latest */, ServerType.SonarQube); - - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - } + "Registered SonarQube.Client.Api.V2_10.GetVersionRequest", + "Registered SonarQube.Client.Api.V3_30.ValidateCredentialsRequest", + "Registered SonarQube.Client.Api.V5_00.GetSourceCodeRequest", + "Registered SonarQube.Client.Api.V5_10.GetLanguagesRequest", + "Registered SonarQube.Client.Api.V5_40.GetModulesRequest", + "Registered SonarQube.Client.Api.V10_2.GetRulesWithCCTRequest", + "Registered SonarQube.Client.Api.V5_50.DownloadStaticFile", + "Registered SonarQube.Client.Api.V6_20.GetProjectsRequest", + "Registered SonarQube.Client.Api.V6_30.GetPluginsRequest", + "Registered SonarQube.Client.Api.V6_30.GetPropertiesRequest", + "Registered SonarQube.Client.Api.V6_50.GetQualityProfileChangeLogRequest", + "Registered SonarQube.Client.Api.V6_50.GetQualityProfilesRequest", + "Registered SonarQube.Client.Api.V6_60.GetNotificationsRequest", + "Registered SonarQube.Client.Api.V6_60.GetRoslynExportProfileRequest", + "Registered SonarQube.Client.Api.V6_60.GetProjectBranchesRequest", + "Registered SonarQube.Client.Api.V7_00.GetOrganizationsRequest", + "Registered SonarQube.Client.Api.V7_20.GetIssuesRequestWrapper`1[SonarQube.Client.Api.V7_20.GetIssuesWithComponentSonarCloudRequest]", + "Registered SonarQube.Client.Api.V8_6.GetHotspotRequest", + "Registered SonarQube.Client.Api.V7_20.GetExclusionsRequest", + "Registered SonarQube.Client.Api.V9_7.SearchHotspotRequest", + "Registered SonarQube.Client.Api.V10_4.TransitionIssueRequestWithAccept", + "Registered SonarQube.Client.Api.V9_9.CommentIssueRequest", + "Registered SonarQube.Client.Api.V9_9.SearchFilesByNameRequest" + }; + + DefaultConfiguration.ConfigureSonarCloud(new UnversionedRequestFactory(logger)); + + DumpDebugMessages(logger); + + logger.DebugMessages.Should().ContainInOrder(expected); + logger.DebugMessages.Count.Should().Be(expected.Length); + } - [TestMethod] - [Description("The following APIs are not implemented on SC (yet). Verify that they are not registered in the factory.")] - public void ConfigureSonarCloud_CheckUnsupportedRequestsAreNotImplemented() - { - var testSubject = DefaultConfiguration.ConfigureSonarCloud(new UnversionedRequestFactory(new TestLogger())); - var serverInfo = new ServerInfo(null /* latest */, ServerType.SonarQube); + [TestMethod] + public void ConfigureSonarQube_CheckAllRequestsImplemented() + { + var testSubject = DefaultConfiguration.ConfigureSonarQube(new RequestFactory(new TestLogger())); + var serverInfo = new ServerInfo(null /* latest */, ServerType.SonarQube); + + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + } - Action act = () => testSubject.Create(serverInfo); - act.Should().Throw().And.Message.Should() - .Be("Could not find factory for 'IGetSonarLintEventStream'."); - } + [TestMethod] + public void ConfigureSonarCloud_CheckAllRequestsImplemented() + { + var testSubject = DefaultConfiguration.ConfigureSonarCloud(new UnversionedRequestFactory(new TestLogger())); + var serverInfo = new ServerInfo(null /* latest */, ServerType.SonarQube); + + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + testSubject.Create(serverInfo).Should().NotBeNull(); + } - private static void DumpDebugMessages(TestLogger logger) + [TestMethod] + [Description("The following APIs are not implemented on SC (yet). Verify that they are not registered in the factory.")] + public void ConfigureSonarCloud_CheckUnsupportedRequestsAreNotImplemented() + { + var testSubject = DefaultConfiguration.ConfigureSonarCloud(new UnversionedRequestFactory(new TestLogger())); + var serverInfo = new ServerInfo(null /* latest */, ServerType.SonarQube); + + Action act = () => testSubject.Create(serverInfo); + act.Should().Throw().And.Message.Should() + .Be("Could not find factory for 'IGetSonarLintEventStream'."); + } + + private static void DumpDebugMessages(TestLogger logger) + { + Debug.WriteLine("Actual registered requests:"); + foreach (var message in logger.DebugMessages) { - Debug.WriteLine("Actual registered requests:"); - foreach (var message in logger.DebugMessages) - { - Debug.WriteLine(message); - } + Debug.WriteLine(message); } } } diff --git a/src/SonarQube.Client.Tests/SonarQubeService_GetSuppressedIssuesAsync.cs b/src/SonarQube.Client.Tests/SonarQubeService_GetSuppressedIssuesAsync.cs index e5fc75e2d2..e0c0bb9fc0 100644 --- a/src/SonarQube.Client.Tests/SonarQubeService_GetSuppressedIssuesAsync.cs +++ b/src/SonarQube.Client.Tests/SonarQubeService_GetSuppressedIssuesAsync.cs @@ -18,66 +18,59 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Collections.Generic; using System.IO; -using System.Linq; using System.Net; using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using SonarQube.Client.Models; using static SonarQube.Client.Tests.Infra.MocksHelper; -namespace SonarQube.Client.Tests +namespace SonarQube.Client.Tests; + +[TestClass] +public class SonarQubeService_GetSuppressedIssuesAsync : SonarQubeService_GetIssuesBase { - [TestClass] - public class SonarQubeService_GetSuppressedIssuesAsync : SonarQubeService_GetIssuesBase + [TestMethod] + public async Task GetSuppressedIssuesAsync_Old_ExampleFromSonarQube() { - [TestMethod] - public async Task GetSuppressedIssuesAsync_Old_ExampleFromSonarQube() - { - await ConnectToSonarQube(); + await ConnectToSonarQube(); - using (var reader = new StreamReader(@"TestResources\IssuesProtobufResponse")) - { - SetupRequest("batch/issues?key=project1", reader.ReadToEnd()); - } + using (var reader = new StreamReader(@"TestResources\IssuesProtobufResponse")) + { + SetupRequest("batch/issues?key=project1", reader.ReadToEnd()); + } - var result = await service.GetSuppressedIssuesAsync("project1", null, null, CancellationToken.None); + var result = await service.GetSuppressedIssuesAsync("project1", null, null, CancellationToken.None); - // TODO: create a protobuf file with more than one issue with different states - // the one above does not have suppressed issues, hence the Count==0 - result.Should().BeEmpty(); + // TODO: create a protobuf file with more than one issue with different states + // the one above does not have suppressed issues, hence the Count==0 + result.Should().BeEmpty(); - messageHandler.VerifyAll(); - } + messageHandler.VerifyAll(); + } - [TestMethod] - public async Task GetSuppressedIssuesAsync_Old_NotFound() - { - await ConnectToSonarQube(); + [TestMethod] + public async Task GetSuppressedIssuesAsync_Old_NotFound() + { + await ConnectToSonarQube(); - SetupRequest("batch/issues?key=project1", "", HttpStatusCode.NotFound); + SetupRequest("batch/issues?key=project1", "", HttpStatusCode.NotFound); - Func>> func = async () => - await service.GetSuppressedIssuesAsync("project1", null, null, CancellationToken.None); + Func>> func = async () => + await service.GetSuppressedIssuesAsync("project1", null, null, CancellationToken.None); - func.Should().ThrowExactly().And - .Message.Should().Be("Response status code does not indicate success: 404 (Not Found)."); + func.Should().ThrowExactly().And + .Message.Should().Be("Response status code does not indicate success: 404 (Not Found)."); - messageHandler.VerifyAll(); - } + messageHandler.VerifyAll(); + } - [TestMethod] - public async Task GetSuppressedIssuesAsync_From_7_20() - { - await ConnectToSonarQube("7.2.0.0"); + [TestMethod] + public async Task GetSuppressedIssuesAsync_From_7_20() + { + await ConnectToSonarQube("7.2.0.0"); - SetupRequest("api/issues/search?projects=shared&statuses=RESOLVED&types=CODE_SMELL&p=1&ps=500", @" + SetupRequest("api/issues/search?projects=shared&statuses=RESOLVED&types=CODE_SMELL&p=1&ps=500", @" { ""total"": 5, ""p"": 1, @@ -113,7 +106,7 @@ public async Task GetSuppressedIssuesAsync_From_7_20() ""components"": [ ] } "); - SetupRequest("api/issues/search?projects=shared&statuses=RESOLVED&types=BUG&p=1&ps=500", @" + SetupRequest("api/issues/search?projects=shared&statuses=RESOLVED&types=BUG&p=1&ps=500", @" { ""total"": 5, ""p"": 1, @@ -170,266 +163,166 @@ public async Task GetSuppressedIssuesAsync_From_7_20() } "); - SetupRequest("api/issues/search?projects=shared&statuses=RESOLVED&types=VULNERABILITY&p=1&ps=500", @" -{ - ""total"": 5, - ""p"": 1, - ""ps"": 100, - ""paging"": { - ""pageIndex"": 1, - ""pageSize"": 100, - ""total"": 5 - }, - ""issues"": [ - { - ""key"": ""AWg9DV27DpKqrfA7luen"", - ""rule"": ""csharpsquid:S1451"", - ""severity"": ""BLOCKER"", - ""component"": ""shared:SharedProject1/SharedClass1.cs"", - ""project"": ""shared"", - ""flows"": [], - ""resolution"": ""WONTFIX"", - ""status"": ""RESOLVED"", - ""message"": ""Add or update the header of this file."", - ""effort"": ""5min"", - ""debt"": ""5min"", - ""author"": """", - ""tags"": [], - ""creationDate"": ""2019-01-11T13:18:25+0100"", - ""updateDate"": ""2019-01-11T14:15:53+0100"", - ""type"": ""VULNERABILITY"", - ""organization"": ""default-organization"", - ""fromHotspot"": false - }, - { - ""key"": ""AWg8adc9_JurIR2zdSvT"", - ""rule"": ""csharpsquid:S3400"", - ""severity"": ""MINOR"", - ""component"": ""shared:SharedProject1/SharedClass1.cs"", - ""project"": ""shared"", - ""line"": 5, - ""hash"": ""be411c6cf1ae5ba7d7c5d6da7355afa1"", - ""textRange"": { - ""startLine"": 5, - ""endLine"": 5, - ""startOffset"": 27, - ""endOffset"": 30 - }, - ""flows"": [], - ""resolution"": ""WONTFIX"", - ""status"": ""RESOLVED"", - ""message"": ""Remove this method and declare a constant for this value."", - ""effort"": ""5min"", - ""debt"": ""5min"", - ""author"": """", - ""tags"": [ - ""confusing"" - ], - ""creationDate"": ""2019-01-11T11:16:30+0100"", - ""updateDate"": ""2019-01-11T11:26:55+0100"", - ""type"": ""VULNERABILITY"", - ""organization"": ""default-organization"" - } - ], - ""components"": [ - { - ""organization"": ""default-organization"", - ""key"": ""shared:SharedProject1/SharedClass1.cs"", - ""uuid"": ""AWg8adNl_JurIR2zdSvQ"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""SharedClass1.cs"", - ""longName"": ""SharedProject1/SharedClass1.cs"", - ""path"": ""SharedProject1/SharedClass1.cs"" + var result = await service.GetSuppressedIssuesAsync("shared", null, null, CancellationToken.None); + + result.Should().HaveCount(2); + + // Module level issues don't have FilePath, hash and line + result[0].FilePath.Should().Be(string.Empty); + result[0].Hash.Should().BeNull(); + result[0].TextRange.Should().BeNull(); + result[0].Message.Should().Be("Mark this assembly with 'System.CLSCompliantAttribute'"); + result[0].ModuleKey.Should().Be("shared:shared:2B470B7D-D47B-4E41-B105-D3938E196082"); + result[0].IsResolved.Should().BeTrue(); + result[0].RuleId.Should().Be("csharpsquid:S3990"); + result[0].Severity.Should().Be(SonarQubeIssueSeverity.Major); + + result[1].FilePath.Should().Be("Program.cs"); + result[1].Hash.Should().Be("0afa1b5e62aa3cfaf1cd9a4e63571cb5"); + result[1].TextRange.Should().BeEquivalentTo(new IssueTextRange(6, 6, 10, 17)); + result[1].Message.Should().Be("Add a 'protected' constructor or the 'static' keyword to the class declaration."); + result[1].ModuleKey.Should().Be("shared:shared:2B470B7D-D47B-4E41-B105-D3938E196082"); + result[1].IsResolved.Should().BeTrue(); + result[1].RuleId.Should().Be("csharpsquid:S1118"); + result[1].Severity.Should().Be(SonarQubeIssueSeverity.Major); + + messageHandler.VerifyAll(); } - ] -} -"); - - var result = await service.GetSuppressedIssuesAsync("shared", null, null, CancellationToken.None); - - result.Should().HaveCount(4); - - // Module level issues don't have FilePath, hash and line - result[0].FilePath.Should().Be(string.Empty); - result[0].Hash.Should().BeNull(); - result[0].TextRange.Should().BeNull(); - result[0].Message.Should().Be("Mark this assembly with 'System.CLSCompliantAttribute'"); - result[0].ModuleKey.Should().Be("shared:shared:2B470B7D-D47B-4E41-B105-D3938E196082"); - result[0].IsResolved.Should().BeTrue(); - result[0].RuleId.Should().Be("csharpsquid:S3990"); - result[0].Severity.Should().Be(SonarQubeIssueSeverity.Major); - - result[1].FilePath.Should().Be("Program.cs"); - result[1].Hash.Should().Be("0afa1b5e62aa3cfaf1cd9a4e63571cb5"); - result[1].TextRange.Should().BeEquivalentTo(new IssueTextRange(6, 6, 10, 17)); - result[1].Message.Should().Be("Add a 'protected' constructor or the 'static' keyword to the class declaration."); - result[1].ModuleKey.Should().Be("shared:shared:2B470B7D-D47B-4E41-B105-D3938E196082"); - result[1].IsResolved.Should().BeTrue(); - result[1].RuleId.Should().Be("csharpsquid:S1118"); - result[1].Severity.Should().Be(SonarQubeIssueSeverity.Major); - - // File level issues don't have hash and line - result[2].FilePath.Should().Be("SharedProject1\\SharedClass1.cs"); - result[2].Hash.Should().BeNull(); - result[2].TextRange.Should().BeNull(); - result[2].Message.Should().Be("Add or update the header of this file."); - result[2].ModuleKey.Should().Be("shared:SharedProject1/SharedClass1.cs"); - result[2].IsResolved.Should().BeTrue(); - result[2].RuleId.Should().Be("csharpsquid:S1451"); - result[2].Severity.Should().Be(SonarQubeIssueSeverity.Blocker); - - result[3].FilePath.Should().Be("SharedProject1\\SharedClass1.cs"); - result[3].Hash.Should().Be("be411c6cf1ae5ba7d7c5d6da7355afa1"); - result[3].TextRange.Should().BeEquivalentTo(new IssueTextRange(5, 5, 27, 30)); - result[3].Message.Should().Be("Remove this method and declare a constant for this value."); - result[3].ModuleKey.Should().Be("shared:SharedProject1/SharedClass1.cs"); - result[3].IsResolved.Should().BeTrue(); - result[3].RuleId.Should().Be("csharpsquid:S3400"); - result[3].Severity.Should().Be(SonarQubeIssueSeverity.Minor); - - messageHandler.VerifyAll(); - } - [TestMethod] - public async Task GetSuppressedIssuesAsync_From_7_20_NotFound() - { - await ConnectToSonarQube("7.2.0.0"); + [TestMethod] + public async Task GetSuppressedIssuesAsync_From_7_20_NotFound() + { + await ConnectToSonarQube("7.2.0.0"); - SetupRequest("api/issues/search?projects=project1&statuses=RESOLVED&types=CODE_SMELL&p=1&ps=500", "", HttpStatusCode.NotFound); + SetupRequest("api/issues/search?projects=project1&statuses=RESOLVED&types=CODE_SMELL&p=1&ps=500", "", HttpStatusCode.NotFound); - Func>> func = async () => - await service.GetSuppressedIssuesAsync("project1", null, null, CancellationToken.None); + Func>> func = async () => + await service.GetSuppressedIssuesAsync("project1", null, null, CancellationToken.None); - func.Should().ThrowExactly().And - .Message.Should().Be("Response status code does not indicate success: 404 (Not Found)."); + func.Should().ThrowExactly().And + .Message.Should().Be("Response status code does not indicate success: 404 (Not Found)."); - messageHandler.VerifyAll(); - } + messageHandler.VerifyAll(); + } - [TestMethod] - public async Task GetSuppressedIssuesAsync_From_7_20_Paging() - { - await ConnectToSonarQube("7.2.0.0"); + [TestMethod] + public async Task GetSuppressedIssuesAsync_From_7_20_Paging() + { + await ConnectToSonarQube("7.2.0.0"); - SetupPagesOfResponses("simplcom", 1001, "CODE_SMELL"); - SetupPageOfResponses("simplcom", 1, 0, "BUG"); - SetupPageOfResponses("simplcom", 1, 0, "VULNERABILITY"); + SetupPagesOfResponses("simplcom", 1001, "CODE_SMELL"); + SetupPageOfResponses("simplcom", 1, 0, "BUG"); - var result = await service.GetSuppressedIssuesAsync("simplcom", null, null, CancellationToken.None); + var result = await service.GetSuppressedIssuesAsync("simplcom", null, null, CancellationToken.None); - result.Should().HaveCount(1001); - result.Select(i => i.FilePath).Should().Match(paths => paths.All(p => p == "Program.cs")); + result.Should().HaveCount(1001); + result.Select(i => i.FilePath).Should().Match(paths => paths.All(p => p == "Program.cs")); - messageHandler.VerifyAll(); - } + messageHandler.VerifyAll(); + } - [TestMethod] - // Note: we're not testing all possible combinations because testing with the - // max number of items is relatively slow (several seconds per iteration) - [DataRow(5, 5, 5)] // No issue types with too many issues - [DataRow(MaxAllowedIssues, 5, 2)] // One issue type with too many issues - [DataRow(1, MaxAllowedIssues, MaxAllowedIssues)] // Multiple issue types with too many issues - public async Task GetSuppressedIssuesAsync_From_7_20_NotifyWhenMaxIssuesReturned( - int numCodeSmells, int numBugs, int numVulnerabilities) - { - await ConnectToSonarQube("7.2.0.0"); + [TestMethod] + // Note: we're not testing all possible combinations because testing with the + // max number of items is relatively slow (several seconds per iteration) + [DataRow(5, 5)] // No issue types with too many issues + [DataRow(MaxAllowedIssues, 5)] // One issue type with too many issues + [DataRow(1, MaxAllowedIssues)] // Multiple issue types with too many issues + public async Task GetSuppressedIssuesAsync_From_7_20_NotifyWhenMaxIssuesReturned( + int numCodeSmells, int numBugs) + { + await ConnectToSonarQube("7.2.0.0"); - SetupPagesOfResponses("proj1", numCodeSmells, "CODE_SMELL"); - SetupPagesOfResponses("proj1", numBugs, "BUG"); - SetupPagesOfResponses("proj1", numVulnerabilities, "VULNERABILITY"); + SetupPagesOfResponses("proj1", numCodeSmells, "CODE_SMELL"); + SetupPagesOfResponses("proj1", numBugs, "BUG"); - var result = await service.GetSuppressedIssuesAsync("proj1", null, null, CancellationToken.None); + var result = await service.GetSuppressedIssuesAsync("proj1", null, null, CancellationToken.None); - result.Should().HaveCount( - Math.Min(MaxAllowedIssues, numCodeSmells) + - Math.Min(MaxAllowedIssues, numBugs) + - Math.Min(MaxAllowedIssues, numVulnerabilities)); + result.Should().HaveCount( + Math.Min(MaxAllowedIssues, numCodeSmells) + + Math.Min(MaxAllowedIssues, numBugs)); - DumpWarningsToConsole(); + DumpWarningsToConsole(); - messageHandler.VerifyAll(); + messageHandler.VerifyAll(); - checkForExpectedWarning(numCodeSmells, "code smells"); - checkForExpectedWarning(numBugs, "bugs"); - checkForExpectedWarning(numVulnerabilities, "vulnerabilities"); - } + checkForExpectedWarning(numCodeSmells, "code smells"); + checkForExpectedWarning(numBugs, "bugs"); + } - [TestMethod] - [DataRow("")] - [DataRow(null)] - public async Task GetSuppressedIssuesAsync_From_7_20_BranchIsNotSpecified_BranchIsNotIncludedInQueryString(string emptyBranch) - { - await ConnectToSonarQube("7.2.0.0"); - messageHandler.Reset(); + [TestMethod] + [DataRow("")] + [DataRow(null)] + public async Task GetSuppressedIssuesAsync_From_7_20_BranchIsNotSpecified_BranchIsNotIncludedInQueryString(string emptyBranch) + { + await ConnectToSonarQube("7.2.0.0"); + messageHandler.Reset(); - SetupHttpRequest(messageHandler, EmptyGetIssuesResponse); - _ = await service.GetSuppressedIssuesAsync("any", emptyBranch, null, CancellationToken.None); + SetupHttpRequest(messageHandler, EmptyGetIssuesResponse); + _ = await service.GetSuppressedIssuesAsync("any", emptyBranch, null, CancellationToken.None); - // Branch is null/empty => should not be passed - var actualRequests = messageHandler.GetSendAsyncRequests(); - actualRequests.Should().HaveCount(3); - actualRequests.Should().NotContain(x => x.RequestUri.Query.Contains("branch")); - } + // Branch is null/empty => should not be passed + var actualRequests = messageHandler.GetSendAsyncRequests(); + actualRequests.Should().HaveCount(2); + actualRequests.Should().NotContain(x => x.RequestUri.Query.Contains("branch")); + } - [TestMethod] - public async Task GetSuppressedIssuesAsync_From_7_20_BranchIsSpecified_BranchIncludedInQueryString() - { - await ConnectToSonarQube("7.2.0.0"); - messageHandler.Reset(); + [TestMethod] + public async Task GetSuppressedIssuesAsync_From_7_20_BranchIsSpecified_BranchIncludedInQueryString() + { + await ConnectToSonarQube("7.2.0.0"); + messageHandler.Reset(); - SetupHttpRequest(messageHandler, EmptyGetIssuesResponse); - _ = await service.GetSuppressedIssuesAsync("any", "aBranch", null, CancellationToken.None); + SetupHttpRequest(messageHandler, EmptyGetIssuesResponse); + _ = await service.GetSuppressedIssuesAsync("any", "aBranch", null, CancellationToken.None); - // The wrapper is expected to make three calls, for code smells, bugs, then vulnerabilities - var actualRequests = messageHandler.GetSendAsyncRequests(); - actualRequests.Should().HaveCount(3); - actualRequests.Should().OnlyContain(x => x.RequestUri.Query.Contains("&branch=aBranch&")); - } + // The wrapper is expected to make three calls, for code smells, bugs, then vulnerabilities + var actualRequests = messageHandler.GetSendAsyncRequests(); + actualRequests.Should().HaveCount(2); + actualRequests.Should().OnlyContain(x => x.RequestUri.Query.Contains("&branch=aBranch&")); + } - [TestMethod] - public async Task GetSuppressedIssuesAsync_From_7_20_IssueKeysAreNotSpecified_IssueKeysAreNotIncludedInQueryString() - { - await ConnectToSonarQube("7.2.0.0"); - messageHandler.Reset(); + [TestMethod] + public async Task GetSuppressedIssuesAsync_From_7_20_IssueKeysAreNotSpecified_IssueKeysAreNotIncludedInQueryString() + { + await ConnectToSonarQube("7.2.0.0"); + messageHandler.Reset(); - SetupHttpRequest(messageHandler, EmptyGetIssuesResponse); - _ = await service.GetSuppressedIssuesAsync("any", null, null, CancellationToken.None); + SetupHttpRequest(messageHandler, EmptyGetIssuesResponse); + _ = await service.GetSuppressedIssuesAsync("any", null, null, CancellationToken.None); - // The wrapper is expected to make three calls, for code smells, bugs, then vulnerabilities - var actualRequests = messageHandler.GetSendAsyncRequests(); - actualRequests.Should().HaveCount(3); - actualRequests.Should().NotContain(x => x.RequestUri.Query.Contains("issues")); - } + // The wrapper is expected to make three calls, for code smells, bugs, then vulnerabilities + var actualRequests = messageHandler.GetSendAsyncRequests(); + actualRequests.Should().HaveCount(2); + actualRequests.Should().NotContain(x => x.RequestUri.Query.Contains("issues")); + } - [TestMethod] - public async Task GetSuppressedIssuesAsync_From_7_20_IssueKeysAreSpecified_IssueKeysAreIncludedInQueryString() - { - await ConnectToSonarQube("7.2.0.0"); - messageHandler.Reset(); + [TestMethod] + public async Task GetSuppressedIssuesAsync_From_7_20_IssueKeysAreSpecified_IssueKeysAreIncludedInQueryString() + { + await ConnectToSonarQube("7.2.0.0"); + messageHandler.Reset(); - SetupHttpRequest(messageHandler, EmptyGetIssuesResponse); - _ = await service.GetSuppressedIssuesAsync("any", null, new[] { "issue1", "issue2" }, CancellationToken.None); + SetupHttpRequest(messageHandler, EmptyGetIssuesResponse); + _ = await service.GetSuppressedIssuesAsync("any", null, new[] { "issue1", "issue2" }, CancellationToken.None); - // The wrapper is expected to make one call with the given issueKeys - var actualRequests = messageHandler.GetSendAsyncRequests(); - actualRequests.Should().ContainSingle(); - actualRequests.Should().OnlyContain(x => x.RequestUri.Query.Contains("issues=issue1%2Cissue2")); - } + // The wrapper is expected to make one call with the given issueKeys + var actualRequests = messageHandler.GetSendAsyncRequests(); + actualRequests.Should().ContainSingle(); + actualRequests.Should().OnlyContain(x => x.RequestUri.Query.Contains("issues=issue1%2Cissue2")); + } - [TestMethod] - public void GetSuppressedIssuesAsync_NotConnected() - { - // No calls to Connect - // No need to setup request, the operation should fail + [TestMethod] + public void GetSuppressedIssuesAsync_NotConnected() + { + // No calls to Connect + // No need to setup request, the operation should fail - Func>> func = async () => - await service.GetSuppressedIssuesAsync("simplcom", null, null, CancellationToken.None); + Func>> func = async () => + await service.GetSuppressedIssuesAsync("simplcom", null, null, CancellationToken.None); - func.Should().ThrowExactly().And - .Message.Should().Be("This operation expects the service to be connected."); + func.Should().ThrowExactly().And + .Message.Should().Be("This operation expects the service to be connected."); - logger.ErrorMessages.Should().Contain("The service is expected to be connected."); - } + logger.ErrorMessages.Should().Contain("The service is expected to be connected."); } } diff --git a/src/SonarQube.Client.Tests/SonarQubeService_GetTaintVulnerabilitiesRequest.cs b/src/SonarQube.Client.Tests/SonarQubeService_GetTaintVulnerabilitiesRequest.cs deleted file mode 100644 index 6087b766a2..0000000000 --- a/src/SonarQube.Client.Tests/SonarQubeService_GetTaintVulnerabilitiesRequest.cs +++ /dev/null @@ -1,1963 +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.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using SonarQube.Client.Models; -using SonarQube.Client.Tests.Infra; -using static SonarQube.Client.Tests.Infra.MocksHelper; - -namespace SonarQube.Client.Tests -{ - [TestClass] - public class SonarQubeService_GetTaintVulnerabilitiesRequest : SonarQubeService_TestBase - { - [TestMethod] - public void GetTaintVulnerabilitiesAsync_NotConnected() - { - // No calls to Connect - // No need to setup request, the operation should fail - Func>> func = async () => - await service.GetTaintVulnerabilitiesAsync(It.IsAny(), It.IsAny(), CancellationToken.None); - - func.Should().ThrowExactly().And - .Message.Should().Be("This operation expects the service to be connected."); - - logger.ErrorMessages.Should().Contain("The service is expected to be connected."); - } - - [TestMethod] - public async Task GetTaintVulnerabilitiesAsync_Response_From_SonarQube() - { - await ConnectToSonarQube("8.6.0.0"); - - SetupRequest("api/issues/search?projects=shared&statuses=OPEN%2CCONFIRMED%2CREOPENED%2CRESOLVED&types=VULNERABILITY&p=1&ps=500", @" -{ - ""total"": 4, - ""p"": 1, - ""ps"": 100, - ""paging"": { - ""pageIndex"": 1, - ""pageSize"": 100, - ""total"": 4 - }, - ""effortTotal"": 72, - ""debtTotal"": 72, - ""issues"": [ - { - ""key"": ""AW0p2QsM-y65ELkujuR4"", - ""rule"": ""java:S4426"", - ""severity"": ""BLOCKER"", - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/crypto/KeyPairUtil.java"", - ""project"": ""com.sonarsource:citytour2019-java"", - ""line"": 12, - ""hash"": ""e6162855f182ddc1652ec2cdea0a085b"", - ""textRange"": { - ""startLine"": 12, - ""endLine"": 12, - ""startOffset"": 6, - ""endOffset"": 32 - }, - ""flows"": [], - ""status"": ""REOPENED"", - ""message"": ""Use a key length of at least 2048 bits."", - ""effort"": ""2min"", - ""debt"": ""2min"", - ""author"": ""alexandre.gigleux@sonarsource.com"", - ""tags"": [ - ""cwe"", - ""owasp-a3"" - ], - ""transitions"": [ - ""confirm"", - ""resolve"", - ""falsepositive"", - ""wontfix"" - ], - ""actions"": [ - ""set_type"", - ""set_tags"", - ""comment"", - ""set_severity"", - ""assign"" - ], - ""comments"": [], - ""creationDate"": ""2019-09-12T13:05:53+0000"", - ""updateDate"": ""2020-08-24T09:10:42+0000"", - ""type"": ""VULNERABILITY"", - ""organization"": ""default-organization"", - ""scope"": ""MAIN"" - }, - { - ""key"": ""AW0p2Qrv-y65ELkujuR0"", - ""rule"": ""java:S3330"", - ""severity"": ""CRITICAL"", - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/hotspots/WritingCookieServlet.java"", - ""project"": ""com.sonarsource:citytour2019-java"", - ""line"": 10, - ""hash"": ""79fd1a1af84dcccce2e0ede4970663ce"", - ""textRange"": { - ""startLine"": 10, - ""endLine"": 10, - ""startOffset"": 15, - ""endOffset"": 21 - }, - ""flows"": [], - ""status"": ""OPEN"", - ""message"": ""Add the \""HttpOnly\"" cookie attribute."", - ""effort"": ""10min"", - ""debt"": ""10min"", - ""assignee"": ""agigleux@github"", - ""author"": ""alexandre.gigleux@sonarsource.com"", - ""tags"": [ - ""cwe"", - ""owasp-a7"", - ""sans-top25-insecure"" - ], - ""transitions"": [ - ""confirm"", - ""resolve"", - ""falsepositive"", - ""wontfix"" - ], - ""actions"": [ - ""set_type"", - ""set_tags"", - ""comment"", - ""set_severity"", - ""assign"" - ], - ""comments"": [], - ""creationDate"": ""2019-09-11T12:25:08+0000"", - ""updateDate"": ""2019-09-11T12:29:47+0000"", - ""type"": ""VULNERABILITY"", - ""organization"": ""default-organization"", - ""scope"": ""MAIN"" - }, - { - ""key"": ""AW0p2Qpn-y65ELkujuRf"", - ""rule"": ""javasecurity:S2076"", - ""severity"": ""BLOCKER"", - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/CommandInjectionVulnerability.java"", - ""project"": ""com.sonarsource:citytour2019-java"", - ""line"": 17, - ""hash"": ""1703916771e6abb765843e62a76fcb5a"", - ""textRange"": { - ""startLine"": 17, - ""endLine"": 17, - ""startOffset"": 4, - ""endOffset"": 25 - }, - ""flows"": [ - { - ""locations"": [ - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/CommandInjectionVulnerability.java"", - ""textRange"": { - ""startLine"": 17, - ""endLine"": 17, - ""startOffset"": 4, - ""endOffset"": 25 - }, - ""msg"": ""tainted value is used to perform a security-sensitive operation"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/CommandInjectionVulnerability.java"", - ""textRange"": { - ""startLine"": 14, - ""endLine"": 14, - ""startOffset"": 6, - ""endOffset"": 40 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/CommandInjectionVulnerability.java"", - ""textRange"": { - ""startLine"": 8, - ""endLine"": 8, - ""startOffset"": 9, - ""endOffset"": 38 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 38, - ""endLine"": 38, - ""startOffset"": 6, - ""endOffset"": 54 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 36, - ""endLine"": 36, - ""startOffset"": 6, - ""endOffset"": 72 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 36, - ""endLine"": 36, - ""startOffset"": 22, - ""endOffset"": 72 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 34, - ""endLine"": 34, - ""startOffset"": 4, - ""endOffset"": 59 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 34, - ""endLine"": 34, - ""startOffset"": 20, - ""endOffset"": 59 - }, - ""msg"": ""this value can be controlled by the user"" - } - ] - } - ], - ""status"": ""OPEN"", - ""message"": ""Refactor this code to not construct the OS command from tainted, user-controlled data."", - ""effort"": ""30min"", - ""debt"": ""30min"", - ""assignee"": ""agigleux@github"", - ""author"": ""alexandre.gigleux@sonarsource.com"", - ""tags"": [ - ""cwe"", - ""owasp-a1"", - ""sans-top25-insecure"" - ], - ""transitions"": [ - ""confirm"", - ""resolve"", - ""falsepositive"", - ""wontfix"" - ], - ""actions"": [ - ""set_type"", - ""set_tags"", - ""comment"", - ""set_severity"", - ""assign"" - ], - ""comments"": [], - ""creationDate"": ""2019-09-11T12:25:08+0000"", - ""updateDate"": ""2019-09-11T12:29:47+0000"", - ""type"": ""VULNERABILITY"", - ""organization"": ""default-organization"", - ""scope"": ""MAIN"" - }, - { - ""key"": ""AW0p2QqO-y65ELkujuRk"", - ""rule"": ""javasecurity:S3649"", - ""severity"": ""BLOCKER"", - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/SQLInjectionVulnerabilityCollectionMultipleFiles.java"", - ""project"": ""com.sonarsource:citytour2019-java"", - ""line"": 20, - ""hash"": ""b07a403eb4e77c8d6c4b3fa5c6408064"", - ""textRange"": { - ""startLine"": 20, - ""endLine"": 20, - ""startOffset"": 6, - ""endOffset"": 23 - }, - ""flows"": [ - { - ""locations"": [ - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/SQLInjectionVulnerabilityCollectionMultipleFiles.java"", - ""textRange"": { - ""startLine"": 20, - ""endLine"": 20, - ""startOffset"": 6, - ""endOffset"": 23 - }, - ""msg"": ""tainted value is used to perform a security-sensitive operation"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/SQLInjectionVulnerabilityCollectionMultipleFiles.java"", - ""textRange"": { - ""startLine"": 9, - ""endLine"": 9, - ""startOffset"": 9, - ""endOffset"": 57 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 43, - ""endLine"": 43, - ""startOffset"": 6, - ""endOffset"": 68 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 43, - ""endLine"": 43, - ""startOffset"": 59, - ""endOffset"": 67 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 41, - ""endLine"": 41, - ""startOffset"": 6, - ""endOffset"": 82 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 41, - ""endLine"": 41, - ""startOffset"": 23, - ""endOffset"": 81 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/BusinessThingsUtils.java"", - ""textRange"": { - ""startLine"": 18, - ""endLine"": 18, - ""startOffset"": 8, - ""endOffset"": 43 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/BusinessThingsUtils.java"", - ""textRange"": { - ""startLine"": 18, - ""endLine"": 18, - ""startOffset"": 15, - ""endOffset"": 42 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/BusinessThingsUtils.java"", - ""textRange"": { - ""startLine"": 17, - ""endLine"": 17, - ""startOffset"": 39, - ""endOffset"": 55 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/BusinessThingsUtils.java"", - ""textRange"": { - ""startLine"": 14, - ""endLine"": 14, - ""startOffset"": 8, - ""endOffset"": 36 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/BusinessThingsUtils.java"", - ""textRange"": { - ""startLine"": 11, - ""endLine"": 11, - ""startOffset"": 27, - ""endOffset"": 50 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 41, - ""endLine"": 41, - ""startOffset"": 23, - ""endOffset"": 81 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 36, - ""endLine"": 36, - ""startOffset"": 6, - ""endOffset"": 72 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 36, - ""endLine"": 36, - ""startOffset"": 22, - ""endOffset"": 72 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 34, - ""endLine"": 34, - ""startOffset"": 4, - ""endOffset"": 59 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 34, - ""endLine"": 34, - ""startOffset"": 20, - ""endOffset"": 59 - }, - ""msg"": ""this value can be controlled by the user"" - } - ] - } - ], - ""status"": ""OPEN"", - ""message"": ""Refactor this code to not construct SQL queries directly from tainted user-controlled data."", - ""effort"": ""30min"", - ""debt"": ""30min"", - ""assignee"": ""agigleux@github"", - ""author"": ""alexandre.gigleux@sonarsource.com"", - ""tags"": [ - ""cert"", - ""cwe"", - ""owasp-a1"", - ""sans-top25-insecure"", - ""sql"" - ], - ""transitions"": [ - ""confirm"", - ""resolve"", - ""falsepositive"", - ""wontfix"" - ], - ""actions"": [ - ""set_type"", - ""set_tags"", - ""comment"", - ""set_severity"", - ""assign"" - ], - ""comments"": [], - ""creationDate"": ""2019-09-11T12:25:08+0100"", - ""updateDate"": ""2019-09-16T10:35:19+1300"", - ""type"": ""VULNERABILITY"", - ""organization"": ""default-organization"", - ""scope"": ""MAIN"" - } - ], - ""components"": [ - { - ""organization"": ""default-organization"", - ""key"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""uuid"": ""AW0p2QjJ-y65ELkujuRO"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""Servlet.java"", - ""longName"": ""src/main/java/foo/security/injection/Servlet.java"", - ""path"": ""src/main/java/foo/security/injection/Servlet.java"" - }, - { - ""organization"": ""default-organization"", - ""key"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/SQLInjectionVulnerabilityCollectionMultipleFiles.java"", - ""uuid"": ""AW05pEYnBvApAtP9iYg6"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""SQLInjectionVulnerabilityCollectionMultipleFiles.java"", - ""longName"": ""src/main/java/foo/security/injection/SQLInjectionVulnerabilityCollectionMultipleFiles.java"", - ""path"": ""src/main/java/foo/security/injection/SQLInjectionVulnerabilityCollectionMultipleFiles.java"" - }, - { - ""organization"": ""default-organization"", - ""key"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/hotspots/WritingCookieServlet.java"", - ""uuid"": ""AW0p2QjJ-y65ELkujuRT"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""WritingCookieServlet.java"", - ""longName"": ""src/main/java/foo/security/hotspots/WritingCookieServlet.java"", - ""path"": ""src/main/java/foo/security/hotspots/WritingCookieServlet.java"" - }, - { - ""organization"": ""default-organization"", - ""key"": ""com.sonarsource:citytour2019-java"", - ""uuid"": ""AW0abn1qGHw5MqdAqloE"", - ""enabled"": true, - ""qualifier"": ""TRK"", - ""name"": ""City Tour - Java project"", - ""longName"": ""City Tour - Java project"" - }, - { - ""organization"": ""default-organization"", - ""key"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/crypto/KeyPairUtil.java"", - ""uuid"": ""AW0p2QjJ-y65ELkujuRX"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""KeyPairUtil.java"", - ""longName"": ""src/main/java/foo/security/crypto/KeyPairUtil.java"", - ""path"": ""src/main/java/foo/security/crypto/KeyPairUtil.java"" - }, - { - ""organization"": ""default-organization"", - ""key"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/BusinessThingsUtils.java"", - ""uuid"": ""AW0p2QjJ-y65ELkujuRJ"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""BusinessThingsUtils.java"", - ""longName"": ""src/main/java/foo/security/injection/BusinessThingsUtils.java"", - ""path"": ""src/main/java/foo/security/injection/BusinessThingsUtils.java"" - }, - { - ""organization"": ""default-organization"", - ""key"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/CommandInjectionVulnerability.java"", - ""uuid"": ""AW0p2QjJ-y65ELkujuRL"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""CommandInjectionVulnerability.java"", - ""longName"": ""src/main/java/foo/security/injection/CommandInjectionVulnerability.java"", - ""path"": ""src/main/java/foo/security/injection/CommandInjectionVulnerability.java"" - } - ], - ""rules"": [ - { - ""key"": ""java:S3330"", - ""name"": ""Creating cookies without the \""HttpOnly\"" flag is security-sensitive"", - ""lang"": ""java"", - ""status"": ""READY"", - ""langName"": ""Java"" - }, - { - ""key"": ""javasecurity:S3649"", - ""name"": ""Database queries should not be vulnerable to injection attacks"", - ""lang"": ""java"", - ""status"": ""READY"", - ""langName"": ""Java"" - }, - { - ""key"": ""javasecurity:S2076"", - ""name"": ""OS commands should not be vulnerable to command injection attacks"", - ""lang"": ""java"", - ""status"": ""READY"", - ""langName"": ""Java"" - }, - { - ""key"": ""java:S4426"", - ""name"": ""Cryptographic keys should be robust"", - ""lang"": ""java"", - ""status"": ""READY"", - ""langName"": ""Java"" - } - ] -} -"); - - var result = await service.GetTaintVulnerabilitiesAsync("shared", null, CancellationToken.None); - - messageHandler.VerifyAll(); - secondaryIssueHashUpdater.Verify(x => x.UpdateHashesAsync(result, service, It.IsAny())); - - // should return only 2 taint out of 4 vulnerabilities - result.Should().HaveCount(2); - - var taint1 = result[0]; - - taint1.IssueKey.Should().Be("AW0p2Qpn-y65ELkujuRf"); - taint1.RuleId.Should().Be("javasecurity:S2076"); - taint1.Message.Should().Be("Refactor this code to not construct the OS command from tainted, user-controlled data."); - taint1.FilePath.Should().Be("src\\main\\java\\foo\\security\\injection\\CommandInjectionVulnerability.java"); - taint1.Hash.Should().Be("1703916771e6abb765843e62a76fcb5a"); - taint1.CreationTimestamp.Should().Be(DateTimeOffset.Parse("2019-09-11T12:25:08+0000")); - taint1.LastUpdateTimestamp.Should().Be(DateTimeOffset.Parse("2019-09-11T12:29:47+0000")); - taint1.Flows.Should().NotBeEmpty(); - taint1.Flows.Count.Should().Be(1); - taint1.Flows[0].Locations.Count.Should().Be(8); - taint1.Flows[0].Locations[0].FilePath.Should().Be("src\\main\\java\\foo\\security\\injection\\CommandInjectionVulnerability.java"); - taint1.Flows[0].Locations[0].TextRange.Should().BeEquivalentTo(new IssueTextRange(17, 17, 4, 25)); - taint1.Flows[0].Locations[0].Message.Should().Be("tainted value is used to perform a security-sensitive operation"); - taint1.Flows[0].Locations[3].FilePath.Should().Be("src\\main\\java\\foo\\security\\injection\\Servlet.java"); - taint1.Flows[0].Locations[3].TextRange.Should().BeEquivalentTo(new IssueTextRange(38, 38, 6, 54)); - taint1.Flows[0].Locations[3].Message.Should().Be("taint value is propagated"); - taint1.TextRange.Should().BeEquivalentTo(new IssueTextRange(17, 17, 4, 25)); - - var taint2 = result[1]; - - taint2.IssueKey.Should().Be("AW0p2QqO-y65ELkujuRk"); - taint2.RuleId.Should().Be("javasecurity:S3649"); - taint2.Message.Should().Be("Refactor this code to not construct SQL queries directly from tainted user-controlled data."); - taint2.FilePath.Should().Be("src\\main\\java\\foo\\security\\injection\\SQLInjectionVulnerabilityCollectionMultipleFiles.java"); - taint2.Hash.Should().Be("b07a403eb4e77c8d6c4b3fa5c6408064"); - taint2.CreationTimestamp.Should().Be(DateTimeOffset.Parse("2019-09-11T12:25:08+0100")); - taint2.LastUpdateTimestamp.Should().Be(DateTimeOffset.Parse("2019-09-16T10:35:19+1300")); - taint2.Flows.Should().NotBeEmpty(); - taint2.Flows.Count.Should().Be(1); - taint2.Flows[0].Locations.Count.Should().Be(16); - taint2.Flows[0].Locations[7].FilePath.Should().Be("src\\main\\java\\foo\\security\\injection\\BusinessThingsUtils.java"); - taint2.Flows[0].Locations[7].TextRange.Should().BeEquivalentTo(new IssueTextRange(18, 18, 15, 42)); - taint2.Flows[0].Locations[7].Message.Should().Be("taint value is propagated"); - taint2.Flows[0].Locations[15].FilePath.Should().Be("src\\main\\java\\foo\\security\\injection\\Servlet.java"); - taint2.Flows[0].Locations[15].TextRange.Should().BeEquivalentTo(new IssueTextRange(34, 34, 20, 59)); - taint2.Flows[0].Locations[15].Message.Should().Be("this value can be controlled by the user"); - taint2.TextRange.Should().BeEquivalentTo(new IssueTextRange(20, 20, 6, 23)); - } - - [TestMethod] - public async Task GetTaintVulnerabilitiesWithContextAsync_Response_From_SonarQube() - { - await ConnectToSonarQube("9.6.0.0"); - - SetupRequest("api/issues/search?additionalFields=ruleDescriptionContextKey&projects=shared&statuses=OPEN%2CCONFIRMED%2CREOPENED%2CRESOLVED&types=VULNERABILITY&p=1&ps=500", @" -{ - ""total"": 4, - ""p"": 1, - ""ps"": 100, - ""paging"": { - ""pageIndex"": 1, - ""pageSize"": 100, - ""total"": 4 - }, - ""effortTotal"": 72, - ""debtTotal"": 72, - ""issues"": [ - { - ""key"": ""AW0p2QsM-y65ELkujuR4"", - ""rule"": ""java:S4426"", - ""severity"": ""BLOCKER"", - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/crypto/KeyPairUtil.java"", - ""project"": ""com.sonarsource:citytour2019-java"", - ""line"": 12, - ""hash"": ""e6162855f182ddc1652ec2cdea0a085b"", - ""textRange"": { - ""startLine"": 12, - ""endLine"": 12, - ""startOffset"": 6, - ""endOffset"": 32 - }, - ""flows"": [], - ""status"": ""REOPENED"", - ""message"": ""Use a key length of at least 2048 bits."", - ""effort"": ""2min"", - ""debt"": ""2min"", - ""author"": ""alexandre.gigleux@sonarsource.com"", - ""tags"": [ - ""cwe"", - ""owasp-a3"" - ], - ""transitions"": [ - ""confirm"", - ""resolve"", - ""falsepositive"", - ""wontfix"" - ], - ""actions"": [ - ""set_type"", - ""set_tags"", - ""comment"", - ""set_severity"", - ""assign"" - ], - ""comments"": [], - ""creationDate"": ""2019-09-12T13:05:53+0000"", - ""updateDate"": ""2020-08-24T09:10:42+0000"", - ""type"": ""VULNERABILITY"", - ""organization"": ""default-organization"", - ""scope"": ""MAIN"" - }, - { - ""key"": ""AW0p2Qrv-y65ELkujuR0"", - ""rule"": ""java:S3330"", - ""severity"": ""CRITICAL"", - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/hotspots/WritingCookieServlet.java"", - ""project"": ""com.sonarsource:citytour2019-java"", - ""line"": 10, - ""hash"": ""79fd1a1af84dcccce2e0ede4970663ce"", - ""textRange"": { - ""startLine"": 10, - ""endLine"": 10, - ""startOffset"": 15, - ""endOffset"": 21 - }, - ""flows"": [], - ""status"": ""OPEN"", - ""message"": ""Add the \""HttpOnly\"" cookie attribute."", - ""effort"": ""10min"", - ""debt"": ""10min"", - ""assignee"": ""agigleux@github"", - ""author"": ""alexandre.gigleux@sonarsource.com"", - ""tags"": [ - ""cwe"", - ""owasp-a7"", - ""sans-top25-insecure"" - ], - ""transitions"": [ - ""confirm"", - ""resolve"", - ""falsepositive"", - ""wontfix"" - ], - ""actions"": [ - ""set_type"", - ""set_tags"", - ""comment"", - ""set_severity"", - ""assign"" - ], - ""comments"": [], - ""creationDate"": ""2019-09-11T12:25:08+0000"", - ""updateDate"": ""2019-09-11T12:29:47+0000"", - ""type"": ""VULNERABILITY"", - ""organization"": ""default-organization"", - ""scope"": ""MAIN"" - }, - { - ""ruleDescriptionContextKey"": ""context"", - ""key"": ""AW0p2Qpn-y65ELkujuRf"", - ""rule"": ""javasecurity:S2076"", - ""severity"": ""BLOCKER"", - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/CommandInjectionVulnerability.java"", - ""project"": ""com.sonarsource:citytour2019-java"", - ""line"": 17, - ""hash"": ""1703916771e6abb765843e62a76fcb5a"", - ""textRange"": { - ""startLine"": 17, - ""endLine"": 17, - ""startOffset"": 4, - ""endOffset"": 25 - }, - - ""flows"": [ - { - ""locations"": [ - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/CommandInjectionVulnerability.java"", - ""textRange"": { - ""startLine"": 17, - ""endLine"": 17, - ""startOffset"": 4, - ""endOffset"": 25 - }, - ""msg"": ""tainted value is used to perform a security-sensitive operation"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/CommandInjectionVulnerability.java"", - ""textRange"": { - ""startLine"": 14, - ""endLine"": 14, - ""startOffset"": 6, - ""endOffset"": 40 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/CommandInjectionVulnerability.java"", - ""textRange"": { - ""startLine"": 8, - ""endLine"": 8, - ""startOffset"": 9, - ""endOffset"": 38 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 38, - ""endLine"": 38, - ""startOffset"": 6, - ""endOffset"": 54 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 36, - ""endLine"": 36, - ""startOffset"": 6, - ""endOffset"": 72 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 36, - ""endLine"": 36, - ""startOffset"": 22, - ""endOffset"": 72 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 34, - ""endLine"": 34, - ""startOffset"": 4, - ""endOffset"": 59 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 34, - ""endLine"": 34, - ""startOffset"": 20, - ""endOffset"": 59 - }, - ""msg"": ""this value can be controlled by the user"" - } - ] - } - ], - ""status"": ""OPEN"", - ""message"": ""Refactor this code to not construct the OS command from tainted, user-controlled data."", - ""effort"": ""30min"", - ""debt"": ""30min"", - ""assignee"": ""agigleux@github"", - ""author"": ""alexandre.gigleux@sonarsource.com"", - ""tags"": [ - ""cwe"", - ""owasp-a1"", - ""sans-top25-insecure"" - ], - ""transitions"": [ - ""confirm"", - ""resolve"", - ""falsepositive"", - ""wontfix"" - ], - ""actions"": [ - ""set_type"", - ""set_tags"", - ""comment"", - ""set_severity"", - ""assign"" - ], - ""comments"": [], - ""creationDate"": ""2019-09-11T12:25:08+0000"", - ""updateDate"": ""2019-09-11T12:29:47+0000"", - ""type"": ""VULNERABILITY"", - ""organization"": ""default-organization"", - ""scope"": ""MAIN"" - }, - { - ""key"": ""AW0p2QqO-y65ELkujuRk"", - ""rule"": ""javasecurity:S3649"", - ""severity"": ""BLOCKER"", - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/SQLInjectionVulnerabilityCollectionMultipleFiles.java"", - ""project"": ""com.sonarsource:citytour2019-java"", - ""line"": 20, - ""hash"": ""b07a403eb4e77c8d6c4b3fa5c6408064"", - ""textRange"": { - ""startLine"": 20, - ""endLine"": 20, - ""startOffset"": 6, - ""endOffset"": 23 - }, - ""flows"": [ - { - ""locations"": [ - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/SQLInjectionVulnerabilityCollectionMultipleFiles.java"", - ""textRange"": { - ""startLine"": 20, - ""endLine"": 20, - ""startOffset"": 6, - ""endOffset"": 23 - }, - ""msg"": ""tainted value is used to perform a security-sensitive operation"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/SQLInjectionVulnerabilityCollectionMultipleFiles.java"", - ""textRange"": { - ""startLine"": 9, - ""endLine"": 9, - ""startOffset"": 9, - ""endOffset"": 57 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 43, - ""endLine"": 43, - ""startOffset"": 6, - ""endOffset"": 68 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 43, - ""endLine"": 43, - ""startOffset"": 59, - ""endOffset"": 67 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 41, - ""endLine"": 41, - ""startOffset"": 6, - ""endOffset"": 82 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 41, - ""endLine"": 41, - ""startOffset"": 23, - ""endOffset"": 81 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/BusinessThingsUtils.java"", - ""textRange"": { - ""startLine"": 18, - ""endLine"": 18, - ""startOffset"": 8, - ""endOffset"": 43 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/BusinessThingsUtils.java"", - ""textRange"": { - ""startLine"": 18, - ""endLine"": 18, - ""startOffset"": 15, - ""endOffset"": 42 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/BusinessThingsUtils.java"", - ""textRange"": { - ""startLine"": 17, - ""endLine"": 17, - ""startOffset"": 39, - ""endOffset"": 55 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/BusinessThingsUtils.java"", - ""textRange"": { - ""startLine"": 14, - ""endLine"": 14, - ""startOffset"": 8, - ""endOffset"": 36 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/BusinessThingsUtils.java"", - ""textRange"": { - ""startLine"": 11, - ""endLine"": 11, - ""startOffset"": 27, - ""endOffset"": 50 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 41, - ""endLine"": 41, - ""startOffset"": 23, - ""endOffset"": 81 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 36, - ""endLine"": 36, - ""startOffset"": 6, - ""endOffset"": 72 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 36, - ""endLine"": 36, - ""startOffset"": 22, - ""endOffset"": 72 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 34, - ""endLine"": 34, - ""startOffset"": 4, - ""endOffset"": 59 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 34, - ""endLine"": 34, - ""startOffset"": 20, - ""endOffset"": 59 - }, - ""msg"": ""this value can be controlled by the user"" - } - ] - } - ], - ""status"": ""OPEN"", - ""message"": ""Refactor this code to not construct SQL queries directly from tainted user-controlled data."", - ""effort"": ""30min"", - ""debt"": ""30min"", - ""assignee"": ""agigleux@github"", - ""author"": ""alexandre.gigleux@sonarsource.com"", - ""tags"": [ - ""cert"", - ""cwe"", - ""owasp-a1"", - ""sans-top25-insecure"", - ""sql"" - ], - ""transitions"": [ - ""confirm"", - ""resolve"", - ""falsepositive"", - ""wontfix"" - ], - ""actions"": [ - ""set_type"", - ""set_tags"", - ""comment"", - ""set_severity"", - ""assign"" - ], - ""comments"": [], - ""creationDate"": ""2019-09-11T12:25:08+0100"", - ""updateDate"": ""2019-09-16T10:35:19+1300"", - ""type"": ""VULNERABILITY"", - ""organization"": ""default-organization"", - ""scope"": ""MAIN"" - } - ], - ""components"": [ - { - ""organization"": ""default-organization"", - ""key"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""uuid"": ""AW0p2QjJ-y65ELkujuRO"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""Servlet.java"", - ""longName"": ""src/main/java/foo/security/injection/Servlet.java"", - ""path"": ""src/main/java/foo/security/injection/Servlet.java"" - }, - { - ""organization"": ""default-organization"", - ""key"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/SQLInjectionVulnerabilityCollectionMultipleFiles.java"", - ""uuid"": ""AW05pEYnBvApAtP9iYg6"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""SQLInjectionVulnerabilityCollectionMultipleFiles.java"", - ""longName"": ""src/main/java/foo/security/injection/SQLInjectionVulnerabilityCollectionMultipleFiles.java"", - ""path"": ""src/main/java/foo/security/injection/SQLInjectionVulnerabilityCollectionMultipleFiles.java"" - }, - { - ""organization"": ""default-organization"", - ""key"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/hotspots/WritingCookieServlet.java"", - ""uuid"": ""AW0p2QjJ-y65ELkujuRT"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""WritingCookieServlet.java"", - ""longName"": ""src/main/java/foo/security/hotspots/WritingCookieServlet.java"", - ""path"": ""src/main/java/foo/security/hotspots/WritingCookieServlet.java"" - }, - { - ""organization"": ""default-organization"", - ""key"": ""com.sonarsource:citytour2019-java"", - ""uuid"": ""AW0abn1qGHw5MqdAqloE"", - ""enabled"": true, - ""qualifier"": ""TRK"", - ""name"": ""City Tour - Java project"", - ""longName"": ""City Tour - Java project"" - }, - { - ""organization"": ""default-organization"", - ""key"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/crypto/KeyPairUtil.java"", - ""uuid"": ""AW0p2QjJ-y65ELkujuRX"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""KeyPairUtil.java"", - ""longName"": ""src/main/java/foo/security/crypto/KeyPairUtil.java"", - ""path"": ""src/main/java/foo/security/crypto/KeyPairUtil.java"" - }, - { - ""organization"": ""default-organization"", - ""key"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/BusinessThingsUtils.java"", - ""uuid"": ""AW0p2QjJ-y65ELkujuRJ"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""BusinessThingsUtils.java"", - ""longName"": ""src/main/java/foo/security/injection/BusinessThingsUtils.java"", - ""path"": ""src/main/java/foo/security/injection/BusinessThingsUtils.java"" - }, - { - ""organization"": ""default-organization"", - ""key"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/CommandInjectionVulnerability.java"", - ""uuid"": ""AW0p2QjJ-y65ELkujuRL"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""CommandInjectionVulnerability.java"", - ""longName"": ""src/main/java/foo/security/injection/CommandInjectionVulnerability.java"", - ""path"": ""src/main/java/foo/security/injection/CommandInjectionVulnerability.java"" - } - ], - ""rules"": [ - { - ""key"": ""java:S3330"", - ""name"": ""Creating cookies without the \""HttpOnly\"" flag is security-sensitive"", - ""lang"": ""java"", - ""status"": ""READY"", - ""langName"": ""Java"" - }, - { - ""key"": ""javasecurity:S3649"", - ""name"": ""Database queries should not be vulnerable to injection attacks"", - ""lang"": ""java"", - ""status"": ""READY"", - ""langName"": ""Java"" - }, - { - ""key"": ""javasecurity:S2076"", - ""name"": ""OS commands should not be vulnerable to command injection attacks"", - ""lang"": ""java"", - ""status"": ""READY"", - ""langName"": ""Java"" - }, - { - ""key"": ""java:S4426"", - ""name"": ""Cryptographic keys should be robust"", - ""lang"": ""java"", - ""status"": ""READY"", - ""langName"": ""Java"" - } - ] -} -"); - - var result = await service.GetTaintVulnerabilitiesAsync("shared", null, CancellationToken.None); - - messageHandler.VerifyAll(); - secondaryIssueHashUpdater.Verify(x => x.UpdateHashesAsync(result, service, It.IsAny())); - - // should return only 2 taint out of 4 vulnerabilities - result.Should().HaveCount(2); - - var taint1 = result[0]; - - taint1.IssueKey.Should().Be("AW0p2Qpn-y65ELkujuRf"); - taint1.RuleId.Should().Be("javasecurity:S2076"); - taint1.Message.Should().Be("Refactor this code to not construct the OS command from tainted, user-controlled data."); - taint1.FilePath.Should().Be("src\\main\\java\\foo\\security\\injection\\CommandInjectionVulnerability.java"); - taint1.Hash.Should().Be("1703916771e6abb765843e62a76fcb5a"); - taint1.CreationTimestamp.Should().Be(DateTimeOffset.Parse("2019-09-11T12:25:08+0000")); - taint1.LastUpdateTimestamp.Should().Be(DateTimeOffset.Parse("2019-09-11T12:29:47+0000")); - taint1.Flows.Should().NotBeEmpty(); - taint1.Flows.Count.Should().Be(1); - taint1.Flows[0].Locations.Count.Should().Be(8); - taint1.Flows[0].Locations[0].FilePath.Should().Be("src\\main\\java\\foo\\security\\injection\\CommandInjectionVulnerability.java"); - taint1.Flows[0].Locations[0].TextRange.Should().BeEquivalentTo(new IssueTextRange(17, 17, 4, 25)); - taint1.Flows[0].Locations[0].Message.Should().Be("tainted value is used to perform a security-sensitive operation"); - taint1.Flows[0].Locations[3].FilePath.Should().Be("src\\main\\java\\foo\\security\\injection\\Servlet.java"); - taint1.Flows[0].Locations[3].TextRange.Should().BeEquivalentTo(new IssueTextRange(38, 38, 6, 54)); - taint1.Flows[0].Locations[3].Message.Should().Be("taint value is propagated"); - taint1.TextRange.Should().BeEquivalentTo(new IssueTextRange(17, 17, 4, 25)); - taint1.Context.Should().Be("context"); - - var taint2 = result[1]; - - taint2.IssueKey.Should().Be("AW0p2QqO-y65ELkujuRk"); - taint2.RuleId.Should().Be("javasecurity:S3649"); - taint2.Message.Should().Be("Refactor this code to not construct SQL queries directly from tainted user-controlled data."); - taint2.FilePath.Should().Be("src\\main\\java\\foo\\security\\injection\\SQLInjectionVulnerabilityCollectionMultipleFiles.java"); - taint2.Hash.Should().Be("b07a403eb4e77c8d6c4b3fa5c6408064"); - taint2.CreationTimestamp.Should().Be(DateTimeOffset.Parse("2019-09-11T12:25:08+0100")); - taint2.LastUpdateTimestamp.Should().Be(DateTimeOffset.Parse("2019-09-16T10:35:19+1300")); - taint2.Flows.Should().NotBeEmpty(); - taint2.Flows.Count.Should().Be(1); - taint2.Flows[0].Locations.Count.Should().Be(16); - taint2.Flows[0].Locations[7].FilePath.Should().Be("src\\main\\java\\foo\\security\\injection\\BusinessThingsUtils.java"); - taint2.Flows[0].Locations[7].TextRange.Should().BeEquivalentTo(new IssueTextRange(18, 18, 15, 42)); - taint2.Flows[0].Locations[7].Message.Should().Be("taint value is propagated"); - taint2.Flows[0].Locations[15].FilePath.Should().Be("src\\main\\java\\foo\\security\\injection\\Servlet.java"); - taint2.Flows[0].Locations[15].TextRange.Should().BeEquivalentTo(new IssueTextRange(34, 34, 20, 59)); - taint2.Flows[0].Locations[15].Message.Should().Be("this value can be controlled by the user"); - taint2.TextRange.Should().BeEquivalentTo(new IssueTextRange(20, 20, 6, 23)); - taint2.Context.Should().BeNull(); - } - - [TestMethod] - public async Task GetTaintVulnerabilitiesWithCCTAsync_Response_From_SonarQube() - { - await ConnectToSonarQube("10.2.0.0"); - - SetupRequest("api/issues/search?additionalFields=ruleDescriptionContextKey&projects=shared&statuses=OPEN%2CCONFIRMED%2CREOPENED%2CRESOLVED&types=VULNERABILITY&p=1&ps=500", @" -{ - ""total"": 4, - ""p"": 1, - ""ps"": 100, - ""paging"": { - ""pageIndex"": 1, - ""pageSize"": 100, - ""total"": 4 - }, - ""effortTotal"": 72, - ""debtTotal"": 72, - ""issues"": [ - { - ""key"": ""AW0p2QsM-y65ELkujuR4"", - ""rule"": ""java:S4426"", - ""severity"": ""BLOCKER"", - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/crypto/KeyPairUtil.java"", - ""project"": ""com.sonarsource:citytour2019-java"", - ""line"": 12, - ""hash"": ""e6162855f182ddc1652ec2cdea0a085b"", - ""cleanCodeAttribute"": ""CLEAR"", - ""cleanCodeAttributeCategory"": ""INTENTIONAL"", - ""impacts"": [ - { - ""softwareQuality"": ""MAINTAINABILITY"", - ""severity"": ""HIGH"" - } - ], - ""textRange"": { - ""startLine"": 12, - ""endLine"": 12, - ""startOffset"": 6, - ""endOffset"": 32 - }, - ""flows"": [], - ""status"": ""REOPENED"", - ""message"": ""Use a key length of at least 2048 bits."", - ""effort"": ""2min"", - ""debt"": ""2min"", - ""author"": ""alexandre.gigleux@sonarsource.com"", - ""tags"": [ - ""cwe"", - ""owasp-a3"" - ], - ""transitions"": [ - ""confirm"", - ""resolve"", - ""falsepositive"", - ""wontfix"" - ], - ""actions"": [ - ""set_type"", - ""set_tags"", - ""comment"", - ""set_severity"", - ""assign"" - ], - ""comments"": [], - ""creationDate"": ""2019-09-12T13:05:53+0000"", - ""updateDate"": ""2020-08-24T09:10:42+0000"", - ""type"": ""VULNERABILITY"", - ""organization"": ""default-organization"", - ""scope"": ""MAIN"" - }, - { - ""key"": ""AW0p2Qrv-y65ELkujuR0"", - ""rule"": ""java:S3330"", - ""severity"": ""CRITICAL"", - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/hotspots/WritingCookieServlet.java"", - ""project"": ""com.sonarsource:citytour2019-java"", - ""line"": 10, - ""hash"": ""79fd1a1af84dcccce2e0ede4970663ce"", - ""cleanCodeAttribute"": ""CLEAR"", - ""cleanCodeAttributeCategory"": ""INTENTIONAL"", - ""impacts"": [ - { - ""softwareQuality"": ""MAINTAINABILITY"", - ""severity"": ""HIGH"" - } - ], - ""textRange"": { - ""startLine"": 10, - ""endLine"": 10, - ""startOffset"": 15, - ""endOffset"": 21 - }, - ""flows"": [], - ""status"": ""OPEN"", - ""message"": ""Add the \""HttpOnly\"" cookie attribute."", - ""effort"": ""10min"", - ""debt"": ""10min"", - ""assignee"": ""agigleux@github"", - ""author"": ""alexandre.gigleux@sonarsource.com"", - ""tags"": [ - ""cwe"", - ""owasp-a7"", - ""sans-top25-insecure"" - ], - ""transitions"": [ - ""confirm"", - ""resolve"", - ""falsepositive"", - ""wontfix"" - ], - ""actions"": [ - ""set_type"", - ""set_tags"", - ""comment"", - ""set_severity"", - ""assign"" - ], - ""comments"": [], - ""creationDate"": ""2019-09-11T12:25:08+0000"", - ""updateDate"": ""2019-09-11T12:29:47+0000"", - ""type"": ""VULNERABILITY"", - ""organization"": ""default-organization"", - ""scope"": ""MAIN"" - }, - { - ""ruleDescriptionContextKey"": ""context"", - ""key"": ""AW0p2Qpn-y65ELkujuRf"", - ""rule"": ""javasecurity:S2076"", - ""severity"": ""BLOCKER"", - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/CommandInjectionVulnerability.java"", - ""project"": ""com.sonarsource:citytour2019-java"", - ""line"": 17, - ""hash"": ""1703916771e6abb765843e62a76fcb5a"", - ""cleanCodeAttribute"": ""CLEAR"", - ""cleanCodeAttributeCategory"": ""INTENTIONALITY"", - ""impacts"": [ - { - ""softwareQuality"": ""MAINTAINABILITY"", - ""severity"": ""HIGH"" - }, - { - ""softwareQuality"": ""SECURITY"", - ""severity"": ""HIGH"" - } - ], - ""textRange"": { - ""startLine"": 17, - ""endLine"": 17, - ""startOffset"": 4, - ""endOffset"": 25 - }, - ""flows"": [ - { - ""locations"": [ - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/CommandInjectionVulnerability.java"", - ""textRange"": { - ""startLine"": 17, - ""endLine"": 17, - ""startOffset"": 4, - ""endOffset"": 25 - }, - ""msg"": ""tainted value is used to perform a security-sensitive operation"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/CommandInjectionVulnerability.java"", - ""textRange"": { - ""startLine"": 14, - ""endLine"": 14, - ""startOffset"": 6, - ""endOffset"": 40 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/CommandInjectionVulnerability.java"", - ""textRange"": { - ""startLine"": 8, - ""endLine"": 8, - ""startOffset"": 9, - ""endOffset"": 38 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 38, - ""endLine"": 38, - ""startOffset"": 6, - ""endOffset"": 54 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 36, - ""endLine"": 36, - ""startOffset"": 6, - ""endOffset"": 72 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 36, - ""endLine"": 36, - ""startOffset"": 22, - ""endOffset"": 72 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 34, - ""endLine"": 34, - ""startOffset"": 4, - ""endOffset"": 59 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 34, - ""endLine"": 34, - ""startOffset"": 20, - ""endOffset"": 59 - }, - ""msg"": ""this value can be controlled by the user"" - } - ] - } - ], - ""status"": ""OPEN"", - ""message"": ""Refactor this code to not construct the OS command from tainted, user-controlled data."", - ""effort"": ""30min"", - ""debt"": ""30min"", - ""assignee"": ""agigleux@github"", - ""author"": ""alexandre.gigleux@sonarsource.com"", - ""tags"": [ - ""cwe"", - ""owasp-a1"", - ""sans-top25-insecure"" - ], - ""transitions"": [ - ""confirm"", - ""resolve"", - ""falsepositive"", - ""wontfix"" - ], - ""actions"": [ - ""set_type"", - ""set_tags"", - ""comment"", - ""set_severity"", - ""assign"" - ], - ""comments"": [], - ""creationDate"": ""2019-09-11T12:25:08+0000"", - ""updateDate"": ""2019-09-11T12:29:47+0000"", - ""type"": ""VULNERABILITY"", - ""organization"": ""default-organization"", - ""scope"": ""MAIN"" - }, - { - ""key"": ""AW0p2QqO-y65ELkujuRk"", - ""rule"": ""javasecurity:S3649"", - ""severity"": ""BLOCKER"", - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/SQLInjectionVulnerabilityCollectionMultipleFiles.java"", - ""project"": ""com.sonarsource:citytour2019-java"", - ""line"": 20, - ""hash"": ""b07a403eb4e77c8d6c4b3fa5c6408064"", - ""cleanCodeAttribute"": ""DISTINCT"", - ""cleanCodeAttributeCategory"": ""ADAPTABILITY"", - ""impacts"": [ - { - ""softwareQuality"": ""RELIABILITY"", - ""severity"": ""LOW"" - }, - { - ""softwareQuality"": ""SECURITY"", - ""severity"": ""HIGH"" - } - ], - ""textRange"": { - ""startLine"": 20, - ""endLine"": 20, - ""startOffset"": 6, - ""endOffset"": 23 - }, - ""flows"": [ - { - ""locations"": [ - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/SQLInjectionVulnerabilityCollectionMultipleFiles.java"", - ""textRange"": { - ""startLine"": 20, - ""endLine"": 20, - ""startOffset"": 6, - ""endOffset"": 23 - }, - ""msg"": ""tainted value is used to perform a security-sensitive operation"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/SQLInjectionVulnerabilityCollectionMultipleFiles.java"", - ""textRange"": { - ""startLine"": 9, - ""endLine"": 9, - ""startOffset"": 9, - ""endOffset"": 57 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 43, - ""endLine"": 43, - ""startOffset"": 6, - ""endOffset"": 68 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 43, - ""endLine"": 43, - ""startOffset"": 59, - ""endOffset"": 67 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 41, - ""endLine"": 41, - ""startOffset"": 6, - ""endOffset"": 82 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 41, - ""endLine"": 41, - ""startOffset"": 23, - ""endOffset"": 81 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/BusinessThingsUtils.java"", - ""textRange"": { - ""startLine"": 18, - ""endLine"": 18, - ""startOffset"": 8, - ""endOffset"": 43 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/BusinessThingsUtils.java"", - ""textRange"": { - ""startLine"": 18, - ""endLine"": 18, - ""startOffset"": 15, - ""endOffset"": 42 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/BusinessThingsUtils.java"", - ""textRange"": { - ""startLine"": 17, - ""endLine"": 17, - ""startOffset"": 39, - ""endOffset"": 55 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/BusinessThingsUtils.java"", - ""textRange"": { - ""startLine"": 14, - ""endLine"": 14, - ""startOffset"": 8, - ""endOffset"": 36 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/BusinessThingsUtils.java"", - ""textRange"": { - ""startLine"": 11, - ""endLine"": 11, - ""startOffset"": 27, - ""endOffset"": 50 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 41, - ""endLine"": 41, - ""startOffset"": 23, - ""endOffset"": 81 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 36, - ""endLine"": 36, - ""startOffset"": 6, - ""endOffset"": 72 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 36, - ""endLine"": 36, - ""startOffset"": 22, - ""endOffset"": 72 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 34, - ""endLine"": 34, - ""startOffset"": 4, - ""endOffset"": 59 - }, - ""msg"": ""taint value is propagated"" - }, - { - ""component"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""textRange"": { - ""startLine"": 34, - ""endLine"": 34, - ""startOffset"": 20, - ""endOffset"": 59 - }, - ""msg"": ""this value can be controlled by the user"" - } - ] - } - ], - ""status"": ""OPEN"", - ""message"": ""Refactor this code to not construct SQL queries directly from tainted user-controlled data."", - ""effort"": ""30min"", - ""debt"": ""30min"", - ""assignee"": ""agigleux@github"", - ""author"": ""alexandre.gigleux@sonarsource.com"", - ""tags"": [ - ""cert"", - ""cwe"", - ""owasp-a1"", - ""sans-top25-insecure"", - ""sql"" - ], - ""transitions"": [ - ""confirm"", - ""resolve"", - ""falsepositive"", - ""wontfix"" - ], - ""actions"": [ - ""set_type"", - ""set_tags"", - ""comment"", - ""set_severity"", - ""assign"" - ], - ""comments"": [], - ""creationDate"": ""2019-09-11T12:25:08+0100"", - ""updateDate"": ""2019-09-16T10:35:19+1300"", - ""type"": ""VULNERABILITY"", - ""organization"": ""default-organization"", - ""scope"": ""MAIN"" - } - ], - ""components"": [ - { - ""organization"": ""default-organization"", - ""key"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/Servlet.java"", - ""uuid"": ""AW0p2QjJ-y65ELkujuRO"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""Servlet.java"", - ""longName"": ""src/main/java/foo/security/injection/Servlet.java"", - ""path"": ""src/main/java/foo/security/injection/Servlet.java"" - }, - { - ""organization"": ""default-organization"", - ""key"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/SQLInjectionVulnerabilityCollectionMultipleFiles.java"", - ""uuid"": ""AW05pEYnBvApAtP9iYg6"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""SQLInjectionVulnerabilityCollectionMultipleFiles.java"", - ""longName"": ""src/main/java/foo/security/injection/SQLInjectionVulnerabilityCollectionMultipleFiles.java"", - ""path"": ""src/main/java/foo/security/injection/SQLInjectionVulnerabilityCollectionMultipleFiles.java"" - }, - { - ""organization"": ""default-organization"", - ""key"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/hotspots/WritingCookieServlet.java"", - ""uuid"": ""AW0p2QjJ-y65ELkujuRT"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""WritingCookieServlet.java"", - ""longName"": ""src/main/java/foo/security/hotspots/WritingCookieServlet.java"", - ""path"": ""src/main/java/foo/security/hotspots/WritingCookieServlet.java"" - }, - { - ""organization"": ""default-organization"", - ""key"": ""com.sonarsource:citytour2019-java"", - ""uuid"": ""AW0abn1qGHw5MqdAqloE"", - ""enabled"": true, - ""qualifier"": ""TRK"", - ""name"": ""City Tour - Java project"", - ""longName"": ""City Tour - Java project"" - }, - { - ""organization"": ""default-organization"", - ""key"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/crypto/KeyPairUtil.java"", - ""uuid"": ""AW0p2QjJ-y65ELkujuRX"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""KeyPairUtil.java"", - ""longName"": ""src/main/java/foo/security/crypto/KeyPairUtil.java"", - ""path"": ""src/main/java/foo/security/crypto/KeyPairUtil.java"" - }, - { - ""organization"": ""default-organization"", - ""key"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/BusinessThingsUtils.java"", - ""uuid"": ""AW0p2QjJ-y65ELkujuRJ"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""BusinessThingsUtils.java"", - ""longName"": ""src/main/java/foo/security/injection/BusinessThingsUtils.java"", - ""path"": ""src/main/java/foo/security/injection/BusinessThingsUtils.java"" - }, - { - ""organization"": ""default-organization"", - ""key"": ""com.sonarsource:citytour2019-java:src/main/java/foo/security/injection/CommandInjectionVulnerability.java"", - ""uuid"": ""AW0p2QjJ-y65ELkujuRL"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""CommandInjectionVulnerability.java"", - ""longName"": ""src/main/java/foo/security/injection/CommandInjectionVulnerability.java"", - ""path"": ""src/main/java/foo/security/injection/CommandInjectionVulnerability.java"" - } - ], - ""rules"": [ - { - ""key"": ""java:S3330"", - ""name"": ""Creating cookies without the \""HttpOnly\"" flag is security-sensitive"", - ""lang"": ""java"", - ""status"": ""READY"", - ""langName"": ""Java"" - }, - { - ""key"": ""javasecurity:S3649"", - ""name"": ""Database queries should not be vulnerable to injection attacks"", - ""lang"": ""java"", - ""status"": ""READY"", - ""langName"": ""Java"" - }, - { - ""key"": ""javasecurity:S2076"", - ""name"": ""OS commands should not be vulnerable to command injection attacks"", - ""lang"": ""java"", - ""status"": ""READY"", - ""langName"": ""Java"" - }, - { - ""key"": ""java:S4426"", - ""name"": ""Cryptographic keys should be robust"", - ""lang"": ""java"", - ""status"": ""READY"", - ""langName"": ""Java"" - } - ] -} -"); - - var result = await service.GetTaintVulnerabilitiesAsync("shared", null, CancellationToken.None); - - messageHandler.VerifyAll(); - secondaryIssueHashUpdater.Verify(x => x.UpdateHashesAsync(result, service, It.IsAny())); - - // should return only 2 taint out of 4 vulnerabilities - result.Should().HaveCount(2); - - var taint1 = result[0]; - - var taint1Impacts = new Dictionary - { - { SonarQubeSoftwareQuality.Maintainability, SonarQubeSoftwareQualitySeverity.High }, - { SonarQubeSoftwareQuality.Security, SonarQubeSoftwareQualitySeverity.High } - }; - - taint1.IssueKey.Should().Be("AW0p2Qpn-y65ELkujuRf"); - taint1.RuleId.Should().Be("javasecurity:S2076"); - taint1.Message.Should().Be("Refactor this code to not construct the OS command from tainted, user-controlled data."); - taint1.FilePath.Should().Be("src\\main\\java\\foo\\security\\injection\\CommandInjectionVulnerability.java"); - taint1.Hash.Should().Be("1703916771e6abb765843e62a76fcb5a"); - taint1.CreationTimestamp.Should().Be(DateTimeOffset.Parse("2019-09-11T12:25:08+0000")); - taint1.LastUpdateTimestamp.Should().Be(DateTimeOffset.Parse("2019-09-11T12:29:47+0000")); - taint1.Flows.Should().NotBeEmpty(); - taint1.Flows.Count.Should().Be(1); - taint1.Flows[0].Locations.Count.Should().Be(8); - taint1.Flows[0].Locations[0].FilePath.Should().Be("src\\main\\java\\foo\\security\\injection\\CommandInjectionVulnerability.java"); - taint1.Flows[0].Locations[0].TextRange.Should().BeEquivalentTo(new IssueTextRange(17, 17, 4, 25)); - taint1.Flows[0].Locations[0].Message.Should().Be("tainted value is used to perform a security-sensitive operation"); - taint1.Flows[0].Locations[3].FilePath.Should().Be("src\\main\\java\\foo\\security\\injection\\Servlet.java"); - taint1.Flows[0].Locations[3].TextRange.Should().BeEquivalentTo(new IssueTextRange(38, 38, 6, 54)); - taint1.Flows[0].Locations[3].Message.Should().Be("taint value is propagated"); - taint1.TextRange.Should().BeEquivalentTo(new IssueTextRange(17, 17, 4, 25)); - taint1.Context.Should().Be("context"); - taint1.CleanCodeAttribute.Should().Be(SonarQubeCleanCodeAttribute.Clear); - taint1.DefaultImpacts.Should().BeEquivalentTo(taint1Impacts); - - var taint2 = result[1]; - - var taint2Impacts = new Dictionary - { - { SonarQubeSoftwareQuality.Reliability, SonarQubeSoftwareQualitySeverity.Low }, - { SonarQubeSoftwareQuality.Security, SonarQubeSoftwareQualitySeverity.High } - }; - - taint2.IssueKey.Should().Be("AW0p2QqO-y65ELkujuRk"); - taint2.RuleId.Should().Be("javasecurity:S3649"); - taint2.Message.Should().Be("Refactor this code to not construct SQL queries directly from tainted user-controlled data."); - taint2.FilePath.Should().Be("src\\main\\java\\foo\\security\\injection\\SQLInjectionVulnerabilityCollectionMultipleFiles.java"); - taint2.Hash.Should().Be("b07a403eb4e77c8d6c4b3fa5c6408064"); - taint2.CreationTimestamp.Should().Be(DateTimeOffset.Parse("2019-09-11T12:25:08+0100")); - taint2.LastUpdateTimestamp.Should().Be(DateTimeOffset.Parse("2019-09-16T10:35:19+1300")); - taint2.Flows.Should().NotBeEmpty(); - taint2.Flows.Count.Should().Be(1); - taint2.Flows[0].Locations.Count.Should().Be(16); - taint2.Flows[0].Locations[7].FilePath.Should().Be("src\\main\\java\\foo\\security\\injection\\BusinessThingsUtils.java"); - taint2.Flows[0].Locations[7].TextRange.Should().BeEquivalentTo(new IssueTextRange(18, 18, 15, 42)); - taint2.Flows[0].Locations[7].Message.Should().Be("taint value is propagated"); - taint2.Flows[0].Locations[15].FilePath.Should().Be("src\\main\\java\\foo\\security\\injection\\Servlet.java"); - taint2.Flows[0].Locations[15].TextRange.Should().BeEquivalentTo(new IssueTextRange(34, 34, 20, 59)); - taint2.Flows[0].Locations[15].Message.Should().Be("this value can be controlled by the user"); - taint2.TextRange.Should().BeEquivalentTo(new IssueTextRange(20, 20, 6, 23)); - taint2.Context.Should().BeNull(); - taint2.CleanCodeAttribute.Should().Be(SonarQubeCleanCodeAttribute.Distinct); - taint2.DefaultImpacts.Should().BeEquivalentTo(taint2Impacts); - } - - [TestMethod] - [DataRow("")] - [DataRow(null)] - public async Task GetTaintVulnerabilitiesAsync_BranchIsNotSpecified_BranchIsNotIncludedInQueryString(string emptyBranch) - { - await ConnectToSonarQube("8.6.0.0"); - messageHandler.Reset(); - - SetupHttpRequest(messageHandler, EmptyGetIssuesResponse); - _ = await service.GetTaintVulnerabilitiesAsync("any", emptyBranch, CancellationToken.None); - - // Branch is null/empty => should not be passed - var actualRequests = messageHandler.GetSendAsyncRequests(); - actualRequests.Should().ContainSingle(); - actualRequests[0].RequestUri.Query.Contains("branch").Should().BeFalse(); - } - - [TestMethod] - public async Task GetTaintVulnerabilitiesAsync_BranchIsSpecified_BranchIncludedInQueryString() - { - await ConnectToSonarQube("8.6.0.0"); - messageHandler.Reset(); - - SetupHttpRequest(messageHandler, EmptyGetIssuesResponse); - _ = await service.GetTaintVulnerabilitiesAsync("any", "aBranch", CancellationToken.None); - - var actualRequests = messageHandler.GetSendAsyncRequests(); - actualRequests.Should().ContainSingle(); - actualRequests[0].RequestUri.Query.Contains("&branch=aBranch&").Should().BeTrue(); - } - } -} diff --git a/src/SonarQube.Client/Api/DefaultConfiguration.cs b/src/SonarQube.Client/Api/DefaultConfiguration.cs index 638088b768..df4278c584 100644 --- a/src/SonarQube.Client/Api/DefaultConfiguration.cs +++ b/src/SonarQube.Client/Api/DefaultConfiguration.cs @@ -20,86 +20,81 @@ using SonarQube.Client.Requests; -namespace SonarQube.Client.Api +namespace SonarQube.Client.Api; + +internal static class DefaultConfiguration { - internal static class DefaultConfiguration + public static RequestFactory ConfigureSonarQube(RequestFactory requestFactory) { - public static RequestFactory ConfigureSonarQube(RequestFactory requestFactory) - { - requestFactory - .RegisterRequest("2.1") - .RegisterRequest("2.1") - .RegisterRequest("2.1") - .RegisterRequest("2.6") - .RegisterRequest("3.3") - .RegisterRequest("5.0") - .RegisterRequest("5.1") - .RegisterRequest("5.1") - .RegisterRequest("5.2") - .RegisterRequest("5.2") - .RegisterRequest("5.2") - .RegisterRequest("5.4") - .RegisterRequest("5.5") - .RegisterRequest("5.5") - .RegisterRequest("6.2") - .RegisterRequest("6.2") - .RegisterRequest("6.3") - .RegisterRequest("6.3") - .RegisterRequest("6.5") - .RegisterRequest("6.5") - .RegisterRequest("6.6") - .RegisterRequest("6.6") - .RegisterRequest("6.6") - .RegisterRequest("7.0") - .RegisterRequest>("7.2") - .RegisterRequest("8.6") - .RegisterRequest("8.6") - .RegisterRequest("7.2") - .RegisterRequest("9.4") - .RegisterRequest("9.5") - .RegisterRequest("9.6") - .RegisterRequest("9.6") - .RegisterRequest("9.7") - .RegisterRequest("10.2") - .RegisterRequest("10.2") - .RegisterRequest("10.2") - .RegisterRequest("9.9") - .RegisterRequest("10.4") - .RegisterRequest("9.9") - .RegisterRequest("9.9"); + requestFactory + .RegisterRequest("2.1") + .RegisterRequest("2.1") + .RegisterRequest("2.1") + .RegisterRequest("2.6") + .RegisterRequest("3.3") + .RegisterRequest("5.0") + .RegisterRequest("5.1") + .RegisterRequest("5.1") + .RegisterRequest("5.2") + .RegisterRequest("5.2") + .RegisterRequest("5.2") + .RegisterRequest("5.4") + .RegisterRequest("5.5") + .RegisterRequest("5.5") + .RegisterRequest("6.2") + .RegisterRequest("6.2") + .RegisterRequest("6.3") + .RegisterRequest("6.3") + .RegisterRequest("6.5") + .RegisterRequest("6.5") + .RegisterRequest("6.6") + .RegisterRequest("6.6") + .RegisterRequest("6.6") + .RegisterRequest("7.0") + .RegisterRequest>("7.2") + .RegisterRequest("8.6") + .RegisterRequest("7.2") + .RegisterRequest("9.4") + .RegisterRequest("9.5") + .RegisterRequest("9.6") + .RegisterRequest("9.7") + .RegisterRequest("10.2") + .RegisterRequest("10.2") + .RegisterRequest("9.9") + .RegisterRequest("10.4") + .RegisterRequest("9.9") + .RegisterRequest("9.9"); - return requestFactory; - } + return requestFactory; + } - public static UnversionedRequestFactory ConfigureSonarCloud(UnversionedRequestFactory requestFactory) - { - requestFactory - .RegisterRequest() - .RegisterRequest() - .RegisterRequest() - .RegisterRequest() - .RegisterRequest() - .RegisterRequest() - .RegisterRequest() - .RegisterRequest() - .RegisterRequest() - .RegisterRequest() - .RegisterRequest() - .RegisterRequest() - .RegisterRequest() - .RegisterRequest() - .RegisterRequest() - .RegisterRequest() - .RegisterRequest>() - .RegisterRequest() - .RegisterRequest() - .RegisterRequest() - .RegisterRequest() - .RegisterRequest() - .RegisterRequest() - .RegisterRequest(); + public static UnversionedRequestFactory ConfigureSonarCloud(UnversionedRequestFactory requestFactory) + { + requestFactory + .RegisterRequest() + .RegisterRequest() + .RegisterRequest() + .RegisterRequest() + .RegisterRequest() + .RegisterRequest() + .RegisterRequest() + .RegisterRequest() + .RegisterRequest() + .RegisterRequest() + .RegisterRequest() + .RegisterRequest() + .RegisterRequest() + .RegisterRequest() + .RegisterRequest() + .RegisterRequest() + .RegisterRequest>() + .RegisterRequest() + .RegisterRequest() + .RegisterRequest() + .RegisterRequest() + .RegisterRequest() + .RegisterRequest(); - return requestFactory; - } + return requestFactory; } } diff --git a/src/SonarQube.Client/Api/IGetIssuesRequest.cs b/src/SonarQube.Client/Api/IGetIssuesRequest.cs index 06fb141ab6..6df278d088 100644 --- a/src/SonarQube.Client/Api/IGetIssuesRequest.cs +++ b/src/SonarQube.Client/Api/IGetIssuesRequest.cs @@ -21,28 +21,25 @@ using SonarQube.Client.Models; using SonarQube.Client.Requests; -namespace SonarQube.Client.Api -{ - interface IGetIssuesRequest : IRequest - { - string ProjectKey { get; set; } +namespace SonarQube.Client.Api; - string Statuses { get; set; } +interface IGetIssuesRequest : IRequest +{ + string ProjectKey { get; set; } - /// - /// The branch name to fetch. - /// - /// If the value is null/empty, the main branch will be fetched - string Branch { get; set; } + string Statuses { get; set; } - string[] IssueKeys { get; set; } + /// + /// The branch name to fetch. + /// + /// If the value is null/empty, the main branch will be fetched + string Branch { get; set; } - string RuleId { get; set; } + string[] IssueKeys { get; set; } - string ComponentKey { get; set; } + string RuleId { get; set; } - bool IncludeTaint { get; set; } + string ComponentKey { get; set; } - // Update when adding properties here. - } + // Update when adding properties here. } diff --git a/src/SonarQube.Client/Api/V5_10/GetIssuesRequest.cs b/src/SonarQube.Client/Api/V5_10/GetIssuesRequest.cs index 699e7064a5..82e887aa98 100644 --- a/src/SonarQube.Client/Api/V5_10/GetIssuesRequest.cs +++ b/src/SonarQube.Client/Api/V5_10/GetIssuesRequest.cs @@ -18,13 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Collections.Generic; using System.ComponentModel; using System.IO; -using System.Linq; using System.Net.Http; -using System.Threading.Tasks; using Google.Protobuf; using Newtonsoft.Json; using SonarQube.Client.Helpers; @@ -32,83 +28,79 @@ using SonarQube.Client.Models; using SonarQube.Client.Requests; -namespace SonarQube.Client.Api.V5_10 -{ - public class GetIssuesRequest : RequestBase, IGetIssuesRequest - { - private HashSet statuses = new HashSet(); // Prevent null reference exceptions when Statuses is never set +namespace SonarQube.Client.Api.V5_10; - [JsonProperty("key")] - public virtual string ProjectKey { get; set; } +public class GetIssuesRequest : RequestBase, IGetIssuesRequest +{ + private HashSet statuses = new HashSet(); // Prevent null reference exceptions when Statuses is never set - [JsonProperty("statuses", DefaultValueHandling = DefaultValueHandling.Ignore), DefaultValue(null)] - public string Statuses - { - get { return null; } // Always return null to prevent this value from serialization - set { statuses = new HashSet(value.Split(',')); } - } + [JsonProperty("key")] + public virtual string ProjectKey { get; set; } - [JsonIgnore] // We don't support the branch parameter in v5.10 - public string Branch { get; set; } + [JsonProperty("statuses", DefaultValueHandling = DefaultValueHandling.Ignore), DefaultValue(null)] + public string Statuses + { + get { return null; } // Always return null to prevent this value from serialization + set { statuses = new HashSet(value.Split(',')); } + } - [JsonIgnore] // We decided not to support it for API calls older than v7.20 - public string[] IssueKeys { get; set; } + [JsonIgnore] // We don't support the branch parameter in v5.10 + public string Branch { get; set; } - protected override string Path => "batch/issues"; + [JsonIgnore] // We decided not to support it for API calls older than v7.20 + public string[] IssueKeys { get; set; } - [JsonIgnore] // We decided not to support it for API calls older than v7.20 - public string RuleId { get; set; } + protected override string Path => "batch/issues"; - [JsonIgnore] // We decided not to support it for API calls older than v7.20 - public string ComponentKey { get; set; } + [JsonIgnore] // We decided not to support it for API calls older than v7.20 + public string RuleId { get; set; } - [JsonIgnore] - public bool IncludeTaint { get; set; } = true; + [JsonIgnore] // We decided not to support it for API calls older than v7.20 + public string ComponentKey { get; set; } - protected async override Task> ReadResponseAsync(HttpResponseMessage httpResponse) + protected async override Task> ReadResponseAsync(HttpResponseMessage httpResponse) + { + if (!httpResponse.IsSuccessStatusCode) { - if (!httpResponse.IsSuccessStatusCode) - { - return new Result(httpResponse, null); - } - - var byteArray = await httpResponse.Content.ReadAsByteArrayAsync(); - // Protobuf for C# throws when trying to read outside of the buffer and ReadAsStreamAsync returns a non - // seekable stream so we can't determine when to stop. The hack is to use an intermediate MemoryStream - // so we can control when to stop reading. - // Note we might want to use FileStream instead to avoid intensive memory usage. - using (var stream = new MemoryStream(byteArray)) - { - var result = ReadFromProtobufStream(stream, ServerIssue.Parser) - // This Web API does not support server-side filtering, so we do it on the client - .Where(issue => statuses.Contains(issue.Status)) - .Select(ToSonarQubeIssue) - .ToArray(); - return new Result(httpResponse, result); - } + return new Result(httpResponse, null); } - protected override SonarQubeIssue[] ParseResponse(string response) + var byteArray = await httpResponse.Content.ReadAsByteArrayAsync(); + // Protobuf for C# throws when trying to read outside of the buffer and ReadAsStreamAsync returns a non + // seekable stream so we can't determine when to stop. The hack is to use an intermediate MemoryStream + // so we can control when to stop reading. + // Note we might want to use FileStream instead to avoid intensive memory usage. + using (var stream = new MemoryStream(byteArray)) { - throw new NotSupportedException("This method will not be called because we override ReadResponse."); + var result = ReadFromProtobufStream(stream, ServerIssue.Parser) + // This Web API does not support server-side filtering, so we do it on the client + .Where(issue => statuses.Contains(issue.Status)) + .Select(ToSonarQubeIssue) + .ToArray(); + return new Result(httpResponse, result); } + } + + protected override SonarQubeIssue[] ParseResponse(string response) + { + throw new NotSupportedException("This method will not be called because we override ReadResponse."); + } - private static IEnumerable ReadFromProtobufStream(Stream stream, MessageParser parser) - where T : IMessage + private static IEnumerable ReadFromProtobufStream(Stream stream, MessageParser parser) + where T : IMessage + { + while (stream.Position < stream.Length) { - while (stream.Position < stream.Length) - { - yield return parser.ParseDelimitedFrom(stream); - } + yield return parser.ParseDelimitedFrom(stream); } - - private static SonarQubeIssue ToSonarQubeIssue(ServerIssue issue) => - new SonarQubeIssue(issue.Key, FilePathNormalizer.NormalizeSonarQubePath(issue.Path), issue.Checksum, issue.Msg, - issue.ModuleKey, issue.RuleKey, issue.Status == "RESOLVED", - severity: SonarQubeIssueSeverity.Unknown, - creationTimestamp: DateTimeOffset.MinValue, // we don't support the timestamp fields for these versions of SQ - lastUpdateTimestamp: DateTimeOffset.MinValue, - textRange: new IssueTextRange(issue.Line, issue.Line, 0, 0), - flows: null); } + + private static SonarQubeIssue ToSonarQubeIssue(ServerIssue issue) => + new SonarQubeIssue(issue.Key, FilePathNormalizer.NormalizeSonarQubePath(issue.Path), issue.Checksum, issue.Msg, + issue.ModuleKey, issue.RuleKey, issue.Status == "RESOLVED", + severity: SonarQubeIssueSeverity.Unknown, + creationTimestamp: DateTimeOffset.MinValue, // we don't support the timestamp fields for these versions of SQ + lastUpdateTimestamp: DateTimeOffset.MinValue, + textRange: new IssueTextRange(issue.Line, issue.Line, 0, 0), + flows: null); } diff --git a/src/SonarQube.Client/Api/V7_20/GetIssuesRequest.cs b/src/SonarQube.Client/Api/V7_20/GetIssuesRequest.cs index f6b0d57c5e..80aa958d66 100644 --- a/src/SonarQube.Client/Api/V7_20/GetIssuesRequest.cs +++ b/src/SonarQube.Client/Api/V7_20/GetIssuesRequest.cs @@ -18,10 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Collections.Generic; using System.ComponentModel; -using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using SonarQube.Client.Api.Common; @@ -29,162 +26,158 @@ using SonarQube.Client.Models; using SonarQube.Client.Requests; -namespace SonarQube.Client.Api.V7_20 -{ - /// - /// Generic get issues class. It does not support as it is server-type-specific. - /// See and for more details. - /// - internal class GetIssuesRequest : PagedRequestBase - { - [JsonProperty("projects")] - public virtual string ProjectKey { get; set; } +namespace SonarQube.Client.Api.V7_20; - [JsonProperty("statuses")] - public string Statuses { get; set; } - - [JsonProperty("issues")] - public string IssueKeysAsString => IssueKeys == null ? null : string.Join(",", IssueKeys); +/// +/// Generic get issues class. It does not support as it is server-type-specific. +/// See and for more details. +/// +internal class GetIssuesRequest : PagedRequestBase +{ + [JsonProperty("projects")] + public virtual string ProjectKey { get; set; } - [JsonIgnore] - public string[] IssueKeys { get; set; } + [JsonProperty("statuses")] + public string Statuses { get; set; } - //These are normally comma seperated values but we need only one for now. If we need to pass more than 1 value we'll need to change the structure similar to issueKeys - //For now it's not needed. - [JsonProperty("rules")] - public string RuleId { get; set; } + [JsonProperty("issues")] + public string IssueKeysAsString => IssueKeys == null ? null : string.Join(",", IssueKeys); - [JsonIgnore] - public bool IncludeTaint { get; set; } = true; + [JsonIgnore] + public string[] IssueKeys { get; set; } - // Notes: - // 1) Branch support is not available in SQ Community edition. SQ will just ignore it. - // 2) SonarQube has supported the parameter since v6.6. However, the LTS at the point - // we added added branch-awareness to SLVS was v8.9.10. To minimise the amount of - // work on the SLVS side, we'll add branch support from SQ v7.2. - [JsonProperty("branch", DefaultValueHandling = DefaultValueHandling.Ignore), DefaultValue("")] - public string Branch { get; set; } + //These are normally comma seperated values but we need only one for now. If we need to pass more than 1 value we'll need to change the structure similar to issueKeys + //For now it's not needed. + [JsonProperty("rules")] + public string RuleId { get; set; } - // This property is not present in the IGetIssuesRequest interface, it is meant to be - // set by the GetIssuesRequestWrapper to add additional parameters to the API calls. - [JsonProperty("types")] - public string Types { get; set; } + // Notes: + // 1) Branch support is not available in SQ Community edition. SQ will just ignore it. + // 2) SonarQube has supported the parameter since v6.6. However, the LTS at the point + // we added added branch-awareness to SLVS was v8.9.10. To minimise the amount of + // work on the SLVS side, we'll add branch support from SQ v7.2. + [JsonProperty("branch", DefaultValueHandling = DefaultValueHandling.Ignore), DefaultValue("")] + public string Branch { get; set; } - protected override string Path => "api/issues/search"; + // This property is not present in the IGetIssuesRequest interface, it is meant to be + // set by the GetIssuesRequestWrapper to add additional parameters to the API calls. + [JsonProperty("types")] + public string Types { get; set; } - protected override SonarQubeIssue[] ParseResponse(string response) - { - var root = JObject.Parse(response); + protected override string Path => "api/issues/search"; - // This is a paged request so ParseResponse will be called once for each "page" - // of the response. However, we expect each page to be self-contained, so we want - // to rebuild the lookup each time. - componentKeyPathLookup = root.GetComponentKeyPathLookup(); + protected override SonarQubeIssue[] ParseResponse(string response) + { + var root = JObject.Parse(response); - return root["issues"] - .ToObject() - .Select(ToSonarQubeIssue) - .ToArray(); - } + // This is a paged request so ParseResponse will be called once for each "page" + // of the response. However, we expect each page to be self-contained, so we want + // to rebuild the lookup each time. + componentKeyPathLookup = root.GetComponentKeyPathLookup(); - #region Json data classes -> public read-only class conversion methods + return root["issues"] + .ToObject() + .Select(ToSonarQubeIssue) + .ToArray(); + } - /// - /// Lookup component key -> path for files. Each response contains normalized data, containing - /// issues and components, where each issue's "component" property points to a component with - /// the same "key". We obtain the FilePath of each issue from its corresponding component. - /// - private protected ILookup componentKeyPathLookup; + #region Json data classes -> public read-only class conversion methods - private SonarQubeIssue ToSonarQubeIssue(ServerIssue issue) => - new SonarQubeIssue(issue.IssueKey, ComputePath(issue.Component), issue.Hash, issue.Message, ComputeModuleKey(issue), - issue.CompositeRuleKey, issue.Status == "RESOLVED", - SonarQubeIssueSeverityConverter.Convert(issue.Severity), - issue.CreationDate, - issue.UpdateDate, - issue.TextRange.ToIssueTextRange(), - ToIssueFlows(issue.Flows), - issue.ContextKey); + /// + /// Lookup component key -> path for files. Each response contains normalized data, containing + /// issues and components, where each issue's "component" property points to a component with + /// the same "key". We obtain the FilePath of each issue from its corresponding component. + /// + private protected ILookup componentKeyPathLookup; - private protected string ComputePath(string component) => - FilePathNormalizer.NormalizeSonarQubePath(componentKeyPathLookup[component].FirstOrDefault() ?? string.Empty); + private SonarQubeIssue ToSonarQubeIssue(ServerIssue issue) => + new SonarQubeIssue(issue.IssueKey, ComputePath(issue.Component), issue.Hash, issue.Message, ComputeModuleKey(issue), + issue.CompositeRuleKey, issue.Status == "RESOLVED", + SonarQubeIssueSeverityConverter.Convert(issue.Severity), + issue.CreationDate, + issue.UpdateDate, + issue.TextRange.ToIssueTextRange(), + ToIssueFlows(issue.Flows), + issue.ContextKey); - private protected static string ComputeModuleKey(ServerIssue issue) => - issue.SubProject ?? issue.Component; + private protected string ComputePath(string component) => + FilePathNormalizer.NormalizeSonarQubePath(componentKeyPathLookup[component].FirstOrDefault() ?? string.Empty); - private protected List ToIssueFlows(ServerIssueFlow[] serverIssueFlows) => - serverIssueFlows?.Select(ToIssueFlow).ToList(); + private protected static string ComputeModuleKey(ServerIssue issue) => + issue.SubProject ?? issue.Component; - private protected IssueFlow ToIssueFlow(ServerIssueFlow serverIssueFlow) => - new IssueFlow(serverIssueFlow.Locations?.Select(ToIssueLocation).ToList()); + private protected List ToIssueFlows(ServerIssueFlow[] serverIssueFlows) => + serverIssueFlows?.Select(ToIssueFlow).ToList(); - private protected IssueLocation ToIssueLocation(ServerIssueLocation serverIssueLocation) => - new IssueLocation(ComputePath(serverIssueLocation.Component), serverIssueLocation.Component, serverIssueLocation.TextRange.ToIssueTextRange(), serverIssueLocation.Message); + private protected IssueFlow ToIssueFlow(ServerIssueFlow serverIssueFlow) => + new IssueFlow(serverIssueFlow.Locations?.Select(ToIssueLocation).ToList()); - #endregion Json data classes -> public read-only class conversion methods + private protected IssueLocation ToIssueLocation(ServerIssueLocation serverIssueLocation) => + new IssueLocation(ComputePath(serverIssueLocation.Component), serverIssueLocation.Component, serverIssueLocation.TextRange.ToIssueTextRange(), serverIssueLocation.Message); - #region JSON data classes + #endregion Json data classes -> public read-only class conversion methods - private protected class ServerIssue - { - [JsonProperty("key")] - public string IssueKey { get; set; } + #region JSON data classes - [JsonProperty("rule")] - public string CompositeRuleKey { get; set; } + private protected class ServerIssue + { + [JsonProperty("key")] + public string IssueKey { get; set; } - [JsonProperty("component")] - public string Component { get; set; } + [JsonProperty("rule")] + public string CompositeRuleKey { get; set; } - [JsonProperty("subProject")] - public string SubProject { get; set; } + [JsonProperty("component")] + public string Component { get; set; } - [JsonProperty("hash")] - public string Hash { get; set; } + [JsonProperty("subProject")] + public string SubProject { get; set; } - [JsonProperty("message")] - public string Message { get; set; } + [JsonProperty("hash")] + public string Hash { get; set; } - [JsonProperty("status")] - public string Status { get; set; } + [JsonProperty("message")] + public string Message { get; set; } - [JsonProperty("severity")] - public string Severity { get; set; } + [JsonProperty("status")] + public string Status { get; set; } - [JsonProperty("textRange")] - public ServerIssueTextRange TextRange { get; set; } + [JsonProperty("severity")] + public string Severity { get; set; } - [JsonProperty("creationDate")] - public virtual DateTimeOffset CreationDate { get; set; } + [JsonProperty("textRange")] + public ServerIssueTextRange TextRange { get; set; } - [JsonProperty("updateDate")] - public virtual DateTimeOffset UpdateDate { get; set; } + [JsonProperty("creationDate")] + public virtual DateTimeOffset CreationDate { get; set; } - [JsonProperty("flows")] - public ServerIssueFlow[] Flows { get; set; } + [JsonProperty("updateDate")] + public virtual DateTimeOffset UpdateDate { get; set; } - [JsonProperty("ruleDescriptionContextKey")] - public string ContextKey { get; set; } - } + [JsonProperty("flows")] + public ServerIssueFlow[] Flows { get; set; } - private protected sealed class ServerIssueFlow - { - [JsonProperty("locations")] - public ServerIssueLocation[] Locations { get; set; } - } + [JsonProperty("ruleDescriptionContextKey")] + public string ContextKey { get; set; } + } - private protected sealed class ServerIssueLocation - { - [JsonProperty("component")] - public string Component { get; set; } + private protected sealed class ServerIssueFlow + { + [JsonProperty("locations")] + public ServerIssueLocation[] Locations { get; set; } + } - [JsonProperty("textRange")] - public ServerIssueTextRange TextRange { get; set; } + private protected sealed class ServerIssueLocation + { + [JsonProperty("component")] + public string Component { get; set; } - [JsonProperty("msg")] - public string Message { get; set; } - } + [JsonProperty("textRange")] + public ServerIssueTextRange TextRange { get; set; } - #endregion // JSON data classes + [JsonProperty("msg")] + public string Message { get; set; } } + + #endregion // JSON data classes } diff --git a/src/SonarQube.Client/Api/V7_20/GetIssuesRequestWrapper.cs b/src/SonarQube.Client/Api/V7_20/GetIssuesRequestWrapper.cs index 8c44b38718..f142ee65f2 100644 --- a/src/SonarQube.Client/Api/V7_20/GetIssuesRequestWrapper.cs +++ b/src/SonarQube.Client/Api/V7_20/GetIssuesRequestWrapper.cs @@ -18,105 +18,89 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Linq; using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; using SonarQube.Client.Logging; using SonarQube.Client.Models; -namespace SonarQube.Client.Api.V7_20 +namespace SonarQube.Client.Api.V7_20; + +/// +/// The SonarQube 10k API result limit problem (https://github.com/SonarSource/sonarlint-visualstudio/issues/776): +/// SonarQube will return the first 10k results from any query.The suppressed issues in large +/// projects could be more than 10k and SLVS will not hide those which are not returned by the +/// server. +/// +/// To reduce the effects of this limitation we will retrieve issues in batches by issue type. +/// The same approach is used in the other flavours of SonarLint. +/// +/// This class should be removed if/when SonarQube removes the 10k API result limitation. +/// +internal class GetIssuesRequestWrapper : IGetIssuesRequest + where T : GetIssuesWithComponentRequest, new() { - /// - /// The SonarQube 10k API result limit problem (https://github.com/SonarSource/sonarlint-visualstudio/issues/776): - /// SonarQube will return the first 10k results from any query.The suppressed issues in large - /// projects could be more than 10k and SLVS will not hide those which are not returned by the - /// server. - /// - /// To reduce the effects of this limitation we will retrieve issues in batches by issue type. - /// The same approach is used in the other flavours of SonarLint. - /// - /// This class should be removed if/when SonarQube removes the 10k API result limitation. - /// - internal class GetIssuesRequestWrapper : IGetIssuesRequest - where T : GetIssuesWithComponentRequest, new() - { - private readonly T innerRequest = new T(); + private readonly T innerRequest = new T(); - public string ProjectKey { get; set; } + public string ProjectKey { get; set; } - public string Statuses { get; set; } + public string Statuses { get; set; } - public string Branch { get; set; } + public string Branch { get; set; } - public string[] IssueKeys { get; set; } + public string[] IssueKeys { get; set; } - public string RuleId { get; set; } + public string RuleId { get; set; } - public string ComponentKey { get; set; } - public ILogger Logger { get; set; } - public bool IncludeTaint { get; set; } = true; + public string ComponentKey { get; set; } + public ILogger Logger { get; set; } - public async Task InvokeAsync(HttpClient httpClient, CancellationToken token) + public async Task InvokeAsync(HttpClient httpClient, CancellationToken token) + { + // Transfer all IGetIssuesRequest properties to the inner request. If more properties are + // added to IGetIssuesRequest, this block should set them. + innerRequest.ProjectKey = ProjectKey; + innerRequest.Statuses = Statuses; + innerRequest.Branch = Branch; + innerRequest.Logger = Logger; + innerRequest.IssueKeys = IssueKeys; + innerRequest.RuleId = RuleId; + innerRequest.ComponentKey = ComponentKey; + + if (innerRequest.IssueKeys != null) { - // Transfer all IGetIssuesRequest properties to the inner request. If more properties are - // added to IGetIssuesRequest, this block should set them. - innerRequest.ProjectKey = ProjectKey; - innerRequest.Statuses = Statuses; - innerRequest.Branch = Branch; - innerRequest.Logger = Logger; - innerRequest.IssueKeys = IssueKeys; - innerRequest.RuleId = RuleId; - innerRequest.ComponentKey = ComponentKey; - - if (innerRequest.IssueKeys != null) - { - var response = await innerRequest.InvokeAsync(httpClient, token); - - return response; - } - - ResetInnerRequest(); - innerRequest.Types = "CODE_SMELL"; - var codeSmells = await innerRequest.InvokeAsync(httpClient, token); - WarnForApiLimit(codeSmells, innerRequest, "code smells"); - - ResetInnerRequest(); - innerRequest.Types = "BUG"; - var bugs = await innerRequest.InvokeAsync(httpClient, token); - WarnForApiLimit(bugs, innerRequest, "bugs"); - - var vulnerabilities = Array.Empty(); - if (IncludeTaint) - { - ResetInnerRequest(); - innerRequest.Types = "VULNERABILITY"; - vulnerabilities = await innerRequest.InvokeAsync(httpClient, token); - WarnForApiLimit(vulnerabilities, innerRequest, "vulnerabilities"); - } - return codeSmells - .Concat(bugs) - .Concat(vulnerabilities) - .ToArray(); - } + var response = await innerRequest.InvokeAsync(httpClient, token); - private void WarnForApiLimit(SonarQubeIssue[] issues, GetIssuesRequest request, string friendlyIssueType) - { - if (issues.Length == request.ItemsLimit) - { - Logger.Warning($"Sonar web API response limit reached ({request.ItemsLimit} items). Some {friendlyIssueType} might not be suppressed."); - } + return response; } - /// - /// For paged requests the Page property is automatically changed on each invocation. - /// We are resetting it so that our invocations for different issue types could start - /// from the first page. - /// - private void ResetInnerRequest() + ResetInnerRequest(); + innerRequest.Types = "CODE_SMELL"; + var codeSmells = await innerRequest.InvokeAsync(httpClient, token); + WarnForApiLimit(codeSmells, innerRequest, "code smells"); + + ResetInnerRequest(); + innerRequest.Types = "BUG"; + var bugs = await innerRequest.InvokeAsync(httpClient, token); + WarnForApiLimit(bugs, innerRequest, "bugs"); + return codeSmells + .Concat(bugs) + .ToArray(); + } + + private void WarnForApiLimit(SonarQubeIssue[] issues, GetIssuesRequest request, string friendlyIssueType) + { + if (issues.Length == request.ItemsLimit) { - innerRequest.Page = 1; + Logger.Warning($"Sonar web API response limit reached ({request.ItemsLimit} items). Some {friendlyIssueType} might not be suppressed."); } } + + /// + /// For paged requests the Page property is automatically changed on each invocation. + /// We are resetting it so that our invocations for different issue types could start + /// from the first page. + /// + private void ResetInnerRequest() + { + innerRequest.Page = 1; + } } diff --git a/src/SonarQube.Client/Api/V8_6/GetTaintVulnerabilitiesRequest.cs b/src/SonarQube.Client/Api/V8_6/GetTaintVulnerabilitiesRequest.cs deleted file mode 100644 index 30b0b41fac..0000000000 --- a/src/SonarQube.Client/Api/V8_6/GetTaintVulnerabilitiesRequest.cs +++ /dev/null @@ -1,65 +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.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using SonarQube.Client.Api.V7_20; -using SonarQube.Client.Logging; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Api.V8_6 -{ - internal class GetTaintVulnerabilitiesRequest : IGetTaintVulnerabilitiesRequest - { - protected GetIssuesRequest getIssuesRequest = new GetIssuesRequest(); - - public string ProjectKey { get; set; } - public string Branch { get; set; } - public ILogger Logger { get; set; } - - public virtual async Task InvokeAsync(HttpClient httpClient, CancellationToken token) - { - getIssuesRequest.Logger = Logger; - getIssuesRequest.ProjectKey = ProjectKey; - getIssuesRequest.Branch = Branch; - getIssuesRequest.Statuses = "OPEN,CONFIRMED,REOPENED,RESOLVED"; - getIssuesRequest.Types = "VULNERABILITY"; - - var vulnerabilities = await getIssuesRequest.InvokeAsync(httpClient, token); - WarnForApiLimit(vulnerabilities, getIssuesRequest); - - var taintVulnerabilities = vulnerabilities - .Where(x => x.RuleId.Contains("security")) - .ToArray(); - - return taintVulnerabilities; - } - - private void WarnForApiLimit(SonarQubeIssue[] issues, GetIssuesRequest request) - { - if (issues.Length == request.ItemsLimit) - { - Logger.Warning($"Sonar web API response limit reached ({request.ItemsLimit} items). Some vulnerabilities might not be shown."); - } - } - } -} diff --git a/src/SonarQube.Client/ISonarQubeService.cs b/src/SonarQube.Client/ISonarQubeService.cs index 5fcdadf8f5..82156f3993 100644 --- a/src/SonarQube.Client/ISonarQubeService.cs +++ b/src/SonarQube.Client/ISonarQubeService.cs @@ -126,14 +126,6 @@ Task> GetNotificationEventsAsync(string projectKey, Task GetHotspotAsync(string hotspotKey, CancellationToken token); - /// - /// Returns the taint issues for the specified project/branch. - /// - /// The project identifier - /// (optional) The Sonar branch for which taint issues should be returned. If null/empty, - /// the issues for the "main" branch will be returned. - Task> GetTaintVulnerabilitiesAsync(string projectKey, string branch, CancellationToken token); - /// /// Returns the URI to view the specified issue on the server /// diff --git a/src/SonarQube.Client/Models/ServerSentEvents/ClientContract/ITaintIssue.cs b/src/SonarQube.Client/Models/ServerSentEvents/ClientContract/ITaintIssue.cs deleted file mode 100644 index 0b1fb7c7d8..0000000000 --- a/src/SonarQube.Client/Models/ServerSentEvents/ClientContract/ITaintIssue.cs +++ /dev/null @@ -1,136 +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.Collections.Generic; - -namespace SonarQube.Client.Models.ServerSentEvents.ClientContract -{ - public interface ITaintIssue - { - string Key { get; } - string RuleKey { get; } - DateTimeOffset CreationDate { get; } - SonarQubeIssueSeverity Severity { get; } - SonarQubeIssueType Type { get; } - ILocation MainLocation { get; } - IFlow[] Flows { get; } - string Context { get; } - Dictionary DefaultImpacts { get; } - } - - public interface IFlow - { - ILocation[] Locations { get; } - } - - public interface ILocation - { - string FilePath { get; } - string Message { get; } - ITextRange TextRange { get; } - } - - public interface ITextRange - { - int StartLine { get; } - int StartLineOffset { get; } - int EndLine { get; } - int EndLineOffset { get; } - string Hash { get; } - } - - internal class TaintIssue : ITaintIssue - { - public TaintIssue( - string key, - string ruleKey, - DateTimeOffset creationDate, - SonarQubeIssueSeverity severity, - SonarQubeIssueType type, - Dictionary defaultImpacts, - Location mainLocation, - Flow[] flows, - string context) - { - Key = key ?? throw new ArgumentNullException(nameof(key)); - RuleKey = ruleKey ?? throw new ArgumentNullException(nameof(ruleKey)); - CreationDate = creationDate; - Severity = severity; - Type = type; - DefaultImpacts = defaultImpacts; - MainLocation = mainLocation ?? throw new ArgumentNullException(nameof(mainLocation)); - Flows = flows ?? throw new ArgumentNullException(nameof(flows)); - Context = context; - } - - public string Key { get; } - public string RuleKey { get; } - public DateTimeOffset CreationDate { get; } - public SonarQubeIssueSeverity Severity { get; } - public SonarQubeIssueType Type { get; } - public Dictionary DefaultImpacts { get; } - public ILocation MainLocation { get; } - public IFlow[] Flows { get; } - public string Context { get; } - } - - internal class Flow : IFlow - { - public Flow(Location[] locations) - { - Locations = locations ?? throw new ArgumentNullException(nameof(locations)); - } - - public ILocation[] Locations { get; } - } - - internal class Location : ILocation - { - public Location(string filePath, string message, TextRange textRange) - { - FilePath = filePath ?? throw new ArgumentNullException(nameof(filePath)); - Message = message ?? throw new ArgumentNullException(nameof(message)); - TextRange = textRange; - } - - public string FilePath { get; } - public string Message { get; } - public ITextRange TextRange { get; } - } - - internal class TextRange : ITextRange - { - public TextRange(int startLine, int startLineOffset, int endLine, int endLineOffset, string hash) - { - StartLine = startLine; - StartLineOffset = startLineOffset; - EndLine = endLine; - EndLineOffset = endLineOffset; - Hash = hash; - } - - public int StartLine { get; } - public int StartLineOffset { get; } - public int EndLine { get; } - public int EndLineOffset { get; } - public string Hash { get; } - } -} diff --git a/src/SonarQube.Client/Models/ServerSentEvents/ClientContract/ITaintServerEvent.cs b/src/SonarQube.Client/Models/ServerSentEvents/ClientContract/ITaintServerEvent.cs deleted file mode 100644 index 4b0bc8ea36..0000000000 --- a/src/SonarQube.Client/Models/ServerSentEvents/ClientContract/ITaintServerEvent.cs +++ /dev/null @@ -1,104 +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 Newtonsoft.Json; -using SonarQube.Client.Api.Common; -using SonarQube.Client.Helpers; - -namespace SonarQube.Client.Models.ServerSentEvents.ClientContract -{ - public interface ITaintServerEvent : IServerEvent - { - string ProjectKey { get; } - string Key { get; } - } - - /// - /// Represents TaintVulnerabilityRaised server event information - /// - public interface ITaintVulnerabilityRaisedServerEvent : ITaintServerEvent - { - string Branch { get; } - ITaintIssue Issue { get; } - } - - internal class TaintVulnerabilityRaisedServerEvent : ITaintVulnerabilityRaisedServerEvent - { - [JsonConstructor] - public TaintVulnerabilityRaisedServerEvent(string projectKey, - string key, - string branch, - string ruleKey, - [JsonConverter(typeof(MillisecondUnixTimestampDateTimeOffsetConverter))] DateTimeOffset creationDate, - SonarQubeIssueSeverity severity, - SonarQubeIssueType type, - ServerImpact[] impacts, - Location mainLocation, - Flow[] flows, - string ruleDescriptionContextKey) - : this(projectKey, - key, - branch, - new TaintIssue(key, - ruleKey, - creationDate, - severity, - type, - CleanCodeTaxonomyHelpers.ToDefaultImpacts(impacts), - mainLocation, - flows, - ruleDescriptionContextKey)) - { - } - - public TaintVulnerabilityRaisedServerEvent(string projectKey, string key, string branch, ITaintIssue issue) - { - ProjectKey = projectKey ?? throw new ArgumentNullException(nameof(projectKey)); - Key = key ?? throw new ArgumentNullException(nameof(key)); - Branch = branch ?? throw new ArgumentNullException(nameof(branch)); - Issue = issue ?? throw new ArgumentNullException(nameof(issue)); - } - - public string ProjectKey { get; } - public string Key { get; } - public string Branch { get; } - public ITaintIssue Issue { get; } - } - - /// - /// Represents TaintVulnerabilityClosed server event information - /// - public interface ITaintVulnerabilityClosedServerEvent : ITaintServerEvent - { - } - - internal class TaintVulnerabilityClosedServerEvent : ITaintVulnerabilityClosedServerEvent - { - public TaintVulnerabilityClosedServerEvent(string projectKey, string key) - { - ProjectKey = projectKey ?? throw new ArgumentNullException(nameof(projectKey)); - Key = key ?? throw new ArgumentNullException(nameof(key)); - } - - public string ProjectKey { get; } - public string Key { get; } - } -} diff --git a/src/SonarQube.Client/Models/ServerSentEvents/SSEStreamReader.cs b/src/SonarQube.Client/Models/ServerSentEvents/SSEStreamReader.cs index 6429e3ec8b..9c63da80fa 100644 --- a/src/SonarQube.Client/Models/ServerSentEvents/SSEStreamReader.cs +++ b/src/SonarQube.Client/Models/ServerSentEvents/SSEStreamReader.cs @@ -52,8 +52,6 @@ internal class SSEStreamReader : ISSEStreamReader private readonly IDictionary eventTypeToDataTypeMap = new Dictionary { {"IssueChanged", typeof(IssueChangedServerEvent)}, - {"TaintVulnerabilityClosed", typeof(TaintVulnerabilityClosedServerEvent)}, - {"TaintVulnerabilityRaised", typeof(TaintVulnerabilityRaisedServerEvent)}, {"RuleSetChanged", typeof(QualityProfileEvent)}, }; diff --git a/src/SonarQube.Client/SonarQubeService.cs b/src/SonarQube.Client/SonarQubeService.cs index 14ce3faed5..35617bf6fd 100644 --- a/src/SonarQube.Client/SonarQubeService.cs +++ b/src/SonarQube.Client/SonarQubeService.cs @@ -343,7 +343,6 @@ public async Task> GetIssuesForComponentAsync(string proje request.Branch = branch; request.ComponentKey = componentKey; request.RuleId = ruleId; - request.IncludeTaint = false; }, token); } @@ -447,19 +446,6 @@ public async Task TransitionIssueAsync(string is : SonarQubeIssueTransitionResult.CommentAdditionFailed; } - public async Task> GetTaintVulnerabilitiesAsync(string projectKey, string branch, CancellationToken token) - { - var issues = await InvokeCheckedRequestAsync( - request => - { - request.ProjectKey = projectKey; - request.Branch = branch; - }, token); - - await secondaryIssueHashUpdater.UpdateHashesAsync(issues, this, token); - return issues; - } - public Uri GetViewIssueUrl(string projectKey, string issueKey) { EnsureIsConnected(); diff --git a/src/TestInfrastructure/DummyAnalysisIssue.cs b/src/TestInfrastructure/DummyAnalysisIssue.cs index 30b983dfa4..157fc598cf 100644 --- a/src/TestInfrastructure/DummyAnalysisIssue.cs +++ b/src/TestInfrastructure/DummyAnalysisIssue.cs @@ -18,29 +18,28 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Collections.Generic; using SonarLint.VisualStudio.Core.Analysis; -namespace SonarLint.VisualStudio.TestInfrastructure +namespace SonarLint.VisualStudio.TestInfrastructure; + +// Properties are settable to simplify creating test instances +public class DummyAnalysisIssue : IAnalysisIssue { - // Properties are settable to simplify creating test instances - public class DummyAnalysisIssue : IAnalysisIssue - { - public string RuleKey { get; set; } + public Guid? Id { get; set; } + + public string RuleKey { get; set; } + + public AnalysisIssueSeverity? Severity { get; set; } - public AnalysisIssueSeverity Severity { get; set; } - - public SoftwareQualitySeverity? HighestSoftwareQualitySeverity { get; set; } + public Impact HighestImpact { get; set; } - public AnalysisIssueType Type { get; set; } + public AnalysisIssueType? Type { get; set; } - public IReadOnlyList Flows { get; } = Array.Empty(); + public IReadOnlyList Flows { get; } = Array.Empty(); - public IAnalysisIssueLocation PrimaryLocation { get; set; } = new DummyAnalysisIssueLocation(); + public IAnalysisIssueLocation PrimaryLocation { get; set; } = new DummyAnalysisIssueLocation(); - public IReadOnlyList Fixes { get; } = Array.Empty(); + public IReadOnlyList Fixes { get; } = Array.Empty(); - public string RuleDescriptionContextKey { get; set; } - } + public string RuleDescriptionContextKey { get; set; } }