Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,20 @@ namespace SonarLint.VisualStudio.IssueVisualization.Security.UnitTests.ReportVie
[TestClass]
public class HotspotViewModelTests
{
private readonly LocalHotspot hotspot = CreateMockedHotspot("csharp:101",
Guid.NewGuid(),
1,
66,
"remove todo comment",
"myClass.cs");
private HotspotViewModel testSubject;

[TestInitialize]
public void TestInitialize() => testSubject = new HotspotViewModel(hotspot);

[TestMethod]
public void Ctor_InitializesPropertiesAsExpected()
{
var hotspot = CreateMockedHotspot("csharp:101",
Guid.NewGuid(),
1,
66,
"remove todo comment",
"myClass.cs");

var testSubject = new HotspotViewModel(hotspot);

testSubject.LocalHotspot.Should().Be(hotspot);
testSubject.RuleInfo.RuleKey.Should().Be(hotspot.Visualization.RuleId);
testSubject.RuleInfo.IssueId.Should().Be(hotspot.Visualization.IssueId);
Expand All @@ -50,6 +52,22 @@ public void Ctor_InitializesPropertiesAsExpected()
testSubject.Issue.Should().Be(hotspot.Visualization);
}

[TestMethod]
public void ExistsOnServer_LocalHotspot_ReturnsFalse()
{
hotspot.Visualization.Issue.IssueServerKey.Returns((string)null);

testSubject.ExistsOnServer.Should().BeFalse();
}

[TestMethod]
public void ExistsOnServer_ServerHotspot_ReturnsTrue()
{
hotspot.Visualization.Issue.IssueServerKey.Returns(Guid.NewGuid().ToString());

testSubject.ExistsOnServer.Should().BeTrue();
}

private static LocalHotspot CreateMockedHotspot(
string ruleId,
Guid issueId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

using System.Windows;
using SonarLint.VisualStudio.Core;
using SonarLint.VisualStudio.Core.Analysis;
using SonarLint.VisualStudio.Integration.TestInfrastructure;
using SonarLint.VisualStudio.IssueVisualization.Models;
using SonarLint.VisualStudio.IssueVisualization.Security.Hotspots;
using SonarLint.VisualStudio.IssueVisualization.Security.Hotspots.ReviewHotspot;
using SonarLint.VisualStudio.IssueVisualization.Security.IssuesStore;
using SonarLint.VisualStudio.IssueVisualization.Security.ReportView;
using SonarLint.VisualStudio.IssueVisualization.Security.ReportView.Hotspots;
Expand All @@ -31,20 +35,28 @@ namespace SonarLint.VisualStudio.IssueVisualization.Security.UnitTests.ReportVie
[TestClass]
public class HotspotsReportViewModelTest
{
private readonly LocalHotspot serverHotspot = CreateMockedHotspot("myFile.cs", "serverKey");
private ILocalHotspotsStore localHotspotsStore;
private IMessageBox messageBox;
private IReviewHotspotsService reviewHotspotsService;
private HotspotsReportViewModel testSubject;

[TestInitialize]
public void TestInitialize()
{
localHotspotsStore = Substitute.For<ILocalHotspotsStore>();
testSubject = new HotspotsReportViewModel(localHotspotsStore);
reviewHotspotsService = Substitute.For<IReviewHotspotsService>();
messageBox = Substitute.For<IMessageBox>();
testSubject = new HotspotsReportViewModel(localHotspotsStore, reviewHotspotsService, messageBox);
}

[TestMethod]
public void MefCtor_CheckIsExported() =>
MefTestHelpers.CheckTypeCanBeImported<HotspotsReportViewModel, IHotspotsReportViewModel>(
MefTestHelpers.CreateExport<ILocalHotspotsStore>());
MefTestHelpers.CreateExport<ILocalHotspotsStore>(),
MefTestHelpers.CreateExport<IReviewHotspotsService>(),
MefTestHelpers.CreateExport<IMessageBox>()
);

[TestMethod]
public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent<HotspotsReportViewModel>();
Expand Down Expand Up @@ -114,12 +126,93 @@ public void HotspotsChanged_RaisedOnStoreIssuesChanged()
raised.Should().BeTrue();
}

private static LocalHotspot CreateMockedHotspot(string filePath)
[TestMethod]
public async Task ShowHotspotInBrowserAsync_CallsHandler()
{
var hotspot = CreateMockedHotspot("myFile.cs");

await testSubject.ShowHotspotInBrowserAsync(hotspot);

reviewHotspotsService.Received(1).OpenHotspotAsync(hotspot.Visualization.Issue.IssueServerKey).IgnoreAwaitForAssert();
}

[TestMethod]
public async Task GetAllowedStatusesAsync_ChangeStatusPermitted_ReturnsListOfAllowedStatuses()
{
var allowedStatuses = new List<HotspotStatus> { HotspotStatus.Fixed, HotspotStatus.ToReview };
MockChangeStatusPermitted(serverHotspot.Visualization.Issue.IssueServerKey, allowedStatuses);

var result = await testSubject.GetAllowedStatusesAsync(new HotspotViewModel(serverHotspot));

result.Should().BeEquivalentTo(allowedStatuses);
messageBox.DidNotReceive().Show(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<MessageBoxButton>(), Arg.Any<MessageBoxImage>());
}

[TestMethod]
public async Task GetAllowedStatusesAsync_ChangeStatusNotPermitted_ShowsMessageBoxAndReturnsNull()
{
var reason = "Not permitted";
MockChangeStatusNotPermitted(serverHotspot.Visualization.Issue.IssueServerKey, reason);

var result = await testSubject.GetAllowedStatusesAsync(new HotspotViewModel(serverHotspot));

result.Should().BeNull();
messageBox.Received(1).Show(Arg.Is<string>(x => x == string.Format(Resources.ReviewHotspotWindow_CheckReviewPermittedFailureMessage, reason)),
Arg.Is<string>(x => x == Resources.ReviewHotspotWindow_FailureTitle), MessageBoxButton.OK, MessageBoxImage.Error);
}

[TestMethod]
public async Task GetAllowedStatusesAsync_NoStatusSelected_ShowsMessageBoxAndReturnsNull()
{
var result = await testSubject.GetAllowedStatusesAsync(null);

result.Should().BeNull();
messageBox.Received(1).Show(
Arg.Is<string>(x => x == string.Format(Resources.ReviewHotspotWindow_CheckReviewPermittedFailureMessage, Resources.ReviewHotspotWindow_NoStatusSelectedFailureMessage)),
Arg.Is<string>(x => x == Resources.ReviewHotspotWindow_FailureTitle), MessageBoxButton.OK, MessageBoxImage.Error);
}

[TestMethod]
[DataRow(HotspotStatus.Fixed)]
[DataRow(HotspotStatus.ToReview)]
[DataRow(HotspotStatus.Acknowledged)]
[DataRow(HotspotStatus.Safe)]
public async Task ChangeHotspotStatusAsync_Succeeds_ReturnsTrue(HotspotStatus newStatus)
{
var hotspotViewModel = new HotspotViewModel(serverHotspot);
MockReviewHotspot(serverHotspot.Visualization.Issue.IssueServerKey, newStatus, true);

var result = await testSubject.ChangeHotspotStatusAsync(hotspotViewModel, newStatus);

result.Should().BeTrue();
reviewHotspotsService.Received(1).ReviewHotspotAsync(serverHotspot.Visualization.Issue.IssueServerKey, newStatus).IgnoreAwaitForAssert();
messageBox.DidNotReceive().Show(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<MessageBoxButton>(), Arg.Any<MessageBoxImage>());
}

[TestMethod]
[DataRow(HotspotStatus.Fixed)]
[DataRow(HotspotStatus.ToReview)]
[DataRow(HotspotStatus.Acknowledged)]
[DataRow(HotspotStatus.Safe)]
public async Task ChangeHotspotStatusAsync_Fails_ShowsMessageBox(HotspotStatus newStatus)
{
var hotspotViewModel = new HotspotViewModel(serverHotspot);
MockReviewHotspot(serverHotspot.Visualization.Issue.IssueServerKey, newStatus, false);

var result = await testSubject.ChangeHotspotStatusAsync(hotspotViewModel, newStatus);

result.Should().BeFalse();
messageBox.Received(1).Show(Arg.Is<string>(x => x == Resources.ReviewHotspotWindow_ReviewFailureMessage), Arg.Is<string>(x => x == Resources.ReviewHotspotWindow_FailureTitle),
MessageBoxButton.OK, MessageBoxImage.Error);
}

private static LocalHotspot CreateMockedHotspot(string filePath, string hotspotKey = null)
{
var analysisIssueVisualization = Substitute.For<IAnalysisIssueVisualization>();
var analysisIssueBase = Substitute.For<IAnalysisIssueBase>();
analysisIssueBase.PrimaryLocation.FilePath.Returns(filePath);
analysisIssueVisualization.Issue.Returns(analysisIssueBase);
analysisIssueVisualization.Issue.IssueServerKey.Returns(hotspotKey);

return new LocalHotspot(analysisIssueVisualization, default, default);
}
Expand All @@ -136,4 +229,12 @@ private static void VerifyExpectedHotspotGroupViewModel(GroupFileViewModel group
groupFileVm.FilteredIssues.Should().ContainSingle(vm => ((HotspotViewModel)vm).LocalHotspot == expectedHotspot);
}
}

private void MockChangeStatusPermitted(string hotspotKey, List<HotspotStatus> allowedStatuses) =>
reviewHotspotsService.CheckReviewHotspotPermittedAsync(hotspotKey).Returns(new ReviewHotspotPermittedArgs(allowedStatuses));

private void MockChangeStatusNotPermitted(string hotspotKey, string reason) =>
reviewHotspotsService.CheckReviewHotspotPermittedAsync(hotspotKey).Returns(new ReviewHotspotNotPermittedArgs(reason));

private void MockReviewHotspot(string hotspotKey, HotspotStatus newStatus, bool succeeded) => reviewHotspotsService.ReviewHotspotAsync(hotspotKey, newStatus).Returns(succeeded);
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ namespace SonarLint.VisualStudio.IssueVisualization.Security.ReportView.Hotspots
internal class HotspotViewModel : ViewModelBase, IAnalysisIssueViewModel
{
public LocalHotspot LocalHotspot { get; }
public bool ExistsOnServer => LocalHotspot.Visualization.Issue.IssueServerKey != null;

public HotspotViewModel(LocalHotspot localHotspot)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@

using System.Collections.ObjectModel;
using System.ComponentModel.Composition;
using System.Windows;
using SonarLint.VisualStudio.Core;
using SonarLint.VisualStudio.Core.Analysis;
using SonarLint.VisualStudio.IssueVisualization.Security.Hotspots;
using SonarLint.VisualStudio.IssueVisualization.Security.Hotspots.ReviewHotspot;
using SonarLint.VisualStudio.IssueVisualization.Security.IssuesStore;

namespace SonarLint.VisualStudio.IssueVisualization.Security.ReportView.Hotspots;
Expand All @@ -29,6 +33,12 @@ internal interface IHotspotsReportViewModel : IDisposable
{
ObservableCollection<IGroupViewModel> GetHotspotsGroupViewModels();

Task ShowHotspotInBrowserAsync(LocalHotspot localHotspot);

Task<IEnumerable<HotspotStatus>> GetAllowedStatusesAsync(HotspotViewModel selectedHotspotViewModel);

Task<bool> ChangeHotspotStatusAsync(HotspotViewModel selectedHotspotViewModel, HotspotStatus newStatus);

event EventHandler HotspotsChanged;
}

Expand All @@ -37,11 +47,15 @@ internal interface IHotspotsReportViewModel : IDisposable
internal sealed class HotspotsReportViewModel : IHotspotsReportViewModel
{
private readonly ILocalHotspotsStore hotspotsStore;
private readonly IReviewHotspotsService reviewHotspotsService;
private readonly IMessageBox messageBox;

[ImportingConstructor]
public HotspotsReportViewModel(ILocalHotspotsStore hotspotsStore)
public HotspotsReportViewModel(ILocalHotspotsStore hotspotsStore, IReviewHotspotsService reviewHotspotsService, IMessageBox messageBox)
{
this.hotspotsStore = hotspotsStore;
this.reviewHotspotsService = reviewHotspotsService;
this.messageBox = messageBox;
hotspotsStore.IssuesChanged += HotspotsStore_IssuesChanged;
}

Expand All @@ -55,6 +69,35 @@ public ObservableCollection<IGroupViewModel> GetHotspotsGroupViewModels()
return GetGroupViewModel(hotspots);
}

public async Task ShowHotspotInBrowserAsync(LocalHotspot localHotspot) => await reviewHotspotsService.OpenHotspotAsync(localHotspot.Visualization.Issue.IssueServerKey);

public async Task<IEnumerable<HotspotStatus>> GetAllowedStatusesAsync(HotspotViewModel selectedHotspotViewModel)
{
var response = selectedHotspotViewModel == null
? new ReviewHotspotNotPermittedArgs(Resources.ReviewHotspotWindow_NoStatusSelectedFailureMessage)
: await reviewHotspotsService.CheckReviewHotspotPermittedAsync(selectedHotspotViewModel.LocalHotspot.Visualization.Issue.IssueServerKey);
switch (response)
{
case ReviewHotspotPermittedArgs reviewHotspotPermittedArgs:
return reviewHotspotPermittedArgs.AllowedStatuses;
case ReviewHotspotNotPermittedArgs reviewHotspotNotPermittedArgs:
messageBox.Show(string.Format(Resources.ReviewHotspotWindow_CheckReviewPermittedFailureMessage, reviewHotspotNotPermittedArgs.Reason), Resources.ReviewHotspotWindow_FailureTitle,
MessageBoxButton.OK, MessageBoxImage.Error);
break;
}
return null;
}

public async Task<bool> ChangeHotspotStatusAsync(HotspotViewModel selectedHotspotViewModel, HotspotStatus newStatus)
{
var wasChanged = await reviewHotspotsService.ReviewHotspotAsync(selectedHotspotViewModel.LocalHotspot.Visualization.Issue.IssueServerKey, newStatus);
if (!wasChanged)
{
messageBox.Show(Resources.ReviewHotspotWindow_ReviewFailureMessage, Resources.ReviewHotspotWindow_FailureTitle, MessageBoxButton.OK, MessageBoxImage.Error);
}
return wasChanged;
}

private static ObservableCollection<IGroupViewModel> GetGroupViewModel(IEnumerable<IIssueViewModel> issueViewModels)
{
var issuesByFileGrouping = issueViewModels.GroupBy(vm => vm.FilePath);
Expand Down
15 changes: 14 additions & 1 deletion src/IssueViz.Security/ReportView/ReportViewControl.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,16 @@
<MenuItem Click="ViewDependencyRiskInBrowser_OnClick" Style="{StaticResource ViewIssueInBrowserMenuItemStyle}" />
</ContextMenu>

<ContextMenu x:Key="HotspotContextMenu"
Visibility="{Binding Path=ExistsOnServer, Converter={StaticResource TrueToVisibleConverter}}">
<MenuItem Header="{x:Static res:Resources.ChangeStatusMenuItem}"
Click="ChangeHotspotStatusMenuItem_OnClick"
Style="{StaticResource ChangeStatusMenuItemStyle}"/>
<MenuItem Click="ViewHotspotInBrowser_OnClick"
Loaded="ShowHotspotInBrowserMenuItem_OnLoaded"
Style="{StaticResource ViewIssueInBrowserMenuItemStyle}" />
</ContextMenu>

<Style x:Key="ResolutionFilterBorderStyle" TargetType="Button">
<Setter Property="Margin" Value="5, 3"/>
<Setter Property="Padding" Value="2"/>
Expand Down Expand Up @@ -362,7 +372,10 @@

<!--Hotspots styling-->
<DataTemplate DataType="{x:Type hotspots:HotspotViewModel}">
<Grid Margin="5,2" MouseRightButtonDown="TreeViewItem_OnMouseRightButtonDown" ToolTip="{x:Static res:Resources.HotspotsControl_NavigationTooltip}">
<Grid Margin="5,2"
ContextMenu="{StaticResource HotspotContextMenu}"
MouseRightButtonDown="TreeViewItem_OnMouseRightButtonDown"
ToolTip="{x:Static res:Resources.HotspotsControl_NavigationTooltip}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
Expand Down
43 changes: 40 additions & 3 deletions src/IssueViz.Security/ReportView/ReportViewControl.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,16 @@
using System.Windows.Navigation;
using SonarLint.VisualStudio.ConnectedMode.UI;
using SonarLint.VisualStudio.Core;
using SonarLint.VisualStudio.Core.Analysis;
using SonarLint.VisualStudio.Core.Binding;
using SonarLint.VisualStudio.Core.Telemetry;
using SonarLint.VisualStudio.IssueVisualization.Editor;
using SonarLint.VisualStudio.IssueVisualization.IssueVisualizationControl.ViewModels.Commands;
using SonarLint.VisualStudio.IssueVisualization.Security.DependencyRisks;
using SonarLint.VisualStudio.IssueVisualization.Security.Hotspots.HotspotsList.ViewModels;
using SonarLint.VisualStudio.IssueVisualization.Security.ReportView.Hotspots;
using SonarLint.VisualStudio.IssueVisualization.Security.ReviewStatus;
using HotspotViewModel = SonarLint.VisualStudio.IssueVisualization.Security.ReportView.Hotspots.HotspotViewModel;

namespace SonarLint.VisualStudio.IssueVisualization.Security.ReportView;

Expand Down Expand Up @@ -113,15 +116,20 @@ private void ViewDependencyRiskInBrowser_OnClick(object sender, RoutedEventArgs
DependencyRisksReportViewModel.ShowDependencyRiskInBrowser(selectedDependencyRiskViewModel.DependencyRisk);
}

private void DependencyRiskContextMenu_OnLoaded(object sender, RoutedEventArgs e)
private void DependencyRiskContextMenu_OnLoaded(object sender, RoutedEventArgs e) => SetDataContextToReportViewModel<ContextMenu>(sender);

private void SetDataContextToReportViewModel<T>(object sender) where T : FrameworkElement
{
if (sender is ContextMenu contextMenu)
if (sender is T contextMenu)
{
// setting the DataContext directly on the context menu does not work for the TreeViewItem
// workaround that allows setting the DataContext to the ReportViewModel, which is not accessible from the TreeViewItem context menu
// due to the fact that a context menu is a popup and is not part of the visual tree
contextMenu.DataContext = ReportViewModel;
}
}

private void ShowHotspotInBrowserMenuItem_OnLoaded(object sender, RoutedEventArgs e) => SetDataContextToReportViewModel<MenuItem>(sender);

private void TreeViewItem_OnMouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
if (sender is FrameworkElement element && FindParentOfType<TreeViewItem>(element) is { } treeViewItem)
Expand Down Expand Up @@ -180,4 +188,33 @@ private static void ExecuteCommandIfValid(ICommand command, object parameter)
command.Execute(parameter);
}
}

private async void ViewHotspotInBrowser_OnClick(object sender, RoutedEventArgs e)
{
if (ReportViewModel.SelectedItem is HotspotViewModel hotspotViewModel)
{
await HotspotsReportViewModel.ShowHotspotInBrowserAsync(hotspotViewModel.LocalHotspot);
}
}

private async void ChangeHotspotStatusMenuItem_OnClick(object sender, RoutedEventArgs e)
{
if (sender is not MenuItem { DataContext: HotspotViewModel hotspotViewModel } ||
await HotspotsReportViewModel.GetAllowedStatusesAsync(hotspotViewModel) is not { } allowedStatuses)
{
return;
}

var changeHotspotStatusViewModel = new ChangeHotspotStatusViewModel(hotspotViewModel.LocalHotspot.HotspotStatus, allowedStatuses);
var dialog = new ChangeStatusWindow(changeHotspotStatusViewModel, browserService, activeSolutionBoundTracker);
if (dialog.ShowDialog(Application.Current.MainWindow) is true)
{
var newStatus = changeHotspotStatusViewModel.SelectedStatusViewModel.GetCurrentStatus<HotspotStatus>();
var wasChanged = await HotspotsReportViewModel.ChangeHotspotStatusAsync(hotspotViewModel, newStatus);
if (wasChanged && newStatus is HotspotStatus.Fixed or HotspotStatus.Safe)
{
ReportViewModel.GroupViewModels.ToList().ForEach(vm => vm.FilteredIssues.Remove(hotspotViewModel));
}
}
}
}
Loading