Skip to content
This repository was archived by the owner on Jul 28, 2025. It is now read-only.

Commit bfd4eb5

Browse files
committed
feat: adding unit tests and dependancy injection
Signed-off-by: Tyrrellion <samuel.tyrell1@nhs.net>
1 parent 39f44c7 commit bfd4eb5

File tree

7 files changed

+211
-45
lines changed

7 files changed

+211
-45
lines changed

src/ServiceLayer.Mesh/Functions/DiscoveryFunction.cs

Lines changed: 24 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using Azure.Identity;
21
using Azure.Storage.Queues;
32
using Microsoft.Azure.Functions.Worker;
43
using Microsoft.EntityFrameworkCore;
@@ -14,68 +13,57 @@ public class DiscoveryFunction
1413
private readonly ILogger _logger;
1514
private readonly IMeshInboxService _meshInboxService;
1615
private readonly ServiceLayerDbContext _serviceLayerDbContext;
16+
private readonly QueueClient _queueClient;
1717

18-
public DiscoveryFunction(ILoggerFactory loggerFactory, IMeshInboxService meshInboxService, ServiceLayerDbContext serviceLayerDbContext)
18+
public DiscoveryFunction(ILogger<DiscoveryFunction> logger, IMeshInboxService meshInboxService, ServiceLayerDbContext serviceLayerDbContext, QueueClient queueClient)
1919
{
20-
_logger = loggerFactory.CreateLogger<DiscoveryFunction>();
20+
_logger = logger;
2121
_meshInboxService = meshInboxService;
2222
_serviceLayerDbContext = serviceLayerDbContext;
23+
_queueClient = queueClient;
2324
}
2425

2526
[Function("DiscoveryFunction")]
2627
public async Task Run([TimerTrigger("0 */1 * * * *")] TimerInfo myTimer)
2728
{
28-
_logger.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");
29+
_logger.LogInformation($"DiscoveryFunction started at: {DateTime.Now}");
2930

3031
var mailboxId = Environment.GetEnvironmentVariable("MailboxId")
3132
?? throw new InvalidOperationException($"Environment variable 'MailboxId' is not set or is empty.");
3233

3334
var response = await _meshInboxService.GetMessagesAsync(mailboxId);
3435

35-
if (response.Response.Messages.Count() > 500)
36-
{
37-
// TODO: Get next page
38-
// dotnet-mesh-client needs to be updated to support pagination for when inbox containers more than 500 messages
39-
}
36+
_queueClient.CreateIfNotExists();
4037

4138
foreach (var messageId in response.Response.Messages)
4239
{
43-
// Check if message has been seen before
44-
var doesFileIdExist = await _serviceLayerDbContext.MeshFiles.AnyAsync(m => m.FileId == messageId.ToString());
40+
using var transaction = await _serviceLayerDbContext.Database.BeginTransactionAsync();
41+
42+
var existing = await _serviceLayerDbContext.MeshFiles
43+
.AsNoTracking()
44+
.FirstOrDefaultAsync(f => f.FileId == messageId);
4545

46-
if (!doesFileIdExist)
46+
if (existing == null)
4747
{
48-
var meshFile = new MeshFile()
48+
_serviceLayerDbContext.MeshFiles.Add(new MeshFile
4949
{
5050
FileId = messageId,
51-
FileType = "",
51+
FileType = MeshFileType.NbssAppointmentEvents,
5252
MailboxId = mailboxId,
53-
Status = "Discovered"
54-
};
53+
Status = MeshFileStatus.Discovered,
54+
FirstSeenUtc = DateTime.UtcNow,
55+
LastUpdatedUtc = DateTime.UtcNow
56+
});
5557

56-
await _serviceLayerDbContext.MeshFiles.AddAsync(meshFile);
5758
await _serviceLayerDbContext.SaveChangesAsync();
59+
await transaction.CommitAsync();
5860

59-
QueueClient queueClient;
60-
61-
if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development")
62-
{
63-
queueClient = new QueueClient("UseDevelopmentStorage=true", "my-local-queue");
64-
}
65-
else
66-
{
67-
var credential = new ManagedIdentityCredential();
68-
queueClient = new QueueClient(new Uri(Environment.GetEnvironmentVariable("QueueUrl")), credential);
69-
}
70-
71-
queueClient.CreateIfNotExists();
72-
queueClient.SendMessage(messageId);
61+
_queueClient.SendMessage(messageId);
62+
}
63+
else
64+
{
65+
await transaction.RollbackAsync();
7366
}
74-
}
75-
76-
if (myTimer.ScheduleStatus is not null)
77-
{
78-
_logger.LogInformation($"Next timer schedule at: {myTimer.ScheduleStatus.Next}");
7967
}
8068
}
8169
}

src/ServiceLayer.Mesh/Models/MeshFile.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ namespace ServiceLayer.Mesh.Models;
55
public class MeshFile
66
{
77
public required string FileId { get; set; }
8-
public required string FileType { get; set; }
8+
public required MeshFileType FileType { get; set; }
99
public required string MailboxId { get; set; }
10-
public required string Status { get; set; }
10+
public required MeshFileStatus Status { get; set; }
1111
public string? BlobPath { get; set; }
1212
public DateTime FirstSeenUtc { get; set; }
1313
public DateTime LastUpdatedUtc { get; set; }
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace ServiceLayer.Mesh.Models;
2+
3+
public enum MeshFileStatus
4+
{
5+
Discovered,
6+
Extracting,
7+
Extracted,
8+
Transforming,
9+
Transformed,
10+
FailedExtract,
11+
FailedTransform
12+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace ServiceLayer.Mesh.Models;
2+
3+
public enum MeshFileType
4+
{
5+
NbssAppointmentEvents
6+
}

src/ServiceLayer.Mesh/Program.cs

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,53 @@
11
using Microsoft.Azure.Functions.Worker.Builder;
22
using Microsoft.Extensions.Hosting;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Azure.Storage.Queues;
5+
using Azure.Identity;
6+
using Microsoft.EntityFrameworkCore;
37
using NHS.MESH.Client;
48
using ServiceLayer.Mesh.Data;
5-
using Microsoft.EntityFrameworkCore;
6-
using Microsoft.Extensions.DependencyInjection;
79

810
var host = new HostBuilder()
911
.ConfigureFunctionsWebApplication()
1012
.ConfigureServices(services =>
1113
{
14+
// MESH Client config
1215
services
1316
.AddMeshClient(_ => _.MeshApiBaseUrl = Environment.GetEnvironmentVariable("MeshApiBaseUrl"))
1417
.AddMailbox(Environment.GetEnvironmentVariable("BSSMailBox"), new NHS.MESH.Client.Configuration.MailboxConfiguration
1518
{
1619
Password = Environment.GetEnvironmentVariable("MeshPassword"),
1720
SharedKey = Environment.GetEnvironmentVariable("MeshSharedKey"),
18-
//Cert = cert
1921
}).Build();
2022

23+
// EF Core DbContext
2124
services.AddDbContext<ServiceLayerDbContext>(options =>
2225
{
23-
var databaseConnectionString = Environment.GetEnvironmentVariable("DatabaseConnectionString");
24-
if (string.IsNullOrEmpty(databaseConnectionString))
26+
var connectionString = Environment.GetEnvironmentVariable("DatabaseConnectionString");
27+
if (string.IsNullOrEmpty(connectionString))
2528
throw new InvalidOperationException("The connection string has not been initialized.");
2629

27-
options.UseSqlServer(databaseConnectionString);
30+
options.UseSqlServer(connectionString);
31+
});
32+
33+
// Register QueueClient as singleton
34+
services.AddSingleton(provider =>
35+
{
36+
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
37+
var queueUrl = Environment.GetEnvironmentVariable("QueueUrl");
38+
39+
if (string.IsNullOrWhiteSpace(queueUrl))
40+
throw new InvalidOperationException("QueueUrl environment variable is not set.");
41+
42+
if (environment == "Development")
43+
{
44+
return new QueueClient("UseDevelopmentStorage=true", "my-local-queue");
45+
}
46+
else
47+
{
48+
var credential = new ManagedIdentityCredential();
49+
return new QueueClient(new Uri(queueUrl), credential);
50+
}
2851
});
2952
});
3053

@@ -35,5 +58,4 @@
3558
// .ConfigureFunctionsApplicationInsights();
3659

3760
var app = host.Build();
38-
3961
await app.RunAsync();
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
using System;
2+
using System.Linq;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using Microsoft.Extensions.Logging;
6+
using Moq;
7+
using Xunit;
8+
using ServiceLayer.Mesh.Functions;
9+
using ServiceLayer.Mesh.Models;
10+
using ServiceLayer.Mesh.Data;
11+
using Microsoft.EntityFrameworkCore;
12+
using NHS.MESH.Client.Contracts.Services;
13+
using Microsoft.Azure.Functions.Worker;
14+
using NHS.MESH.Client.Models;
15+
using Azure.Storage.Queues;
16+
using Azure.Storage.Queues.Models;
17+
using Azure;
18+
19+
public class DiscoveryFunctionTests
20+
{
21+
private readonly Mock<ILogger<DiscoveryFunction>> _loggerMock;
22+
private readonly Mock<IMeshInboxService> _meshInboxServiceMock;
23+
private readonly ServiceLayerDbContext _dbContext;
24+
private readonly Mock<QueueClient> _queueClientMock;
25+
private readonly DiscoveryFunction _function;
26+
27+
public DiscoveryFunctionTests()
28+
{
29+
_loggerMock = new Mock<ILogger<DiscoveryFunction>>();
30+
_meshInboxServiceMock = new Mock<IMeshInboxService>();
31+
_queueClientMock = new Mock<QueueClient>();
32+
33+
var options = new DbContextOptionsBuilder<ServiceLayerDbContext>()
34+
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
35+
.ConfigureWarnings(warnings =>
36+
warnings.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.InMemoryEventId.TransactionIgnoredWarning))
37+
.Options;
38+
39+
_dbContext = new ServiceLayerDbContext(options);
40+
41+
Environment.SetEnvironmentVariable("MailboxId", "test-mailbox");
42+
Environment.SetEnvironmentVariable("QueueUrl", "https://fakestorageaccount.queue.core.windows.net/testqueue");
43+
44+
_function = new DiscoveryFunction(
45+
_loggerMock.Object,
46+
_meshInboxServiceMock.Object,
47+
_dbContext,
48+
_queueClientMock.Object
49+
);
50+
}
51+
52+
[Fact]
53+
public async Task Run_AddsNewMessageToDbAndQueue()
54+
{
55+
// Arrange
56+
var testMessageId = "test-message-123";
57+
58+
_meshInboxServiceMock.Setup(s => s.GetMessagesAsync("test-mailbox"))
59+
.ReturnsAsync(new MeshResponse<CheckInboxResponse>
60+
{
61+
Response = new CheckInboxResponse { Messages = new[] { testMessageId } }
62+
});
63+
64+
_queueClientMock
65+
.Setup(q => q.SendMessage(It.IsAny<string>(), null, null, It.IsAny<CancellationToken>()))
66+
.Returns(Response.FromValue<SendReceipt>(null, Mock.Of<Response>()));
67+
68+
// Act
69+
await _function.Run(null);
70+
71+
// Assert
72+
var meshFile = _dbContext.MeshFiles.FirstOrDefault(f => f.FileId == testMessageId);
73+
Assert.NotNull(meshFile);
74+
Assert.Equal(MeshFileStatus.Discovered, meshFile.Status);
75+
Assert.Equal("test-mailbox", meshFile.MailboxId);
76+
77+
_queueClientMock.Verify(q => q.SendMessage(It.IsAny<string>()), Times.Once);
78+
}
79+
80+
[Fact]
81+
public async Task Run_DoesNotAddDuplicateMessageOrQueueIt()
82+
{
83+
// Arrange
84+
var duplicateMessageId = "existing-message";
85+
_dbContext.MeshFiles.Add(new MeshFile
86+
{
87+
FileId = duplicateMessageId,
88+
FileType = MeshFileType.NbssAppointmentEvents,
89+
MailboxId = "test-mailbox",
90+
Status = MeshFileStatus.Discovered,
91+
FirstSeenUtc = DateTime.UtcNow,
92+
LastUpdatedUtc = DateTime.UtcNow
93+
});
94+
await _dbContext.SaveChangesAsync();
95+
96+
_meshInboxServiceMock.Setup(s => s.GetMessagesAsync("test-mailbox"))
97+
.ReturnsAsync(new MeshResponse<CheckInboxResponse>
98+
{
99+
Response = new CheckInboxResponse { Messages = new[] { duplicateMessageId } }
100+
});
101+
102+
// Act
103+
await _function.Run(null);
104+
105+
// Assert
106+
var count = _dbContext.MeshFiles.Count(f => f.FileId == duplicateMessageId);
107+
Assert.Equal(1, count);
108+
109+
_queueClientMock.Verify(q => q.SendMessage(It.IsAny<string>()), Times.Never);
110+
}
111+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<IsPackable>false</IsPackable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="coverlet.collector" Version="6.0.4" />
12+
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.4" />
13+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
14+
<PackageReference Include="Moq" Version="4.20.72" />
15+
<PackageReference Include="xunit" Version="2.9.3" />
16+
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2" />
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<ProjectReference Include="../../src/ServiceLayer.Mesh/ServiceLayer.Mesh.csproj" />
21+
</ItemGroup>
22+
23+
<ItemGroup>
24+
<Using Include="Xunit" />
25+
</ItemGroup>
26+
27+
</Project>

0 commit comments

Comments
 (0)