diff --git a/.github/workflows/build-test-http.yml b/.github/workflows/build-test-http.yml index cc2c859..a870ef9 100644 --- a/.github/workflows/build-test-http.yml +++ b/.github/workflows/build-test-http.yml @@ -18,6 +18,5 @@ jobs: with: project_name: DfE.CoreLibs.Http project_path: src/DfE.CoreLibs.Http - run_tests: false secrets: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} \ No newline at end of file diff --git a/DfE.CoreLibs.sln b/DfE.CoreLibs.sln index 6993e7e..cf1c169 100644 --- a/DfE.CoreLibs.sln +++ b/DfE.CoreLibs.sln @@ -17,7 +17,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DfE.CoreLibs.AsyncProcessin EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{3F89DCAD-8EC7-41ED-A08F-A9EFAE263EB4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DfE.CoreLibs.Utilities.Tests", "src\Tests\DfE.CoreLibs.Utilities.Tests\DfE.CoreLibs.Utilities.Tests.csproj", "{07AE8F19-9566-4F0C-92E6-0A2BF122DCC9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DfE.CoreLibs.Utilities.Tests", "src\Tests\DfE.CoreLibs.Utilities.Tests\DfE.CoreLibs.Utilities.Tests.csproj", "{07AE8F19-9566-4F0C-92E6-0A2BF122DCC9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DfE.CoreLibs.Http.Tests", "src\Tests\DfE.CoreLibs.Http.Tests\DfE.CoreLibs.Http.Tests.csproj", "{69529D73-DD34-43A2-9D06-F3783F68F05C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -53,12 +55,17 @@ Global {07AE8F19-9566-4F0C-92E6-0A2BF122DCC9}.Debug|Any CPU.Build.0 = Debug|Any CPU {07AE8F19-9566-4F0C-92E6-0A2BF122DCC9}.Release|Any CPU.ActiveCfg = Release|Any CPU {07AE8F19-9566-4F0C-92E6-0A2BF122DCC9}.Release|Any CPU.Build.0 = Release|Any CPU + {69529D73-DD34-43A2-9D06-F3783F68F05C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {69529D73-DD34-43A2-9D06-F3783F68F05C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {69529D73-DD34-43A2-9D06-F3783F68F05C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {69529D73-DD34-43A2-9D06-F3783F68F05C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {07AE8F19-9566-4F0C-92E6-0A2BF122DCC9} = {3F89DCAD-8EC7-41ED-A08F-A9EFAE263EB4} + {69529D73-DD34-43A2-9D06-F3783F68F05C} = {3F89DCAD-8EC7-41ED-A08F-A9EFAE263EB4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {01D11FBC-6C66-43E4-8F1F-46B105EDD95C} diff --git a/src/DfE.CoreLibs.Http/Middlewares/CorrelationId/Keys.cs b/src/DfE.CoreLibs.Http/Middlewares/CorrelationId/Keys.cs index 370313b..f85373b 100644 --- a/src/DfE.CoreLibs.Http/Middlewares/CorrelationId/Keys.cs +++ b/src/DfE.CoreLibs.Http/Middlewares/CorrelationId/Keys.cs @@ -1,4 +1,8 @@ -namespace DfE.CoreLibs.Http.Middlewares.CorrelationId; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("DfE.CoreLibs.Http.Tests")] + +namespace DfE.CoreLibs.Http.Middlewares.CorrelationId; /// /// The keys used by the correlation id middleware. diff --git a/src/DfE.CoreLibs.Http/ReleaseNotes.md b/src/DfE.CoreLibs.Http/ReleaseNotes.md deleted file mode 100644 index eabfb04..0000000 --- a/src/DfE.CoreLibs.Http/ReleaseNotes.md +++ /dev/null @@ -1,15 +0,0 @@ -# 2.0.2 -Fixed passing in an empty GUID in the x-correlationId header causing an exception. Now if an empty GUID is detected a bad request will be returned. -If an empty guid is returned, the content of the response returned (along with the 400 status code) will be - -`{ StatusCode = 400, Message = Bad Request. x-correlationId header cannot be an empty GUID }` - -# 2.0.1 -Fix to the log output so that `x-correlationId [guid]` is now `x-correlationId: [guid] - -# 2.0.0 -Introduced logging scope output. -Changed to only support GUID correlation ids (breaking changes) - -# 1.0.0 -Initial version \ No newline at end of file diff --git a/src/DfE.CoreLibs.Testing/AutoFixture/Customizations/HttpContextCustomization.cs b/src/DfE.CoreLibs.Testing/AutoFixture/Customizations/HttpContextCustomization.cs new file mode 100644 index 0000000..d65f4bc --- /dev/null +++ b/src/DfE.CoreLibs.Testing/AutoFixture/Customizations/HttpContextCustomization.cs @@ -0,0 +1,13 @@ +using AutoFixture; +using Microsoft.AspNetCore.Http; + +namespace DfE.CoreLibs.Testing.AutoFixture.Customizations +{ + public class HttpContextCustomization : ICustomization + { + public void Customize(IFixture fixture) + { + fixture.Register(() => new DefaultHttpContext()); + } + } +} \ No newline at end of file diff --git a/src/Tests/DfE.CoreLibs.Http.Tests/DfE.CoreLibs.Http.Tests.csproj b/src/Tests/DfE.CoreLibs.Http.Tests/DfE.CoreLibs.Http.Tests.csproj new file mode 100644 index 0000000..4a57d81 --- /dev/null +++ b/src/Tests/DfE.CoreLibs.Http.Tests/DfE.CoreLibs.Http.Tests.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Tests/DfE.CoreLibs.Http.Tests/Middlewares/CorrelationIdMiddlewareTests.cs b/src/Tests/DfE.CoreLibs.Http.Tests/Middlewares/CorrelationIdMiddlewareTests.cs new file mode 100644 index 0000000..f43b423 --- /dev/null +++ b/src/Tests/DfE.CoreLibs.Http.Tests/Middlewares/CorrelationIdMiddlewareTests.cs @@ -0,0 +1,119 @@ +using AutoFixture; +using AutoFixture.Xunit2; +using DfE.CoreLibs.Http.Interfaces; +using DfE.CoreLibs.Http.Middlewares.CorrelationId; +using DfE.CoreLibs.Testing.AutoFixture.Attributes; +using DfE.CoreLibs.Testing.AutoFixture.Customizations; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using NSubstitute; +using System.Net; + +namespace DfE.CoreLibs.Http.Tests.Middlewares +{ + public class CorrelationIdMiddlewareTests + { + private readonly RequestDelegate _nextDelegate; + private readonly ILogger _logger; + private readonly ICorrelationContext _correlationContext; + private readonly CorrelationIdMiddleware _middleware; + + public CorrelationIdMiddlewareTests() + { + _nextDelegate = Substitute.For(); + _logger = Substitute.For>(); + _correlationContext = Substitute.For(); + _middleware = new CorrelationIdMiddleware(_nextDelegate, _logger); + } + + [Theory] + [CustomAutoData(typeof(HttpContextCustomization))] + public async Task Invoke_ShouldSetNewCorrelationId_WhenHeaderNotPresent( + HttpContext context) + { + // Arrange + context.Request.Headers.Remove(Keys.HeaderKey); + context.Response.Body = new System.IO.MemoryStream(); + + _nextDelegate.Invoke(context).Returns(Task.CompletedTask); + + // Act + await _middleware.Invoke(context, _correlationContext); + + // Assert + Assert.True(Guid.TryParse(context.Response.Headers[Keys.HeaderKey], out var correlationId)); + Assert.NotEqual(Guid.Empty, correlationId); + _correlationContext.Received(1).SetContext(Arg.Any()); + } + + [Theory] + [CustomAutoData(typeof(HttpContextCustomization))] + public async Task Invoke_ShouldRetainExistingCorrelationId_WhenHeaderPresent([Frozen] IFixture fixture, + DefaultHttpContext context) + { + // Arrange + var existingCorrelationId = fixture.Create(); + context.Request.Headers[Keys.HeaderKey] = existingCorrelationId.ToString(); + context.Response.Body = new System.IO.MemoryStream(); + + _nextDelegate.Invoke(context).Returns(Task.CompletedTask); + + // Act + await _middleware.Invoke(context, _correlationContext); + + // Assert + Assert.Equal(existingCorrelationId.ToString(), context.Response.Headers[Keys.HeaderKey]); + _correlationContext.Received(1).SetContext(existingCorrelationId); + } + + [Theory] + [CustomAutoData(typeof(HttpContextCustomization))] + public async Task Invoke_ShouldReturnBadRequest_WhenCorrelationIdIsEmpty(DefaultHttpContext context) + { + // Arrange + context.Request.Headers[Keys.HeaderKey] = Guid.Empty.ToString(); + context.Response.Body = new System.IO.MemoryStream(); + + _nextDelegate.Invoke(context).Returns(Task.CompletedTask); + + // Act + await _middleware.Invoke(context, _correlationContext); + + // Assert + Assert.Equal((int)HttpStatusCode.BadRequest, context.Response.StatusCode); + Assert.Contains("Bad Request", ReadResponseBody(context)); + _correlationContext.DidNotReceive().SetContext(Arg.Any()); + } + + [Theory] + [CustomAutoData(typeof(HttpContextCustomization))] + public async Task Invoke_ShouldLogNewGuidWarning_WhenHeaderCannotBeParsed(DefaultHttpContext context) + { + // Arrange + context.Request.Headers[Keys.HeaderKey] = "invalid-guid"; + context.Response.Body = new System.IO.MemoryStream(); + + _nextDelegate.Invoke(context).Returns(Task.CompletedTask); + + // Act + await _middleware.Invoke(context, _correlationContext); + + // Assert + _logger.Received(1).Log( + LogLevel.Warning, + Arg.Any(), + Arg.Is(v => v.ToString().Contains("Detected header x-correlationId, but value cannot be parsed to a GUID")), + Arg.Any(), + Arg.Any>()!); + + _correlationContext.Received(1).SetContext(Arg.Any()); + } + + private static string ReadResponseBody(HttpContext context) + { + context.Response.Body.Seek(0, System.IO.SeekOrigin.Begin); + using var reader = new System.IO.StreamReader(context.Response.Body); + return reader.ReadToEnd(); + } + } +}