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

Commit df2ee5a

Browse files
feat: DTOSS-8747 Integration test (#49)
Signed-off-by: Tyrrellion <[email protected]> Co-authored-by: Tyrrellion <[email protected]>
1 parent 5da9446 commit df2ee5a

File tree

12 files changed

+531
-127
lines changed

12 files changed

+531
-127
lines changed

.gitmodules

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
[submodule "src/dotnet-mesh-client"]
2-
# path = src/Shared/dotnet-mesh-client
32
path = src/dotnet-mesh-client
43
url = https://github.com/NHSDigital/dotnet-mesh-client.git
54
branch = main
5+
[submodule "tests/mesh-sandbox"]
6+
path = tests/mesh-sandbox
7+
url = https://github.com/NHSDigital/mesh-sandbox
8+
branch = main

compose.yaml

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ services:
4848
FileExtractQueueName: "${FileExtractQueueName}"
4949
FileTransformQueueName: "${FileTransformQueueName}"
5050
StaleHours: "${StaleHours}"
51-
MeshApiBaseUrl: "http://mesh_sandbox:80/messageexchange"
51+
MeshApiBaseUrl: "http://mesh-sandbox/messageexchange"
5252
NbssMailboxId: "${NbssMailboxId}"
5353
MeshPassword: "${MeshPassword}"
5454
MeshSharedKey: "${MeshSharedKey}"
@@ -71,6 +71,8 @@ services:
7171
condition: service_healthy
7272
db:
7373
condition: service_healthy
74+
mesh-sandbox:
75+
condition: service_started
7476
volumes:
7577
- mesh-config-data:/azure-functions-host/Secrets/
7678
networks:
@@ -106,10 +108,12 @@ services:
106108
ports:
107109
- "1433:1433"
108110
user: "root"
109-
volumes:
110-
- db-data:/var/opt/mssql
111111
healthcheck:
112-
test: ["CMD-SHELL", "pgrep -f sqlservr || exit 1"]
112+
test:
113+
[
114+
"CMD-SHELL",
115+
"grep -q 'SQL Server is now ready for client connections' /var/opt/mssql/log/errorlog || exit 1",
116+
]
113117
interval: 20s
114118
timeout: 10s
115119
retries: 6
@@ -134,16 +138,34 @@ services:
134138
networks:
135139
- backend
136140

141+
mesh-sandbox:
142+
container_name: mesh-sandbox
143+
build: tests/mesh-sandbox/
144+
ports:
145+
- "8700:80"
146+
deploy:
147+
restart_policy:
148+
condition: on-failure
149+
max_attempts: 3
150+
environment:
151+
- SHARED_KEY=TestKey
152+
- SSL=no
153+
volumes:
154+
# mount a different mailboxes.jsonl to pre created mailboxes
155+
- ../mesh-sandbox/src/mesh_sandbox/store/data/mailboxes.jsonl:/app/mesh_sandbox/store/data/mailboxes.jsonl:ro
156+
- ../mesh-sandbox/src/mesh_sandbox/test_plugin:/app/mesh_sandbox/plugins:ro
157+
# you can mount a directory if you want access the stored messages
158+
- ../mesh-sandbox/messages:/tmp/mesh_store
159+
networks:
160+
- backend
161+
137162
networks:
138163
backend:
139164
name: backend-network
140165
driver: bridge
141166
volumes:
142167
azurite-data:
143168
name: azurite-data
144-
db-data:
145-
name: db-data
146-
driver: local
147169
mesh-config-data:
148170
name: mesh-config-data
149171
driver: local

scripts/tests/unit.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ if [[ "${1:-}" == "--no-build" ]]; then
77
fi
88

99
COVERAGE_DIR="coverage"
10-
TEST_PROJECTS=$(find tests -name '*.csproj')
10+
TEST_PROJECTS=$(find tests -name '*.csproj' -not -name "ServiceLayer.IntegrationTests.csproj")
1111

1212
rm -rf "$COVERAGE_DIR"
1313
mkdir -p "$COVERAGE_DIR"
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using Microsoft.Azure.Functions.Worker;
2+
using Microsoft.Azure.Functions.Worker.Http;
3+
using System.Net;
4+
5+
namespace ServiceLayer.Mesh.Functions;
6+
7+
public static class HealthCheckFunction
8+
{
9+
[Function("HealthCheckFunction")]
10+
public static HttpResponseData Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "health")] HttpRequestData req)
11+
{
12+
return req.CreateResponse(HttpStatusCode.OK);
13+
}
14+
}

src/ServiceLayer.sln

Lines changed: 133 additions & 118 deletions
Large diffs are not rendered by default.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Database
2+
DATABASE_USER=SA
3+
DATABASE_PASSWORD=YourStrong@Passw0rd
4+
DATABASE_NAME=ServiceLayer
5+
DATABASE_HOST=db
6+
DatabaseConnectionString=Server=${DATABASE_HOST};Database=${DATABASE_NAME};User Id=${DATABASE_USER};Password=${DATABASE_PASSWORD};TrustServerCertificate=True
7+
8+
# MESH
9+
MeshSharedKey=TestKey
10+
MeshPassword=password
11+
NbssMailboxId=X26ABC1
12+
MeshApiBaseUrl=http://localhost:8700/messageexchange
13+
14+
# Other
15+
ASPNETCORE_ENVIRONMENT=Development
16+
FileDiscoveryTimerExpression=*/5 * * * * * # Every 5 seconds so that the test doesn't have to wait too long
17+
MeshHandshakeTimerExpression=0 0 0 * * * # Midnight
18+
FileRetryTimerExpression=0 0 * * * *
19+
FileExtractQueueName=file-extract
20+
FileTransformQueueName=file-transform
21+
StaleHours=12
22+
MeshBlobContainerName=incoming-mesh-files
23+
API_PORT=7071
24+
MESH_INGEST_PORT=7072
25+
26+
# Event Grid
27+
EVENT_GRID_TOPIC_URL=https://localhost:60101/api/events
28+
EVENT_GRID_TOPIC_KEY=TheLocal+DevelopmentKey=
29+
30+
# Azurite
31+
AZURITE_ACCOUNT_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw== # Standard default Azurite key
32+
AZURITE_CONNECTION_STRING=DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=${AZURITE_ACCOUNT_KEY};BlobEndpoint=http://azurite:10000/devstoreaccount1;QueueEndpoint=http://azurite:10001/devstoreaccount1;TableEndpoint=http://azurite/devstoreaccount1
33+
AZURITE_BLOB_PORT=10000
34+
AZURITE_QUEUE_PORT=10001
35+
AZURITE_TABLE_PORT=10002
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
using System.Diagnostics;
2+
using System.Net.Http.Headers;
3+
using System.Text.Json;
4+
using System.Text.Json.Serialization;
5+
using Azure.Storage.Blobs;
6+
using Azure.Storage.Blobs.Models;
7+
using Microsoft.EntityFrameworkCore;
8+
using ServiceLayer.Data;
9+
using ServiceLayer.TestUtilities;
10+
11+
namespace ServiceLayer.IntegrationTests;
12+
13+
[CollectionDefinition("DockerComposeCollection")]
14+
public class DockerComposeCollection : ICollectionFixture<DockerComposeFixture>
15+
{
16+
}
17+
18+
[Collection("DockerComposeCollection")]
19+
public class IntegrationTests
20+
{
21+
private readonly string _azuriteAccountKey = Environment.GetEnvironmentVariable("AZURITE_ACCOUNT_KEY")
22+
?? throw new InvalidOperationException($"Environment variable 'AZURITE_ACCOUNT_KEY' is not set.");
23+
private readonly string _azuriteAccountName = Environment.GetEnvironmentVariable("AZURITE_ACCOUNT_NAME")
24+
?? throw new InvalidOperationException($"Environment variable 'AZURITE_ACCOUNT_NAME' is not set.");
25+
private readonly string _azuriteBlobPort = Environment.GetEnvironmentVariable("AZURITE_BLOB_PORT")
26+
?? throw new InvalidOperationException($"Environment variable 'AZURITE_BLOB_PORT' is not set.");
27+
private readonly string _meshIngestPort = Environment.GetEnvironmentVariable("MESH_INGEST_PORT")
28+
?? throw new InvalidOperationException($"Environment variable 'MESH_INGEST_PORT' is not set.");
29+
private readonly string _meshSandboxPort = Environment.GetEnvironmentVariable("MESH_SANDBOX_PORT")
30+
?? throw new InvalidOperationException($"Environment variable 'MESH_SANDBOX_PORT' is not set.");
31+
private readonly string _meshBlobContainerName = Environment.GetEnvironmentVariable("MESH_BLOB_CONTAINER_NAME")
32+
?? throw new InvalidOperationException($"Environment variable 'MESH_BLOB_CONTAINER_NAME' is not set.");
33+
private readonly string _databaseConnectionString = Environment.GetEnvironmentVariable("DATABASE_CONNECTION_STRING")
34+
?? throw new InvalidOperationException($"Environment variable 'DATABASE_CONNECTION_STRING' is not set.");
35+
36+
[Fact]
37+
public async Task FileSentToMeshInbox_FileIsUploadedToBlobContainerAndInsertedIntoDb()
38+
{
39+
// Arrange
40+
await WaitForHealthyService();
41+
42+
// Act
43+
var fileId = await SendFileToMeshInbox("KMK_20250212095121_APPT_87.dat");
44+
45+
// Wait to allow functions to ingest the file
46+
await Task.Delay(45000);
47+
48+
// Assert
49+
Assert.NotNull(fileId);
50+
Assert.True(await WasFileUploadedToBlobContainer(fileId));
51+
Assert.True(await WasFileInsertedIntoDatabase(fileId));
52+
}
53+
54+
private async Task WaitForHealthyService()
55+
{
56+
Console.WriteLine("Waiting for Mesh Ingest Service health check to pass...");
57+
58+
int attemptCounter = 0;
59+
60+
while (attemptCounter < 10)
61+
{
62+
var response = await HttpHelper.SendHttpRequestAsync(HttpMethod.Get, $"http://localhost:{_meshIngestPort}/api/health");
63+
64+
if (response.IsSuccessStatusCode)
65+
{
66+
Console.WriteLine("Mesh Ingest Service is healthy and ready to start ingesting files.");
67+
return;
68+
}
69+
70+
Console.WriteLine("Mesh Ingest Service is unhealthy");
71+
attemptCounter++;
72+
await Task.Delay(5000);
73+
}
74+
75+
Console.WriteLine("Max attempts reached. Mesh Ingest Service is still unhealthy.");
76+
throw new TimeoutException("Timed out waiting on Mesh Ingest Service health check");
77+
}
78+
79+
private async Task<string?> SendFileToMeshInbox(string fileName)
80+
{
81+
byte[] binaryData = await File.ReadAllBytesAsync($"TestData/{fileName}");
82+
var content = new ByteArrayContent(binaryData);
83+
content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
84+
85+
var response = await HttpHelper.SendHttpRequestAsync(
86+
HttpMethod.Post,
87+
$"http://localhost:{_meshSandboxPort}/messageexchange/X26ABC1/outbox",
88+
content,
89+
headers =>
90+
{
91+
headers.Add("Authorization", "NHSMESH X26ABC1:a42f77b9-58de-4b45-b599-2d5bf320b44d:0:202407291437:e3005627136e01706efabcfe72269bc8da3192e90a840ab344ab7f82a39bb5c6");
92+
headers.Add("Mex-Filename", fileName);
93+
headers.Add("Mex-From", "X26ABC1");
94+
headers.Add("Mex-To", "X26ABC1");
95+
headers.Add("Mex-Workflowid", "API-DOCS-TEST");
96+
}
97+
);
98+
99+
string responseBody = await response.Content.ReadAsStringAsync();
100+
101+
var responseObject = JsonSerializer.Deserialize<MeshResponse>(responseBody);
102+
103+
return responseObject?.MessageID;
104+
}
105+
106+
private async Task<bool> WasFileUploadedToBlobContainer(string fileId)
107+
{
108+
var blobConnectionString = $"DefaultEndpointsProtocol=http;AccountName={_azuriteAccountName};AccountKey={_azuriteAccountKey};BlobEndpoint=http://localhost:{_azuriteBlobPort}/{_azuriteAccountName}";
109+
110+
var containerClient = new BlobContainerClient(blobConnectionString, _meshBlobContainerName);
111+
112+
try
113+
{
114+
var blobClient = containerClient.GetBlobClient($"NbssAppointmentEvents/{fileId}");
115+
116+
BlobProperties properties = await blobClient.GetPropertiesAsync();
117+
return true; // If we get properties, the blob exists
118+
}
119+
catch (Exception ex)
120+
{
121+
Console.WriteLine($"An error occurred: {ex.Message}");
122+
return false;
123+
}
124+
}
125+
126+
private async Task<bool> WasFileInsertedIntoDatabase(string fileId)
127+
{
128+
var options = new DbContextOptionsBuilder<ServiceLayerDbContext>()
129+
.UseSqlServer(_databaseConnectionString)
130+
.Options;
131+
132+
var context = new ServiceLayerDbContext(options);
133+
134+
return await context.MeshFiles.AnyAsync(x => x.FileId == fileId);
135+
}
136+
137+
public class MeshResponse
138+
{
139+
[JsonPropertyName("messageID")]
140+
public required string MessageID { get; set; }
141+
}
142+
}
143+
144+
public class DockerComposeFixture : IAsyncLifetime
145+
{
146+
public async Task InitializeAsync()
147+
{
148+
Console.WriteLine("Starting up docker containers...");
149+
150+
var startInfo = new ProcessStartInfo
151+
{
152+
FileName = "docker",
153+
Arguments = "compose --env-file .env.tests up -d svclyr-mesh-ingest mesh-sandbox azurite db db-migrations",
154+
RedirectStandardOutput = true,
155+
RedirectStandardError = true,
156+
UseShellExecute = false,
157+
CreateNoWindow = true
158+
};
159+
160+
using var process = Process.Start(startInfo);
161+
162+
if (process == null)
163+
{
164+
throw new Exception("Failed to start the Docker process.");
165+
}
166+
167+
await process.WaitForExitAsync();
168+
169+
if (process.ExitCode != 0)
170+
{
171+
throw new Exception($"Docker process started but failed, error: {process.StandardError.ReadToEnd()}");
172+
}
173+
174+
Console.WriteLine("Docker containers successfully started");
175+
}
176+
177+
public async Task DisposeAsync()
178+
{
179+
Console.WriteLine("Stopping docker containers...");
180+
181+
var stopInfo = new ProcessStartInfo
182+
{
183+
FileName = "docker",
184+
Arguments = "compose down",
185+
RedirectStandardOutput = true,
186+
RedirectStandardError = true,
187+
UseShellExecute = false,
188+
CreateNoWindow = true
189+
};
190+
191+
using var process = Process.Start(stopInfo);
192+
193+
if (process == null)
194+
{
195+
throw new Exception("Failed to start the Docker process.");
196+
}
197+
198+
await process.WaitForExitAsync();
199+
200+
if (process.ExitCode != 0)
201+
{
202+
throw new Exception($"Docker process started but failed, error: {process.StandardError.ReadToEnd()}");
203+
}
204+
205+
Console.WriteLine("Docker containers stopped");
206+
}
207+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Integration Tests
2+
3+
The integration tests start up docker containers using [compose.yaml](../../compose.yaml) and provide them with the environment variables defined in [.env.tests](.env.tests). Once the containers are up, the integration tests are executed, after the tests have finished, the containers are stopped.
4+
5+
## How to run the tests
6+
7+
To run the integration tests, you also need to pass the following environment variables as arguments when executing `dotnet test`:
8+
9+
- AZURITE_ACCOUNT_KEY
10+
- AZURITE_ACCOUNT_NAME
11+
- AZURITE_BLOB_PORT
12+
- MESH_INGEST_PORT
13+
- MESH_SANDBOX_PORT
14+
- MESH_BLOB_CONTAINER_NAME
15+
- DATABASE_CONNECTION_STRING
16+
17+
E.g.
18+
19+
```sh
20+
dotnet test \
21+
-e AZURITE_ACCOUNT_KEY="Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==" \
22+
-e AZURITE_ACCOUNT_NAME=devstoreaccount1 \
23+
-e AZURITE_BLOB_PORT=10000 \
24+
-e MESH_INGEST_PORT=7072 \
25+
-e MESH_SANDBOX_PORT=8700 \
26+
-e MESH_BLOB_CONTAINER_NAME=incoming-mesh-files \
27+
-e DATABASE_CONNECTION_STRING="Server=localhost;Database=ServiceLayer;User Id=SA;Password=YourStrong@Passw0rd;TrustServerCertificate=True"
28+
```

0 commit comments

Comments
 (0)