From 1de8b1378d0dcc34705c64bd276ae550660f283a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 07:39:58 +0000 Subject: [PATCH 1/5] Add comprehensive tests for CorrelationPropagator, PlatformMeters, and MessageTracer Tests cover: - CorrelationPropagator: inject/extract trace context with/without Activity - PlatformMeters: all 5 Record* methods with MeterListener verification - MessageTracer: all 4 Trace* stage methods plus CompleteSuccess/CompleteFailed Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: devstress <30769729+devstress@users.noreply.github.com> --- .../UnitTests/CorrelationPropagatorTests.cs | 322 ++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 EnterpriseIntegrationPlatform/tests/UnitTests/CorrelationPropagatorTests.cs diff --git a/EnterpriseIntegrationPlatform/tests/UnitTests/CorrelationPropagatorTests.cs b/EnterpriseIntegrationPlatform/tests/UnitTests/CorrelationPropagatorTests.cs new file mode 100644 index 0000000..be56c2e --- /dev/null +++ b/EnterpriseIntegrationPlatform/tests/UnitTests/CorrelationPropagatorTests.cs @@ -0,0 +1,322 @@ +using System.Diagnostics; +using System.Diagnostics.Metrics; +using EnterpriseIntegrationPlatform.Contracts; +using EnterpriseIntegrationPlatform.Observability; +using NUnit.Framework; + +namespace EnterpriseIntegrationPlatform.Tests.Unit; + +[TestFixture] +public class CorrelationPropagatorTests +{ + private ActivityListener _listener = null!; + + [SetUp] + public void SetUp() + { + _listener = new ActivityListener + { + ShouldListenTo = _ => true, + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, + }; + ActivitySource.AddActivityListener(_listener); + } + + [TearDown] + public void TearDown() + { + _listener.Dispose(); + Activity.Current = null; + } + + [Test] + public void InjectTraceContext_NoCurrentActivity_ReturnsEnvelopeUnchanged() + { + Activity.Current = null; + var envelope = IntegrationEnvelope.Create("payload", "src", "TestMsg"); + + var result = CorrelationPropagator.InjectTraceContext(envelope); + + Assert.That(result, Is.SameAs(envelope)); + Assert.That(result.Metadata.ContainsKey(MessageHeaders.TraceId), Is.False); + Assert.That(result.Metadata.ContainsKey(MessageHeaders.SpanId), Is.False); + } + + [Test] + public void InjectTraceContext_WithCurrentActivity_SetsTraceAndSpanHeaders() + { + using var activity = DiagnosticsConfig.ActivitySource.StartActivity("test-inject")!; + var envelope = IntegrationEnvelope.Create("payload", "src", "TestMsg"); + + var result = CorrelationPropagator.InjectTraceContext(envelope); + + Assert.That(result.Metadata[MessageHeaders.TraceId], Is.EqualTo(activity.TraceId.ToString())); + Assert.That(result.Metadata[MessageHeaders.SpanId], Is.EqualTo(activity.SpanId.ToString())); + } + + [Test] + public void ExtractAndStart_WithTraceHeaders_CreatesChildActivity() + { + using var parent = DiagnosticsConfig.ActivitySource.StartActivity("parent")!; + var envelope = IntegrationEnvelope.Create("payload", "src", "TestMsg"); + envelope.Metadata[MessageHeaders.TraceId] = parent.TraceId.ToString(); + envelope.Metadata[MessageHeaders.SpanId] = parent.SpanId.ToString(); + parent.Stop(); + Activity.Current = null; + + using var child = CorrelationPropagator.ExtractAndStart(envelope, "child-stage"); + + Assert.That(child, Is.Not.Null); + Assert.That(child!.ParentId, Does.Contain(parent.TraceId.ToString())); + Assert.That(child.OperationName, Is.EqualTo("child-stage")); + } + + [Test] + public void ExtractAndStart_WithoutTraceHeaders_StartsActivityWithoutParent() + { + Activity.Current = null; + var envelope = IntegrationEnvelope.Create("payload", "src", "TestMsg"); + + using var activity = CorrelationPropagator.ExtractAndStart(envelope, "no-parent-stage"); + + Assert.That(activity, Is.Not.Null); + Assert.That(activity!.OperationName, Is.EqualTo("no-parent-stage")); + } + + [Test] + public void ExtractAndStart_DefaultKind_IsConsumer() + { + var envelope = IntegrationEnvelope.Create("payload", "src", "TestMsg"); + + using var activity = CorrelationPropagator.ExtractAndStart(envelope, "consumer-stage"); + + Assert.That(activity, Is.Not.Null); + Assert.That(activity!.Kind, Is.EqualTo(ActivityKind.Consumer)); + } + + [Test] + public void ExtractAndStart_ExplicitKind_IsRespected() + { + var envelope = IntegrationEnvelope.Create("payload", "src", "TestMsg"); + + using var activity = CorrelationPropagator.ExtractAndStart(envelope, "producer-stage", ActivityKind.Producer); + + Assert.That(activity, Is.Not.Null); + Assert.That(activity!.Kind, Is.EqualTo(ActivityKind.Producer)); + } +} + +[TestFixture] +public class PlatformMetersTests +{ + private MeterListener _meterListener = null!; + private readonly List<(string InstrumentName, long Value, KeyValuePair[] Tags)> _longMeasurements = new(); + private readonly List<(string InstrumentName, double Value, KeyValuePair[] Tags)> _doubleMeasurements = new(); + + [SetUp] + public void SetUp() + { + _longMeasurements.Clear(); + _doubleMeasurements.Clear(); + + _meterListener = new MeterListener(); + _meterListener.InstrumentPublished = (instrument, listener) => + { + if (instrument.Meter.Name == DiagnosticsConfig.ServiceName) + { + listener.EnableMeasurementEvents(instrument); + } + }; + + _meterListener.SetMeasurementEventCallback( + (instrument, measurement, tags, _) => + { + _longMeasurements.Add((instrument.Name, measurement, tags.ToArray())); + }); + + _meterListener.SetMeasurementEventCallback( + (instrument, measurement, tags, _) => + { + _doubleMeasurements.Add((instrument.Name, measurement, tags.ToArray())); + }); + + _meterListener.Start(); + } + + [TearDown] + public void TearDown() + { + _meterListener.Dispose(); + } + + [Test] + public void RecordReceived_IncrementsReceivedCounterAndInFlight() + { + PlatformMeters.RecordReceived("OrderCreated", "OrderService"); + _meterListener.RecordObservableInstruments(); + + var received = _longMeasurements.Where(m => m.InstrumentName == "eip.messages.received").ToList(); + var inFlight = _longMeasurements.Where(m => m.InstrumentName == "eip.messages.in_flight").ToList(); + + Assert.That(received, Has.Count.GreaterThanOrEqualTo(1)); + Assert.That(received.Any(m => m.Value == 1), Is.True); + Assert.That(inFlight, Has.Count.GreaterThanOrEqualTo(1)); + Assert.That(inFlight.Any(m => m.Value == 1), Is.True); + } + + [Test] + public void RecordProcessed_IncrementsProcessedAndDecrementsInFlightAndRecordsDuration() + { + PlatformMeters.RecordProcessed("OrderCreated", 42.5); + _meterListener.RecordObservableInstruments(); + + var processed = _longMeasurements.Where(m => m.InstrumentName == "eip.messages.processed").ToList(); + var inFlight = _longMeasurements.Where(m => m.InstrumentName == "eip.messages.in_flight").ToList(); + var duration = _doubleMeasurements.Where(m => m.InstrumentName == "eip.messages.processing_duration").ToList(); + + Assert.That(processed, Has.Count.GreaterThanOrEqualTo(1)); + Assert.That(processed.Any(m => m.Value == 1), Is.True); + Assert.That(inFlight, Has.Count.GreaterThanOrEqualTo(1)); + Assert.That(inFlight.Any(m => m.Value == -1), Is.True); + Assert.That(duration, Has.Count.GreaterThanOrEqualTo(1)); + Assert.That(duration.Any(m => m.Value == 42.5), Is.True); + } + + [Test] + public void RecordFailed_IncrementsFailedAndDecrementsInFlight() + { + PlatformMeters.RecordFailed("OrderCreated"); + _meterListener.RecordObservableInstruments(); + + var failed = _longMeasurements.Where(m => m.InstrumentName == "eip.messages.failed").ToList(); + var inFlight = _longMeasurements.Where(m => m.InstrumentName == "eip.messages.in_flight").ToList(); + + Assert.That(failed, Has.Count.GreaterThanOrEqualTo(1)); + Assert.That(failed.Any(m => m.Value == 1), Is.True); + Assert.That(inFlight, Has.Count.GreaterThanOrEqualTo(1)); + Assert.That(inFlight.Any(m => m.Value == -1), Is.True); + } + + [Test] + public void RecordDeadLettered_IncrementsDeadLetteredCounter() + { + PlatformMeters.RecordDeadLettered("OrderCreated"); + _meterListener.RecordObservableInstruments(); + + var deadLettered = _longMeasurements.Where(m => m.InstrumentName == "eip.messages.dead_lettered").ToList(); + + Assert.That(deadLettered, Has.Count.GreaterThanOrEqualTo(1)); + Assert.That(deadLettered.Any(m => m.Value == 1), Is.True); + } + + [Test] + public void RecordRetry_IncrementsRetriedCounterWithRetryCountTag() + { + PlatformMeters.RecordRetry("OrderCreated", 3); + _meterListener.RecordObservableInstruments(); + + var retried = _longMeasurements.Where(m => m.InstrumentName == "eip.messages.retried").ToList(); + + Assert.That(retried, Has.Count.GreaterThanOrEqualTo(1)); + Assert.That(retried.Any(m => m.Value == 1), Is.True); + var retryTag = retried.First(m => m.Value == 1).Tags.FirstOrDefault(t => t.Key == "eip.retry.count"); + Assert.That(retryTag.Value, Is.EqualTo(3)); + } +} + +[TestFixture] +public class MessageTracerTests +{ + private ActivityListener _listener = null!; + + [SetUp] + public void SetUp() + { + _listener = new ActivityListener + { + ShouldListenTo = _ => true, + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, + }; + ActivitySource.AddActivityListener(_listener); + } + + [TearDown] + public void TearDown() + { + _listener.Dispose(); + Activity.Current = null; + } + + [Test] + public void TraceIngestion_StartsActivityWithIngestionStage() + { + var envelope = IntegrationEnvelope.Create("payload", "OrderService", "OrderCreated"); + + using var activity = MessageTracer.TraceIngestion(envelope); + + Assert.That(activity, Is.Not.Null); + Assert.That(activity!.OperationName, Is.EqualTo(MessageTracer.StageIngestion)); + Assert.That(activity.GetTagItem(PlatformActivitySource.TagStage), Is.EqualTo("Ingestion")); + Assert.That(activity.GetTagItem(PlatformActivitySource.TagDeliveryStatus), Is.EqualTo("InFlight")); + } + + [Test] + public void TraceRouting_StartsActivityWithRoutingStage() + { + var envelope = IntegrationEnvelope.Create("payload", "OrderService", "OrderCreated"); + + using var activity = MessageTracer.TraceRouting(envelope); + + Assert.That(activity, Is.Not.Null); + Assert.That(activity!.OperationName, Is.EqualTo(MessageTracer.StageRouting)); + Assert.That(activity.GetTagItem(PlatformActivitySource.TagStage), Is.EqualTo("Routing")); + } + + [Test] + public void TraceTransformation_StartsActivityWithTransformationStage() + { + var envelope = IntegrationEnvelope.Create("payload", "OrderService", "OrderCreated"); + + using var activity = MessageTracer.TraceTransformation(envelope); + + Assert.That(activity, Is.Not.Null); + Assert.That(activity!.OperationName, Is.EqualTo(MessageTracer.StageTransformation)); + Assert.That(activity.GetTagItem(PlatformActivitySource.TagStage), Is.EqualTo("Transformation")); + } + + [Test] + public void TraceDelivery_StartsActivityWithDeliveryStage() + { + var envelope = IntegrationEnvelope.Create("payload", "OrderService", "OrderCreated"); + + using var activity = MessageTracer.TraceDelivery(envelope); + + Assert.That(activity, Is.Not.Null); + Assert.That(activity!.OperationName, Is.EqualTo(MessageTracer.StageDelivery)); + Assert.That(activity.GetTagItem(PlatformActivitySource.TagStage), Is.EqualTo("Delivery")); + } + + [Test] + public void CompleteSuccess_SetsOkStatusAndDeliveredTag() + { + using var activity = DiagnosticsConfig.ActivitySource.StartActivity("test-success")!; + + MessageTracer.CompleteSuccess(activity, "OrderCreated", 100.0); + + Assert.That(activity.Status, Is.EqualTo(ActivityStatusCode.Ok)); + Assert.That(activity.GetTagItem(PlatformActivitySource.TagDeliveryStatus), Is.EqualTo("Delivered")); + } + + [Test] + public void CompleteFailed_SetsErrorStatusAndRecordsException() + { + using var activity = DiagnosticsConfig.ActivitySource.StartActivity("test-fail")!; + var ex = new InvalidOperationException("something broke"); + + MessageTracer.CompleteFailed(activity, "OrderCreated", ex); + + Assert.That(activity.Status, Is.EqualTo(ActivityStatusCode.Error)); + Assert.That(activity.GetTagItem(PlatformActivitySource.TagDeliveryStatus), Is.EqualTo("Failed")); + Assert.That(activity.Events.Count(e => e.Name == "exception"), Is.EqualTo(1)); + } +} From 153972f1ca38b814d8ebb234d29cac2df5125b00 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 07:46:57 +0000 Subject: [PATCH 2/5] Add comprehensive unit tests for activity services and model records Tests cover DefaultMessageValidationService, DefaultCompensationActivityService, MessageValidationResult, MessageHistoryEntry, and MessageHistoryStatus enum. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: devstress <30769729+devstress@users.noreply.github.com> --- .../tests/UnitTests/ActivityServiceTests.cs | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 EnterpriseIntegrationPlatform/tests/UnitTests/ActivityServiceTests.cs diff --git a/EnterpriseIntegrationPlatform/tests/UnitTests/ActivityServiceTests.cs b/EnterpriseIntegrationPlatform/tests/UnitTests/ActivityServiceTests.cs new file mode 100644 index 0000000..18836d5 --- /dev/null +++ b/EnterpriseIntegrationPlatform/tests/UnitTests/ActivityServiceTests.cs @@ -0,0 +1,168 @@ +using EnterpriseIntegrationPlatform.Activities; +using EnterpriseIntegrationPlatform.Contracts; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using NSubstitute; +using NUnit.Framework; + +namespace EnterpriseIntegrationPlatform.Tests.Unit; + +[TestFixture] +public sealed class DefaultMessageValidationServiceTests +{ + private DefaultMessageValidationService _sut = null!; + + [SetUp] + public void SetUp() + { + _sut = new DefaultMessageValidationService(); + } + + [Test] + public async Task ValidateAsync_EmptyMessageType_ReturnsFailure() + { + var result = await _sut.ValidateAsync("", """{"id":1}"""); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Reason, Is.EqualTo("Message type must not be empty.")); + } + + [Test] + public async Task ValidateAsync_WhitespaceMessageType_ReturnsFailure() + { + var result = await _sut.ValidateAsync(" ", """{"id":1}"""); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Reason, Is.EqualTo("Message type must not be empty.")); + } + + [Test] + public async Task ValidateAsync_EmptyPayload_ReturnsFailure() + { + var result = await _sut.ValidateAsync("OrderCreated", ""); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Reason, Is.EqualTo("Payload must not be empty.")); + } + + [Test] + public async Task ValidateAsync_WhitespacePayload_ReturnsFailure() + { + var result = await _sut.ValidateAsync("OrderCreated", " "); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Reason, Is.EqualTo("Payload must not be empty.")); + } + + [Test] + public async Task ValidateAsync_NonJsonPayload_ReturnsFailure() + { + var result = await _sut.ValidateAsync("OrderCreated", "plain text"); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Reason, Is.EqualTo("Payload is not valid JSON.")); + } + + [Test] + public async Task ValidateAsync_ValidJsonObject_ReturnsSuccess() + { + var result = await _sut.ValidateAsync("OrderCreated", """{"id":1}"""); + + Assert.That(result.IsValid, Is.True); + Assert.That(result.Reason, Is.Null); + } + + [Test] + public async Task ValidateAsync_ValidJsonArray_ReturnsSuccess() + { + var result = await _sut.ValidateAsync("OrderCreated", "[1,2,3]"); + + Assert.That(result.IsValid, Is.True); + Assert.That(result.Reason, Is.Null); + } + + [Test] + public async Task ValidateAsync_WhitespaceBeforeJson_ReturnsSuccess() + { + var result = await _sut.ValidateAsync("OrderCreated", """ {"id":1}"""); + + Assert.That(result.IsValid, Is.True); + Assert.That(result.Reason, Is.Null); + } +} + +[TestFixture] +public sealed class DefaultCompensationActivityServiceTests +{ + private DefaultCompensationActivityService _sut = null!; + + [SetUp] + public void SetUp() + { + var logger = NullLogger.Instance; + _sut = new DefaultCompensationActivityService(logger); + } + + [Test] + public async Task CompensateAsync_ValidInput_ReturnsTrue() + { + var result = await _sut.CompensateAsync(Guid.NewGuid(), "DebitAccount"); + + Assert.That(result, Is.True); + } + + [Test] + public async Task CompensateAsync_AnyStepName_ReturnsTrue() + { + var result = await _sut.CompensateAsync(Guid.NewGuid(), "AnyArbitraryStep"); + + Assert.That(result, Is.True); + } +} + +[TestFixture] +public sealed class MessageValidationResultTests +{ + [Test] + public void Success_IsValid_ReturnsTrue() + { + var result = MessageValidationResult.Success; + + Assert.That(result.IsValid, Is.True); + Assert.That(result.Reason, Is.Null); + } + + [Test] + public void Failure_IsNotValid_ReturnsFalseWithReason() + { + var result = MessageValidationResult.Failure("Something went wrong."); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Reason, Is.EqualTo("Something went wrong.")); + } +} + +[TestFixture] +public sealed class MessageHistoryEntryTests +{ + [Test] + public void Constructor_SetsProperties() + { + var timestamp = DateTimeOffset.UtcNow; + var entry = new MessageHistoryEntry("Validate", timestamp, MessageHistoryStatus.Completed, "All good"); + + Assert.That(entry.ActivityName, Is.EqualTo("Validate")); + Assert.That(entry.Timestamp, Is.EqualTo(timestamp)); + Assert.That(entry.Status, Is.EqualTo(MessageHistoryStatus.Completed)); + Assert.That(entry.Detail, Is.EqualTo("All good")); + } + + [Test] + public void Status_Enum_HasExpectedValues() + { + Assert.That((int)MessageHistoryStatus.Completed, Is.EqualTo(0)); + Assert.That((int)MessageHistoryStatus.Skipped, Is.EqualTo(1)); + Assert.That((int)MessageHistoryStatus.Failed, Is.EqualTo(2)); + Assert.That((int)MessageHistoryStatus.InProgress, Is.EqualTo(3)); + } +} From 83a522b720cf79352553bcdf9c927bcb2af74b57 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 07:58:24 +0000 Subject: [PATCH 3/5] Add tests for HttpEnrichmentSource and DatabaseEnrichmentSource Add 12 comprehensive unit tests covering: - HttpEnrichmentSource: constructor guards, successful fetch, URL placeholder replacement, and non-success status code handling - DatabaseEnrichmentSource: constructor guards, no-rows returns null, and row-with-data returns populated JsonObject Uses lightweight ADO.NET test doubles for database tests since DbCommand non-virtual members prevent NSubstitute mocking. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: devstress <30769729+devstress@users.noreply.github.com> --- .../HttpDatabaseEnrichmentSourceTests.cs | 332 ++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100644 EnterpriseIntegrationPlatform/tests/UnitTests/HttpDatabaseEnrichmentSourceTests.cs diff --git a/EnterpriseIntegrationPlatform/tests/UnitTests/HttpDatabaseEnrichmentSourceTests.cs b/EnterpriseIntegrationPlatform/tests/UnitTests/HttpDatabaseEnrichmentSourceTests.cs new file mode 100644 index 0000000..9668eee --- /dev/null +++ b/EnterpriseIntegrationPlatform/tests/UnitTests/HttpDatabaseEnrichmentSourceTests.cs @@ -0,0 +1,332 @@ +using System.Data; +using System.Data.Common; +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Text.Json.Nodes; +using EnterpriseIntegrationPlatform.Processing.Transform; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using NSubstitute; +using NUnit.Framework; + +namespace EnterpriseIntegrationPlatform.Tests.Unit; + +// ───── HttpEnrichmentSourceTests ───── + +[TestFixture] +public sealed class HttpEnrichmentSourceTests +{ + private sealed class MockHandler : DelegatingHandler + { + public Func ResponseFactory { get; set; } = + _ => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{}") + }; + + protected override Task SendAsync( + HttpRequestMessage request, CancellationToken cancellationToken) + => Task.FromResult(ResponseFactory(request)); + } + + private MockHandler _handler = null!; + private IHttpClientFactory _httpClientFactory = null!; + private ContentEnricherOptions _options = null!; + private ILogger _logger = null!; + + [SetUp] + public void SetUp() + { + _handler = new MockHandler(); + var client = new HttpClient(_handler); + _httpClientFactory = Substitute.For(); + _httpClientFactory.CreateClient("ContentEnricher").Returns(client); + + _options = new ContentEnricherOptions + { + EndpointUrlTemplate = "https://api.example.com/customers/{key}", + LookupKeyPath = "customerId", + MergeTargetPath = "customer" + }; + + _logger = NullLogger.Instance; + } + + [TearDown] + public void TearDown() + { + _handler.Dispose(); + } + + [Test] + public void Constructor_NullHttpClientFactory_ThrowsArgumentNullException() + { + Assert.That( + () => new HttpEnrichmentSource(null!, _options, _logger), + Throws.ArgumentNullException); + } + + [Test] + public void Constructor_NullOptions_ThrowsArgumentNullException() + { + Assert.That( + () => new HttpEnrichmentSource(_httpClientFactory, null!, _logger), + Throws.ArgumentNullException); + } + + [Test] + public void Constructor_NullLogger_ThrowsArgumentNullException() + { + Assert.That( + () => new HttpEnrichmentSource(_httpClientFactory, _options, null!), + Throws.ArgumentNullException); + } + + [Test] + public async Task FetchAsync_SuccessfulResponse_ReturnsJsonNode() + { + _handler.ResponseFactory = _ => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("""{"name":"test"}""") + }; + + var sut = new HttpEnrichmentSource(_httpClientFactory, _options, _logger); + var result = await sut.FetchAsync("42"); + + Assert.That(result, Is.Not.Null); + Assert.That(result!["name"]!.GetValue(), Is.EqualTo("test")); + } + + [Test] + public async Task FetchAsync_ReplacesKeyPlaceholderInUrl() + { + Uri? capturedUri = null; + _handler.ResponseFactory = req => + { + capturedUri = req.RequestUri; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{}") + }; + }; + + var sut = new HttpEnrichmentSource(_httpClientFactory, _options, _logger); + await sut.FetchAsync("abc-123"); + + Assert.That(capturedUri, Is.Not.Null); + Assert.That(capturedUri!.ToString(), Does.Contain("abc-123")); + Assert.That(capturedUri.ToString(), Does.Not.Contain("{key}")); + } + + [Test] + public void FetchAsync_NonSuccessStatusCode_ThrowsHttpRequestException() + { + _handler.ResponseFactory = _ => new HttpResponseMessage(HttpStatusCode.InternalServerError); + + var sut = new HttpEnrichmentSource(_httpClientFactory, _options, _logger); + + Assert.That( + async () => await sut.FetchAsync("fail"), + Throws.TypeOf()); + } +} + +// ───── DatabaseEnrichmentSourceTests ───── + +[TestFixture] +public sealed class DatabaseEnrichmentSourceTests +{ + private const string Sql = "SELECT name, tier FROM customers WHERE id = @key"; + private const string ParamName = "@key"; + + private ILogger _logger = null!; + + [SetUp] + public void SetUp() + { + _logger = NullLogger.Instance; + } + + [Test] + public void Constructor_NullConnectionFactory_ThrowsArgumentNullException() + { + Assert.That( + () => new DatabaseEnrichmentSource(null!, Sql, ParamName, _logger), + Throws.ArgumentNullException); + } + + [Test] + public void Constructor_EmptySql_ThrowsArgumentException() + { + Func factory = () => Substitute.For(); + Assert.That( + () => new DatabaseEnrichmentSource(factory, "", ParamName, _logger), + Throws.TypeOf()); + } + + [Test] + public void Constructor_EmptyParameterName_ThrowsArgumentException() + { + Func factory = () => Substitute.For(); + Assert.That( + () => new DatabaseEnrichmentSource(factory, Sql, "", _logger), + Throws.TypeOf()); + } + + [Test] + public void Constructor_NullLogger_ThrowsArgumentNullException() + { + Func factory = () => Substitute.For(); + Assert.That( + () => new DatabaseEnrichmentSource(factory, Sql, ParamName, null!), + Throws.ArgumentNullException); + } + + [Test] + public async Task FetchAsync_NoRows_ReturnsNull() + { + var fakeReader = new FakeDbDataReader(columns: [], rows: []); + var fakeCommand = new FakeDbCommand(fakeReader); + var fakeConnection = new FakeDbConnection(fakeCommand); + + var sut = new DatabaseEnrichmentSource(() => fakeConnection, Sql, ParamName, _logger); + var result = await sut.FetchAsync("missing-key"); + + Assert.That(result, Is.Null); + } + + [Test] + public async Task FetchAsync_WithRow_ReturnsJsonObject() + { + var columns = new[] { "name", "tier" }; + var rows = new[] { new object[] { "Alice", "gold" } }; + var fakeReader = new FakeDbDataReader(columns, rows); + var fakeCommand = new FakeDbCommand(fakeReader); + var fakeConnection = new FakeDbConnection(fakeCommand); + + var sut = new DatabaseEnrichmentSource(() => fakeConnection, Sql, ParamName, _logger); + var result = await sut.FetchAsync("C-42"); + + Assert.That(result, Is.Not.Null); + var obj = result as JsonObject; + Assert.That(obj, Is.Not.Null); + Assert.That(obj!["name"]!.GetValue(), Is.EqualTo("Alice")); + Assert.That(obj["tier"]!.GetValue(), Is.EqualTo("gold")); + } + + // ───── Lightweight ADO.NET test doubles ───── + + private sealed class FakeDbConnection(DbCommand command) : DbConnection + { + [AllowNull] + public override string ConnectionString { get; set; } = string.Empty; + public override string Database => string.Empty; + public override string DataSource => string.Empty; + public override string ServerVersion => string.Empty; + public override ConnectionState State => ConnectionState.Open; + public override void ChangeDatabase(string databaseName) { } + public override void Close() { } + public override void Open() { } + protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) => null!; + protected override DbCommand CreateDbCommand() => command; + } + + private sealed class FakeDbCommand(DbDataReader reader) : DbCommand + { + private readonly FakeDbParameterCollection _parameters = new(); + [AllowNull] + public override string CommandText { get; set; } = string.Empty; + public override int CommandTimeout { get; set; } + public override CommandType CommandType { get; set; } + public override bool DesignTimeVisible { get; set; } + public override UpdateRowSource UpdatedRowSource { get; set; } + protected override DbConnection? DbConnection { get; set; } + protected override DbParameterCollection DbParameterCollection => _parameters; + protected override DbTransaction? DbTransaction { get; set; } + public override void Cancel() { } + public override int ExecuteNonQuery() => 0; + public override object? ExecuteScalar() => null; + public override void Prepare() { } + protected override DbParameter CreateDbParameter() => new FakeDbParameter(); + protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) => reader; + } + + private sealed class FakeDbParameter : DbParameter + { + public override DbType DbType { get; set; } + public override ParameterDirection Direction { get; set; } + public override bool IsNullable { get; set; } + [AllowNull] + public override string ParameterName { get; set; } = string.Empty; + public override int Size { get; set; } + [AllowNull] + public override string SourceColumn { get; set; } = string.Empty; + public override bool SourceColumnNullMapping { get; set; } + public override object? Value { get; set; } + public override void ResetDbType() { } + } + + private sealed class FakeDbParameterCollection : DbParameterCollection + { + private readonly List _items = []; + public override int Count => _items.Count; + public override object SyncRoot => _items; + public override int Add(object value) { _items.Add((DbParameter)value); return _items.Count - 1; } + public override void AddRange(Array values) { } + public override void Clear() => _items.Clear(); + public override bool Contains(object value) => _items.Contains((DbParameter)value); + public override bool Contains(string value) => _items.Exists(p => p.ParameterName == value); + public override void CopyTo(Array array, int index) { } + public override IEnumerator GetEnumerator() => _items.GetEnumerator(); + public override int IndexOf(object value) => _items.IndexOf((DbParameter)value); + public override int IndexOf(string parameterName) => _items.FindIndex(p => p.ParameterName == parameterName); + public override void Insert(int index, object value) => _items.Insert(index, (DbParameter)value); + public override void Remove(object value) => _items.Remove((DbParameter)value); + public override void RemoveAt(int index) => _items.RemoveAt(index); + public override void RemoveAt(string parameterName) => _items.RemoveAll(p => p.ParameterName == parameterName); + protected override DbParameter GetParameter(int index) => _items[index]; + protected override DbParameter GetParameter(string parameterName) => _items.First(p => p.ParameterName == parameterName); + protected override void SetParameter(int index, DbParameter value) => _items[index] = value; + protected override void SetParameter(string parameterName, DbParameter value) + { + var idx = IndexOf(parameterName); + if (idx >= 0) _items[idx] = value; + } + } + + private sealed class FakeDbDataReader(string[] columns, object[][] rows) : DbDataReader + { + private int _currentRow = -1; + public override int FieldCount => columns.Length; + public override int RecordsAffected => 0; + public override bool HasRows => rows.Length > 0; + public override bool IsClosed => false; + public override int Depth => 0; + public override object this[int ordinal] => rows[_currentRow][ordinal]; + public override object this[string name] => this[GetOrdinal(name)]; + public override bool Read() => ++_currentRow < rows.Length; + public override string GetName(int ordinal) => columns[ordinal]; + public override int GetOrdinal(string name) => Array.IndexOf(columns, name); + public override object GetValue(int ordinal) => rows[_currentRow][ordinal]; + public override bool IsDBNull(int ordinal) => rows[_currentRow][ordinal] is null || rows[_currentRow][ordinal] == DBNull.Value; + public override bool GetBoolean(int ordinal) => (bool)GetValue(ordinal); + public override byte GetByte(int ordinal) => (byte)GetValue(ordinal); + public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int bufferOffset, int length) => 0; + public override char GetChar(int ordinal) => (char)GetValue(ordinal); + public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int bufferOffset, int length) => 0; + public override string GetDataTypeName(int ordinal) => "string"; + public override DateTime GetDateTime(int ordinal) => (DateTime)GetValue(ordinal); + public override decimal GetDecimal(int ordinal) => (decimal)GetValue(ordinal); + public override double GetDouble(int ordinal) => (double)GetValue(ordinal); + public override Type GetFieldType(int ordinal) => GetValue(ordinal).GetType(); + public override float GetFloat(int ordinal) => (float)GetValue(ordinal); + public override Guid GetGuid(int ordinal) => (Guid)GetValue(ordinal); + public override short GetInt16(int ordinal) => (short)GetValue(ordinal); + public override int GetInt32(int ordinal) => (int)GetValue(ordinal); + public override long GetInt64(int ordinal) => (long)GetValue(ordinal); + public override string GetString(int ordinal) => (string)GetValue(ordinal); + public override int GetValues(object[] values) { Array.Copy(rows[_currentRow], values, Math.Min(values.Length, FieldCount)); return Math.Min(values.Length, FieldCount); } + public override bool NextResult() => false; + public override IEnumerator GetEnumerator() => Enumerable.Empty().GetEnumerator(); + } +} From 05cbb6f6aa7d3cdd3d7a510bcb0c58d55541a66c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 08:02:17 +0000 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20Phase=2035=20=E2=80=94=20Observabil?= =?UTF-8?q?ity,=20Validation=20&=20Enrichment=20Test=20Hardening=20(Chunks?= =?UTF-8?q?=20350=E2=80=93352)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 43 new unit tests. UnitTests total: 1886 (was 1843). Chunk 350: CorrelationPropagator, PlatformMeters & MessageTracer Tests (17 tests) Chunk 351: Activity Service & Message Validation Tests (14 tests) Chunk 352: HttpEnrichmentSource & DatabaseEnrichmentSource Tests (12 tests) Agent-Logs-Url: https://github.com/devstress/My3DLearning/sessions/12fd4d83-5324-48f8-99d7-9365b3f569dc Co-authored-by: devstress <30769729+devstress@users.noreply.github.com> --- .../rules/completion-log.md | 40 +++++++++++++++++++ .../rules/milestones.md | 33 +++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/EnterpriseIntegrationPlatform/rules/completion-log.md b/EnterpriseIntegrationPlatform/rules/completion-log.md index a0d330a..0824b91 100644 --- a/EnterpriseIntegrationPlatform/rules/completion-log.md +++ b/EnterpriseIntegrationPlatform/rules/completion-log.md @@ -4,6 +4,46 @@ Detailed record of completed chunks, files created/modified, and notes. See `milestones.md` for current phase status and next chunk. +## Chunk 352 — HttpEnrichmentSource & DatabaseEnrichmentSource Tests + +- **Date**: 2026-04-09 +- **Phase**: 35 — Observability, Validation & Enrichment Test Hardening +- **Status**: done +- **Goal**: Dedicated unit tests for HttpEnrichmentSource (6 tests) and DatabaseEnrichmentSource (6 tests) covering constructor validation, successful enrichment, URL key substitution, HTTP error handling, empty DB results, and column-to-JSON mapping. +- **Files created**: + - `tests/UnitTests/HttpDatabaseEnrichmentSourceTests.cs` — 12 tests: HttpEnrichmentSource (constructor null factory/options/logger, FetchAsync success returns JsonNode, FetchAsync replaces key placeholder, FetchAsync non-success throws), DatabaseEnrichmentSource (constructor null factory, empty SQL, empty param, null logger, FetchAsync no rows returns null, FetchAsync with row returns JsonObject) +- **Notes**: + - HttpEnrichmentSource tests use a lightweight MockHandler (DelegatingHandler subclass) for HTTP interception + - DatabaseEnrichmentSource tests use concrete ADO.NET test doubles (InMemoryDbConnection/Command/Reader) since DbCommand.Parameters is non-virtual + - All 12 tests pass. UnitTests total: 1886 (cumulative with chunks 350–351) + +## Chunk 351 — Activity Service & Message Validation Tests + +- **Date**: 2026-04-09 +- **Phase**: 35 — Observability, Validation & Enrichment Test Hardening +- **Status**: done +- **Goal**: Dedicated unit tests for DefaultMessageValidationService (8 tests), DefaultCompensationActivityService (2 tests), MessageValidationResult factory methods (2 tests), and MessageHistoryEntry record (2 tests). +- **Files created**: + - `tests/UnitTests/ActivityServiceTests.cs` — 14 tests across 4 fixtures: DefaultMessageValidationServiceTests (empty/whitespace type, empty/whitespace payload, non-JSON, valid JSON object, valid JSON array, whitespace-prefixed JSON), DefaultCompensationActivityServiceTests (valid input returns true, any step returns true), MessageValidationResultTests (Success/Failure factories), MessageHistoryEntryTests (constructor properties, enum values) +- **Notes**: + - DefaultMessageValidationService validates: non-empty message type, non-empty payload, payload starts with `{` or `[` + - DefaultCompensationActivityService always returns true (logs compensation; real implementations override) + - All 14 tests pass + +## Chunk 350 — CorrelationPropagator, PlatformMeters & MessageTracer Tests + +- **Date**: 2026-04-09 +- **Phase**: 35 — Observability, Validation & Enrichment Test Hardening +- **Status**: done +- **Goal**: Dedicated unit tests for CorrelationPropagator (6 tests), PlatformMeters (5 tests), and MessageTracer (6 tests) covering trace context injection/extraction, metric recording via MeterListener, and message lifecycle tracing. +- **Files created**: + - `tests/UnitTests/CorrelationPropagatorTests.cs` — 17 tests across 3 fixtures: CorrelationPropagatorTests (inject without Activity, inject with Activity sets TraceId/SpanId, extract with valid headers, extract without headers, extract default kind, extract explicit kind), PlatformMetersTests (RecordReceived, RecordProcessed, RecordFailed, RecordDeadLettered, RecordRetry via MeterListener), MessageTracerTests (TraceIngestion/Routing/Transformation/Delivery, CompleteSuccess sets Ok status, CompleteFailed sets Error status) +- **Notes**: + - CorrelationPropagator tests use ActivityListener to capture Activities from DiagnosticsConfig.ActivitySource + - PlatformMeters tests use System.Diagnostics.Metrics.MeterListener to verify counter/histogram/gauge recordings + - MessageTracer tests verify that trace stages create properly tagged Activity spans + - All 17 tests pass + ## Chunk 344 — AI & Remaining DI Tests - **Date**: 2026-04-09 diff --git a/EnterpriseIntegrationPlatform/rules/milestones.md b/EnterpriseIntegrationPlatform/rules/milestones.md index a73b016..63c4b58 100644 --- a/EnterpriseIntegrationPlatform/rules/milestones.md +++ b/EnterpriseIntegrationPlatform/rules/milestones.md @@ -248,3 +248,36 @@ Tests verify every `Add*` method registers the correct service types in the cont ### Next Chunk Phase 34 is complete. No remaining chunks. + +--- + +## Phase 35 — Observability, Validation & Enrichment Test Hardening + +> **Origin:** Audit revealed that CorrelationPropagator, PlatformMeters, and MessageTracer +> (core observability infrastructure used by every service) had **zero dedicated unit tests**. +> DefaultMessageValidationService and DefaultCompensationActivityService (core activity services) +> were also untested. HttpEnrichmentSource and DatabaseEnrichmentSource (external enrichment +> patterns) lacked tests. This phase closes these test gaps. + +| Chunk | Description | Status | +|-------|-------------|--------| +| 350 | **CorrelationPropagator, PlatformMeters & MessageTracer Tests** — see `rules/completion-log.md` | `done` | +| 351 | **Activity Service & Message Validation Tests** — see `rules/completion-log.md` | `done` | +| 352 | **HttpEnrichmentSource & DatabaseEnrichmentSource Tests** — see `rules/completion-log.md` | `done` | + +### Summary + +Phase 35 complete — 3 chunks (350–352). 43 new unit tests. UnitTests total: 1886 (was 1843). +CorrelationPropagator now has 6 tests covering trace context injection/extraction. +PlatformMeters has 5 tests verifying counter/histogram/gauge recordings via MeterListener. +MessageTracer has 6 tests covering all 4 trace stage methods plus success/failure completion. +DefaultMessageValidationService has 8 tests covering all validation paths. +DefaultCompensationActivityService has 2 tests. MessageValidationResult factory methods tested. +MessageHistoryEntry record and enum values tested. HttpEnrichmentSource has 6 tests with mock +HTTP handler. DatabaseEnrichmentSource has 6 tests with mock ADO.NET infrastructure. + +--- + +### Next Chunk + +Phase 35 is complete. No remaining chunks. From b547112e18da2c2723a6741d0463a83a948c62a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:00:55 +0000 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20Phase=2036=20=E2=80=94=20Auth,=20Ob?= =?UTF-8?q?servability=20&=20System=20Management=20Test=20Hardening=20(Chu?= =?UTF-8?q?nks=20360=E2=80=93362,=20+33=20tests)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chunk 360: ApiKeyAuthenticationHandler — 10 tests Chunk 361: LokiObservabilityEventLog — 11 tests Chunk 362: ControlBusPublisher & DlqManagementService — 12 tests UnitTests: 1886 → 1919. Build: 0 errors. Agent-Logs-Url: https://github.com/devstress/My3DLearning/sessions/7937c5ff-5a24-41bd-ad89-325a0dc71764 Co-authored-by: devstress <30769729+devstress@users.noreply.github.com> --- .../rules/completion-log.md | 43 +++ .../rules/milestones.md | 28 +- .../src/Admin.Api/Admin.Api.csproj | 3 + .../ApiKeyAuthenticationHandlerTests.cs | 197 ++++++++++++ .../UnitTests/ControlBusPublisherTests.cs | 252 +++++++++++++++ .../LokiObservabilityEventLogTests.cs | 293 ++++++++++++++++++ .../tests/UnitTests/UnitTests.csproj | 3 + 7 files changed, 818 insertions(+), 1 deletion(-) create mode 100644 EnterpriseIntegrationPlatform/tests/UnitTests/ApiKeyAuthenticationHandlerTests.cs create mode 100644 EnterpriseIntegrationPlatform/tests/UnitTests/ControlBusPublisherTests.cs create mode 100644 EnterpriseIntegrationPlatform/tests/UnitTests/LokiObservabilityEventLogTests.cs diff --git a/EnterpriseIntegrationPlatform/rules/completion-log.md b/EnterpriseIntegrationPlatform/rules/completion-log.md index 0824b91..d86c8cf 100644 --- a/EnterpriseIntegrationPlatform/rules/completion-log.md +++ b/EnterpriseIntegrationPlatform/rules/completion-log.md @@ -4,6 +4,49 @@ Detailed record of completed chunks, files created/modified, and notes. See `milestones.md` for current phase status and next chunk. +## Chunk 362 — ControlBusPublisher & DlqManagementService Tests + +- **Date**: 2026-04-09 +- **Phase**: 36 — Auth, Observability & System Management Test Hardening +- **Status**: done +- **Goal**: Dedicated unit tests for ControlBusPublisher (10 tests) covering publish success/failure, command intent, topic routing, argument validation, subscribe registration. DlqManagementService (2 tests) covering replay delegation and filter passthrough. +- **Files created**: + - `tests/UnitTests/ControlBusPublisherTests.cs` — 12 tests across 2 fixtures: ControlBusPublisherTests (PublishCommandAsync success/topic/intent/failure/null command/empty type, SubscribeAsync registration/null handler/empty type, constructor null producer/consumer), DlqManagementServiceTests (ResubmitAsync delegates to replayer, passes filter) +- **Notes**: + - ControlBusPublisher.PublishCommandAsync sets MessageIntent.Command on the envelope + - NSubstitute When..Do pattern used to capture generic envelope argument + - All 12 tests pass. UnitTests total: 1919 (cumulative with chunks 360–361) + +## Chunk 361 — LokiObservabilityEventLog Tests + +- **Date**: 2026-04-09 +- **Phase**: 36 — Auth, Observability & System Management Test Hardening +- **Status**: done +- **Goal**: Dedicated unit tests for LokiObservabilityEventLog covering RecordAsync (Loki push + in-memory fallback), GetByCorrelationId (Loki query + fallback), GetByBusinessKey (fallback + case-insensitive matching). +- **Files created**: + - `tests/UnitTests/LokiObservabilityEventLogTests.cs` — 11 tests: RecordAsync (posts to push endpoint, Loki unavailable doesn't throw, always stores in fallback, correct content type, payload contains correlation_id, multiple events stored), GetByCorrelationId (Loki returns results, no matching returns empty), GetByBusinessKey (Loki unavailable uses fallback, case-insensitive fallback) +- **Notes**: + - Uses MockLokiHandler (DelegatingHandler) for HTTP interception with separate push/query status codes + - Uses reflection to clear the static FallbackStore between tests for isolation + - Loki query_range response format: values are [timestamp_string, json_string] — log line is a JSON-escaped string + - All 11 tests pass + +## Chunk 360 — ApiKeyAuthenticationHandler Tests + +- **Date**: 2026-04-09 +- **Phase**: 36 — Auth, Observability & System Management Test Hardening +- **Status**: done +- **Goal**: Dedicated unit tests for the security-critical ApiKeyAuthenticationHandler covering all authentication paths. +- **Files created/modified**: + - `tests/UnitTests/ApiKeyAuthenticationHandlerTests.cs` — 10 tests: missing header fails, invalid key fails, valid key succeeds, valid key sets Admin role claim, sets Name claim, sets apikey_prefix claim with masking, multiple configured keys accepts any, case-sensitive comparison rejects wrong case, empty configured keys rejects all, short key masks entirely + - `src/Admin.Api/Admin.Api.csproj` — added `` + - `tests/UnitTests/UnitTests.csproj` — added `` +- **Notes**: + - ApiKeyAuthenticationHandler is `internal sealed` — InternalsVisibleTo enables direct test instantiation + - Tests use `AuthenticationHandler.InitializeAsync` + `AuthenticateAsync` pattern with `DefaultHttpContext` + - MaskKey masks keys longer than 4 chars to `first4****`, short keys to `****` + - All 10 tests pass + ## Chunk 352 — HttpEnrichmentSource & DatabaseEnrichmentSource Tests - **Date**: 2026-04-09 diff --git a/EnterpriseIntegrationPlatform/rules/milestones.md b/EnterpriseIntegrationPlatform/rules/milestones.md index 63c4b58..b647ed8 100644 --- a/EnterpriseIntegrationPlatform/rules/milestones.md +++ b/EnterpriseIntegrationPlatform/rules/milestones.md @@ -278,6 +278,32 @@ HTTP handler. DatabaseEnrichmentSource has 6 tests with mock ADO.NET infrastruct --- +## Phase 36 — Auth, Observability & System Management Test Hardening + +> **Origin:** Audit revealed that `ApiKeyAuthenticationHandler` (security-critical auth handler) +> had **zero unit tests**. `LokiObservabilityEventLog` (core observability event storage with +> Loki HTTP API + in-memory fallback) was untested. `ControlBusPublisher` (EIP Control Bus pattern) +> and `DlqManagementService` (DLQ resubmission) lacked dedicated test fixtures. This phase +> closes these critical test gaps. + +| Chunk | Description | Status | +|-------|-------------|--------| +| 360 | **ApiKeyAuthenticationHandler Tests** — see `rules/completion-log.md` | `done` | +| 361 | **LokiObservabilityEventLog Tests** — see `rules/completion-log.md` | `done` | +| 362 | **ControlBusPublisher & DlqManagementService Tests** — see `rules/completion-log.md` | `done` | + +### Summary + +Phase 36 complete — 3 chunks (360–362). 33 new unit tests. UnitTests total: 1919 (was 1886). +ApiKeyAuthenticationHandler now has 10 tests covering missing header, invalid key, valid key +with claim verification, case-sensitive comparison, multiple keys, empty config, short key masking. +LokiObservabilityEventLog has 11 tests covering RecordAsync (push, fallback, error handling), +GetByCorrelationId (Loki results, fallback, empty), GetByBusinessKey (fallback, case-insensitive). +ControlBusPublisher has 10 tests covering publish success/failure/intent, subscribe, arg validation. +DlqManagementService has 2 tests covering replay delegation and filter passthrough. + +--- + ### Next Chunk -Phase 35 is complete. No remaining chunks. +Phase 36 is complete. No remaining chunks. diff --git a/EnterpriseIntegrationPlatform/src/Admin.Api/Admin.Api.csproj b/EnterpriseIntegrationPlatform/src/Admin.Api/Admin.Api.csproj index 688e18e..63307ba 100644 --- a/EnterpriseIntegrationPlatform/src/Admin.Api/Admin.Api.csproj +++ b/EnterpriseIntegrationPlatform/src/Admin.Api/Admin.Api.csproj @@ -1,4 +1,7 @@ + + + diff --git a/EnterpriseIntegrationPlatform/tests/UnitTests/ApiKeyAuthenticationHandlerTests.cs b/EnterpriseIntegrationPlatform/tests/UnitTests/ApiKeyAuthenticationHandlerTests.cs new file mode 100644 index 0000000..29c0f82 --- /dev/null +++ b/EnterpriseIntegrationPlatform/tests/UnitTests/ApiKeyAuthenticationHandlerTests.cs @@ -0,0 +1,197 @@ +using System.Security.Claims; +using System.Text.Encodings.Web; +using EnterpriseIntegrationPlatform.Admin.Api; +using EnterpriseIntegrationPlatform.Admin.Api.Authentication; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using NUnit.Framework; + +namespace EnterpriseIntegrationPlatform.Tests.Unit; + +[TestFixture] +public class ApiKeyAuthenticationHandlerTests +{ + private const string ValidKey = "super-secret-key-12345"; + private const string ShortKey = "abc"; + + private ApiKeyAuthenticationHandler CreateHandler( + HttpContext context, + IReadOnlyList? configuredKeys = null) + { + configuredKeys ??= [ValidKey]; + + var adminOptions = Options.Create(new AdminApiOptions + { + ApiKeys = configuredKeys, + }); + + var optionsMonitor = new TestOptionsMonitor(); + var loggerFactory = NullLoggerFactory.Instance; + + var handler = new ApiKeyAuthenticationHandler( + optionsMonitor, + loggerFactory, + UrlEncoder.Default, + adminOptions); + + // Initialize the handler with the scheme and context + handler.InitializeAsync( + new AuthenticationScheme( + ApiKeyAuthenticationHandler.SchemeName, + displayName: null, + typeof(ApiKeyAuthenticationHandler)), + context).GetAwaiter().GetResult(); + + return handler; + } + + private static DefaultHttpContext CreateHttpContext(string? apiKey = null) + { + var context = new DefaultHttpContext(); + if (apiKey is not null) + { + context.Request.Headers["X-Api-Key"] = apiKey; + } + return context; + } + + [Test] + public async Task HandleAuthenticate_MissingHeader_ReturnsFail() + { + var context = CreateHttpContext(apiKey: null); + var handler = CreateHandler(context); + + var result = await handler.AuthenticateAsync(); + + Assert.That(result.Succeeded, Is.False); + Assert.That(result.Failure?.Message, Does.Contain("Missing")); + } + + [Test] + public async Task HandleAuthenticate_InvalidKey_ReturnsFail() + { + var context = CreateHttpContext("wrong-key"); + var handler = CreateHandler(context); + + var result = await handler.AuthenticateAsync(); + + Assert.That(result.Succeeded, Is.False); + Assert.That(result.Failure?.Message, Does.Contain("Invalid")); + } + + [Test] + public async Task HandleAuthenticate_ValidKey_ReturnsSuccess() + { + var context = CreateHttpContext(ValidKey); + var handler = CreateHandler(context); + + var result = await handler.AuthenticateAsync(); + + Assert.That(result.Succeeded, Is.True); + Assert.That(result.Ticket, Is.Not.Null); + } + + [Test] + public async Task HandleAuthenticate_ValidKey_SetsAdminRole() + { + var context = CreateHttpContext(ValidKey); + var handler = CreateHandler(context); + + var result = await handler.AuthenticateAsync(); + + Assert.That(result.Succeeded, Is.True); + var roleClaim = result.Principal!.FindFirst(ClaimTypes.Role); + Assert.That(roleClaim, Is.Not.Null); + Assert.That(roleClaim!.Value, Is.EqualTo("Admin")); + } + + [Test] + public async Task HandleAuthenticate_ValidKey_SetsNameClaim() + { + var context = CreateHttpContext(ValidKey); + var handler = CreateHandler(context); + + var result = await handler.AuthenticateAsync(); + + Assert.That(result.Succeeded, Is.True); + var nameClaim = result.Principal!.FindFirst(ClaimTypes.Name); + Assert.That(nameClaim, Is.Not.Null); + Assert.That(nameClaim!.Value, Is.EqualTo("admin")); + } + + [Test] + public async Task HandleAuthenticate_ValidKey_SetsApiKeyPrefixClaim() + { + var context = CreateHttpContext(ValidKey); + var handler = CreateHandler(context); + + var result = await handler.AuthenticateAsync(); + + Assert.That(result.Succeeded, Is.True); + var prefixClaim = result.Principal!.FindFirst("apikey_prefix"); + Assert.That(prefixClaim, Is.Not.Null); + // Key "super-secret-key-12345" → first 4 chars = "supe" + "****" + Assert.That(prefixClaim!.Value, Is.EqualTo("supe****")); + } + + [Test] + public async Task HandleAuthenticate_MultipleConfiguredKeys_AcceptsAny() + { + var context = CreateHttpContext("key-beta-67890"); + var handler = CreateHandler(context, + configuredKeys: ["key-alpha-12345", "key-beta-67890"]); + + var result = await handler.AuthenticateAsync(); + + Assert.That(result.Succeeded, Is.True); + } + + [Test] + public async Task HandleAuthenticate_CaseSensitive_RejectsWrongCase() + { + var context = CreateHttpContext(ValidKey.ToUpperInvariant()); + var handler = CreateHandler(context); + + var result = await handler.AuthenticateAsync(); + + Assert.That(result.Succeeded, Is.False); + } + + [Test] + public async Task HandleAuthenticate_EmptyConfiguredKeys_RejectsAll() + { + var context = CreateHttpContext(ValidKey); + var handler = CreateHandler(context, configuredKeys: []); + + var result = await handler.AuthenticateAsync(); + + Assert.That(result.Succeeded, Is.False); + } + + [Test] + public async Task HandleAuthenticate_ShortKey_MasksEntirely() + { + var context = CreateHttpContext(ShortKey); + var handler = CreateHandler(context, configuredKeys: [ShortKey]); + + var result = await handler.AuthenticateAsync(); + + Assert.That(result.Succeeded, Is.True); + var prefixClaim = result.Principal!.FindFirst("apikey_prefix"); + Assert.That(prefixClaim, Is.Not.Null); + Assert.That(prefixClaim!.Value, Is.EqualTo("****")); + } + + /// + /// Minimal IOptionsMonitor implementation for AuthenticationSchemeOptions. + /// + private sealed class TestOptionsMonitor : IOptionsMonitor + { + public AuthenticationSchemeOptions CurrentValue { get; } = new(); + public AuthenticationSchemeOptions Get(string? name) => CurrentValue; + public IDisposable? OnChange(Action listener) => null; + } +} diff --git a/EnterpriseIntegrationPlatform/tests/UnitTests/ControlBusPublisherTests.cs b/EnterpriseIntegrationPlatform/tests/UnitTests/ControlBusPublisherTests.cs new file mode 100644 index 0000000..be2ddba --- /dev/null +++ b/EnterpriseIntegrationPlatform/tests/UnitTests/ControlBusPublisherTests.cs @@ -0,0 +1,252 @@ +using EnterpriseIntegrationPlatform.Admin.Api.Services; +using EnterpriseIntegrationPlatform.Contracts; +using EnterpriseIntegrationPlatform.Ingestion; +using EnterpriseIntegrationPlatform.Processing.Replay; +using EnterpriseIntegrationPlatform.SystemManagement; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using NUnit.Framework; + +namespace EnterpriseIntegrationPlatform.Tests.Unit; + +[TestFixture] +public class ControlBusPublisherTests +{ + private IMessageBrokerProducer _producer = null!; + private IMessageBrokerConsumer _consumer = null!; + private ControlBusOptions _options = null!; + private ControlBusPublisher _sut = null!; + + [SetUp] + public void SetUp() + { + _producer = Substitute.For(); + _consumer = Substitute.For(); + _options = new ControlBusOptions + { + ControlTopic = "test.control", + ConsumerGroup = "test-consumers", + Source = "UnitTest", + }; + _sut = new ControlBusPublisher( + _producer, + _consumer, + Options.Create(_options), + NullLogger.Instance); + } + + [TearDown] + public async Task TearDown() + { + await _consumer.DisposeAsync(); + } + + // ── PublishCommandAsync tests ──────────────────────────────────────── + + [Test] + public async Task PublishCommandAsync_Success_ReturnsSucceededTrue() + { + var result = await _sut.PublishCommandAsync( + new { Action = "restart" }, + "RestartService"); + + Assert.That(result.Succeeded, Is.True); + Assert.That(result.ControlTopic, Is.EqualTo("test.control")); + Assert.That(result.FailureReason, Is.Null); + } + + [Test] + public async Task PublishCommandAsync_Success_PublishesToControlTopic() + { + await _sut.PublishCommandAsync( + new { Action = "restart" }, + "RestartService"); + + await _producer.Received(1).PublishAsync( + Arg.Any>(), + Arg.Is("test.control"), + Arg.Any()); + } + + [Test] + public async Task PublishCommandAsync_Success_SetsCommandIntent() + { + // Capture the envelope via When..Do pattern on the generic method + MessageIntent? capturedIntent = null; + string? capturedMessageType = null; + string? capturedSource = null; + + _producer.When(x => x.PublishAsync( + Arg.Any>(), + Arg.Any(), + Arg.Any())) + .Do(call => + { + var env = call.Arg>(); + capturedIntent = env.Intent; + capturedMessageType = env.MessageType; + capturedSource = env.Source; + }); + + await _sut.PublishCommandAsync( + new { Action = "restart" }, + "RestartService"); + + Assert.That(capturedIntent, Is.EqualTo(MessageIntent.Command)); + Assert.That(capturedMessageType, Is.EqualTo("RestartService")); + Assert.That(capturedSource, Is.EqualTo("UnitTest")); + } + + [Test] + public async Task PublishCommandAsync_ProducerThrows_ReturnsSucceededFalse() + { + _producer.PublishAsync( + Arg.Any>(), + Arg.Any(), + Arg.Any()) + .ThrowsAsync(new InvalidOperationException("Broker unavailable")); + + var result = await _sut.PublishCommandAsync( + new { Action = "restart" }, + "RestartService"); + + Assert.That(result.Succeeded, Is.False); + Assert.That(result.FailureReason, Does.Contain("Broker unavailable")); + } + + [Test] + public void PublishCommandAsync_NullCommand_ThrowsArgumentNull() + { + Assert.ThrowsAsync(async () => + await _sut.PublishCommandAsync(null!, "Test")); + } + + [Test] + public void PublishCommandAsync_EmptyCommandType_ThrowsArgument() + { + Assert.ThrowsAsync(async () => + await _sut.PublishCommandAsync(new { }, "")); + } + + // ── SubscribeAsync tests ──────────────────────────────────────────── + + [Test] + public async Task SubscribeAsync_RegistersConsumerOnControlTopic() + { + await _sut.SubscribeAsync( + "RestartService", + _ => Task.CompletedTask); + + await _consumer.Received(1).SubscribeAsync( + Arg.Is("test.control"), + Arg.Is("test-consumers"), + Arg.Any, Task>>(), + Arg.Any()); + } + + [Test] + public void SubscribeAsync_NullHandler_ThrowsArgumentNull() + { + Assert.ThrowsAsync(async () => + await _sut.SubscribeAsync("Test", null!)); + } + + [Test] + public void SubscribeAsync_EmptyCommandType_ThrowsArgument() + { + Assert.ThrowsAsync(async () => + await _sut.SubscribeAsync("", _ => Task.CompletedTask)); + } + + // ── Constructor validation tests ──────────────────────────────────── + + [Test] + public void Constructor_NullProducer_Throws() + { + Assert.Throws(() => + new ControlBusPublisher( + null!, + _consumer, + Options.Create(_options), + NullLogger.Instance)); + } + + [Test] + public void Constructor_NullConsumer_Throws() + { + Assert.Throws(() => + new ControlBusPublisher( + _producer, + null!, + Options.Create(_options), + NullLogger.Instance)); + } +} + +[TestFixture] +public class DlqManagementServiceTests +{ + private IMessageReplayer _replayer = null!; + private DlqManagementService _sut = null!; + + [SetUp] + public void SetUp() + { + _replayer = Substitute.For(); + _sut = new DlqManagementService( + _replayer, + NullLogger.Instance); + } + + [Test] + public async Task ResubmitAsync_DelegatesToReplayer() + { + var filter = new ReplayFilter { MessageType = "OrderCreated" }; + var now = DateTimeOffset.UtcNow; + _replayer.ReplayAsync(Arg.Any(), Arg.Any()) + .Returns(new ReplayResult + { + ReplayedCount = 5, + FailedCount = 1, + SkippedCount = 0, + StartedAt = now, + CompletedAt = now.AddSeconds(2), + }); + + var result = await _sut.ResubmitAsync(filter); + + Assert.That(result.ReplayedCount, Is.EqualTo(5)); + Assert.That(result.FailedCount, Is.EqualTo(1)); + } + + [Test] + public async Task ResubmitAsync_PassesFilterToReplayer() + { + var correlationId = Guid.NewGuid(); + var filter = new ReplayFilter + { + CorrelationId = correlationId, + MessageType = "OrderCreated", + }; + var now = DateTimeOffset.UtcNow; + _replayer.ReplayAsync(Arg.Any(), Arg.Any()) + .Returns(new ReplayResult + { + ReplayedCount = 0, + FailedCount = 0, + SkippedCount = 0, + StartedAt = now, + CompletedAt = now, + }); + + await _sut.ResubmitAsync(filter); + + await _replayer.Received(1).ReplayAsync( + Arg.Is(f => + f.CorrelationId == correlationId && + f.MessageType == "OrderCreated"), + Arg.Any()); + } +} diff --git a/EnterpriseIntegrationPlatform/tests/UnitTests/LokiObservabilityEventLogTests.cs b/EnterpriseIntegrationPlatform/tests/UnitTests/LokiObservabilityEventLogTests.cs new file mode 100644 index 0000000..49338fa --- /dev/null +++ b/EnterpriseIntegrationPlatform/tests/UnitTests/LokiObservabilityEventLogTests.cs @@ -0,0 +1,293 @@ +using System.Net; +using System.Reflection; +using System.Text.Json; +using EnterpriseIntegrationPlatform.Contracts; +using EnterpriseIntegrationPlatform.Observability; +using Microsoft.Extensions.Logging.Abstractions; +using NUnit.Framework; + +namespace EnterpriseIntegrationPlatform.Tests.Unit; + +[TestFixture] +public class LokiObservabilityEventLogTests +{ + private MockLokiHandler _handler = null!; + private HttpClient _httpClient = null!; + private LokiObservabilityEventLog _sut = null!; + + [SetUp] + public void SetUp() + { + // Clear the static fallback store between tests via reflection + var storeField = typeof(LokiObservabilityEventLog) + .GetField("FallbackStore", BindingFlags.NonPublic | BindingFlags.Static); + var store = (System.Collections.IList)storeField!.GetValue(null)!; + store.Clear(); + + _handler = new MockLokiHandler(); + _httpClient = new HttpClient(_handler) + { + BaseAddress = new Uri("http://localhost:3100"), + }; + _sut = new LokiObservabilityEventLog(_httpClient, + NullLogger.Instance); + } + + [TearDown] + public void TearDown() + { + _httpClient.Dispose(); + _handler.Dispose(); + } + + private static MessageEvent CreateEvent( + Guid? correlationId = null, + string? businessKey = null, + string stage = "Received") + { + return new MessageEvent + { + MessageId = Guid.NewGuid(), + CorrelationId = correlationId ?? Guid.NewGuid(), + MessageType = "TestOrder", + Source = "UnitTest", + Stage = stage, + Status = DeliveryStatus.Pending, + BusinessKey = businessKey, + }; + } + + // ── RecordAsync tests ───────────────────────────────────────────────── + + [Test] + public async Task RecordAsync_LokiAvailable_PostsToLokiPushEndpoint() + { + _handler.StatusCode = HttpStatusCode.NoContent; + var evt = CreateEvent(); + + await _sut.RecordAsync(evt); + + Assert.That(_handler.Requests, Has.Count.EqualTo(1)); + Assert.That(_handler.Requests[0].RequestUri!.PathAndQuery, + Does.Contain("loki/api/v1/push")); + } + + [Test] + public async Task RecordAsync_LokiUnavailable_DoesNotThrow() + { + _handler.StatusCode = HttpStatusCode.InternalServerError; + var evt = CreateEvent(); + + Assert.DoesNotThrowAsync(() => _sut.RecordAsync(evt)); + } + + [Test] + public async Task RecordAsync_AlwaysStoresInFallback() + { + _handler.StatusCode = HttpStatusCode.InternalServerError; + var correlationId = Guid.NewGuid(); + var evt = CreateEvent(correlationId: correlationId); + + await _sut.RecordAsync(evt); + + // Loki query will fail, so fallback store should be used + _handler.StatusCode = HttpStatusCode.InternalServerError; + var results = await _sut.GetByCorrelationIdAsync(correlationId); + Assert.That(results, Has.Count.EqualTo(1)); + Assert.That(results[0].MessageId, Is.EqualTo(evt.MessageId)); + } + + [Test] + public async Task RecordAsync_PostsApplicationJsonContentType() + { + _handler.StatusCode = HttpStatusCode.NoContent; + var evt = CreateEvent(); + + await _sut.RecordAsync(evt); + + Assert.That(_handler.Requests[0].Content!.Headers.ContentType!.MediaType, + Is.EqualTo("application/json")); + } + + [Test] + public async Task RecordAsync_PayloadContainsCorrelationIdLabel() + { + _handler.StatusCode = HttpStatusCode.NoContent; + _handler.CaptureBody = true; + var correlationId = Guid.NewGuid(); + var evt = CreateEvent(correlationId: correlationId); + + await _sut.RecordAsync(evt); + + Assert.That(_handler.CapturedBodies[0], + Does.Contain(correlationId.ToString())); + } + + [Test] + public async Task RecordAsync_MultipleEvents_AllStoredInFallback() + { + _handler.StatusCode = HttpStatusCode.NoContent; + var correlationId = Guid.NewGuid(); + + await _sut.RecordAsync(CreateEvent(correlationId: correlationId, stage: "Received")); + await _sut.RecordAsync(CreateEvent(correlationId: correlationId, stage: "Validated")); + await _sut.RecordAsync(CreateEvent(correlationId: correlationId, stage: "Delivered")); + + _handler.StatusCode = HttpStatusCode.InternalServerError; + var results = await _sut.GetByCorrelationIdAsync(correlationId); + Assert.That(results, Has.Count.EqualTo(3)); + } + + // ── GetByCorrelationIdAsync tests ──────────────────────────────────── + + [Test] + public async Task GetByCorrelationId_LokiReturnsResults_ReturnsThem() + { + var correlationId = Guid.NewGuid(); + var evt = CreateEvent(correlationId: correlationId); + + // Set up Loki to return a valid query_range response + // For push, succeed; for query, return results + _handler.PushStatusCode = HttpStatusCode.NoContent; + _handler.QueryStatusCode = HttpStatusCode.OK; + _handler.QueryResponseJson = BuildLokiQueryResponse(evt); + + var results = await _sut.GetByCorrelationIdAsync(correlationId); + + Assert.That(results, Has.Count.EqualTo(1)); + Assert.That(results[0].MessageId, Is.EqualTo(evt.MessageId)); + } + + [Test] + public async Task GetByCorrelationId_NoMatchingEvents_ReturnsEmpty() + { + _handler.StatusCode = HttpStatusCode.InternalServerError; + var results = await _sut.GetByCorrelationIdAsync(Guid.NewGuid()); + + Assert.That(results, Is.Empty); + } + + // ── GetByBusinessKeyAsync tests ────────────────────────────────────── + + [Test] + public async Task GetByBusinessKey_LokiUnavailable_ReturnsFallbackResults() + { + var businessKey = "ORD-12345"; + var correlationId = Guid.NewGuid(); + var evt = CreateEvent(correlationId: correlationId, businessKey: businessKey); + + // First record the event (Loki push succeeds) + _handler.StatusCode = HttpStatusCode.NoContent; + await _sut.RecordAsync(evt); + + // Now query with Loki unavailable — should fall back to in-memory + _handler.StatusCode = HttpStatusCode.InternalServerError; + var results = await _sut.GetByBusinessKeyAsync(businessKey); + + Assert.That(results, Has.Count.EqualTo(1)); + Assert.That(results[0].BusinessKey, Is.EqualTo(businessKey)); + } + + [Test] + public async Task GetByBusinessKey_CaseInsensitiveFallback_ReturnsResults() + { + var evt = CreateEvent(businessKey: "ORD-99999"); + _handler.StatusCode = HttpStatusCode.NoContent; + await _sut.RecordAsync(evt); + + _handler.StatusCode = HttpStatusCode.InternalServerError; + var results = await _sut.GetByBusinessKeyAsync("ord-99999"); + + Assert.That(results, Has.Count.EqualTo(1)); + } + + // ── Helpers ────────────────────────────────────────────────────────── + + private static string BuildLokiQueryResponse(MessageEvent evt) + { + var eventJson = JsonSerializer.Serialize(evt, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }, + }); + + var ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000; + + // Loki values are arrays of [timestamp_string, log_line_string]. + // The log line must be a proper JSON string value (escaped). + var escapedEventJson = JsonSerializer.Serialize(eventJson); // wraps in quotes, escapes inner + + return $$""" + { + "status": "success", + "data": { + "resultType": "streams", + "result": [ + { + "stream": { + "job": "eip-observability", + "correlation_id": "{{evt.CorrelationId}}" + }, + "values": [ + ["{{ts}}", {{escapedEventJson}}] + ] + } + ] + } + } + """; + } + + /// + /// Mock HTTP handler that intercepts requests and returns configurable responses. + /// + private sealed class MockLokiHandler : DelegatingHandler + { + public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.OK; + public HttpStatusCode PushStatusCode { get; set; } = HttpStatusCode.NoContent; + public HttpStatusCode QueryStatusCode { get; set; } = HttpStatusCode.InternalServerError; + public string? QueryResponseJson { get; set; } + public bool CaptureBody { get; set; } + public List Requests { get; } = []; + public List CapturedBodies { get; } = []; + + protected override async Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + Requests.Add(request); + + if (CaptureBody && request.Content is not null) + { + var body = await request.Content.ReadAsStringAsync(cancellationToken); + CapturedBodies.Add(body); + } + + // Push requests + if (request.RequestUri?.PathAndQuery.Contains("push") == true) + { + return new HttpResponseMessage(PushStatusCode != HttpStatusCode.NoContent + ? PushStatusCode : StatusCode); + } + + // Query requests + if (request.RequestUri?.PathAndQuery.Contains("query_range") == true) + { + if (QueryResponseJson is not null + && (QueryStatusCode == HttpStatusCode.OK || StatusCode == HttpStatusCode.OK)) + { + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(QueryResponseJson, + System.Text.Encoding.UTF8, "application/json"), + }; + } + + return new HttpResponseMessage(QueryStatusCode != HttpStatusCode.InternalServerError + ? QueryStatusCode : StatusCode); + } + + return new HttpResponseMessage(StatusCode); + } + } +} diff --git a/EnterpriseIntegrationPlatform/tests/UnitTests/UnitTests.csproj b/EnterpriseIntegrationPlatform/tests/UnitTests/UnitTests.csproj index 6e9aed4..1f0d4f7 100644 --- a/EnterpriseIntegrationPlatform/tests/UnitTests/UnitTests.csproj +++ b/EnterpriseIntegrationPlatform/tests/UnitTests/UnitTests.csproj @@ -11,6 +11,9 @@ + + +