Skip to content
Open
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
@@ -0,0 +1,49 @@
/*
* SonarLint for Visual Studio
* Copyright (C) 2016-2025 SonarSource Sàrl
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

using SonarLint.VisualStudio.Core.SmartNotification;
using SonarLint.VisualStudio.TestInfrastructure;

namespace SonarLint.VisualStudio.Core.UnitTests.SmartNotification;

[TestClass]
public class SmartNotificationServiceTests
{
[TestMethod]
public void MefCtor_CheckIsExported() =>
MefTestHelpers.CheckTypeCanBeImported<SmartNotificationService, ISmartNotificationService>();

[TestMethod]
public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent<SmartNotificationService>();

[TestMethod]
public void ShowSmartNotification_InvokesNotificationReceivedEvent_WithCorrectNotification()
{
var service = new SmartNotificationService();
Core.SmartNotification.SmartNotification receivedNotification = null;
var expectedNotification = new Core.SmartNotification.SmartNotification("A notification", "http://localhost:9000/project/overview", ["SCOPE_ID1"], "CATEGORY", "http://localhost:9000", DateTimeOffset.Now);
service.NotificationReceived += (_, args) => { receivedNotification = args.Notification; };

service.ShowSmartNotification(expectedNotification);

receivedNotification.Should().NotBeNull("the NotificationReceived event should have been invoked.");
receivedNotification.Should().BeEquivalentTo(expectedNotification, options => options.ComparingByMembers<Core.SmartNotification.SmartNotification>());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,16 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

using System;
namespace SonarLint.VisualStudio.Core.SmartNotification;

namespace SonarQube.Client.Models
public interface ISmartNotificationService
{
public class SonarQubeNotification
{
public string Category { get; }
event EventHandler<NotificationReceivedEventArgs> NotificationReceived;

public string Message { get; }

public Uri Link { get; }

public DateTimeOffset Date { get; }
void ShowSmartNotification(SmartNotification notification);
}

public SonarQubeNotification(string category, string message, Uri link, DateTimeOffset date)
{
Category = category;
Message = message;
Link = link;
Date = date;
}
}
public class NotificationReceivedEventArgs(SmartNotification notification) : EventArgs
{
public SmartNotification Notification { get; set; } = notification;
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,6 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

using System.Threading.Tasks;
namespace SonarLint.VisualStudio.Core.SmartNotification;

namespace SonarLint.VisualStudio.Integration.Notifications
{
public interface ISonarQubeNotificationService
{
Task StartAsync(string projectKey, NotificationData notificationData);

void Stop();

NotificationData GetNotificationData();

INotificationIndicatorViewModel Model { get; }
}
}
public record SmartNotification(string Text, string Link, HashSet<string> ScopeIds, string Category, string ConnectionId, DateTimeOffset Date);
32 changes: 32 additions & 0 deletions src/Core/SmartNotification/SmartNotificationService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* SonarLint for Visual Studio
* Copyright (C) 2016-2025 SonarSource Sàrl
* 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;

namespace SonarLint.VisualStudio.Core.SmartNotification;

[Export(typeof(ISmartNotificationService))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class SmartNotificationService : ISmartNotificationService
{
public event EventHandler<NotificationReceivedEventArgs> NotificationReceived;

public void ShowSmartNotification(SmartNotification notification) => NotificationReceived?.Invoke(this, new NotificationReceivedEventArgs(notification));
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* SonarLint for Visual Studio
* Copyright (C) 2016-2025 SonarSource Sàrl
* mailto:info AT sonarsource DOT com
Expand All @@ -20,20 +20,21 @@

using SonarLint.VisualStudio.Core;
using SonarLint.VisualStudio.Core.Binding;
using SonarLint.VisualStudio.Core.SmartNotification;
using SonarLint.VisualStudio.Core.SystemAbstractions;
using SonarLint.VisualStudio.Integration.Notifications;
using SonarLint.VisualStudio.TestInfrastructure;
using SonarQube.Client.Models;

namespace SonarLint.VisualStudio.Integration.UnitTests.Notifications;

[TestClass]
public class NotificationIndicatorViewModelTests
{
private static readonly SonarQubeNotification[] TestEvents =
private static readonly SmartNotification[] TestEvents =
[
new("foo", "foo", new Uri("http://foo.com"), new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.FromHours(2)))
new("foo", "http://foo.com", ["SCOPE_ID"], "foo", "connectionId", new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.FromHours(2)))
];
private ISmartNotificationService smartNotificationService;
private IActiveSolutionBoundTracker activeSolutionBoundTracker;
private IBrowserService browserService;
private NotificationIndicatorViewModel testSubject;
Expand All @@ -44,14 +45,19 @@ public class NotificationIndicatorViewModelTests
public void TestInitialize()
{
timer = Substitute.For<ITimer>();
smartNotificationService = Substitute.For<ISmartNotificationService>();
browserService = Substitute.For<IBrowserService>();
activeSolutionBoundTracker = Substitute.For<IActiveSolutionBoundTracker>();
threadHandling = new NoOpThreadHandler();
testSubject = new NotificationIndicatorViewModel(browserService, activeSolutionBoundTracker, threadHandling, timer);
testSubject = new NotificationIndicatorViewModel(smartNotificationService, browserService, activeSolutionBoundTracker, threadHandling, timer);
}

[TestMethod]
public void Ctor_SubscribesToEvents() => activeSolutionBoundTracker.ReceivedWithAnyArgs(1).SolutionBindingChanged += Arg.Any<EventHandler<ActiveSolutionBindingEventArgs>>();
public void Ctor_SubscribesToEvents()
{
smartNotificationService.ReceivedWithAnyArgs(1).NotificationReceived += Arg.Any<EventHandler<NotificationReceivedEventArgs>>();
activeSolutionBoundTracker.ReceivedWithAnyArgs(1).SolutionBindingChanged += Arg.Any<EventHandler<ActiveSolutionBindingEventArgs>>();
}

[TestMethod]
public void Text_Raises_PropertyChanged()
Expand Down Expand Up @@ -158,7 +164,7 @@ public void SetNotificationEvents_SetEvents_SetsHasUnreadEvents()
SetupModelWithNotifications(true, false, TestEvents);
testSubject.HasUnreadEvents.Should().BeFalse();

SetupModelWithNotifications(true, true, new SonarQubeNotification[0]);
SetupModelWithNotifications(true, true, []);
testSubject.HasUnreadEvents.Should().BeFalse();

SetupModelWithNotifications(true, true, null);
Expand All @@ -172,7 +178,7 @@ public void SetNotificationEvents_SetEvents_SetsHasUnreadEvents()
public void HasUnreadEvents_RunOnUIThread()
{
var mockThreadHandling = Substitute.For<IThreadHandling>();
var notificationViewModel = new NotificationIndicatorViewModel(browserService, activeSolutionBoundTracker, mockThreadHandling, timer);
var notificationViewModel = new NotificationIndicatorViewModel(smartNotificationService, browserService, activeSolutionBoundTracker, mockThreadHandling, timer);
notificationViewModel.AreNotificationsEnabled = true;
notificationViewModel.IsIconVisible = true;

Expand All @@ -190,7 +196,7 @@ public void NavigateToNotification_NotificationNavigated()

testSubject.NavigateToNotification.Execute(notification);

browserService.Received(1).Navigate("http://localhost:2000/");
browserService.Received(1).Navigate("http://localhost:2000");
}

[TestMethod]
Expand Down Expand Up @@ -220,9 +226,26 @@ public void Dispose_UnsubscribesFromEvents()
{
testSubject.Dispose();

smartNotificationService.ReceivedWithAnyArgs(1).NotificationReceived -= Arg.Any<EventHandler<NotificationReceivedEventArgs>>();
activeSolutionBoundTracker.ReceivedWithAnyArgs(1).SolutionBindingChanged -= Arg.Any<EventHandler<ActiveSolutionBindingEventArgs>>();
}

[TestMethod]
public void NotificationReceived_SetsNotificationEvents()
{
testSubject.AreNotificationsEnabled = true;
testSubject.IsIconVisible = true;
var smartNotification = new SmartNotification("Test message", "http://localhost:9000/project", ["scope1"], "QUALITY_GATE", "connectionId", DateTimeOffset.Now);

smartNotificationService.NotificationReceived += Raise.EventWith(new NotificationReceivedEventArgs(smartNotification));

testSubject.NotificationEvents.Should().HaveCount(1);
testSubject.NotificationEvents[0].Category.Should().Be("QUALITY_GATE");
testSubject.NotificationEvents[0].Text.Should().Be("Test message");
testSubject.NotificationEvents[0].Link.Should().Be("http://localhost:9000/project");
testSubject.HasUnreadEvents.Should().BeTrue();
}

[TestMethod]
[DataRow(SonarLintMode.Connected)]
[DataRow(SonarLintMode.LegacyConnected)]
Expand Down Expand Up @@ -257,12 +280,57 @@ public void SolutionBindingChanged_Standalone_IsCloudIsFalse()
testSubject.IsCloud.Should().BeFalse();
}

[TestMethod]
[DataRow(SonarLintMode.Connected)]
[DataRow(SonarLintMode.LegacyConnected)]
public void SolutionBindingChanged_BoundToServer_IsIconVisibleIsTrue(SonarLintMode sonarLintMode)
{
var bindingConfiguration = CreateBindingConfiguration(new ServerConnection.SonarQube(new Uri("http://localhost")), sonarLintMode);

activeSolutionBoundTracker.SolutionBindingChanged += Raise.EventWith(new ActiveSolutionBindingEventArgs(bindingConfiguration));

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

[TestMethod]
public void SolutionBindingChanged_Standalone_IsIconVisibleIsFalse()
{
var standaloneConfiguration = new BindingConfiguration(null, SonarLintMode.Standalone, string.Empty);

activeSolutionBoundTracker.SolutionBindingChanged += Raise.EventWith(new ActiveSolutionBindingEventArgs(standaloneConfiguration));

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

[TestMethod]
[DataRow(true)]
[DataRow(false)]
public void SolutionBindingChanged_BoundToServer_AreNotificationsEnabledReflectsSettings(bool isSmartNotificationsEnabled)
{
var serverConnection = new ServerConnection.SonarQube(new Uri("http://localhost"), new ServerConnectionSettings(isSmartNotificationsEnabled));
var bindingConfiguration = CreateBindingConfiguration(serverConnection, SonarLintMode.Connected);

activeSolutionBoundTracker.SolutionBindingChanged += Raise.EventWith(new ActiveSolutionBindingEventArgs(bindingConfiguration));

testSubject.AreNotificationsEnabled.Should().Be(isSmartNotificationsEnabled);
}

[TestMethod]
public void SolutionBindingChanged_Standalone_AreNotificationsEnabledIsFalse()
{
var standaloneConfiguration = new BindingConfiguration(null, SonarLintMode.Standalone, string.Empty);

activeSolutionBoundTracker.SolutionBindingChanged += Raise.EventWith(new ActiveSolutionBindingEventArgs(standaloneConfiguration));

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

private static BindingConfiguration CreateBindingConfiguration(ServerConnection serverConnection, SonarLintMode mode) =>
new(new BoundServerProject("my solution", "my project", serverConnection), mode, string.Empty);

private static SonarQubeNotification CreateNotification(string category, string url = "http://localhost") => new(category, "test", new Uri(url), DateTimeOffset.Now);
private static SmartNotification CreateNotification(string category, string url = "http://localhost") => new("test", url, [], category, "connectionId", DateTimeOffset.Now);

private void SetupModelWithNotifications(bool areEnabled, bool areVisible, SonarQubeNotification[] events)
private void SetupModelWithNotifications(bool areEnabled, bool areVisible, SmartNotification[] events)
{
testSubject.AreNotificationsEnabled = areEnabled;
testSubject.IsIconVisible = areVisible;
Expand Down
Loading