Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VIH-10839 Replace polling for hearings with events #2219

Merged
merged 25 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
44bee4b
Send message to participants
oliver-scott Aug 27, 2024
0a09aef
Update event handlers
oliver-scott Aug 29, 2024
240da29
Merge branch 'master' into VIH-10839-todays-hearings
oliver-scott Aug 29, 2024
b27ac7f
Update tests
oliver-scott Aug 29, 2024
5169e83
Refactor
oliver-scott Aug 29, 2024
0b18630
Push hearing details updated event
oliver-scott Aug 29, 2024
fb5a609
Register on startup
oliver-scott Aug 29, 2024
990ba54
Add missing event handlers, use conference id instead of hearing id
oliver-scott Aug 29, 2024
be00542
Merge branch 'master' into VIH-10839-todays-hearings
oliver-scott Sep 4, 2024
c53422c
Update event type
oliver-scott Sep 4, 2024
75c1779
Participants - replace polling
oliver-scott Sep 4, 2024
a7ca74b
Remove unused event
oliver-scott Sep 4, 2024
0e31f6f
Remove unused code
oliver-scott Sep 4, 2024
dff8e5a
Push messages to staff members
oliver-scott Sep 5, 2024
10c9e09
Hosts - replace polling
oliver-scott Sep 5, 2024
5195915
Avoid messaging staff members twice
oliver-scott Sep 6, 2024
b140ca4
Force refresh the cache before publishing event
oliver-scott Sep 6, 2024
7fd5a73
Merge branch 'master' into VIH-10839-todays-hearings
oliver-scott Sep 6, 2024
31eaab9
Incorporate new event handlers into notifiers
oliver-scott Sep 9, 2024
e8ba488
Remove conference from cache when cancelling
oliver-scott Sep 9, 2024
0d61bbc
Merge branch 'master' into VIH-10839-todays-hearings
oliver-scott Sep 9, 2024
3f25ea8
Replace if with linq where statement
oliver-scott Sep 10, 2024
7364588
Handle endpoint updates
oliver-scott Sep 10, 2024
28ecf2a
Merge branch 'master' into VIH-10839-todays-hearings
oliver-scott Sep 11, 2024
88ed5eb
Merge branch 'master' into VIH-10839-todays-hearings
oliver-scott Sep 11, 2024
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
6 changes: 6 additions & 0 deletions VideoWeb/VideoWeb.Common/Caching/ConferenceCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,11 @@ public async Task<Conference> GetOrAddConferenceAsync(Guid id, Func<Task<(Confer
conference = await Task.FromResult(memoryCache.Get<Conference>(id));
return conference;
}

public Task RemoveConferenceAsync(Conference conference, CancellationToken cancellationToken = default)
{
memoryCache.Remove(conference.Id);
return Task.CompletedTask;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ public async Task<Conference> GetOrAddConferenceAsync(Guid id, Func<Task<(Confer

return conference;
}

public async Task RemoveConferenceAsync(Conference conference, CancellationToken cancellationToken = default)
{
await RemoveFromCache(conference.Id, cancellationToken);
}

protected override string GetKey(Guid key)
{
Expand Down
1 change: 1 addition & 0 deletions VideoWeb/VideoWeb.Common/Caching/IConferenceCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ public interface IConferenceCache
Task AddConferenceAsync(ConferenceDetailsResponse conferenceResponse, HearingDetailsResponseV2 hearingDetailsResponse, CancellationToken cancellationToken = default);
Task UpdateConferenceAsync(Conference conference, CancellationToken cancellationToken = default);
Task <Conference> GetOrAddConferenceAsync(Guid id, Func<Task<(ConferenceDetailsResponse, HearingDetailsResponseV2)>> addConferenceDetailsFactory, CancellationToken cancellationToken = default);
Task RemoveConferenceAsync(Conference conference, CancellationToken cancellationToken = default);
}
}
6 changes: 6 additions & 0 deletions VideoWeb/VideoWeb.Common/ConferenceService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public interface IConferenceService
public Task<Conference> ForceGetConference(Guid conferenceId, CancellationToken cancellationToken = default);
public Task UpdateConferenceAsync(Conference conference, CancellationToken cancellationToken = default);
public Task<IEnumerable<Conference>> GetConferences(IEnumerable<Guid> conferenceIds, CancellationToken cancellationToken = default);
public Task RemoveConference(Conference conference, CancellationToken cancellationToken = default);
}

public class ConferenceService(
Expand Down Expand Up @@ -69,4 +70,9 @@ public async Task<IEnumerable<Conference>> GetConferences(IEnumerable<Guid> conf
var ids = conferenceIds.ToArray();
return await Task.WhenAll(ids.Select(id => GetConference(id, cancellationToken)));
}

public async Task RemoveConference(Conference conference, CancellationToken cancellationToken = default)
{
await conferenceCache.RemoveConferenceAsync(conference, cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
using VideoWeb.Common.Models;
using VideoWeb.EventHub.Enums;

namespace VideoWeb.EventHub.Handlers
Expand All @@ -20,8 +21,19 @@ protected override Task PublishStatusAsync(CallbackEvent callbackEvent)
}
private async Task PublishNewConferenceAddedMessage(Guid conferenceId)
{
foreach (var participant in SourceConference.Participants)
shaed-parkar marked this conversation as resolved.
Show resolved Hide resolved
{
// Staff members already receive a message via the staff members group below, so don't message them here as well
if (participant.Role != Role.StaffMember)
await HubContext.Clients.Group(participant.Username.ToLowerInvariant())
.NewConferenceAddedMessage(SourceConference.Id);
}

await HubContext.Clients.Group(Hub.EventHub.VhOfficersGroupName)
.NewConferenceAddedMessage(conferenceId);

await HubContext.Clients.Group(Hub.EventHub.StaffMembersGroupName)
.NewConferenceAddedMessage(conferenceId);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;
using VideoWeb.Common.Models;
using VideoWeb.Contract.Responses;
using EventType = VideoWeb.EventHub.Enums.EventType;

Expand All @@ -25,13 +26,20 @@ private async Task PublishParticipantsUpdatedMessage(List<ParticipantResponse> u
{
foreach (var participant in participantsToNotify)
{
await HubContext.Clients.Group(participant.UserName.ToLowerInvariant())
.ParticipantsUpdatedMessage(SourceConference.Id, updatedParticipants);
Logger.LogTrace("{UserName} | Role: {Role}", participant.UserName,
participant.Role);
// Staff members already receive a message via the staff members group below, so don't message them here as well
if (participant.Role != Role.StaffMember)
shaed-parkar marked this conversation as resolved.
Show resolved Hide resolved
{
await HubContext.Clients.Group(participant.UserName.ToLowerInvariant())
.ParticipantsUpdatedMessage(SourceConference.Id, updatedParticipants);
Logger.LogTrace("{UserName} | Role: {Role}", participant.UserName,
participant.Role);
}
}

await HubContext.Clients.Group(Hub.EventHub.VhOfficersGroupName)
.ParticipantsUpdatedMessage(SourceConference.Id, updatedParticipants);

await HubContext.Clients.Group(Hub.EventHub.StaffMembersGroupName)
.ParticipantsUpdatedMessage(SourceConference.Id, updatedParticipants);
}
}
30 changes: 24 additions & 6 deletions VideoWeb/VideoWeb.EventHub/Hub/EventHubClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,15 @@ public class EventHub(
{
public static string VhOfficersGroupName => "VhOfficers";
public static string DefaultAdminName => "Admin";
public static string StaffMembersGroupName => "StaffMembers";

public override async Task OnConnectedAsync()
{
var isAdmin = IsSenderAdmin();
var isStaffMember = IsSenderStaffMember();

await AddUserToUserGroup(isAdmin);
await AddUserToConferenceGroups(isAdmin);
await AddUserToUserGroup(isAdmin, isStaffMember);
await AddUserToConferenceGroups(isAdmin || isStaffMember);

await base.OnConnectedAsync();

Expand All @@ -59,12 +61,17 @@ public async Task AddToGroup(string conferenceId)
}
}

private async Task AddUserToUserGroup(bool isAdmin)
private async Task AddUserToUserGroup(bool isAdmin, bool isStaffMember)
{
if (isAdmin)
{
await Groups.AddToGroupAsync(Context.ConnectionId, VhOfficersGroupName);
}

if (isStaffMember)
shaed-parkar marked this conversation as resolved.
Show resolved Hide resolved
{
await Groups.AddToGroupAsync(Context.ConnectionId, StaffMembersGroupName);
}

await Groups.AddToGroupAsync(Context.ConnectionId, Context.User.Identity!.Name!.ToLowerInvariant());
}
Expand All @@ -86,20 +93,26 @@ public override async Task OnDisconnectedAsync(Exception exception)
}

var isAdmin = IsSenderAdmin();
await RemoveUserFromUserGroup(isAdmin);
await RemoveUserFromConferenceGroups(isAdmin);
var isStaffMember = IsSenderStaffMember();
await RemoveUserFromUserGroup(isAdmin, isStaffMember);
await RemoveUserFromConferenceGroups(isAdmin || isStaffMember);
await userProfileService.ClearUserCache(username);
await appRoleService.ClearUserCache(username);

await base.OnDisconnectedAsync(exception);
}

private async Task RemoveUserFromUserGroup(bool isAdmin)
private async Task RemoveUserFromUserGroup(bool isAdmin, bool isStaffMember)
{
if (isAdmin)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, VhOfficersGroupName);
}

if (isStaffMember)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, StaffMembersGroupName);
}

await Groups.RemoveFromGroupAsync(Context.ConnectionId, Context.User.Identity.Name.ToLowerInvariant());
}
Expand Down Expand Up @@ -129,6 +142,11 @@ private bool IsSenderAdmin()
{
return Context.User.IsInRole(AppRoles.VhOfficerRole);
}

private bool IsSenderStaffMember()
{
return Context.User.IsInRole(AppRoles.StaffMember);
}

private string GetObfuscatedUsernameAsync(string username)
{
Expand Down
2 changes: 2 additions & 0 deletions VideoWeb/VideoWeb.EventHub/Hub/IEventHubClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ Task ReceiveMessage(Guid conferenceId, string from, string fromDisplayName, stri
Task NewConferenceAddedMessage(Guid conferenceId);
Task AllocationHearings(string csoUserName, List<HearingDetailRequest> hearings);
Task EndpointsUpdated(Guid conferenceId, UpdateEndpointsDto endpoints);
Task HearingCancelledMessage(Guid conferenceId);
Task HearingDetailsUpdatedMessage(Guid conferenceId);

/// <summary>
/// Request a participant's local mute be update. Not to be confused with remote mute (and lock).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,15 @@ public void RegisterUsersForHubContext(IEnumerable<Participant> participants)

EventHubContextMock.Setup(x => x.Clients.Group(EventHub.Hub.EventHub.VhOfficersGroupName))
.Returns(EventHubClientMock.Object);

EventHubContextMock.Setup(x => x.Clients.Group(EventHub.Hub.EventHub.StaffMembersGroupName))
.Returns(EventHubClientMock.Object);
}

public void RegisterParticipantForHubContext(Participant participant)
{
EventHubContextMock.Setup(x => x.Clients.Group(participant.Username.ToLowerInvariant()))
.Returns(EventHubClientMock.Object);
}

public Conference BuildConferenceForTest()
Expand Down
16 changes: 16 additions & 0 deletions VideoWeb/VideoWeb.UnitTests/Cache/ConferenceCacheTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using NUnit.Framework;
using VideoWeb.Common.Caching;
using VideoWeb.Common.Models;
using VideoWeb.UnitTests.Builders;

namespace VideoWeb.UnitTests.Cache
{
Expand Down Expand Up @@ -73,5 +74,20 @@ public async Task GetOrAddConferenceAsync_should_return_conference_when_cache_do
result.Should().NotBeNull();
result.Id.Should().Be(conferenceDetails.Id);
}

[Test]
public async Task RemoveConferenceAsync_should_remove_conference_from_cache()
{
// Arrange
var conference = new ConferenceCacheModelBuilder().Build();
_memoryCache.Set(conference.Id, conference);

// Act
await _conferenceCache.RemoveConferenceAsync(conference);

// Assert
var conferenceInCache = _memoryCache.Get(conference.Id);
conferenceInCache.Should().BeNull();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,28 @@ public async Task GetOrAddConferenceAsync_should_return_conference_when_cache_do
result.Should().BeEquivalentTo(conference);
}

[Test]
public async Task RemoveConferenceAsync_should_remove_conference_from_cache()
{
// Arrange
var conferenceResponse = CreateConferenceResponse();
var hearingDetails = CreateHearingResponse(conferenceResponse);
var conference = ConferenceCacheMapper.MapConferenceToCacheModel(conferenceResponse, hearingDetails);
var serialisedConference = JsonConvert.SerializeObject(conference, SerializerSettings);
var rawData = Encoding.UTF8.GetBytes(serialisedConference);
_distributedCacheMock
.Setup(x => x.GetAsync(conference.Id.ToString(), CancellationToken.None))
.ReturnsAsync(rawData);

var cache = new DistributedConferenceCache(_distributedCacheMock.Object, _loggerMock.Object);

// Act
await cache.RemoveConferenceAsync(conference);

// Assert
_distributedCacheMock.Verify(x => x.RemoveAsync(conference.Id.ToString(), default), Times.Once);
}

private static JsonSerializerSettings SerializerSettings => new () { TypeNameHandling = TypeNameHandling.Objects, Formatting = Formatting.None };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Threading.Tasks;
using BookingsApi.Client;
using Moq;
using NUnit.Framework;
using VideoApi.Client;
using VideoWeb.Common;
using VideoWeb.Common.Caching;
using VideoWeb.Common.Models;
using VideoWeb.UnitTests.Builders;

namespace VideoWeb.UnitTests.Common.ConferenceServiceTests
{
public class RemoveConferenceTests
{
private ConferenceService _conferenceService;
private Conference _conference;
private Mock<IConferenceCache> _conferenceCacheMock;

[SetUp]
public void SetUp()
{
_conference = new ConferenceCacheModelBuilder().Build();
_conferenceCacheMock = new Mock<IConferenceCache>();
var videoApiClientMock = new Mock<IVideoApiClient>();
var bookingsApiClientMock = new Mock<IBookingsApiClient>();
_conferenceService = new ConferenceService(_conferenceCacheMock.Object,
videoApiClientMock.Object,
bookingsApiClientMock.Object);
}

[Test]
public async Task Should_remove_conference()
{
// Arrange & Act
await _conferenceService.RemoveConference(_conference);

// Assert
_conferenceCacheMock.Verify(x => x.RemoveConferenceAsync(_conference, default), Times.Once);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc;
using Moq;
using NUnit.Framework;
using VideoWeb.Common;
using VideoWeb.Common.Models;
using VideoWeb.Helpers.Interfaces;
using VideoWeb.UnitTests.Builders;

namespace VideoWeb.UnitTests.Controllers.InternalEventController
{
public class HearingCancelledTests : InternalEventControllerTests
{
private Conference _conference;
private Mock<IConferenceService> _conferenceServiceMock;
private Mock<IHearingCancelledEventNotifier> _notifierMock;

[SetUp]
public void Setup()
{
_conference = new ConferenceCacheModelBuilder().Build();
_conferenceServiceMock = Mocker.Mock<IConferenceService>();
_conferenceServiceMock
.Setup(x => x.GetConference(It.Is<Guid>(id => id == _conference.Id), It.IsAny<CancellationToken>()))
.ReturnsAsync(_conference);
_notifierMock = Mocker.Mock<IHearingCancelledEventNotifier>();
}

[Test]
public async Task should_push_event()
{
// Arrange & Act
var result = await Controller.HearingCancelled(_conference.Id);

// Assert
result.Should().BeOfType<NoContentResult>();

_conferenceServiceMock.Verify(x => x.RemoveConference(_conference, default), Times.Once);
_notifierMock.Verify(x => x.PushHearingCancelledEvent(_conference), Times.Once);
}
}
}
Loading
Loading