diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Common/ApiHelper/ApiRequestHelper.cs b/BookingQueueSubscriber/BookingQueueSubscriber.Common/ApiHelper/ApiRequestHelper.cs new file mode 100644 index 00000000..8a780df1 --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Common/ApiHelper/ApiRequestHelper.cs @@ -0,0 +1,38 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace BookingQueueSubscriber.Common.ApiHelper +{ + public static class ApiRequestHelper + { + public static string SerialiseRequestToSnakeCaseJson(object request) + { + DefaultContractResolver contractResolver = new DefaultContractResolver + { + NamingStrategy = new SnakeCaseNamingStrategy() + }; + + return JsonConvert.SerializeObject(request, new JsonSerializerSettings + { + ContractResolver = contractResolver, + Formatting = Formatting.Indented + }); + } + + public static T DeserialiseSnakeCaseJsonToResponse(string response) + { + var contractResolver = new DefaultContractResolver + { + NamingStrategy = new SnakeCaseNamingStrategy() + }; + + return JsonConvert.DeserializeObject(response, new JsonSerializerSettings + { + ContractResolver = contractResolver, + Formatting = Formatting.Indented, + TypeNameHandling = TypeNameHandling.Objects + }); + } + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Common/ApiHelper/ApiUriFactory.cs b/BookingQueueSubscriber/BookingQueueSubscriber.Common/ApiHelper/ApiUriFactory.cs new file mode 100644 index 00000000..37d10a73 --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Common/ApiHelper/ApiUriFactory.cs @@ -0,0 +1,32 @@ +using System; + +namespace BookingQueueSubscriber.Common.ApiHelper +{ + public class ApiUriFactory + { + public ParticipantsEndpoints ParticipantsEndpoints { get; } + public ConferenceEndpoints ConferenceEndpoints { get; } + + public ApiUriFactory() + { + ParticipantsEndpoints = new ParticipantsEndpoints(); + ConferenceEndpoints = new ConferenceEndpoints(); + } + } + + public class ParticipantsEndpoints + { + private string ApiRoot => "conferences"; + + public string AddParticipantsToConference(Guid conferenceId) => $"{ApiRoot}/{conferenceId}/participants"; + + public string RemoveParticipantFromConference(Guid conferenceId, Guid participantId) => + $"{ApiRoot}/{conferenceId}/participants/{participantId}"; + } + + public class ConferenceEndpoints + { + private string ApiRoot => "conferences"; + public string BookNewConference => $"{ApiRoot}"; + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Common/BookingQueueSubscriber.Common.csproj b/BookingQueueSubscriber/BookingQueueSubscriber.Common/BookingQueueSubscriber.Common.csproj new file mode 100644 index 00000000..cc11e10b --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Common/BookingQueueSubscriber.Common.csproj @@ -0,0 +1,22 @@ + + + + netcoreapp2.2 + + + + + + + + + + + + + + + + + + diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Common/Configuration/AzureAdConfiguration.cs b/BookingQueueSubscriber/BookingQueueSubscriber.Common/Configuration/AzureAdConfiguration.cs new file mode 100644 index 00000000..b8176d37 --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Common/Configuration/AzureAdConfiguration.cs @@ -0,0 +1,10 @@ +namespace BookingQueueSubscriber.Common.Configuration +{ + public class AzureAdConfiguration + { + public string ClientId { get; set; } + public string ClientSecret { get; set; } + public string Authority { get; set; } + public string TenantId { get; set; } + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Common/Configuration/ConfigLoader.cs b/BookingQueueSubscriber/BookingQueueSubscriber.Common/Configuration/ConfigLoader.cs new file mode 100644 index 00000000..d4b42470 --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Common/Configuration/ConfigLoader.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; + +namespace BookingQueueSubscriber.Common.Configuration +{ + public class ConfigLoader + { + public readonly IConfigurationRoot ConfigRoot; + + public ConfigLoader() + { + var configRootBuilder = new ConfigurationBuilder() + .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables(); + ConfigRoot = configRootBuilder.Build(); + } + + public IOptions ReadAzureAdSettings() + { + return Options.Create(ConfigRoot.GetSection("AzureAd").Get()); + } + + public IOptions ReadHearingServiceSettings() + { + return Options.Create(ConfigRoot.GetSection("VhServices").Get()); + } + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Common/Configuration/HearingServicesConfiguration.cs b/BookingQueueSubscriber/BookingQueueSubscriber.Common/Configuration/HearingServicesConfiguration.cs new file mode 100644 index 00000000..1dd3e22a --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Common/Configuration/HearingServicesConfiguration.cs @@ -0,0 +1,8 @@ +namespace BookingQueueSubscriber.Common.Configuration +{ + public class HearingServicesConfiguration + { + public string VideoApiUrl { get; set; } + public string VideoApiResourceId { get; set; } + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Common/Security/AzureTokenProvider.cs b/BookingQueueSubscriber/BookingQueueSubscriber.Common/Security/AzureTokenProvider.cs new file mode 100644 index 00000000..86d00a47 --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Common/Security/AzureTokenProvider.cs @@ -0,0 +1,48 @@ +using System; +using BookingQueueSubscriber.Common.Configuration; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Clients.ActiveDirectory; + +namespace BookingQueueSubscriber.Common.Security +{ + public interface IAzureTokenProvider + { + string GetClientAccessToken(string clientId, string clientSecret, string clientResource); + AuthenticationResult GetAuthorisationResult(string clientId, string clientSecret, string clientResource); + } + + public class AzureAzureTokenProvider : IAzureTokenProvider + { + private readonly AzureAdConfiguration _azureAdConfiguration; + + public AzureAzureTokenProvider(IOptions azureAdConfiguration) + { + _azureAdConfiguration = azureAdConfiguration.Value; + } + + public string GetClientAccessToken(string clientId, string clientSecret, string clientResource) + { + var result = GetAuthorisationResult(clientId, clientSecret, clientResource); + return result.AccessToken; + } + + public AuthenticationResult GetAuthorisationResult(string clientId, string clientSecret, string clientResource) + { + AuthenticationResult result; + var credential = new ClientCredential(clientId, clientSecret); + var authContext = + new AuthenticationContext($"{_azureAdConfiguration.Authority}{_azureAdConfiguration.TenantId}"); + + try + { + result = authContext.AcquireTokenAsync(clientResource, credential).Result; + } + catch (AdalException) + { + throw new UnauthorizedAccessException(); + } + + return result; + } + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Services/BookingQueueSubscriber.Services.csproj b/BookingQueueSubscriber/BookingQueueSubscriber.Services/BookingQueueSubscriber.Services.csproj new file mode 100644 index 00000000..2e909a49 --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Services/BookingQueueSubscriber.Services.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp2.2 + + + + + + + diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Services/IntegrationEvents/HearingCancelledIntegrationEvent.cs b/BookingQueueSubscriber/BookingQueueSubscriber.Services/IntegrationEvents/HearingCancelledIntegrationEvent.cs new file mode 100644 index 00000000..0f58aa24 --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Services/IntegrationEvents/HearingCancelledIntegrationEvent.cs @@ -0,0 +1,11 @@ +using System; +using BookingQueueSubscriber.Services.MessageHandlers; + +namespace BookingQueueSubscriber.Services.IntegrationEvents +{ + public class HearingCancelledIntegrationEvent : IntegrationEvent + { + public Guid HearingId { get; set; } + public override IntegrationEventType EventType => IntegrationEventType.HearingCancelled; + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Services/IntegrationEvents/HearingDetailsUpdatedIntegrationEvent.cs b/BookingQueueSubscriber/BookingQueueSubscriber.Services/IntegrationEvents/HearingDetailsUpdatedIntegrationEvent.cs new file mode 100644 index 00000000..863a1097 --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Services/IntegrationEvents/HearingDetailsUpdatedIntegrationEvent.cs @@ -0,0 +1,11 @@ +using BookingQueueSubscriber.Services.MessageHandlers; +using BookingQueueSubscriber.Services.MessageHandlers.Dtos; + +namespace BookingQueueSubscriber.Services.IntegrationEvents +{ + public class HearingDetailsUpdatedIntegrationEvent : IntegrationEvent + { + public HearingDto Hearing { get; set; } + public override IntegrationEventType EventType => IntegrationEventType.HearingDetailsUpdated; + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Services/IntegrationEvents/HearingIsReadyForVideoIntegrationEvent.cs b/BookingQueueSubscriber/BookingQueueSubscriber.Services/IntegrationEvents/HearingIsReadyForVideoIntegrationEvent.cs new file mode 100644 index 00000000..f7bad6b6 --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Services/IntegrationEvents/HearingIsReadyForVideoIntegrationEvent.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using BookingQueueSubscriber.Services.MessageHandlers; +using BookingQueueSubscriber.Services.MessageHandlers.Dtos; + +namespace BookingQueueSubscriber.Services.IntegrationEvents +{ + public class HearingIsReadyForVideoIntegrationEvent : IntegrationEvent + { + public HearingDto Hearing { get; set; } + public IList Participants { get; set; } + public override IntegrationEventType EventType => IntegrationEventType.HearingIsReadyForVideo; + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Services/IntegrationEvents/IntegrationEvent.cs b/BookingQueueSubscriber/BookingQueueSubscriber.Services/IntegrationEvents/IntegrationEvent.cs new file mode 100644 index 00000000..c2bbea71 --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Services/IntegrationEvents/IntegrationEvent.cs @@ -0,0 +1,9 @@ +using BookingQueueSubscriber.Services.MessageHandlers; + +namespace BookingQueueSubscriber.Services.IntegrationEvents +{ + public class IntegrationEvent + { + public virtual IntegrationEventType EventType { get; } + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Services/IntegrationEvents/ParticipantAddedIntegrationEvent.cs b/BookingQueueSubscriber/BookingQueueSubscriber.Services/IntegrationEvents/ParticipantAddedIntegrationEvent.cs new file mode 100644 index 00000000..d59f2548 --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Services/IntegrationEvents/ParticipantAddedIntegrationEvent.cs @@ -0,0 +1,14 @@ +using System; +using BookingQueueSubscriber.Services.MessageHandlers; +using BookingQueueSubscriber.Services.MessageHandlers.Dtos; + +namespace BookingQueueSubscriber.Services.IntegrationEvents +{ + public class ParticipantAddedIntegrationEvent: IntegrationEvent + { + + public Guid HearingId { get; } + public ParticipantDto Participant { get; } + public override IntegrationEventType EventType => IntegrationEventType.ParticipantAdded; + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Services/IntegrationEvents/ParticipantRemovedIntegrationEvent.cs b/BookingQueueSubscriber/BookingQueueSubscriber.Services/IntegrationEvents/ParticipantRemovedIntegrationEvent.cs new file mode 100644 index 00000000..025febb5 --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Services/IntegrationEvents/ParticipantRemovedIntegrationEvent.cs @@ -0,0 +1,19 @@ +using System; +using BookingQueueSubscriber.Services.MessageHandlers; + +namespace BookingQueueSubscriber.Services.IntegrationEvents +{ + public class ParticipantRemovedIntegrationEvent : IntegrationEvent + { + public ParticipantRemovedIntegrationEvent(Guid hearingId, Guid participantId) + { + HearingId = hearingId; + ParticipantId = participantId; + } + + public Guid HearingId { get; } + public Guid ParticipantId { get; } + + public override IntegrationEventType EventType => IntegrationEventType.ParticipantRemoved; + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Services/Mappers/HearingToBookConferenceMapper.cs b/BookingQueueSubscriber/BookingQueueSubscriber.Services/Mappers/HearingToBookConferenceMapper.cs new file mode 100644 index 00000000..5cfc5f2f --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Services/Mappers/HearingToBookConferenceMapper.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Linq; +using BookingQueueSubscriber.Services.MessageHandlers.Dtos; +using BookingQueueSubscriber.Services.VideoApi.Contracts; + +namespace BookingQueueSubscriber.Services.Mappers +{ + public class HearingToBookConferenceMapper + { + public BookNewConferenceRequest MapToBookNewConferenceRequest(HearingDto hearingDto, IEnumerable participantDtos) + { + var participantMapper = new ParticipantToParticipantRequestMapper(); + var participants = participantDtos.Select(participantMapper.MapToParticipantRequest).ToList(); + + var request = new BookNewConferenceRequest + { + CaseNumber = hearingDto.CaseNumber, + CaseName = hearingDto.CaseName, + CaseType = hearingDto.CaseType, + ScheduledDuration = hearingDto.ScheduledDuration, + ScheduledDateTime = hearingDto.ScheduledDateTime, + HearingRefId = hearingDto.HearingId, + Participants = participants + }; + return request; + } + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Services/Mappers/ParticipantToParticipantRequestMapper.cs b/BookingQueueSubscriber/BookingQueueSubscriber.Services/Mappers/ParticipantToParticipantRequestMapper.cs new file mode 100644 index 00000000..20f6f4fc --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Services/Mappers/ParticipantToParticipantRequestMapper.cs @@ -0,0 +1,24 @@ +using System; +using BookingQueueSubscriber.Services.MessageHandlers.Dtos; +using BookingQueueSubscriber.Services.VideoApi.Contracts; + +namespace BookingQueueSubscriber.Services.Mappers +{ + public class ParticipantToParticipantRequestMapper + { + public ParticipantRequest MapToParticipantRequest(ParticipantDto participantDto) + { + var request = new ParticipantRequest + { + Name = participantDto.Fullname, + Username = participantDto.Username, + DisplayName = participantDto.DisplayName, + UserRole = Enum.Parse(participantDto.UserRole), + CaseTypeGroup = participantDto.CaseGroupType.ToString(), + ParticipantRefId = participantDto.ParticipantId + }; + + return request; + } + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/BookingsMessage.cs b/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/BookingsMessage.cs new file mode 100644 index 00000000..0e5dcca7 --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/BookingsMessage.cs @@ -0,0 +1,12 @@ +using System; +using BookingQueueSubscriber.Services.IntegrationEvents; + +namespace BookingQueueSubscriber.Services.MessageHandlers +{ + public class BookingsMessage + { + public Guid Id { get; set; } + public DateTime Timestamp { get; set; } + public IntegrationEvent IntegrationEvent { get; set; } + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/Core/MessageHandlerBase.cs b/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/Core/MessageHandlerBase.cs new file mode 100644 index 00000000..c702ca61 --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/Core/MessageHandlerBase.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading.Tasks; +using BookingQueueSubscriber.Services.IntegrationEvents; + +namespace BookingQueueSubscriber.Services.MessageHandlers.Core +{ + public interface IMessageHandler + { + IntegrationEventType IntegrationEventType { get; } + Type BodyType { get; } + Task HandleAsync(IntegrationEvent integrationEvent); + } + + public abstract class MessageHandlerBase : IMessageHandler + { + protected IVideoApiService VideoApiService { get; } + public abstract IntegrationEventType IntegrationEventType { get; } + public abstract Type BodyType { get; } + + public abstract Task HandleAsync(IntegrationEvent integrationEvent); + + protected MessageHandlerBase(IVideoApiService videoApiService) + { + VideoApiService = videoApiService; + } + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/Core/MessageHandlerFactory.cs b/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/Core/MessageHandlerFactory.cs new file mode 100644 index 00000000..ac9cbef8 --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/Core/MessageHandlerFactory.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace BookingQueueSubscriber.Services.MessageHandlers.Core +{ + public interface IMessageHandlerFactory + { + IMessageHandler Get(IntegrationEventType integrationEventType); + } + + public class MessageHandlerFactory : IMessageHandlerFactory + { + private readonly IEnumerable _messageHandlers; + + public MessageHandlerFactory(IEnumerable messageHandlers) + { + _messageHandlers = messageHandlers; + } + + public IMessageHandler Get(IntegrationEventType integrationEventType) + { + var eventHandler = _messageHandlers.SingleOrDefault(x => x.IntegrationEventType == integrationEventType); + if (eventHandler == null) + throw new ArgumentOutOfRangeException(nameof(integrationEventType), + $"MessageHandler cannot be found for messageType: {integrationEventType.ToString()}"); + return eventHandler; + } + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/Dtos/HearingDto.cs b/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/Dtos/HearingDto.cs new file mode 100644 index 00000000..cda1c43a --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/Dtos/HearingDto.cs @@ -0,0 +1,14 @@ +using System; + +namespace BookingQueueSubscriber.Services.MessageHandlers.Dtos +{ + public class HearingDto + { + public Guid HearingId { get; set; } + public DateTime ScheduledDateTime { get; set; } + public int ScheduledDuration { get; set; } + public string CaseType { get; set; } + public string CaseName { get; set; } + public string CaseNumber { get; set; } + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/Dtos/ParticipantDto.cs b/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/Dtos/ParticipantDto.cs new file mode 100644 index 00000000..3098c10d --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/Dtos/ParticipantDto.cs @@ -0,0 +1,24 @@ +using System; + +namespace BookingQueueSubscriber.Services.MessageHandlers.Dtos +{ + public class ParticipantDto + { + public Guid ParticipantId { get; set; } + public string Fullname { get; set; } + public string Username { get; set; } + public string DisplayName { get; set; } + public string HearingRole { get; set; } + public string UserRole { get; set; } + public CaseRoleGroup CaseGroupType { get; set; } + public string Representee { get; set; } + } + + public enum CaseRoleGroup + { + PartyGroup0 = 0, + PartyGroup1 = 1, + PartyGroup2 = 2, + PartyGroup3 = 3, + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/HearingCancelledHandler.cs b/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/HearingCancelledHandler.cs new file mode 100644 index 00000000..0e873db7 --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/HearingCancelledHandler.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading.Tasks; +using BookingQueueSubscriber.Services.IntegrationEvents; +using BookingQueueSubscriber.Services.MessageHandlers.Core; + +namespace BookingQueueSubscriber.Services.MessageHandlers +{ + public class HearingCancelledHandler : MessageHandlerBase + { + public HearingCancelledHandler(IVideoApiService videoApiService) : base(videoApiService) + { + } + + public override IntegrationEventType IntegrationEventType => IntegrationEventType.HearingCancelled; + public override Type BodyType { get; } + + public override Task HandleAsync(IntegrationEvent integrationEvent) + { + throw new System.NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/HearingDetailsUpdatedHandler.cs b/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/HearingDetailsUpdatedHandler.cs new file mode 100644 index 00000000..e80785bf --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/HearingDetailsUpdatedHandler.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading.Tasks; +using BookingQueueSubscriber.Services.IntegrationEvents; +using BookingQueueSubscriber.Services.MessageHandlers.Core; + +namespace BookingQueueSubscriber.Services.MessageHandlers +{ + public class HearingDetailsUpdatedHandler : MessageHandlerBase + { + public HearingDetailsUpdatedHandler(IVideoApiService videoApiService) : base(videoApiService) + { + } + + public override IntegrationEventType IntegrationEventType => IntegrationEventType.HearingDetailsUpdated; + public override Type BodyType { get; } + + public override Task HandleAsync(IntegrationEvent integrationEvent) + { + throw new System.NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/HearingReadyForVideoHandler.cs b/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/HearingReadyForVideoHandler.cs new file mode 100644 index 00000000..8f0677f3 --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/HearingReadyForVideoHandler.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading.Tasks; +using BookingQueueSubscriber.Services.IntegrationEvents; +using BookingQueueSubscriber.Services.Mappers; +using BookingQueueSubscriber.Services.MessageHandlers.Core; + +namespace BookingQueueSubscriber.Services.MessageHandlers +{ + public class HearingReadyForVideoHandler : MessageHandlerBase + { + public HearingReadyForVideoHandler(IVideoApiService videoApiService) : base(videoApiService) + { + } + + public override IntegrationEventType IntegrationEventType => IntegrationEventType.HearingIsReadyForVideo; + public override Type BodyType => typeof(HearingIsReadyForVideoIntegrationEvent); + + public override async Task HandleAsync(IntegrationEvent integrationEvent) + { + var hearingReadyEvent = ValidateArgs(integrationEvent); + var request = + new HearingToBookConferenceMapper().MapToBookNewConferenceRequest(hearingReadyEvent.Hearing, + hearingReadyEvent.Participants); + await VideoApiService.BookNewConferenceAsync(request).ConfigureAwait(false); + } + + private HearingIsReadyForVideoIntegrationEvent ValidateArgs(IntegrationEvent integrationEvent) + { + var hearingReadyEvent = (HearingIsReadyForVideoIntegrationEvent) integrationEvent; + if (hearingReadyEvent == null) + { + throw new ArgumentNullException(nameof(integrationEvent), + $"Expected message to be of type {typeof(HearingIsReadyForVideoIntegrationEvent)}"); + } + + return hearingReadyEvent; + } + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/IntegrationEventType.cs b/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/IntegrationEventType.cs new file mode 100644 index 00000000..faca8c4f --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/IntegrationEventType.cs @@ -0,0 +1,11 @@ +namespace BookingQueueSubscriber.Services.MessageHandlers +{ + public enum IntegrationEventType + { + HearingIsReadyForVideo, + ParticipantAdded, + ParticipantRemoved, + HearingDetailsUpdated, + HearingCancelled + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/ParticipantAddedHandler.cs b/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/ParticipantAddedHandler.cs new file mode 100644 index 00000000..ded13736 --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/ParticipantAddedHandler.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading.Tasks; +using BookingQueueSubscriber.Services.IntegrationEvents; +using BookingQueueSubscriber.Services.MessageHandlers.Core; + +namespace BookingQueueSubscriber.Services.MessageHandlers +{ + public class ParticipantAddedHandler : MessageHandlerBase + { + public ParticipantAddedHandler(IVideoApiService videoApiService) : base(videoApiService) + { + } + + public override IntegrationEventType IntegrationEventType => IntegrationEventType.ParticipantAdded; + public override Type BodyType { get; } + + public override Task HandleAsync(IntegrationEvent integrationEvent) + { + throw new System.NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/ParticipantRemovedHandler.cs b/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/ParticipantRemovedHandler.cs new file mode 100644 index 00000000..0a8166b8 --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Services/MessageHandlers/ParticipantRemovedHandler.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading.Tasks; +using BookingQueueSubscriber.Services.IntegrationEvents; +using BookingQueueSubscriber.Services.MessageHandlers.Core; + +namespace BookingQueueSubscriber.Services.MessageHandlers +{ + public class ParticipantRemovedHandler : MessageHandlerBase + { + public ParticipantRemovedHandler(IVideoApiService videoApiService) : base(videoApiService) + { + } + + public override IntegrationEventType IntegrationEventType => IntegrationEventType.ParticipantRemoved; + public override Type BodyType { get; } + + public override Task HandleAsync(IntegrationEvent integrationEvent) + { + throw new System.NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Services/VideoApi/Contracts/BookNewConferenceRequest.cs b/BookingQueueSubscriber/BookingQueueSubscriber.Services/VideoApi/Contracts/BookNewConferenceRequest.cs new file mode 100644 index 00000000..9ee7b177 --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Services/VideoApi/Contracts/BookNewConferenceRequest.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace BookingQueueSubscriber.Services.VideoApi.Contracts +{ + public class BookNewConferenceRequest + { + public Guid HearingRefId { get; set; } + public string CaseType { get; set; } + public DateTime ScheduledDateTime { get; set; } + public string CaseNumber { get; set; } + public string CaseName { get; set; } + public int ScheduledDuration { get; set; } + public List Participants { get; set; } + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Services/VideoApi/Contracts/ParticipantRequest.cs b/BookingQueueSubscriber/BookingQueueSubscriber.Services/VideoApi/Contracts/ParticipantRequest.cs new file mode 100644 index 00000000..8d2f4b4a --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Services/VideoApi/Contracts/ParticipantRequest.cs @@ -0,0 +1,25 @@ +using System; + +namespace BookingQueueSubscriber.Services.VideoApi.Contracts +{ + public class ParticipantRequest + { + public Guid ParticipantRefId { get; set; } + public string Name { get; set; } + public string DisplayName { get; set; } + public string Username { get; set; } + public UserRole UserRole { get; set; } + public string CaseTypeGroup { get; set; } + } + + public enum UserRole + { + None = 0, + CaseAdmin = 1, + VideoHearingsOfficer = 2, + HearingFacilitationSupport = 3, + Judge = 4, + Individual = 5, + Representative = 6 + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.Services/VideoApiService.cs b/BookingQueueSubscriber/BookingQueueSubscriber.Services/VideoApiService.cs new file mode 100644 index 00000000..cbb36c42 --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.Services/VideoApiService.cs @@ -0,0 +1,71 @@ +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using BookingQueueSubscriber.Common.ApiHelper; +using BookingQueueSubscriber.Common.Configuration; +using BookingQueueSubscriber.Common.Security; +using BookingQueueSubscriber.Services.VideoApi.Contracts; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; + +namespace BookingQueueSubscriber.Services +{ + public interface IVideoApiService + { + Task BookNewConferenceAsync(BookNewConferenceRequest request); + } + + public class VideoApiService : IVideoApiService + { + private readonly IMemoryCache _memoryCache; + private readonly IAzureTokenProvider _azureTokenProvider; + private readonly AzureAdConfiguration _azureAdConfiguration; + private readonly ApiUriFactory _apiUriFactory; + private readonly HearingServicesConfiguration _hearingServices; + + private string _tokenCacheKey => "VideoApiServiceToken"; + + public VideoApiService(IMemoryCache memoryCache, IAzureTokenProvider azureTokenProvider, + IOptions hearingServicesConfig, + IOptions azureAdConfiguration) + { + _memoryCache = memoryCache; + _azureTokenProvider = azureTokenProvider; + _apiUriFactory = new ApiUriFactory(); + _hearingServices = hearingServicesConfig.Value; + _azureAdConfiguration = azureAdConfiguration.Value; + } + + public async Task BookNewConferenceAsync(BookNewConferenceRequest request) + { + var jsonBody = ApiRequestHelper.SerialiseRequestToSnakeCaseJson(request); + var httpContent = new StringContent(jsonBody, Encoding.UTF8, "application/json"); + + var bearerToken = GetBearerToken(); + using (var httpClient = new HttpClient()) + { + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken); + httpClient.BaseAddress = new Uri(_hearingServices.VideoApiUrl); + var response = + await httpClient.PostAsync(_apiUriFactory.ConferenceEndpoints.BookNewConference, httpContent); + response.EnsureSuccessStatusCode(); + } + } + + private string GetBearerToken() + { + var token = _memoryCache.Get(_tokenCacheKey); + if (!string.IsNullOrEmpty(token)) return token; + + var authenticationResult = _azureTokenProvider.GetAuthorisationResult(_azureAdConfiguration.ClientId, + _azureAdConfiguration.ClientSecret, _hearingServices.VideoApiResourceId); + token = authenticationResult.AccessToken; + var tokenExpireDateTime = authenticationResult.ExpiresOn.DateTime.AddMinutes(-1); + _memoryCache.Set(_tokenCacheKey, token, tokenExpireDateTime); + + return token; + } + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/BookingQueueSubscriber.UnitTests.csproj b/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/BookingQueueSubscriber.UnitTests.csproj index 3a547441..faa38594 100644 --- a/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/BookingQueueSubscriber.UnitTests.csproj +++ b/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/BookingQueueSubscriber.UnitTests.csproj @@ -5,21 +5,26 @@ - + all runtime; build; native; contentfiles; analyzers + - + - - + + + + + + diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/BookingQueueSubscriberFunction/RunTests.cs b/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/BookingQueueSubscriberFunction/RunTests.cs new file mode 100644 index 00000000..5bb0b43f --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/BookingQueueSubscriberFunction/RunTests.cs @@ -0,0 +1,70 @@ +using BookingQueueSubscriber.Services.MessageHandlers.Core; +using BookingQueueSubscriber.Services.VideoApi.Contracts; +using BookingQueueSubscriber.UnitTests.MessageHandlers; +using Moq; +using NUnit.Framework; + +namespace BookingQueueSubscriber.UnitTests.BookingQueueSubscriberFunction +{ + public class RunTests : MessageHandlerTestBase + { + + [Test] + public void should_handle_hearing_ready_for_video_integration_event() + { + var message = @" + { + 'id':'35ca2c9f-fde6-4eee-a6e4-3ac9497301d3', + 'timestamp':'2019-05-07T09:35:53.234909Z', + 'integration_event':{ + 'hearing':{ + 'hearing_id':'b487fbbc-b214-4f63-9ba9-0f142a6f1801', + 'scheduled_date_time':'2019-05-07T23:00:00Z', + 'scheduled_duration':1, + 'case_type':'Civil Money Claims', + 'case_number':'Number1', + 'case_name':'Name1' + }, + 'participants':[ + { + 'participant_id':'2b655923-49b9-440b-afdd-3eb0ef5d6d30', + 'fullname':'Title1 FirstName1 LastName1', + 'username':'erna_maggio@zboncak.us', + 'display_name':'DisplayName1', + 'hearing_role':'Claimant LIP', + 'user_role':'Individual', + 'case_group_type':'partyGroup1', + 'representee':'' + }, + { + 'participant_id':'f0eb5870-18c0-4469-ba06-49e6a0e24729', + 'fullname':'Title2 FirstName2 LastName2', + 'username':'al.turcotte@koelpin.ca', + 'display_name':'DisplayName2', + 'hearing_role':'Solicitor', + 'user_role':'Representative', + 'case_group_type':'partyGroup1', + 'representee':'Representee2' + }, + { + 'participant_id':'8fc9b816-29ba-437f-b11f-2ee913e5cefc', + 'fullname':'Title5 FirstName5 LastName5', + 'username':'carrie.pouros@veum.info', + 'display_name':'DisplayName5', + 'hearing_role':'Judge', + 'user_role':'Judge', + 'case_group_type':'partyGroup0', + 'representee':'' + } + ], + 'event_type':'hearingIsReadyForVideo' + } + } +"; + VideoApiServiceMock.Setup(x => x.BookNewConferenceAsync(It.IsAny())); + + BookingQueueSubscriber.BookingQueueSubscriberFunction.Run(message, new LoggerFake(), new MessageHandlerFactory(MessageHandlersList)); + VideoApiServiceMock.Verify(x => x.BookNewConferenceAsync(It.IsAny()), Times.Once); + } + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/EventHandlerTests.cs b/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/EventHandlerTests.cs deleted file mode 100644 index 9068be56..00000000 --- a/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/EventHandlerTests.cs +++ /dev/null @@ -1,16 +0,0 @@ -using FluentAssertions; -using NUnit.Framework; - -namespace BookingQueueSubscriber.UnitTests -{ - public class EventHandlerTests - { - [Test] - public void should_pass_dummytest() - { - var message = "{'eventType': 'HearingIsReadyForVideoIntegrationEvent'}"; - BookingQueueSubscriberFunction.Run(message, new LoggerFake()); - true.Should().BeTrue(); - } - } -} diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/LoggerFake.cs b/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/LoggerFake.cs index 43b0f96e..58b6440b 100644 --- a/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/LoggerFake.cs +++ b/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/LoggerFake.cs @@ -3,7 +3,7 @@ namespace BookingQueueSubscriber.UnitTests { - public class LoggerFake : Microsoft.Extensions.Logging.ILogger + public class LoggerFake : ILogger { public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/Mappers/HearingToBookConferenceMapperTests.cs b/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/Mappers/HearingToBookConferenceMapperTests.cs new file mode 100644 index 00000000..606d9287 --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/Mappers/HearingToBookConferenceMapperTests.cs @@ -0,0 +1,46 @@ +using System; +using BookingQueueSubscriber.Services.Mappers; +using BookingQueueSubscriber.Services.MessageHandlers.Dtos; +using BookingQueueSubscriber.Services.VideoApi.Contracts; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; + +namespace BookingQueueSubscriber.UnitTests.Mappers +{ + public class HearingToBookConferenceMapperTests + { + [Test] + public void should_map_hearing_dto_to_book_new_conference_request() + { + var mapper = new HearingToBookConferenceMapper(); + var hearingDto = CreateHearingDto(); + var participants = Builder.CreateListOfSize(4) + .All().With(x => x.UserRole = UserRole.Individual.ToString()).Build(); + + var request = mapper.MapToBookNewConferenceRequest(hearingDto, participants); + + request.Should().NotBeNull(); + request.Should().BeEquivalentTo(hearingDto, options => + options + .Excluding(o => o.HearingId) + ); + request.HearingRefId.Should().Be(hearingDto.HearingId); + request.Participants.Count.Should().Be(participants.Count); + } + + private static HearingDto CreateHearingDto() + { + var dto = new HearingDto + { + HearingId = Guid.NewGuid(), + CaseNumber = "Test1234", + CaseType = "Civil Money Claims", + CaseName = "Automated Case vs Humans", + ScheduledDuration = 60, + ScheduledDateTime = DateTime.UtcNow + }; + return dto; + } + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/Mappers/ParticipantToParticipantRequestMapperTests.cs b/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/Mappers/ParticipantToParticipantRequestMapperTests.cs new file mode 100644 index 00000000..6af80aeb --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/Mappers/ParticipantToParticipantRequestMapperTests.cs @@ -0,0 +1,43 @@ +using BookingQueueSubscriber.Services.Mappers; +using BookingQueueSubscriber.Services.MessageHandlers.Dtos; +using BookingQueueSubscriber.Services.VideoApi.Contracts; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; + +namespace BookingQueueSubscriber.UnitTests.Mappers +{ + public class ParticipantToParticipantRequestMapperTests + { + [Test] + public void should_map_participant_dto_to_participant_request() + { + var mapper = new ParticipantToParticipantRequestMapper(); + var participantDto = CreateParticipantDto(); + + var request = mapper.MapToParticipantRequest(participantDto); + + request.Should().NotBeNull(); + request.Should().BeEquivalentTo(participantDto, options => + options + .Excluding(o => o.ParticipantId) + .Excluding(o => o.Fullname) + .Excluding(o => o.UserRole) + .Excluding(o => o.CaseGroupType) + .Excluding(o => o.HearingRole) + .Excluding(o => o.Representee) + ); + request.ParticipantRefId.Should().Be(participantDto.ParticipantId); + request.Name.Should().Be(participantDto.Fullname); + request.UserRole.ToString().Should().Be(participantDto.UserRole); + request.CaseTypeGroup.Should().Be(participantDto.CaseGroupType.ToString()); + } + + private static ParticipantDto CreateParticipantDto() + { + return Builder.CreateNew() + .With(x => x.UserRole = UserRole.Individual.ToString()) + .Build(); + } + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/MessageHandlers/HearingReadyForVideoHandlerTests.cs b/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/MessageHandlers/HearingReadyForVideoHandlerTests.cs new file mode 100644 index 00000000..1ef5c004 --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/MessageHandlers/HearingReadyForVideoHandlerTests.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using BookingQueueSubscriber.Services.IntegrationEvents; +using BookingQueueSubscriber.Services.MessageHandlers; +using BookingQueueSubscriber.Services.MessageHandlers.Dtos; +using BookingQueueSubscriber.Services.VideoApi.Contracts; +using FizzWare.NBuilder; +using Moq; +using NUnit.Framework; + +namespace BookingQueueSubscriber.UnitTests.MessageHandlers +{ + public class HearingReadyForVideoHandlerTests : MessageHandlerTestBase + { + [Test] + public async Task should_call_video_api_when_request_is_valid() + { + var messageHandler = new HearingReadyForVideoHandler(VideoApiServiceMock.Object); + + var integrationEvent = CreateEvent(); + await messageHandler.HandleAsync(integrationEvent); + VideoApiServiceMock.Verify(x => x.BookNewConferenceAsync(It.IsAny()), Times.Once); + } + + private static HearingIsReadyForVideoIntegrationEvent CreateEvent() + { + var hearingDto = new HearingDto + { + HearingId = Guid.NewGuid(), + CaseNumber = "Test1234", + CaseType = "Civil Money Claims", + CaseName = "Automated Case vs Humans", + ScheduledDuration = 60, + ScheduledDateTime = DateTime.UtcNow + }; + var participants = Builder.CreateListOfSize(4) + .All().With(x => x.UserRole = UserRole.Individual.ToString()).Build().ToList(); + + var message = new HearingIsReadyForVideoIntegrationEvent + { + Hearing = hearingDto, + Participants = participants + }; + return message; + } + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/MessageHandlers/MessageHandlerFactoryTests.cs b/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/MessageHandlers/MessageHandlerFactoryTests.cs new file mode 100644 index 00000000..7d10684d --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/MessageHandlers/MessageHandlerFactoryTests.cs @@ -0,0 +1,35 @@ +using System; +using BookingQueueSubscriber.Services.IntegrationEvents; +using BookingQueueSubscriber.Services.MessageHandlers; +using BookingQueueSubscriber.Services.MessageHandlers.Core; +using FluentAssertions; +using NUnit.Framework; + +namespace BookingQueueSubscriber.UnitTests.MessageHandlers +{ + public class MessageHandlerFactoryTests : MessageHandlerTestBase + { + [TestCase(IntegrationEventType.HearingIsReadyForVideo, typeof(HearingReadyForVideoHandler))] + [TestCase(IntegrationEventType.ParticipantAdded, typeof(ParticipantAddedHandler))] + [TestCase(IntegrationEventType.ParticipantRemoved, typeof(ParticipantRemovedHandler))] + [TestCase(IntegrationEventType.HearingDetailsUpdated, typeof(HearingDetailsUpdatedHandler))] + [TestCase(IntegrationEventType.HearingCancelled, typeof(HearingCancelledHandler))] + public void should_return_instance_of_message_handler_for_given_message_type(IntegrationEventType integrationEventType, + Type messageHandlerType) + { + var messageHandlerFactory = new MessageHandlerFactory(MessageHandlersList); + + var handler = messageHandlerFactory.Get(integrationEventType); + handler.Should().BeOfType(messageHandlerType); + } + + [Test] + public void should_load_handler_for_message() + { + var messageHandlerFactory = new MessageHandlerFactory(MessageHandlersList); + var integrationEvent = new HearingIsReadyForVideoIntegrationEvent(); + var handler = messageHandlerFactory.Get(integrationEvent.EventType); + handler.Should().BeOfType(); + } + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/MessageHandlers/MessageHandlerTestBase.cs b/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/MessageHandlers/MessageHandlerTestBase.cs new file mode 100644 index 00000000..13d5e014 --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/MessageHandlers/MessageHandlerTestBase.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using BookingQueueSubscriber.Services; +using BookingQueueSubscriber.Services.MessageHandlers; +using BookingQueueSubscriber.Services.MessageHandlers.Core; +using Moq; +using NUnit.Framework; + +namespace BookingQueueSubscriber.UnitTests.MessageHandlers +{ + public abstract class MessageHandlerTestBase + { + protected List MessageHandlersList { get; set; } + protected Mock VideoApiServiceMock { get; set; } + + [SetUp] + public void Setup() + { + VideoApiServiceMock = new Mock(); + MessageHandlersList = new List + { + new HearingReadyForVideoHandler(VideoApiServiceMock.Object), + new ParticipantAddedHandler(VideoApiServiceMock.Object), + new ParticipantRemovedHandler(VideoApiServiceMock.Object), + new HearingDetailsUpdatedHandler(VideoApiServiceMock.Object), + new HearingCancelledHandler(VideoApiServiceMock.Object) + }; + } + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber.sln b/BookingQueueSubscriber/BookingQueueSubscriber.sln index 4168bad3..26de0e1f 100644 --- a/BookingQueueSubscriber/BookingQueueSubscriber.sln +++ b/BookingQueueSubscriber/BookingQueueSubscriber.sln @@ -9,6 +9,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{4774AAF7 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BookingQueueSubscriber.UnitTests", "BookingQueueSubscriber.UnitTests\BookingQueueSubscriber.UnitTests.csproj", "{F356930E-88E9-4B0E-AA46-179E12BCFCD6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BookingQueueSubscriber.Common", "BookingQueueSubscriber.Common\BookingQueueSubscriber.Common.csproj", "{53EEB5C6-83ED-45B0-A63A-7E5D977B1DB1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BookingQueueSubscriber.Services", "BookingQueueSubscriber.Services\BookingQueueSubscriber.Services.csproj", "{0DC349FE-8D8F-436D-8C95-DE92B8BA35EE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -23,6 +27,14 @@ Global {F356930E-88E9-4B0E-AA46-179E12BCFCD6}.Debug|Any CPU.Build.0 = Debug|Any CPU {F356930E-88E9-4B0E-AA46-179E12BCFCD6}.Release|Any CPU.ActiveCfg = Release|Any CPU {F356930E-88E9-4B0E-AA46-179E12BCFCD6}.Release|Any CPU.Build.0 = Release|Any CPU + {53EEB5C6-83ED-45B0-A63A-7E5D977B1DB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {53EEB5C6-83ED-45B0-A63A-7E5D977B1DB1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {53EEB5C6-83ED-45B0-A63A-7E5D977B1DB1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {53EEB5C6-83ED-45B0-A63A-7E5D977B1DB1}.Release|Any CPU.Build.0 = Release|Any CPU + {0DC349FE-8D8F-436D-8C95-DE92B8BA35EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0DC349FE-8D8F-436D-8C95-DE92B8BA35EE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0DC349FE-8D8F-436D-8C95-DE92B8BA35EE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0DC349FE-8D8F-436D-8C95-DE92B8BA35EE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/BookingQueueSubscriber/BookingQueueSubscriber/BookingQueueSubscriber.csproj b/BookingQueueSubscriber/BookingQueueSubscriber/BookingQueueSubscriber.csproj index 0b1bbd11..2893a01c 100644 --- a/BookingQueueSubscriber/BookingQueueSubscriber/BookingQueueSubscriber.csproj +++ b/BookingQueueSubscriber/BookingQueueSubscriber/BookingQueueSubscriber.csproj @@ -1,19 +1,33 @@ - + netcoreapp2.2 v2 + F6705640-D918-4180-B98A-BAB7ADAA4817 - - + + + + - PreserveNewest + Always - PreserveNewest - Never + Always + + Always + + + + + Always + Always + + + + \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber/BookingQueueSubscriberFunction.cs b/BookingQueueSubscriber/BookingQueueSubscriber/BookingQueueSubscriberFunction.cs index 35dae2f8..ec11dbf7 100644 --- a/BookingQueueSubscriber/BookingQueueSubscriber/BookingQueueSubscriberFunction.cs +++ b/BookingQueueSubscriber/BookingQueueSubscriber/BookingQueueSubscriberFunction.cs @@ -1,15 +1,59 @@ +using System; +using System.Threading.Tasks; +using BookingQueueSubscriber.Common.ApiHelper; +using BookingQueueSubscriber.Services.IntegrationEvents; +using BookingQueueSubscriber.Services.MessageHandlers; +using BookingQueueSubscriber.Services.MessageHandlers.Core; using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Host; using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; +using Willezone.Azure.WebJobs.Extensions.DependencyInjection; namespace BookingQueueSubscriber { public static class BookingQueueSubscriberFunction { [FunctionName("BookingQueueSubscriberFunction")] - public static void Run([ServiceBusTrigger("%queueName%", Connection = "ServiceBusConnection")]string bookingQueueItem, ILogger log) + public static async Task Run([ServiceBusTrigger("%queueName%", Connection = "ServiceBusConnection")] + string bookingQueueItem, + ILogger log, + [Inject]IMessageHandlerFactory messageHandlerFactory) { - log.LogInformation($"C# ServiceBus queue trigger function processed message: {bookingQueueItem}"); + // get handler + var bookingsMessage = + ApiRequestHelper.DeserialiseSnakeCaseJsonToResponse(bookingQueueItem); + log.LogInformation($"event type {bookingsMessage.IntegrationEvent.EventType}"); + var handler = messageHandlerFactory.Get(bookingsMessage.IntegrationEvent.EventType); + log.LogDebug($"using handler {handler.GetType()}"); + + // deserialize into correct contract + var eventRaw = JObject.Parse(bookingQueueItem)["integration_event"].ToString(); + var bodyType = handler.BodyType; + var typedEvent = DeserialiseSnakeCaseJsonToIntegrationEvent(eventRaw, bodyType); + log.LogDebug($"deserialising to {bodyType}"); + log.LogDebug($"{eventRaw}"); + + // execute handler + await handler.HandleAsync(typedEvent).ConfigureAwait(false); + log.LogInformation($"Process message {bookingsMessage.Id} - {bookingsMessage.IntegrationEvent}"); + } + + private static IntegrationEvent DeserialiseSnakeCaseJsonToIntegrationEvent(string response, Type type) + { + var contractResolver = new DefaultContractResolver + { + NamingStrategy = new SnakeCaseNamingStrategy() + }; + + var settings = new JsonSerializerSettings + { + ContractResolver = contractResolver + }; + + var dto = JsonConvert.DeserializeObject(response, type, settings); + return (IntegrationEvent) dto; } } } diff --git a/BookingQueueSubscriber/BookingQueueSubscriber/Startup.cs b/BookingQueueSubscriber/BookingQueueSubscriber/Startup.cs new file mode 100644 index 00000000..7ebe3835 --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber/Startup.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using BookingQueueSubscriber; +using BookingQueueSubscriber.Common.Configuration; +using BookingQueueSubscriber.Common.Security; +using BookingQueueSubscriber.Services; +using BookingQueueSubscriber.Services.MessageHandlers.Core; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Willezone.Azure.WebJobs.Extensions.DependencyInjection; + +[assembly: WebJobsStartup(typeof(Startup))] +namespace BookingQueueSubscriber +{ + internal class Startup : IWebJobsStartup + { + public void Configure(IWebJobsBuilder builder) => + builder.AddDependencyInjection(ConfigureServices); + + private static void ConfigureServices(IServiceCollection services) + { + services.AddMemoryCache(); + RegisterSettings(services); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + RegisterMessageHandlers(services); + } + + private static void RegisterSettings(IServiceCollection services) + { + var configLoader = new ConfigLoader(); + services.Configure(options => configLoader.ConfigRoot.Bind("AzureAd",options)); + services.Configure(options => + configLoader.ConfigRoot.Bind("VhServices", options)); + } + + private static void RegisterMessageHandlers(IServiceCollection serviceCollection) + { + var messageHandlers = GetAllTypesOf().ToList(); + foreach (var messageHandler in messageHandlers) + { + if (messageHandler.IsInterface || messageHandler.IsAbstract) continue; + var serviceType = messageHandler.GetInterfaces()[0]; + serviceCollection.AddScoped(serviceType, messageHandler); + } + } + + private static IEnumerable GetAllTypesOf() + { + var @interface = typeof (T); + return @interface.IsInterface + ? AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(assembly => assembly.GetTypes()) + .Where(type => !type.IsInterface + && !type.IsAbstract + && type.GetInterfaces().Contains(@interface)) + : new Type[] {}; + } + } +} \ No newline at end of file diff --git a/BookingQueueSubscriber/BookingQueueSubscriber/appsettings.json b/BookingQueueSubscriber/BookingQueueSubscriber/appsettings.json new file mode 100644 index 00000000..27bbd500 --- /dev/null +++ b/BookingQueueSubscriber/BookingQueueSubscriber/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index 5a19364a..a54c23ff 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,12 @@ # vh-booking-queue-subscriber + Subscriber for booking queue + +## Download the local.settings.json + +Run the following command into a terminal in the same directory as the BookingQueueSubscriber.csproj file + +```bash +cd BookingQueueSubscriber/BookingQueueSubscriber +func azure functionapp fetch-app-settings vh-booking-queue-subscriber-dev +``` diff --git a/azure-pipelines.yml b/azure-pipelines.yml index d15f9978..478e90b3 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -4,8 +4,8 @@ variables: apiDirectory: 'BookingQueueSubscriber/BookingQueueSubscriber' sonarCloudExtraProperties: | sonar.cs.opencover.reportsPaths=$(Common.TestResultsDirectory)\Coverage\coverage.opencover.xml - - coverletCoverageExclusions: '' + sonar.coverage.exclusions=**/VideoApiService.cs, **/Startup.cs, **/Testing.Common/**, **/BookingQueueSubscriber.Common/ApiHelper/**, **/BookingQueueSubscriber.Common/Helper/Configuration/**, **/BookingQueueSubscriber.Common/Helper/Security/** + coverletCoverageExclusions: '[*]BookingQueueSubscriber.Common.*,[BookingQueueSubscriber.Services]BookingQueueSubscriber.Services.VideoApiService,[*]BookingQueueSubscriber.UnitTests.*,[BookingQueueSubscriber]BookingQueueSubscriber.Startup' integrationTestsAppSettingsTransform: '' dalWorkingDirectory: '' keyVaultName: vhcoreinfrahtdev # Used to get secrets for integration tests diff --git a/run_coverage.bat b/run_coverage.bat new file mode 100644 index 00000000..272f2737 --- /dev/null +++ b/run_coverage.bat @@ -0,0 +1,8 @@ +rmdir /q /s Artifacts + +SET exclude=\"[*]BookingQueueSubscriber.Common.*,[BookingQueueSubscriber.Services]BookingQueueSubscriber.Services.VideoApiService,[*]BookingQueueSubscriber.UnitTests.*,[BookingQueueSubscriber]BookingQueueSubscriber.Startup" +dotnet test --no-build BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/BookingQueueSubscriber.UnitTests.csproj /p:CollectCoverage=true /p:CoverletOutputFormat="\"opencover,cobertura,json,lcov\"" /p:CoverletOutput=../Artifacts/Coverage/ /p:MergeWith='../Artifacts/Coverage/coverage.json' /p:Exclude="${exclude}" + +reportgenerator -reports:Artifacts/Coverage/coverage.opencover.xml -targetDir:Artifacts/Coverage/Report -reporttypes:HtmlInline_AzurePipelines + +"Artifacts/Coverage/Report/index.htm" \ No newline at end of file diff --git a/run_coverage.sh b/run_coverage.sh new file mode 100644 index 00000000..c6bdae31 --- /dev/null +++ b/run_coverage.sh @@ -0,0 +1,9 @@ +#bash +rm -rf Artifacts + +exclude=\"[*]BookingQueueSubscriber.Common.*,[BookingQueueSubscriber.Services]BookingQueueSubscriber.Services.VideoApiService,[*]BookingQueueSubscriber.UnitTests.*,[BookingQueueSubscriber]BookingQueueSubscriber.Startup\" +dotnet test --no-build BookingQueueSubscriber/BookingQueueSubscriber.UnitTests/BookingQueueSubscriber.UnitTests.csproj /p:CollectCoverage=true /p:CoverletOutputFormat="\"opencover,cobertura,json,lcov\"" /p:CoverletOutput=../../Artifacts/Coverage/ /p:MergeWith='../Artifacts/Coverage/coverage.json' /p:Exclude="${exclude}" + +~/.dotnet/tools/reportgenerator -reports:Artifacts/Coverage/coverage.opencover.xml -targetDir:./Artifacts/Coverage/Report -reporttypes:HtmlInline_AzurePipelines + +open ./Artifacts/Coverage/Report/index.htm \ No newline at end of file