Skip to content

Commit

Permalink
fix: Separate out the cache clearing into another class
Browse files Browse the repository at this point in the history
  • Loading branch information
katie-gardner committed Jul 15, 2024
1 parent 4375845 commit 274870e
Show file tree
Hide file tree
Showing 9 changed files with 101 additions and 52 deletions.
22 changes: 3 additions & 19 deletions src/Dfe.PlanTech.AzureFunctions/Functions/QueueReceiver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@
using Microsoft.Extensions.Logging;
using System.Text;
using System.Text.Json;
using Dfe.PlanTech.AzureFunctions.Services;
using Dfe.PlanTech.AzureFunctions.Utils;

namespace Dfe.PlanTech.AzureFunctions;

public class QueueReceiver(
ContentfulOptions contentfulOptions,
CacheRefreshConfiguration cacheRefreshConfiguration,
ILoggerFactory loggerFactory,
CmsDbContext db,
JsonToEntityMappers mappers,
IMessageRetryHandler messageRetryHandler,
IHttpHandler httpHandler)
ICacheHandler cacheHandler)
: BaseFunction(loggerFactory.CreateLogger<QueueReceiver>())
{
/// <summary>
Expand Down Expand Up @@ -86,7 +86,7 @@ private async Task ProcessMessage(ServiceBusReceivedMessage message, ServiceBusM
}

await messageActions.CompleteMessageAsync(message, cancellationToken);
await RequestCacheClear();
await cacheHandler.RequestCacheClear();
}
catch (Exception ex) when (ex is JsonException or CmsEventException)
{
Expand All @@ -109,22 +109,6 @@ private async Task ProcessMessage(ServiceBusReceivedMessage message, ServiceBusM
}
}

/// <summary>
/// Makes a call to the plan tech web app that invalidates the database cache.
/// </summary>
private async Task RequestCacheClear()
{
if (cacheRefreshConfiguration.ApiKeyName is null)
{
Logger.LogError("No Api Key name has been configured but is required for clearing the website cache");
return;
}
var request = new HttpRequestMessage(HttpMethod.Post, cacheRefreshConfiguration.Endpoint);
request.Headers.Add(cacheRefreshConfiguration.ApiKeyName, cacheRefreshConfiguration.ApiKeyValue);

await httpHandler.SendAsync(request);
}

public virtual Task<int> ProcessEntityRemovalEvent(MappedEntity mapped, CancellationToken cancellationToken)
{
if (mapped.ExistingEntity == null)
Expand Down
27 changes: 27 additions & 0 deletions src/Dfe.PlanTech.AzureFunctions/Services/CacheHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Dfe.PlanTech.Domain.Persistence.Models;
using Microsoft.Extensions.Logging;

namespace Dfe.PlanTech.AzureFunctions.Services;

public class CacheHandler(
HttpClient httpClient,
CacheRefreshConfiguration cacheRefreshConfiguration,
ILogger<CacheHandler> logger): ICacheHandler
{
/// <summary>
/// Makes a call to the plan tech web app that invalidates the database cache.
/// </summary>
public async Task RequestCacheClear()
{
if (cacheRefreshConfiguration.ApiKeyName is null)
{
logger.LogError("No Api Key name has been configured but is required for clearing the website cache");
return;
}

var request = new HttpRequestMessage(HttpMethod.Post, cacheRefreshConfiguration.Endpoint);
request.Headers.Add(cacheRefreshConfiguration.ApiKeyName, cacheRefreshConfiguration.ApiKeyValue);

await httpClient.SendAsync(request);
}
}
6 changes: 6 additions & 0 deletions src/Dfe.PlanTech.AzureFunctions/Services/ICacheHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Dfe.PlanTech.AzureFunctions.Services;

public interface ICacheHandler
{
Task RequestCacheClear();
}
4 changes: 3 additions & 1 deletion src/Dfe.PlanTech.AzureFunctions/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Dfe.PlanTech.AzureFunctions.Config;
using Dfe.PlanTech.AzureFunctions.Services;
using Dfe.PlanTech.AzureFunctions.Utils;

namespace Dfe.PlanTech.AzureFunctions
Expand Down Expand Up @@ -49,7 +50,8 @@ public static void ConfigureServices(IServiceCollection services, IConfiguration
configuration["WEBSITE_CACHE_CLEAR_ENDPOINT"],
configuration["WEBSITE_CACHE_CLEAR_APIKEY_NAME"],
configuration["WEBSITE_CACHE_CLEAR_APIKEY_VALUE"]));
services.AddTransient<IHttpHandler, HttpHandler>();
services.AddHttpClient<CacheHandler>();
services.AddTransient<ICacheHandler, CacheHandler>();

services.AddOptions<MessageRetryHandlingOptions>()
.Configure<IConfiguration>((settings, configuration) =>
Expand Down
9 changes: 0 additions & 9 deletions src/Dfe.PlanTech.AzureFunctions/Utils/HttpHandler.cs

This file was deleted.

6 changes: 0 additions & 6 deletions src/Dfe.PlanTech.AzureFunctions/Utils/IHttpHandler.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Dfe.PlanTech.AzureFunctions.Services;
using Dfe.PlanTech.AzureFunctions.Utils;
using NSubstitute.ExceptionExtensions;
using MockQueryable.NSubstitute;
Expand All @@ -24,9 +25,7 @@ public class QueueReceiverTests
{
private const string bodyJsonStr = "{\"metadata\":{\"tags\":[]},\"fields\":{\"internalName\":{\"en-US\":\"TestingQuestion\"},\"text\":{\"en-US\":\"TestingQuestion\"},\"helpText\":{\"en-US\":\"HelpText\"},\"answers\":{\"en-US\":[{\"sys\":{\"type\":\"Link\",\"linkType\":\"Entry\",\"id\":\"4QscetbCYG4MUsGdoDU0C3\"}}]},\"slug\":{\"en-US\":\"testing-slug\"}},\"sys\":{\"type\":\"Entry\",\"id\":\"2VSR0emw0SPy8dlR9XlgfF\",\"space\":{\"sys\":{\"type\":\"Link\",\"linkType\":\"Space\",\"id\":\"py5afvqdlxgo\"}},\"environment\":{\"sys\":{\"id\":\"dev\",\"type\":\"Link\",\"linkType\":\"Environment\"}},\"contentType\":{\"sys\":{\"type\":\"Link\",\"linkType\":\"ContentType\",\"id\":\"question\"}},\"createdBy\":{\"sys\":{\"type\":\"Link\",\"linkType\":\"User\",\"id\":\"5yhMQOCN9P2vGpfjyZKiey\"}},\"updatedBy\":{\"sys\":{\"type\":\"Link\",\"linkType\":\"User\",\"id\":\"4hiJvkyVWdhTt6c4ZoDkMf\"}},\"revision\":13,\"createdAt\":\"2023-12-04T14:36:46.614Z\",\"updatedAt\":\"2023-12-15T16:16:45.034Z\"}}";
private const string _contentId = "2VSR0emw0SPy8dlR9XlgfF";
private const string _refresh_api_key_name = "X-WEBSITE-CACHE-CLEAR-API-KEY";
private const string _refresh_api_key_value = "mock-refresh-api-key";
private const string _refresh_endpoint = "mock-refresh-endpoint";

private readonly QueueReceiver _queueReceiver;

private readonly ILoggerFactory _loggerFactoryMock;
Expand All @@ -35,8 +34,8 @@ public class QueueReceiverTests
private readonly EntityRetriever _entityRetrieverMock;
private readonly JsonToEntityMappers _jsonToEntityMappers;
private readonly IMessageRetryHandler _messageRetryHandlerMock;
private readonly IHttpHandler _httpHandler;
private readonly CacheRefreshConfiguration _cacheRefreshConfiguration;
private readonly ICacheHandler _cacheHandler;


private readonly static QuestionDbEntity _contentComponent = new() { Archived = false, Published = true, Deleted = false, Id = _contentId };
private readonly static QuestionDbEntity _otherContentComponent = new() { Archived = false, Published = true, Deleted = false, Id = "other-content-component" };
Expand All @@ -51,7 +50,7 @@ public QueueReceiverTests()
_loggerFactoryMock = Substitute.For<ILoggerFactory>();
_loggerMock = Substitute.For<ILogger>();
_messageRetryHandlerMock = Substitute.For<IMessageRetryHandler>();
_httpHandler = Substitute.For<IHttpHandler>();
_cacheHandler = Substitute.For<ICacheHandler>();

_loggerFactoryMock.CreateLogger<Arg.AnyType>().Returns((callinfo) =>
{
Expand Down Expand Up @@ -101,8 +100,7 @@ public QueueReceiverTests()
return Task.FromResult(1);
});

_cacheRefreshConfiguration = new CacheRefreshConfiguration(_refresh_endpoint, _refresh_api_key_name, _refresh_api_key_value);
_queueReceiver = new(new ContentfulOptions(true), _cacheRefreshConfiguration, _loggerFactoryMock, _cmsDbContextMock, _jsonToEntityMappers, _messageRetryHandlerMock, _httpHandler);
_queueReceiver = new(new ContentfulOptions(true), _loggerFactoryMock, _cmsDbContextMock, _jsonToEntityMappers, _messageRetryHandlerMock, _cacheHandler);
DbSet<ContentComponentDbEntity> contentComponentsMock = MockContentComponents();
_cmsDbContextMock.ContentComponents = contentComponentsMock;

Expand Down Expand Up @@ -364,7 +362,7 @@ public async Task QueueReceiverDbWriter_Should_ExitEarly_When_Event_Is_Save_And_
.When(mock => mock
.GetExistingDbEntity(Arg.Is<ContentComponentDbEntity>(entity => entity.Id == _contentId), default)
.Returns(_contentComponent));
var queueReceiver = new QueueReceiver(new ContentfulOptions(false), _cacheRefreshConfiguration, _loggerFactoryMock, _cmsDbContextMock, _jsonToEntityMappers, _messageRetryHandlerMock, _httpHandler);
var queueReceiver = new QueueReceiver(new ContentfulOptions(false), _loggerFactoryMock, _cmsDbContextMock, _jsonToEntityMappers, _messageRetryHandlerMock, _cacheHandler);

ServiceBusReceivedMessage serviceBusReceivedMessageMock = Substitute.For<ServiceBusReceivedMessage>();
ServiceBusMessageActions serviceBusMessageActionsMock = Substitute.For<ServiceBusMessageActions>();
Expand All @@ -378,7 +376,7 @@ public async Task QueueReceiverDbWriter_Should_ExitEarly_When_Event_Is_Save_And_
await serviceBusMessageActionsMock.Received().CompleteMessageAsync(Arg.Any<ServiceBusReceivedMessage>(), Arg.Any<CancellationToken>());

await _cmsDbContextMock.Received(0).SaveChangesAsync();
Assert.Empty(_httpHandler.ReceivedCalls());
await _cacheHandler.Received(0).RequestCacheClear();
}

[Theory]
Expand All @@ -390,7 +388,7 @@ public async Task QueueReceiverDbWriter_Should_Save_When_Event_Is_Save_And_Entit
.When(mock => mock
.GetExistingDbEntity(Arg.Is<ContentComponentDbEntity>(entity => entity.Id == _otherContentComponent.Id), default)
.Returns(_otherContentComponent));
var queueReceiver = new QueueReceiver(new ContentfulOptions(false), _cacheRefreshConfiguration, _loggerFactoryMock, _cmsDbContextMock, _jsonToEntityMappers, _messageRetryHandlerMock, _httpHandler);
var queueReceiver = new QueueReceiver(new ContentfulOptions(false), _loggerFactoryMock, _cmsDbContextMock, _jsonToEntityMappers, _messageRetryHandlerMock, _cacheHandler);

ServiceBusReceivedMessage serviceBusReceivedMessageMock = Substitute.For<ServiceBusReceivedMessage>();
ServiceBusMessageActions serviceBusMessageActionsMock = Substitute.For<ServiceBusMessageActions>();
Expand All @@ -404,12 +402,7 @@ public async Task QueueReceiverDbWriter_Should_Save_When_Event_Is_Save_And_Entit
await serviceBusMessageActionsMock.Received().CompleteMessageAsync(Arg.Any<ServiceBusReceivedMessage>(), Arg.Any<CancellationToken>());

await _cmsDbContextMock.Received(1).SaveChangesAsync();
IEnumerable<string>? headerValues;
await _httpHandler.Received(1).SendAsync(Arg.Is<HttpRequestMessage>(
request => request.RequestUri != null
&& request.RequestUri.ToString() == _refresh_endpoint
&& request.Headers.TryGetValues(_refresh_api_key_name, out headerValues)
&& headerValues.Contains(_refresh_api_key_value)));
await _cacheHandler.Received(1).RequestCacheClear();
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Dfe.PlanTech.AzureFunctions.UnitTests.Helpers;

public class MockHttpHandler : HttpMessageHandler
{
public readonly List<HttpRequestMessage> Requests = [];
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
Requests.Add(request);
return await Task.FromResult(new HttpResponseMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Dfe.PlanTech.AzureFunctions.Services;
using Dfe.PlanTech.AzureFunctions.UnitTests.Helpers;
using Dfe.PlanTech.Domain.Persistence.Models;
using Microsoft.Extensions.Logging;
using NSubstitute;

namespace Dfe.PlanTech.AzureFunctions.UnitTests.Services;

public class CacheHandlerTests
{
private const string _refresh_api_key_name = "X-WEBSITE-CACHE-CLEAR-API-KEY";
private const string _refresh_api_key_value = "mock-refresh-api-key";
private const string _refresh_endpoint = "http://mock-refresh-endpoint/";

private readonly CacheRefreshConfiguration _cacheRefreshConfiguration;
private readonly HttpClient _httpClient;
private readonly MockHttpHandler _httpMessageHandler;
private readonly CacheHandler _cacheHandler;
private readonly ILogger<CacheHandler> _logger;

public CacheHandlerTests()
{
_cacheRefreshConfiguration = new CacheRefreshConfiguration(_refresh_endpoint, _refresh_api_key_name, _refresh_api_key_value);
_httpMessageHandler = new MockHttpHandler();
_httpClient = new HttpClient(_httpMessageHandler);
_logger = Substitute.For<ILogger<CacheHandler>>();
_cacheHandler = new CacheHandler(_httpClient, _cacheRefreshConfiguration, _logger);
}

[Fact]
public async Task CacheHandler_Should_Make_Request_With_CorrectHeaders()
{
await _cacheHandler.RequestCacheClear();
Assert.Single(_httpMessageHandler.Requests);
var request = _httpMessageHandler.Requests.First();
Assert.NotNull(request.RequestUri);
Assert.Equal(_refresh_endpoint, request.RequestUri.ToString());
Assert.True(request.Headers.TryGetValues(_refresh_api_key_name, out var headerValues));
Assert.Contains(_refresh_api_key_value, headerValues);
}
}

0 comments on commit 274870e

Please sign in to comment.