From ceadd654e6bd3a7f863d6731bf8445e8df0c38d2 Mon Sep 17 00:00:00 2001 From: RasmusGraabaek Date: Thu, 12 Dec 2024 10:06:10 +0100 Subject: [PATCH 1/5] fix: Summer/Winter time are mapped wrong, when resolution is greater than Hourly. (#1416) * New unit test project * rename * Add new project to meta tests * Update tests and CI * Ensure correct winter/summer time correction * fix assembly reference * Update PeriodFactoryTests.cs --------- Co-authored-by: Ebbe Knudsen --- .github/workflows/ci-dotnet.yml | 5 ++ source/Edi.Filtered.slnf | 1 + source/Edi.Repository.sln | 7 ++ .../IntegrationTests/Behaviours/MetaTests.cs | 2 + .../IntegrationTests/IntegrationTests.csproj | 1 + .../Databricks/Factories/PeriodFactory.cs | 19 +++-- .../Factories/PeriodFactoryTests.cs | 71 +++++++++++++++++++ .../OutgoingMessages.UnitTests.csproj | 35 +++++++++ 8 files changed, 134 insertions(+), 7 deletions(-) create mode 100644 source/OutgoingMessages.UnitTests/Infrastructure/Databricks/Factories/PeriodFactoryTests.cs create mode 100644 source/OutgoingMessages.UnitTests/OutgoingMessages.UnitTests.csproj diff --git a/.github/workflows/ci-dotnet.yml b/.github/workflows/ci-dotnet.yml index 2eca394785..1c8f74a3e7 100644 --- a/.github/workflows/ci-dotnet.yml +++ b/.github/workflows/ci-dotnet.yml @@ -33,8 +33,13 @@ jobs: tests_filter_expression: - name: Architecture Tests paths: \source\ArchitectureTests\bin\Release\net8.0\Energinet.DataHub.EDI.ArchitectureTests.dll + - name: Tests paths: \source\Tests\bin\Release\net8.0\Energinet.DataHub.EDI.Tests.dll + + - name: OutgoingMessages Unit Tests + paths: \source\OutgoingMessages.UnitTests\bin\Release\net8.0\Energinet.DataHub.EDI.OutgoingMessages.UnitTests.dll + uses: Energinet-DataHub/.github/.github/workflows/dotnet-postbuild-test.yml@v14 with: download_attempt_limit: 30 diff --git a/source/Edi.Filtered.slnf b/source/Edi.Filtered.slnf index 8606218993..8220c52915 100644 --- a/source/Edi.Filtered.slnf +++ b/source/Edi.Filtered.slnf @@ -41,6 +41,7 @@ "OutgoingMessages.Infrastructure\\OutgoingMessages.Infrastructure.csproj", "OutgoingMessages.IntegrationTests\\OutgoingMessages.IntegrationTests.csproj", "OutgoingMessages.Interfaces\\OutgoingMessages.Interfaces.csproj", + "OutgoingMessages.UnitTests\\OutgoingMessages.UnitTests.csproj", "Process.Application\\Process.Application.csproj", "Process.Domain\\Process.Domain.csproj", "Process.Infrastructure\\Process.Infrastructure.csproj", diff --git a/source/Edi.Repository.sln b/source/Edi.Repository.sln index 8554bbad89..063fa835ba 100644 --- a/source/Edi.Repository.sln +++ b/source/Edi.Repository.sln @@ -206,6 +206,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MasterData.Domain", "Master EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ArchivedMessages.Domain", "ArchivedMessages.Domain\ArchivedMessages.Domain.csproj", "{5DBB75FB-38DC-47ED-987B-255A2D48186C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OutgoingMessages.UnitTests", "OutgoingMessages.UnitTests\OutgoingMessages.UnitTests.csproj", "{2A087DB2-8A23-489C-979D-AE8D1783B52A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -452,6 +454,10 @@ Global {5DBB75FB-38DC-47ED-987B-255A2D48186C}.Debug|Any CPU.Build.0 = Debug|Any CPU {5DBB75FB-38DC-47ED-987B-255A2D48186C}.Release|Any CPU.ActiveCfg = Release|Any CPU {5DBB75FB-38DC-47ED-987B-255A2D48186C}.Release|Any CPU.Build.0 = Release|Any CPU + {2A087DB2-8A23-489C-979D-AE8D1783B52A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A087DB2-8A23-489C-979D-AE8D1783B52A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A087DB2-8A23-489C-979D-AE8D1783B52A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2A087DB2-8A23-489C-979D-AE8D1783B52A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -548,6 +554,7 @@ Global {0E787394-5AD2-4920-900F-E802EDB726B6} = {ED4BC8EC-5C20-4250-B2DD-9FEAEF726D8E} {4A238A51-ED28-4039-8046-ED7B1336C614} = {ED4BC8EC-5C20-4250-B2DD-9FEAEF726D8E} {5DBB75FB-38DC-47ED-987B-255A2D48186C} = {0B7C34CA-1AC7-4D94-B44B-5357EFE13F88} + {2A087DB2-8A23-489C-979D-AE8D1783B52A} = {75A9EB8E-EEE3-443B-B497-F624E454DC22} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8C390B97-896A-4AAD-8609-3C20E80966D2} diff --git a/source/IntegrationTests/Behaviours/MetaTests.cs b/source/IntegrationTests/Behaviours/MetaTests.cs index 876d22e40a..5689b17f99 100644 --- a/source/IntegrationTests/Behaviours/MetaTests.cs +++ b/source/IntegrationTests/Behaviours/MetaTests.cs @@ -42,6 +42,7 @@ public static void Given_TestNames_When_CheckingConvention_Then_AllSatisfies() var integrationEventsIntegrationTestsAssembly = Assembly.GetAssembly(typeof(IntegrationEventsFixture)); var masterDataIntegrationTestsAssembly = Assembly.GetAssembly(typeof(MasterDataFixture)); var incomingMessageIntegrationTestsAssembly = Assembly.GetAssembly(typeof(IncomingMessagesTestFixture)); + var outgoingMessagesUnitTestsAssembly = Assembly.GetAssembly(typeof(OutgoingMessages.UnitTests.Infrastructure.Databricks.Factories.PeriodFactoryTests)); var allTypes = new[] { @@ -50,6 +51,7 @@ public static void Given_TestNames_When_CheckingConvention_Then_AllSatisfies() integrationEventsIntegrationTestsAssembly, masterDataIntegrationTestsAssembly, incomingMessageIntegrationTestsAssembly, + outgoingMessagesUnitTestsAssembly, }.SelectMany(x => x?.GetTypes()!); var allTestNames = allTypes.Where( diff --git a/source/IntegrationTests/IntegrationTests.csproj b/source/IntegrationTests/IntegrationTests.csproj index c5bc3ab323..36e98ebf99 100644 --- a/source/IntegrationTests/IntegrationTests.csproj +++ b/source/IntegrationTests/IntegrationTests.csproj @@ -75,6 +75,7 @@ limitations under the License. + diff --git a/source/OutgoingMessages.Infrastructure/Databricks/Factories/PeriodFactory.cs b/source/OutgoingMessages.Infrastructure/Databricks/Factories/PeriodFactory.cs index 4bfbbe20d0..03939f9b2c 100644 --- a/source/OutgoingMessages.Infrastructure/Databricks/Factories/PeriodFactory.cs +++ b/source/OutgoingMessages.Infrastructure/Databricks/Factories/PeriodFactory.cs @@ -69,14 +69,9 @@ public static Instant GetEndDateWithResolutionOffset( case var res when res == Resolution.Hourly: return timeForLatestPoint.Plus(Duration.FromHours(1)); case var res when res == Resolution.Daily: - return timeForLatestPoint.Plus(Duration.FromDays(1)); + return EnsureMidnight(timeForLatestPoint, daysToAdd: 1); case var res when res == Resolution.Monthly: - { - var timeForLatestPointInLocalTime = timeForLatestPoint.InZone(_dkTimeZone).LocalDateTime; - var endAtMidnightInLocalTime = timeForLatestPointInLocalTime.PlusMonths(1).Date.AtMidnight(); - var endAtMidnightInUtc = endAtMidnightInLocalTime.InZoneStrictly(_dkTimeZone); - return endAtMidnightInUtc.ToInstant(); - } + return EnsureMidnight(timeForLatestPoint, monthsToAdd: 1); default: throw new ArgumentOutOfRangeException( @@ -85,4 +80,14 @@ public static Instant GetEndDateWithResolutionOffset( "Unknown resolution"); } } + + private static Instant EnsureMidnight(Instant date, int daysToAdd = 0, int monthsToAdd = 0) + { + var localDate = date.InZone(_dkTimeZone).LocalDateTime; + var midnight = localDate + .PlusMonths(monthsToAdd) + .PlusDays(daysToAdd).Date + .AtMidnight(); + return midnight.InZoneStrictly(_dkTimeZone).ToInstant(); + } } diff --git a/source/OutgoingMessages.UnitTests/Infrastructure/Databricks/Factories/PeriodFactoryTests.cs b/source/OutgoingMessages.UnitTests/Infrastructure/Databricks/Factories/PeriodFactoryTests.cs new file mode 100644 index 0000000000..c024d5e26d --- /dev/null +++ b/source/OutgoingMessages.UnitTests/Infrastructure/Databricks/Factories/PeriodFactoryTests.cs @@ -0,0 +1,71 @@ +// Copyright 2020 Energinet DataHub A/S +// +// Licensed under the Apache License, Version 2.0 (the "License2"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Energinet.DataHub.EDI.BuildingBlocks.Domain.Models; +using Energinet.DataHub.EDI.OutgoingMessages.Infrastructure.Databricks.Factories; +using FluentAssertions; +using NodaTime.Text; + +namespace Energinet.DataHub.EDI.OutgoingMessages.UnitTests.Infrastructure.Databricks.Factories; + +public class PeriodFactoryTests +{ + [Theory] + + // From summer time to winter time + [InlineData("2021-10-30T22:00:00Z", nameof(Resolution.Daily), "2021-10-31T23:00:00Z")] + [InlineData("2021-09-30T22:00:00Z", nameof(Resolution.Monthly), "2021-10-31T23:00:00Z")] + + // From winter time to summer time + [InlineData("2024-03-30T23:00:00Z", nameof(Resolution.Daily), "2024-03-31T22:00:00Z")] + [InlineData("2024-02-29T23:00:00Z", nameof(Resolution.Monthly), "2024-03-31T22:00:00Z")] + public void Given_SummerWinterTimeChangeDate_When_Mapping_Then_ReturnsExpectedDateWithSummerWinterTimeCorrection(string date, string resolution, string expected) + { + // Arrange + var dateAsInstant = InstantPattern.ExtendedIso.Parse(date).Value; + var domainResolution = Resolution.FromName(resolution); + var expectedDate = InstantPattern.ExtendedIso.Parse(expected).Value; + + // Act + var actual = PeriodFactory.GetEndDateWithResolutionOffset(domainResolution, dateAsInstant); + + // Assert + actual.Should().Be(expectedDate); + } + + [Theory] + [InlineData("2021-10-26T22:00:00Z", nameof(Resolution.Daily), "2021-10-27T22:00:00Z")] + [InlineData("2021-07-31T22:00:00Z", nameof(Resolution.Monthly), "2021-08-31T22:00:00Z")] + + // From summer time to winter time + [InlineData("2021-10-31T02:00:00Z", nameof(Resolution.QuarterHourly), "2021-10-31T02:15:00Z")] + [InlineData("2021-10-31T02:00:00Z", nameof(Resolution.Hourly), "2021-10-31T03:00:00Z")] + + // From winter time to summer time + [InlineData("2024-03-31T03:00:00Z", nameof(Resolution.QuarterHourly), "2024-03-31T03:15:00Z")] + [InlineData("2024-03-31T03:00:00Z", nameof(Resolution.Hourly), "2024-03-31T04:00:00Z")] + public void Given_DatesWithoutSummerWinterTimeChange_When_Mapping_Then_ReturnsExpectedDateWithNoCorrection(string date, string resolution, string expected) + { + // Arrange + var dateAsInstant = InstantPattern.ExtendedIso.Parse(date).Value; + var domainResolution = Resolution.FromName(resolution); + var expectedDate = InstantPattern.ExtendedIso.Parse(expected).Value; + + // Act + var actual = PeriodFactory.GetEndDateWithResolutionOffset(domainResolution, dateAsInstant); + + // Assert + actual.Should().Be(expectedDate); + } +} diff --git a/source/OutgoingMessages.UnitTests/OutgoingMessages.UnitTests.csproj b/source/OutgoingMessages.UnitTests/OutgoingMessages.UnitTests.csproj new file mode 100644 index 0000000000..e4e6992c25 --- /dev/null +++ b/source/OutgoingMessages.UnitTests/OutgoingMessages.UnitTests.csproj @@ -0,0 +1,35 @@ + + + + Energinet.DataHub.EDI.OutgoingMessages.UnitTests + Energinet.DataHub.EDI.OutgoingMessages.UnitTests + false + true + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + From 09ae752accafed171a5bf404b5c2e7d3218b1652 Mon Sep 17 00:00:00 2001 From: Ebbe Knudsen Date: Thu, 12 Dec 2024 16:16:02 +0100 Subject: [PATCH 2/5] Only deny incoming message if it is an RSM-12 (#1420) --- source/B2BApi/IncomingMessages/IncomingMessageReceiver.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/source/B2BApi/IncomingMessages/IncomingMessageReceiver.cs b/source/B2BApi/IncomingMessages/IncomingMessageReceiver.cs index 77361ee5e0..05199ac41f 100644 --- a/source/B2BApi/IncomingMessages/IncomingMessageReceiver.cs +++ b/source/B2BApi/IncomingMessages/IncomingMessageReceiver.cs @@ -59,7 +59,9 @@ public async Task RunAsync( var stopwatch = Stopwatch.StartNew(); var cancellationToken = request.GetCancellationToken(hostCancellationToken); - if (!await _featureFlagManager.ReceiveMeteredDataForMeasurementPointsAsync().ConfigureAwait(false)) + if (incomingDocumentTypeName != null && + incomingDocumentTypeName.Equals(IncomingDocumentType.NotifyValidatedMeasureData.Name, StringComparison.OrdinalIgnoreCase) + && !await _featureFlagManager.ReceiveMeteredDataForMeasurementPointsAsync().ConfigureAwait(false)) { /* * The HTTP 403 Forbidden client error response status code indicates that the server understood the request From 41f962829e50293d0c93e804aa4d397e9edb440f Mon Sep 17 00:00:00 2001 From: Mathias Ormstrup Bjerregaard <152613120+MWO1024@users.noreply.github.com> Date: Fri, 13 Dec 2024 08:20:36 +0100 Subject: [PATCH 3/5] feat: Add document validation to RSM-12 behaviour tests (#1413) --- ...ivenMeteredDataForMeasurementPointTests.cs | 114 ++++++- ...orMeasurementPointCimJsonDocumentWriter.cs | 27 +- ...ForMeasurementPointCimXmlDocumentWriter.cs | 43 ++- ...ForMeasurementPointMarketActivityRecord.cs | 4 +- ...ifyValidatedMeasureDataDocumentAsserter.cs | 49 +++ ...idatedMeasureDataDocumentAssertionInput.cs | 49 ++- ...zeMeteredDataForMeasurementPointHandler.cs | 4 +- .../MeteredDateForMeasurementPointBuilder.cs | 14 + ...eredDateForMeasurementPointJsonDocument.cs | 292 +++++++++++++++--- ...teredDateForMeasurementPointXmlDocument.cs | 151 +++++++-- .../RSM012/AssertPointDocumentFieldsInput.cs | 19 ++ ...DateForMeasurementPointDocumentDocument.cs | 54 +++- ...eForMeasurementPointDocumentWriterTests.cs | 127 +++++++- .../RSM012/OptionalPointDocumentFields.cs | 20 ++ .../RSM012/RequiredPointDocumentFields.cs | 17 + .../OutgoingMessages/RSM012/SampleData.cs | 2 + 16 files changed, 844 insertions(+), 142 deletions(-) create mode 100644 source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertPointDocumentFieldsInput.cs create mode 100644 source/Tests/Infrastructure/OutgoingMessages/RSM012/OptionalPointDocumentFields.cs create mode 100644 source/Tests/Infrastructure/OutgoingMessages/RSM012/RequiredPointDocumentFields.cs diff --git a/source/IntegrationTests/Behaviours/IncomingRequests/GivenMeteredDataForMeasurementPointTests.cs b/source/IntegrationTests/Behaviours/IncomingRequests/GivenMeteredDataForMeasurementPointTests.cs index 8683f43b4b..a5ab09b00f 100644 --- a/source/IntegrationTests/Behaviours/IncomingRequests/GivenMeteredDataForMeasurementPointTests.cs +++ b/source/IntegrationTests/Behaviours/IncomingRequests/GivenMeteredDataForMeasurementPointTests.cs @@ -12,11 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Diagnostics.CodeAnalysis; using System.Text; using Energinet.DataHub.EDI.BuildingBlocks.Domain.Models; using Energinet.DataHub.EDI.IntegrationTests.Fixtures; using Energinet.DataHub.EDI.OutgoingMessages.IntegrationTests.DocumentAsserters; using Energinet.DataHub.EDI.OutgoingMessages.Interfaces.Models.Peek; +using Energinet.DataHub.EDI.Tests.Infrastructure.OutgoingMessages.RSM012; using FluentAssertions; using FluentAssertions.Execution; using NodaTime; @@ -26,6 +28,10 @@ namespace Energinet.DataHub.EDI.IntegrationTests.Behaviours.IncomingRequests; +[SuppressMessage( + "StyleCop.CSharp.ReadabilityRules", + "SA1118:Parameter should not span multiple lines", + Justification = "Readability")] public sealed class GivenMeteredDataForMeasurementPointTests( IntegrationTestFixture integrationTestFixture, ITestOutputHelper testOutputHelper) @@ -50,15 +56,18 @@ public async Task When_ActorPeeksAllMessages_Then_ReceivesOneDocumentWithCorrect var transactionIdPrefix = Guid.NewGuid().ToString("N"); + var transactionId1 = $"{transactionIdPrefix}-1"; + var transactionId2 = $"{transactionIdPrefix}-2"; + await GivenReceivedMeteredDataForMeasurementPoint( documentFormat: DocumentFormat.Xml, senderActorNumber: currentActor.ActorNumber, [ - ($"{transactionIdPrefix}-1", + (transactionId1, InstantPattern.General.Parse("2024-11-28T13:51:42Z").Value, InstantPattern.General.Parse("2024-11-29T09:15:28Z").Value, Resolution.Hourly), - ($"{transactionIdPrefix}-2", + (transactionId2, InstantPattern.General.Parse("2024-11-24T18:51:58Z").Value, InstantPattern.General.Parse("2024-11-25T03:39:45Z").Value, Resolution.QuarterHourly), @@ -75,12 +84,111 @@ await GivenReceivedMeteredDataForMeasurementPoint( peekFormat); // Assert + peekResults.Should().HaveCount(2); + foreach (var peekResultDto in peekResults) { + // This is not pretty, but it works for now + var foo = new StreamReader(peekResultDto.Bundle); + var content = await foo.ReadToEndAsync(); + var isTransOne = content.Contains(transactionId1); + peekResultDto.Bundle.Position = 0; + await ThenNotifyValidatedMeasureDataDocumentIsCorrect( peekResultDto.Bundle, peekFormat, - new NotifyValidatedMeasureDataDocumentAssertionInput()); + new NotifyValidatedMeasureDataDocumentAssertionInput( + new RequiredHeaderDocumentFields( + "E23", + "8100000000115", + "A10", + "5790001330552", + "A10", + "DGL", + "DDQ", + "2024-07-01T14:57:09Z"), + new OptionalHeaderDocumentFields( + "23", + [ + isTransOne + ? new AssertSeriesDocumentFieldsInput( + 1, + new RequiredSeriesFields( + TransactionId.From(string.Join(string.Empty, transactionId1.Reverse())), + "579999993331812345", + "A10", + "E17", + "KWH", + new RequiredPeriodDocumentFields( + "PT1H", + "2024-11-28T13:51Z", + "2024-11-29T09:15Z", + Enumerable.Range(1, 24) + .Select( + i => new AssertPointDocumentFieldsInput( + new RequiredPointDocumentFields(i), + new OptionalPointDocumentFields("A03", 1000 + i))) + .ToList())), + new OptionalSeriesFields( + transactionId1, + "2022-12-17T09:30:47Z", + null, + null, + "8716867000030")) + : new AssertSeriesDocumentFieldsInput( + 1, + new RequiredSeriesFields( + TransactionId.From(string.Join(string.Empty, transactionId2.Reverse())), + "579999993331812345", + "A10", + "E17", + "KWH", + new RequiredPeriodDocumentFields( + "PT15M", + "2024-11-24T18:51Z", + "2024-11-25T03:39Z", + Enumerable.Range(1, 96) + .Select( + i => new AssertPointDocumentFieldsInput( + new RequiredPointDocumentFields(i), + new OptionalPointDocumentFields("A03", 1000 + i))) + .ToList())), + new OptionalSeriesFields( + transactionId2, + "2022-12-17T09:30:47Z", + null, + null, + "8716867000030")), + ]))); } } + + [Theory] + [MemberData(nameof(PeekFormats))] + public async Task AndGiven_MessageIsEmpty_When_ActorPeeksAllMessages_Then_ReceivesNoMessages( + DocumentFormat peekFormat) + { + // Arrange + var senderSpy = CreateServiceBusSenderSpy(); + var currentActor = (ActorNumber: ActorNumber.Create("1111111111111"), ActorRole: ActorRole.GridAccessProvider); + + GivenNowIs(Instant.FromUtc(2024, 7, 1, 14, 57, 09)); + GivenAuthenticatedActorIs(currentActor.ActorNumber, currentActor.ActorRole); + + await GivenReceivedMeteredDataForMeasurementPoint( + documentFormat: DocumentFormat.Xml, + senderActorNumber: currentActor.ActorNumber, + []); + + await WhenMeteredDataForMeasurementPointProcessIsInitialized(senderSpy.LatestMessage!); + + // Act + var peekResults = await WhenActorPeeksAllMessages( + ActorNumber.Create("8100000000115"), + ActorRole.EnergySupplier, + peekFormat); + + // Assert + peekResults.Should().BeEmpty(); + } } diff --git a/source/OutgoingMessages.Domain/DocumentWriters/RSM012/MeteredDateForMeasurementPointCimJsonDocumentWriter.cs b/source/OutgoingMessages.Domain/DocumentWriters/RSM012/MeteredDateForMeasurementPointCimJsonDocumentWriter.cs index 29c186afc4..70112431d9 100644 --- a/source/OutgoingMessages.Domain/DocumentWriters/RSM012/MeteredDateForMeasurementPointCimJsonDocumentWriter.cs +++ b/source/OutgoingMessages.Domain/DocumentWriters/RSM012/MeteredDateForMeasurementPointCimJsonDocumentWriter.cs @@ -64,18 +64,18 @@ private Document ParseFrom(OutgoingMessageHeader header, IReadOnlyCollection(); + var meteredDataForMeasurementSeries = new Collection(); foreach (var activityRecord in transactions.Select(t => _parser.From(t))) { - meteredDataForMeasurementPoints.Add( - new MeteredDataForMeasurementPoint( + meteredDataForMeasurementSeries.Add( + new MeteredDataForMeasurementSeries( activityRecord.TransactionId.Value, activityRecord.MarketEvaluationPointNumber, activityRecord.MarketEvaluationPointType, activityRecord.OriginalTransactionIdReferenceId?.Value, activityRecord.Product, activityRecord.QuantityMeasureUnit.Code, - activityRecord.RegistrationDateTime.ToString(), + activityRecord.RegistrationDateTime?.ToString(), new Period( activityRecord.Resolution.Code, new TimeInterval( @@ -100,7 +100,7 @@ private Document ParseFrom(OutgoingMessageHeader header, IReadOnlyCollection meteredDataForMeasurementPoints) + IReadOnlyCollection meteredDataForMeasurementSeries) { [JsonPropertyName("mRID")] public string MessageId { get; init; } = messageId; @@ -150,17 +150,18 @@ internal class MeteredDateForMeasurementPoint( public ValueObject Type { get; init; } = ValueObject.Create(typeCode); [JsonPropertyName("Series")] - public IReadOnlyCollection MeteredDataForMeasurementPoints { get; init; } = meteredDataForMeasurementPoints; + public IReadOnlyCollection MeteredDataForMeasurementSeries { get; init; } = + meteredDataForMeasurementSeries; } -internal class MeteredDataForMeasurementPoint( +internal class MeteredDataForMeasurementSeries( string transactionId, string marketEvaluationPointNumber, string marketEvaluationPointType, string? originalTransactionIdReferenceId, - string product, + string? product, string quantityMeasureUnit, - string registrationDateTime, + string? registrationDateTime, Period period) { [JsonPropertyName("mRID")] @@ -177,13 +178,15 @@ internal class MeteredDataForMeasurementPoint( public string? OriginalTransactionIdReferenceId { get; init; } = originalTransactionIdReferenceId; //TODO: what does this field represent? [JsonPropertyName("product")] - public string Product { get; init; } = product; + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Product { get; init; } = product; [JsonPropertyName("quantity_Measure_Unit.name")] public ValueObject QuantityMeasureUnit { get; init; } = ValueObject.Create(quantityMeasureUnit); [JsonPropertyName("registration_DateAndOrTime.dateTime")] - public string RegistrationDateTime { get; init; } = registrationDateTime; + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? RegistrationDateTime { get; init; } = registrationDateTime; [JsonPropertyName("Period")] public Period Period { get; init; } = period; diff --git a/source/OutgoingMessages.Domain/DocumentWriters/RSM012/MeteredDateForMeasurementPointCimXmlDocumentWriter.cs b/source/OutgoingMessages.Domain/DocumentWriters/RSM012/MeteredDateForMeasurementPointCimXmlDocumentWriter.cs index 4de6b1db6f..f8f72049f3 100644 --- a/source/OutgoingMessages.Domain/DocumentWriters/RSM012/MeteredDateForMeasurementPointCimXmlDocumentWriter.cs +++ b/source/OutgoingMessages.Domain/DocumentWriters/RSM012/MeteredDateForMeasurementPointCimXmlDocumentWriter.cs @@ -40,20 +40,43 @@ protected override async Task WriteMarketActivityRecordsAsync( XNamespace @namespace = "urn:ediel.org:measure:notifyvalidatedmeasuredata:0:1"; foreach (var activityRecord in ParseFrom(marketActivityPayloads)) { - var seriesElement = new XElement( - @namespace + "Series", - new XElement(@namespace + "mRID", activityRecord.TransactionId.Value), - new XElement( + var seriesElement = new XElement(@namespace + "Series"); + seriesElement.Add(new XElement(@namespace + "mRID", activityRecord.TransactionId.Value)); + + if (activityRecord.OriginalTransactionIdReferenceId is not null) + { + seriesElement.Add( + new XElement( @namespace + "originalTransactionIDReference_Series.mRID", - activityRecord.OriginalTransactionIdReferenceId?.Value), + activityRecord.OriginalTransactionIdReferenceId?.Value)); + } + + seriesElement.Add( new XElement( @namespace + "marketEvaluationPoint.mRID", new XAttribute("codingScheme", "A10"), - activityRecord.MarketEvaluationPointNumber), - new XElement(@namespace + "marketEvaluationPoint.type", activityRecord.MarketEvaluationPointType), - new XElement(@namespace + "registration_DateAndOrTime.dateTime", activityRecord.RegistrationDateTime), - new XElement(@namespace + "product", activityRecord.Product), - new XElement(@namespace + "quantity_Measure_Unit.name", activityRecord.QuantityMeasureUnit.Code), + activityRecord.MarketEvaluationPointNumber)); + + seriesElement.Add( + new XElement(@namespace + "marketEvaluationPoint.type", activityRecord.MarketEvaluationPointType)); + + if (activityRecord.RegistrationDateTime is not null) + { + seriesElement.Add( + new XElement( + @namespace + "registration_DateAndOrTime.dateTime", + activityRecord.RegistrationDateTime)); + } + + if (activityRecord.Product is not null) + { + seriesElement.Add(new XElement(@namespace + "product", activityRecord.Product)); + } + + seriesElement.Add( + new XElement(@namespace + "quantity_Measure_Unit.name", activityRecord.QuantityMeasureUnit.Code)); + + seriesElement.Add( new XElement( @namespace + "Period", new XElement(@namespace + "resolution", activityRecord.Resolution.Code), diff --git a/source/OutgoingMessages.Domain/DocumentWriters/RSM012/MeteredDateForMeasurementPointMarketActivityRecord.cs b/source/OutgoingMessages.Domain/DocumentWriters/RSM012/MeteredDateForMeasurementPointMarketActivityRecord.cs index 0497fc9770..60fa8acb05 100644 --- a/source/OutgoingMessages.Domain/DocumentWriters/RSM012/MeteredDateForMeasurementPointMarketActivityRecord.cs +++ b/source/OutgoingMessages.Domain/DocumentWriters/RSM012/MeteredDateForMeasurementPointMarketActivityRecord.cs @@ -22,9 +22,9 @@ public sealed record MeteredDateForMeasurementPointMarketActivityRecord( string MarketEvaluationPointNumber, string MarketEvaluationPointType, TransactionId? OriginalTransactionIdReferenceId, - string Product, + string? Product, MeasurementUnit QuantityMeasureUnit, - Instant RegistrationDateTime, + Instant? RegistrationDateTime, Resolution Resolution, Instant StartedDateTime, Instant EndedDateTime, diff --git a/source/OutgoingMessages.IntegrationTests/DocumentAsserters/NotifyValidatedMeasureDataDocumentAsserter.cs b/source/OutgoingMessages.IntegrationTests/DocumentAsserters/NotifyValidatedMeasureDataDocumentAsserter.cs index fffd5a879a..10fc54eb03 100644 --- a/source/OutgoingMessages.IntegrationTests/DocumentAsserters/NotifyValidatedMeasureDataDocumentAsserter.cs +++ b/source/OutgoingMessages.IntegrationTests/DocumentAsserters/NotifyValidatedMeasureDataDocumentAsserter.cs @@ -52,6 +52,55 @@ public static async Task AssertCorrectDocumentAsync( _ => throw new ArgumentOutOfRangeException(nameof(documentFormat), documentFormat, null), }; + var requiredHeaderDocumentFields = assertionInput.RequiredHeaderDocumentFields; + + // Required fields + asserter + .MessageIdExists() + .HasBusinessReason(requiredHeaderDocumentFields.BusinessReasonCode) + .HasSenderId( + requiredHeaderDocumentFields.SenderId, + requiredHeaderDocumentFields.SenderScheme) + .HasSenderRole(requiredHeaderDocumentFields.SenderRole) + .HasReceiverId( + requiredHeaderDocumentFields.ReceiverId, + requiredHeaderDocumentFields.ReceiverScheme) + .HasReceiverRole(requiredHeaderDocumentFields.ReceiverRole) + .HasTimestamp(requiredHeaderDocumentFields.Timestamp); + + var optionalHeaderDocumentFields = assertionInput.OptionalHeaderDocumentFields; + + // Optional fields + asserter.HasBusinessSectorType(optionalHeaderDocumentFields.BusinessSectorType); + + foreach (var assertSeriesDocumentFieldsInput in optionalHeaderDocumentFields.AssertSeriesDocumentFieldsInput) + { + var (seriesIndex, requiredSeriesFields, optionalSeriesFields) = assertSeriesDocumentFieldsInput; + + // Required series fields + asserter + .HasTransactionId(seriesIndex, requiredSeriesFields.TransactionId) + .HasMeteringPointNumber( + seriesIndex, + requiredSeriesFields.MeteringPointNumber, + requiredSeriesFields.MeteringPointScheme) + .HasMeteringPointType(seriesIndex, requiredSeriesFields.MeteringPointType) + .HasQuantityMeasureUnit(seriesIndex, requiredSeriesFields.QuantityMeasureUnit) + // Required period fields + .HasResolution(seriesIndex, requiredSeriesFields.RequiredPeriodDocumentFields.Resolution) + .HasStartedDateTime(seriesIndex, requiredSeriesFields.RequiredPeriodDocumentFields.StartedDateTime) + .HasEndedDateTime(seriesIndex, requiredSeriesFields.RequiredPeriodDocumentFields.EndedDateTime) + .HasPoints(seriesIndex, requiredSeriesFields.RequiredPeriodDocumentFields.Points.ToList().AsReadOnly()); + + // Optional series fields + asserter + .HasOriginalTransactionIdReferenceId(seriesIndex, optionalSeriesFields.OriginalTransactionIdReferenceId) + .HasRegistrationDateTime(seriesIndex, optionalSeriesFields.RegistrationDateTime) + .HasProduct(seriesIndex, optionalSeriesFields.Product) + .HasInDomain(seriesIndex, optionalSeriesFields.InDomain) + .HasOutDomain(seriesIndex, optionalSeriesFields.OutDomain); + } + await asserter .DocumentIsValidAsync(); } diff --git a/source/OutgoingMessages.IntegrationTests/DocumentAsserters/NotifyValidatedMeasureDataDocumentAssertionInput.cs b/source/OutgoingMessages.IntegrationTests/DocumentAsserters/NotifyValidatedMeasureDataDocumentAssertionInput.cs index 31fb059832..b29584be35 100644 --- a/source/OutgoingMessages.IntegrationTests/DocumentAsserters/NotifyValidatedMeasureDataDocumentAssertionInput.cs +++ b/source/OutgoingMessages.IntegrationTests/DocumentAsserters/NotifyValidatedMeasureDataDocumentAssertionInput.cs @@ -12,8 +12,51 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Energinet.DataHub.EDI.BuildingBlocks.Domain.Models; +using Energinet.DataHub.EDI.Tests.Infrastructure.OutgoingMessages.RSM012; + namespace Energinet.DataHub.EDI.OutgoingMessages.IntegrationTests.DocumentAsserters; -public class NotifyValidatedMeasureDataDocumentAssertionInput -{ -} +public readonly record struct RequiredHeaderDocumentFields( + string BusinessReasonCode, + string ReceiverId, + string ReceiverScheme, + string SenderId, + string SenderScheme, + string SenderRole, + string ReceiverRole, + string Timestamp); + +public sealed record OptionalHeaderDocumentFields( + string? BusinessSectorType, + IReadOnlyCollection AssertSeriesDocumentFieldsInput); + +public sealed record NotifyValidatedMeasureDataDocumentAssertionInput( + RequiredHeaderDocumentFields RequiredHeaderDocumentFields, + OptionalHeaderDocumentFields OptionalHeaderDocumentFields); + +public sealed record AssertSeriesDocumentFieldsInput( + int SeriesIndex, + RequiredSeriesFields RequiredSeriesFields, + OptionalSeriesFields OptionalSeriesFields); + +public sealed record RequiredSeriesFields( + TransactionId TransactionId, + string MeteringPointNumber, + string MeteringPointScheme, + string MeteringPointType, + string QuantityMeasureUnit, + RequiredPeriodDocumentFields RequiredPeriodDocumentFields); + +public sealed record OptionalSeriesFields( + string? OriginalTransactionIdReferenceId, + string? RegistrationDateTime, + string? InDomain, + string? OutDomain, + string? Product); + +public sealed record RequiredPeriodDocumentFields( + string Resolution, + string StartedDateTime, + string EndedDateTime, + IReadOnlyCollection Points); diff --git a/source/Process.Application/ProcessInitializationHandlers/InitializeMeteredDataForMeasurementPointHandler.cs b/source/Process.Application/ProcessInitializationHandlers/InitializeMeteredDataForMeasurementPointHandler.cs index 69971d5df5..1b4b14e1e3 100644 --- a/source/Process.Application/ProcessInitializationHandlers/InitializeMeteredDataForMeasurementPointHandler.cs +++ b/source/Process.Application/ProcessInitializationHandlers/InitializeMeteredDataForMeasurementPointHandler.cs @@ -62,10 +62,10 @@ await _outgoingMessagesClient.EnqueueAndCommitAsync( new Actor(ActorNumber.Create("8100000000115"), ActorRole.EnergySupplier), BusinessReason.FromCode(marketMessage.BusinessReason), new MeteredDataForMeasurementPointMessageSeriesDto( - TransactionId.From(series.TransactionId), + TransactionId.From(string.Join(string.Empty, series.TransactionId.Reverse())), series.MeteringPointLocationId!, series.MeteringPointType!, - null, + TransactionId.From(series.TransactionId), series.ProductNumber!, MeasurementUnit.FromCode(series.ProductUnitType!), InstantPattern.General.Parse(marketMessage.CreatedAt).Value, diff --git a/source/Tests/Factories/MeteredDateForMeasurementPointBuilder.cs b/source/Tests/Factories/MeteredDateForMeasurementPointBuilder.cs index a12626dded..46efe7bdfd 100644 --- a/source/Tests/Factories/MeteredDateForMeasurementPointBuilder.cs +++ b/source/Tests/Factories/MeteredDateForMeasurementPointBuilder.cs @@ -49,4 +49,18 @@ public MeteredDateForMeasurementPointMarketActivityRecord BuildMeteredDataForMea SampleData.EndedDateTime, SampleData.Points); } + + public MeteredDateForMeasurementPointMarketActivityRecord BuildMinimalMeteredDataForMeasurementPoint() => + new( + SampleData.TransactionId, + SampleData.MeteringPointNumber, + SampleData.MeteringPointType, + null, + null, + SampleData.QuantityMeasureUnit, + null, + SampleData.Resolution, + SampleData.StartedDateTime, + SampleData.EndedDateTime, + SampleData.MinimalPoints); } diff --git a/source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertMeteredDateForMeasurementPointJsonDocument.cs b/source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertMeteredDateForMeasurementPointJsonDocument.cs index 6f68b9e5b3..99bb6666cc 100644 --- a/source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertMeteredDateForMeasurementPointJsonDocument.cs +++ b/source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertMeteredDateForMeasurementPointJsonDocument.cs @@ -15,7 +15,6 @@ using System.Text.Json; using Energinet.DataHub.EDI.BuildingBlocks.Domain.Models; using Energinet.DataHub.EDI.IncomingMessages.Domain.Schemas.Cim.Json; -using Energinet.DataHub.EDI.OutgoingMessages.Domain.DocumentWriters.RSM012; using FluentAssertions; using Json.Schema; using Xunit; @@ -36,9 +35,9 @@ public AssertMeteredDateForMeasurementPointJsonDocument(Stream documentStream) Assert.Equal("E66", _root.GetProperty("type").GetProperty("value").ToString()); } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasMessageId(string expectedMessageId) + public IAssertMeteredDateForMeasurementPointDocumentDocument MessageIdExists() { - Assert.Equal(expectedMessageId, _root.GetProperty("mRID").ToString()); + _root.TryGetProperty("mRID", out _).Should().BeTrue("property 'mRID' should be present"); return this; } @@ -80,93 +79,291 @@ public IAssertMeteredDateForMeasurementPointDocumentDocument HasTimestamp(string return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasTransactionId(TransactionId expectedTransactionId) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasBusinessSectorType( + string? expectedBusinessSectorType) { - Assert.Equal(expectedTransactionId.Value, FirstTimeSeriesElement().GetProperty("mRID").GetString()); + if (expectedBusinessSectorType is null) + { + _root + .TryGetProperty("businessSector.type", out _) + .Should() + .BeFalse("property 'businessSector.type' should not be present"); + + return this; + } + + _root + .GetProperty("businessSector.type") + .GetProperty("value") + .GetString() + .Should() + .Be(expectedBusinessSectorType); + + return this; + } + + public IAssertMeteredDateForMeasurementPointDocumentDocument HasTransactionId( + int seriesIndex, + TransactionId expectedTransactionId) + { + Assert.Equal(expectedTransactionId.Value, GetTimeSeriesElement(seriesIndex).GetProperty("mRID").GetString()); return this; } public IAssertMeteredDateForMeasurementPointDocumentDocument HasMeteringPointNumber( + int seriesIndex, string expectedMeteringPointNumber, string expectedSchemeCode) { - Assert.Equal(expectedMeteringPointNumber, FirstTimeSeriesElement().GetProperty("marketEvaluationPoint.mRID").GetProperty("value").GetString()); - Assert.Equal(expectedSchemeCode, FirstTimeSeriesElement().GetProperty("marketEvaluationPoint.mRID").GetProperty("codingScheme").GetString()); + GetTimeSeriesElement(seriesIndex) + .GetProperty("marketEvaluationPoint.mRID") + .GetProperty("value") + .GetString() + .Should() + .Be(expectedMeteringPointNumber); + + GetTimeSeriesElement(seriesIndex) + .GetProperty("marketEvaluationPoint.mRID") + .GetProperty("codingScheme") + .GetString() + .Should() + .Be(expectedSchemeCode); + return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasMeteringPointType(string expectedMeteringPointType) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasMeteringPointType( + int seriesIndex, + string expectedMeteringPointType) { - Assert.Equal(expectedMeteringPointType, FirstTimeSeriesElement().GetProperty("marketEvaluationPoint.type").GetProperty("value").GetString()); + GetTimeSeriesElement(seriesIndex) + .GetProperty("marketEvaluationPoint.type") + .GetProperty("value") + .GetString() + .Should() + .Be(expectedMeteringPointType); + return this; } public IAssertMeteredDateForMeasurementPointDocumentDocument HasOriginalTransactionIdReferenceId( + int seriesIndex, string? expectedOriginalTransactionIdReferenceId) { - Assert.Equal(expectedOriginalTransactionIdReferenceId, FirstTimeSeriesElement().GetProperty("originalTransactionIDReference_Series.mRID").GetString()); + if (expectedOriginalTransactionIdReferenceId is null) + { + GetTimeSeriesElement(seriesIndex) + .TryGetProperty("originalTransactionIDReference_Series.mRID", out _) + .Should() + .BeFalse("property 'originalTransactionIDReference_Series.mRID' should not be present"); + + return this; + } + + GetTimeSeriesElement(seriesIndex) + .GetProperty("originalTransactionIDReference_Series.mRID") + .GetString() + .Should() + .Be(expectedOriginalTransactionIdReferenceId); + + return this; + } + + public IAssertMeteredDateForMeasurementPointDocumentDocument HasProduct(int seriesIndex, string? expectedProduct) + { + if (expectedProduct is null) + { + GetTimeSeriesElement(seriesIndex) + .TryGetProperty("product", out _) + .Should() + .BeFalse("property 'product' should not be present"); + + return this; + } + + GetTimeSeriesElement(seriesIndex).GetProperty("product").GetString().Should().Be(expectedProduct); + + return this; + } + + public IAssertMeteredDateForMeasurementPointDocumentDocument HasQuantityMeasureUnit( + int seriesIndex, + string expectedQuantityMeasureUnit) + { + GetTimeSeriesElement(seriesIndex) + .GetProperty("quantity_Measure_Unit.name") + .GetProperty("value") + .GetString() + .Should() + .Be(expectedQuantityMeasureUnit); + return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasProduct(string expectedProduct) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasRegistrationDateTime( + int seriesIndex, + string? expectedRegistrationDateTime) { - Assert.Equal(expectedProduct, FirstTimeSeriesElement().GetProperty("product").GetString()); + if (expectedRegistrationDateTime is null) + { + GetTimeSeriesElement(seriesIndex) + .TryGetProperty("registration_DateAndOrTime.dateTime", out _) + .Should() + .BeFalse("property 'registration_DateAndOrTime.dateTime' should not be present"); + + return this; + } + + GetTimeSeriesElement(seriesIndex) + .GetProperty("registration_DateAndOrTime.dateTime") + .GetString() + .Should() + .Be(expectedRegistrationDateTime); + return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasQuantityMeasureUnit(string expectedQuantityMeasureUnit) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasResolution( + int seriesIndex, + string expectedResolution) { - Assert.Equal(expectedQuantityMeasureUnit, FirstTimeSeriesElement().GetProperty("quantity_Measure_Unit.name").GetProperty("value").GetString()); + GetTimeSeriesPeriodElement(seriesIndex).GetProperty("resolution").GetString().Should().Be(expectedResolution); + return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasRegistrationDateTime(string expectedRegistrationDateTime) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasStartedDateTime( + int seriesIndex, + string expectedStartedDateTime) { - Assert.Equal(expectedRegistrationDateTime, FirstTimeSeriesElement().GetProperty("registration_DateAndOrTime.dateTime").GetString()); + GetTimeSeriesPeriodElement(seriesIndex) + .GetProperty("timeInterval") + .GetProperty("start") + .GetProperty("value") + .GetString() + .Should() + .Be(expectedStartedDateTime); + return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasResolution(string expectedResolution) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasEndedDateTime( + int seriesIndex, + string expectedEndedDateTime) { - Assert.Equal(expectedResolution, FirstTimeSeriesPeriodElement().GetProperty("resolution").GetString()); + GetTimeSeriesPeriodElement(seriesIndex) + .GetProperty("timeInterval") + .GetProperty("end") + .GetProperty("value") + .GetString() + .Should() + .Be(expectedEndedDateTime); + return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasStartedDateTime(string expectedStartedDateTime) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasInDomain(int seriesIndex, string? expectedInDomain) { - Assert.Equal(expectedStartedDateTime, FirstTimeSeriesPeriodElement().GetProperty("timeInterval").GetProperty("start").GetProperty("value").GetString()); + if (expectedInDomain is null) + { + GetTimeSeriesElement(seriesIndex) + .TryGetProperty("in_Domain.mRID", out _) + .Should() + .BeFalse("property 'in_Domain.mRID' should not be present"); + + return this; + } + + GetTimeSeriesElement(seriesIndex) + .GetProperty("in_Domain.mRID") + .GetString() + .Should() + .Be(expectedInDomain); + return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasEndedDateTime(string expectedEndedDateTime) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasOutDomain(int seriesIndex, string? expectedOutDomain) { - Assert.Equal(expectedEndedDateTime, FirstTimeSeriesPeriodElement().GetProperty("timeInterval").GetProperty("end").GetProperty("value").GetString()); + if (expectedOutDomain is null) + { + GetTimeSeriesElement(seriesIndex) + .TryGetProperty("out_Domain.mRID", out _) + .Should() + .BeFalse("property 'out_Domain.mRID' should not be present"); + + return this; + } + + GetTimeSeriesElement(seriesIndex) + .GetProperty("out_Domain.mRID") + .GetString() + .Should() + .Be(expectedOutDomain); + return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasPoints(IReadOnlyList expectedPoints) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasPoints( + int seriesIndex, + IReadOnlyList expectedPoints) { - var points = FirstTimeSeriesPeriodElement().GetProperty("Point").EnumerateArray().ToList(); - Assert.Equal(expectedPoints.Count, points.Count); + var points = GetTimeSeriesPeriodElement(seriesIndex).GetProperty("Point").EnumerateArray().ToList(); + + points.Should().HaveCount(expectedPoints.Count); for (var i = 0; i < expectedPoints.Count; i++) { - var expectedPoint = expectedPoints[i]; + var (requiredPointDocumentFields, optionalPointDocumentFields) = expectedPoints[i]; var actualPoint = points[i]; - Assert.Equal(expectedPoint.Position, actualPoint.GetProperty("position").GetProperty("value").GetInt32()); - // Assert.Equal(expectedPoint.Quality, actualPoint.TryGetProperty("quality", out var quality) ? quality.GetProperty("value").GetString() : null); - Assert.Equal(expectedPoint.Quantity, actualPoint.TryGetProperty("quantity", out var quantity) ? quantity.GetInt32() : null); + actualPoint.GetProperty("position") + .GetProperty("value") + .GetInt32() + .Should() + .Be(requiredPointDocumentFields.Position); + + if (optionalPointDocumentFields.Quality != null) + { + actualPoint.GetProperty("quality") + .GetProperty("value") + .GetString() + .Should() + .Be(optionalPointDocumentFields.Quality); + } + else + { + AssertPropertyNotPresent(actualPoint, "quality"); + } + + if (optionalPointDocumentFields.Quantity != null) + { + actualPoint.GetProperty("quantity").GetDecimal().Should().Be(optionalPointDocumentFields.Quantity); + } + else + { + AssertPropertyNotPresent(actualPoint, "quantity"); + } } return this; + + void AssertPropertyNotPresent(JsonElement actualPoint, string propertyName) + { + actualPoint.TryGetProperty(propertyName, out _) + .Should() + .BeFalse($"property '{propertyName}' should not be present"); + } } public async Task DocumentIsValidAsync() { var schema = await _schemas.GetSchemaAsync("NOTIFYVALIDATEDMEASUREDATA", "0", CancellationToken.None).ConfigureAwait(false); var validationResult = IsValid(_document, schema!); - validationResult.IsValid.Should().BeTrue(string.Join("\n", validationResult.Errors)); + validationResult.IsValid.Should() + .BeTrue( + $"the following errors were unexpected:\n\n{string.Join("\n", validationResult.Errors)}\n\nfor the document\n\n{_document.RootElement}"); + return this; } @@ -176,40 +373,33 @@ public async Task Documen var result = schema.Evaluate(jsonDocument, new EvaluationOptions() { OutputFormat = OutputFormat.Hierarchical, }); if (result.IsValid == false) { - errors.Add(FindErrorsForInvalidEvaluation(result)); + errors.AddRange(FindErrorsForInvalidEvaluation(result).Where(e => !string.IsNullOrEmpty(e))); } return (result.IsValid, errors); } - private string FindErrorsForInvalidEvaluation(EvaluationResults result) + private IEnumerable FindErrorsForInvalidEvaluation(EvaluationResults result) { - if (!result.IsValid) + var errors = new List(); + + if (result is { IsValid: false, Errors: not null }) { - foreach (var detail in result.Details) - { - return FindErrorsForInvalidEvaluation(detail); - } + var propertyName = result.InstanceLocation.ToString(); + errors.AddRange(result.Errors.Select(error => $"{propertyName}: {error}")); } - if (!result.HasErrors || result.Errors == null) return string.Empty; - - var propertyName = result.InstanceLocation.ToString(); - foreach (var error in result.Errors) + foreach (var detail in result.Details) { - return $"{propertyName}: {error}"; + errors.AddRange(FindErrorsForInvalidEvaluation(detail)); } - return string.Empty; + return errors; } - private JsonElement FirstTimeSeriesElement() - { - return _root.GetProperty("Series").EnumerateArray().ToList()[0]; - } + private JsonElement GetTimeSeriesElement(int seriesIndex) => + _root.GetProperty("Series").EnumerateArray().ToList()[seriesIndex - 1]; - private JsonElement FirstTimeSeriesPeriodElement() - { - return _root.GetProperty("Series").EnumerateArray().ToList()[0].GetProperty("Period"); - } + private JsonElement GetTimeSeriesPeriodElement(int seriesIndex) => + _root.GetProperty("Series").EnumerateArray().ToList()[seriesIndex - 1].GetProperty("Period"); } diff --git a/source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertMeteredDateForMeasurementPointXmlDocument.cs b/source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertMeteredDateForMeasurementPointXmlDocument.cs index eb2f320a97..110447d7a1 100644 --- a/source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertMeteredDateForMeasurementPointXmlDocument.cs +++ b/source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertMeteredDateForMeasurementPointXmlDocument.cs @@ -13,7 +13,6 @@ // limitations under the License. using Energinet.DataHub.EDI.BuildingBlocks.Domain.Models; -using Energinet.DataHub.EDI.OutgoingMessages.Domain.DocumentWriters.RSM012; using Energinet.DataHub.EDI.Tests.Infrastructure.OutgoingMessages.Asserts; using FluentAssertions; @@ -29,7 +28,7 @@ public AssertMeteredDateForMeasurementPointXmlDocument(AssertXmlDocument documen _documentAsserter.HasValue("type", "E66"); } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasMessageId(string expectedMessageId) + public IAssertMeteredDateForMeasurementPointDocumentDocument MessageIdExists() { _documentAsserter.ElementExists("mRID"); return this; @@ -73,109 +72,203 @@ public IAssertMeteredDateForMeasurementPointDocumentDocument HasTimestamp(string return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasTransactionId(TransactionId expectedTransactionId) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasBusinessSectorType( + string? expectedBusinessSectorType) { - _documentAsserter.HasValue("Series[1]/mRID", expectedTransactionId.Value); + if (expectedBusinessSectorType == null) + { + _documentAsserter.IsNotPresent("businessSector.type"); + } + else + { + _documentAsserter.HasValue("businessSector.type", expectedBusinessSectorType); + } + + return this; + } + + public IAssertMeteredDateForMeasurementPointDocumentDocument HasTransactionId( + int seriesIndex, + TransactionId expectedTransactionId) + { + _documentAsserter.HasValue($"Series[{seriesIndex}]/mRID", expectedTransactionId.Value); return this; } public IAssertMeteredDateForMeasurementPointDocumentDocument HasMeteringPointNumber( + int seriesIndex, string expectedMeteringPointNumber, string expectedSchemeCode) { - _documentAsserter.HasValue("Series[1]/marketEvaluationPoint.mRID", expectedMeteringPointNumber); + _documentAsserter.HasValue($"Series[{seriesIndex}]/marketEvaluationPoint.mRID", expectedMeteringPointNumber); return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasMeteringPointType(string expectedMeteringPointType) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasMeteringPointType( + int seriesIndex, + string expectedMeteringPointType) { - _documentAsserter.HasValue("Series[1]/marketEvaluationPoint.type", expectedMeteringPointType); + _documentAsserter.HasValue($"Series[{seriesIndex}]/marketEvaluationPoint.type", expectedMeteringPointType); return this; } public IAssertMeteredDateForMeasurementPointDocumentDocument HasOriginalTransactionIdReferenceId( + int seriesIndex, string? expectedOriginalTransactionIdReferenceId) { if (expectedOriginalTransactionIdReferenceId == null) { - _documentAsserter.IsNotPresent("Series[1]/originalTransactionIDReference_Series.mRID"); + _documentAsserter.IsNotPresent($"Series[{seriesIndex}]/originalTransactionIDReference_Series.mRID"); } else { _documentAsserter.HasValue( - "Series[1]/originalTransactionIDReference_Series.mRID", + $"Series[{seriesIndex}]/originalTransactionIDReference_Series.mRID", expectedOriginalTransactionIdReferenceId); } return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasProduct(string expectedProduct) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasProduct(int seriesIndex, string? expectedProduct) { - _documentAsserter.HasValue("Series[1]/product", expectedProduct); + if (expectedProduct is null) + { + _documentAsserter.IsNotPresent($"Series[{seriesIndex}]/product"); + return this; + } + + _documentAsserter.HasValue($"Series[{seriesIndex}]/product", expectedProduct); return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasQuantityMeasureUnit(string expectedQuantityMeasureUnit) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasQuantityMeasureUnit( + int seriesIndex, + string expectedQuantityMeasureUnit) { - _documentAsserter.HasValue("Series[1]/quantity_Measure_Unit.name", expectedQuantityMeasureUnit); + _documentAsserter.HasValue($"Series[{seriesIndex}]/quantity_Measure_Unit.name", expectedQuantityMeasureUnit); return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasRegistrationDateTime(string expectedRegistrationDateTime) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasRegistrationDateTime( + int seriesIndex, + string? expectedRegistrationDateTime) { - _documentAsserter.HasValue("Series[1]/registration_DateAndOrTime.dateTime", expectedRegistrationDateTime); + if (expectedRegistrationDateTime is null) + { + _documentAsserter.IsNotPresent($"Series[{seriesIndex}]/registration_DateAndOrTime.dateTime"); + return this; + } + + _documentAsserter.HasValue( + $"Series[{seriesIndex}]/registration_DateAndOrTime.dateTime", + expectedRegistrationDateTime); return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasResolution(string expectedResolution) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasResolution( + int seriesIndex, + string expectedResolution) { - _documentAsserter.HasValue("Series[1]/Period/resolution", expectedResolution); + _documentAsserter.HasValue($"Series[{seriesIndex}]/Period/resolution", expectedResolution); return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasStartedDateTime(string expectedStartedDateTime) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasStartedDateTime( + int seriesIndex, + string expectedStartedDateTime) { _documentAsserter - .HasValue("Series[1]/Period/timeInterval/start", expectedStartedDateTime); + .HasValue($"Series[{seriesIndex}]/Period/timeInterval/start", expectedStartedDateTime); return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasEndedDateTime(string expectedEndedDateTime) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasEndedDateTime( + int seriesIndex, + string expectedEndedDateTime) { _documentAsserter - .HasValue("Series[1]/Period/timeInterval/end", expectedEndedDateTime); + .HasValue($"Series[{seriesIndex}]/Period/timeInterval/end", expectedEndedDateTime); return this; } - public IAssertMeteredDateForMeasurementPointDocumentDocument HasPoints(IReadOnlyList expectedPoints) + public IAssertMeteredDateForMeasurementPointDocumentDocument HasInDomain(int seriesIndex, string? expectedInDomain) + { + if (expectedInDomain is null) + { + _documentAsserter.IsNotPresent($"Series[{seriesIndex}]/in_Domain.mRID"); + return this; + } + + _documentAsserter.HasValue($"Series[{seriesIndex}]/in_Domain.mRID", expectedInDomain); + + return this; + } + + public IAssertMeteredDateForMeasurementPointDocumentDocument HasOutDomain( + int seriesIndex, + string? expectedOutDomain) + { + if (expectedOutDomain is null) + { + _documentAsserter.IsNotPresent($"Series[{seriesIndex}]/out_Domain.mRID"); + return this; + } + + _documentAsserter.HasValue($"Series[{seriesIndex}]/out_Domain.mRID", expectedOutDomain); + + return this; + } + + public IAssertMeteredDateForMeasurementPointDocumentDocument HasPoints( + int seriesIndex, + IReadOnlyList expectedPoints) { var pointsInDocument = _documentAsserter - .GetElements("Series[1]/Period/Point")!; + .GetElements($"Series[{seriesIndex}]/Period/Point")!; pointsInDocument.Should().HaveSameCount(expectedPoints); for (var i = 0; i < expectedPoints.Count; i++) { - var expectedPoint = expectedPoints[i]; + var (requiredPointDocumentFields, optionalPointDocumentFields) = expectedPoints[i]; _documentAsserter - .HasValue($"Series[1]/Period/Point[{i + 1}]/position", expectedPoint.Position.ToString()); + .HasValue( + $"Series[{seriesIndex}]/Period/Point[{i + 1}]/position", + requiredPointDocumentFields.Position.ToString()); - if (expectedPoint.Quantity != null) + if (optionalPointDocumentFields.Quantity != null) { _documentAsserter - .HasValue($"Series[1]/Period/Point[{i + 1}]/quantity", expectedPoint.Quantity!.ToString()!); + .HasValue( + $"Series[{seriesIndex}]/Period/Point[{i + 1}]/quantity", + optionalPointDocumentFields.Quantity!.ToString()!); + } + else + { + AssertElementNotPresent($"Series[{seriesIndex}]/Period/Point[{i + 1}]/quantity"); } - if (expectedPoint.Quality != null) + if (optionalPointDocumentFields.Quality != null) { _documentAsserter - .HasValue($"Series[1]/Period/Point[{i + 1}]/quality", expectedPoint.Quality); + .HasValue( + $"Series[{seriesIndex}]/Period/Point[{i + 1}]/quality", + optionalPointDocumentFields.Quality); + } + else + { + AssertElementNotPresent($"Series[{seriesIndex}]/Period/Point[{i + 1}]/quality"); } } return this; + + void AssertElementNotPresent(string xpath) + { + _documentAsserter.IsNotPresent(xpath); + } } public async Task DocumentIsValidAsync() diff --git a/source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertPointDocumentFieldsInput.cs b/source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertPointDocumentFieldsInput.cs new file mode 100644 index 0000000000..0c08cb5a77 --- /dev/null +++ b/source/Tests/Infrastructure/OutgoingMessages/RSM012/AssertPointDocumentFieldsInput.cs @@ -0,0 +1,19 @@ +// Copyright 2020 Energinet DataHub A/S +// +// Licensed under the Apache License, Version 2.0 (the "License2"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Energinet.DataHub.EDI.Tests.Infrastructure.OutgoingMessages.RSM012; + +public sealed record AssertPointDocumentFieldsInput( + RequiredPointDocumentFields Required, + OptionalPointDocumentFields Optional); diff --git a/source/Tests/Infrastructure/OutgoingMessages/RSM012/IAssertMeteredDateForMeasurementPointDocumentDocument.cs b/source/Tests/Infrastructure/OutgoingMessages/RSM012/IAssertMeteredDateForMeasurementPointDocumentDocument.cs index c3768bc936..37c0d27591 100644 --- a/source/Tests/Infrastructure/OutgoingMessages/RSM012/IAssertMeteredDateForMeasurementPointDocumentDocument.cs +++ b/source/Tests/Infrastructure/OutgoingMessages/RSM012/IAssertMeteredDateForMeasurementPointDocumentDocument.cs @@ -13,13 +13,12 @@ // limitations under the License. using Energinet.DataHub.EDI.BuildingBlocks.Domain.Models; -using Energinet.DataHub.EDI.OutgoingMessages.Domain.DocumentWriters.RSM012; namespace Energinet.DataHub.EDI.Tests.Infrastructure.OutgoingMessages.RSM012; public interface IAssertMeteredDateForMeasurementPointDocumentDocument { - IAssertMeteredDateForMeasurementPointDocumentDocument HasMessageId(string expectedMessageId); + IAssertMeteredDateForMeasurementPointDocumentDocument MessageIdExists(); IAssertMeteredDateForMeasurementPointDocumentDocument HasBusinessReason(string expectedBusinessReasonCode); @@ -33,27 +32,56 @@ public interface IAssertMeteredDateForMeasurementPointDocumentDocument IAssertMeteredDateForMeasurementPointDocumentDocument HasTimestamp(string expectedTimestamp); - IAssertMeteredDateForMeasurementPointDocumentDocument HasTransactionId(TransactionId expectedTransactionId); + IAssertMeteredDateForMeasurementPointDocumentDocument HasBusinessSectorType(string? expectedBusinessSectorType); - IAssertMeteredDateForMeasurementPointDocumentDocument HasMeteringPointNumber(string expectedMeteringPointNumber, string expectedSchemeCode); + IAssertMeteredDateForMeasurementPointDocumentDocument HasTransactionId( + int seriesIndex, + TransactionId expectedTransactionId); - IAssertMeteredDateForMeasurementPointDocumentDocument HasMeteringPointType(string expectedMeteringPointType); + IAssertMeteredDateForMeasurementPointDocumentDocument HasMeteringPointNumber( + int seriesIndex, + string expectedMeteringPointNumber, + string expectedSchemeCode); - IAssertMeteredDateForMeasurementPointDocumentDocument HasOriginalTransactionIdReferenceId(string? expectedOriginalTransactionIdReferenceId); + IAssertMeteredDateForMeasurementPointDocumentDocument HasMeteringPointType( + int seriesIndex, + string expectedMeteringPointType); - IAssertMeteredDateForMeasurementPointDocumentDocument HasProduct(string expectedProduct); + IAssertMeteredDateForMeasurementPointDocumentDocument HasOriginalTransactionIdReferenceId( + int seriesIndex, + string? expectedOriginalTransactionIdReferenceId); - IAssertMeteredDateForMeasurementPointDocumentDocument HasQuantityMeasureUnit(string expectedQuantityMeasureUnit); + IAssertMeteredDateForMeasurementPointDocumentDocument HasProduct(int seriesIndex, string? expectedProduct); - IAssertMeteredDateForMeasurementPointDocumentDocument HasRegistrationDateTime(string expectedRegistrationDateTime); + IAssertMeteredDateForMeasurementPointDocumentDocument HasQuantityMeasureUnit( + int seriesIndex, + string expectedQuantityMeasureUnit); - IAssertMeteredDateForMeasurementPointDocumentDocument HasResolution(string expectedResolution); + IAssertMeteredDateForMeasurementPointDocumentDocument HasRegistrationDateTime( + int seriesIndex, + string? expectedRegistrationDateTime); - IAssertMeteredDateForMeasurementPointDocumentDocument HasStartedDateTime(string expectedStartedDateTime); + IAssertMeteredDateForMeasurementPointDocumentDocument HasResolution(int seriesIndex, string expectedResolution); - IAssertMeteredDateForMeasurementPointDocumentDocument HasEndedDateTime(string expectedEndedDateTime); + IAssertMeteredDateForMeasurementPointDocumentDocument HasStartedDateTime( + int seriesIndex, + string expectedStartedDateTime); - IAssertMeteredDateForMeasurementPointDocumentDocument HasPoints(IReadOnlyList expectedPoints); + IAssertMeteredDateForMeasurementPointDocumentDocument HasEndedDateTime( + int seriesIndex, + string expectedEndedDateTime); + + IAssertMeteredDateForMeasurementPointDocumentDocument HasInDomain( + int seriesIndex, + string? expectedInDomain); + + IAssertMeteredDateForMeasurementPointDocumentDocument HasOutDomain( + int seriesIndex, + string? expectedOutDomain); + + IAssertMeteredDateForMeasurementPointDocumentDocument HasPoints( + int seriesIndex, + IReadOnlyList expectedPoints); /// /// Asserts document validity diff --git a/source/Tests/Infrastructure/OutgoingMessages/RSM012/MeteredDateForMeasurementPointDocumentWriterTests.cs b/source/Tests/Infrastructure/OutgoingMessages/RSM012/MeteredDateForMeasurementPointDocumentWriterTests.cs index 199aff0bd4..9172e38224 100644 --- a/source/Tests/Infrastructure/OutgoingMessages/RSM012/MeteredDateForMeasurementPointDocumentWriterTests.cs +++ b/source/Tests/Infrastructure/OutgoingMessages/RSM012/MeteredDateForMeasurementPointDocumentWriterTests.cs @@ -40,7 +40,7 @@ public class MeteredDateForMeasurementPointDocumentWriterTests(DocumentValidatio [Theory] [InlineData(nameof(DocumentFormat.Xml))] [InlineData(nameof(DocumentFormat.Json))] - public async Task Can_create_notifyValidatedMeasureData_document(string documentFormat) + public async Task Can_create_maximal_notifyValidatedMeasureData_document(string documentFormat) { // Arrange var messageBuilder = _meteredDateForMeasurementPointBuilder; @@ -54,44 +54,137 @@ public async Task Can_create_notifyValidatedMeasureData_document(string document // Assert using var assertionScope = new AssertionScope(); await AssertDocument(document, DocumentFormat.FromName(documentFormat)) - .HasMessageId(SampleData.MessageId) + .MessageIdExists() + .HasBusinessReason(SampleData.BusinessReason.Code) + .HasSenderId(SampleData.SenderActorNumber, "A10") + .HasSenderRole(SampleData.SenderActorRole) + .HasReceiverId(SampleData.ReceiverActorNumber, "A10") + .HasReceiverRole(SampleData.ReceiverActorRole) + .HasTimestamp(SampleData.TimeStamp.ToString()) + .HasTransactionId(1, SampleData.TransactionId) + .HasMeteringPointNumber(1, SampleData.MeteringPointNumber, "A10") + .HasMeteringPointType(1, SampleData.MeteringPointType) + .HasOriginalTransactionIdReferenceId(1, SampleData.OriginalTransactionIdReferenceId?.Value) + .HasProduct(1, SampleData.Product) + .HasQuantityMeasureUnit(1, SampleData.QuantityMeasureUnit.Code) + .HasRegistrationDateTime(1, SampleData.RegistrationDateTime.ToString()) + .HasResolution(1, SampleData.Resolution.Code) + .HasStartedDateTime( + 1, + SampleData.StartedDateTime.ToString("yyyy-MM-dd'T'HH:mm'Z'", CultureInfo.InvariantCulture)) + .HasEndedDateTime( + 1, + SampleData.EndedDateTime.ToString("yyyy-MM-dd'T'HH:mm'Z'", CultureInfo.InvariantCulture)) + .HasPoints( + 1, + SampleData.Points.Select( + p => new AssertPointDocumentFieldsInput( + new RequiredPointDocumentFields(p.Position), + new OptionalPointDocumentFields(p.Quality, p.Quantity))) + .ToList()) + .DocumentIsValidAsync(); + } + + [Theory] + [InlineData(nameof(DocumentFormat.Xml))] + [InlineData(nameof(DocumentFormat.Json))] + public async Task Can_create_minimal_series_notifyValidatedMeasureData_document(string documentFormat) + { + // Arrange + var messageBuilder = _meteredDateForMeasurementPointBuilder; + + // Act + var document = await WriteDocument( + messageBuilder.BuildHeader(), + messageBuilder.BuildMinimalMeteredDataForMeasurementPoint(), + DocumentFormat.FromName(documentFormat)); + + // Assert + using var assertionScope = new AssertionScope(); + await AssertDocument(document, DocumentFormat.FromName(documentFormat)) + .MessageIdExists() + .HasBusinessReason(SampleData.BusinessReason.Code) + .HasSenderId(SampleData.SenderActorNumber, "A10") + .HasSenderRole(SampleData.SenderActorRole) + .HasReceiverId(SampleData.ReceiverActorNumber, "A10") + .HasReceiverRole(SampleData.ReceiverActorRole) + .HasTimestamp(SampleData.TimeStamp.ToString()) + .HasTransactionId(1, SampleData.TransactionId) + .HasMeteringPointNumber(1, SampleData.MeteringPointNumber, "A10") + .HasMeteringPointType(1, SampleData.MeteringPointType) + .HasOriginalTransactionIdReferenceId(1, null) + .HasProduct(1, null) + .HasQuantityMeasureUnit(1, SampleData.QuantityMeasureUnit.Code) + .HasRegistrationDateTime(1, null) + .HasResolution(1, SampleData.Resolution.Code) + .HasStartedDateTime( + 1, + SampleData.StartedDateTime.ToString("yyyy-MM-dd'T'HH:mm'Z'", CultureInfo.InvariantCulture)) + .HasEndedDateTime( + 1, + SampleData.EndedDateTime.ToString("yyyy-MM-dd'T'HH:mm'Z'", CultureInfo.InvariantCulture)) + .HasPoints( + 1, + SampleData.MinimalPoints.Select( + p => new AssertPointDocumentFieldsInput( + new RequiredPointDocumentFields(p.Position), + OptionalPointDocumentFields.NoOptionalFields())) + .ToList()) + .DocumentIsValidAsync(); + } + + [Theory] + [InlineData(nameof(DocumentFormat.Xml))] + [InlineData(nameof(DocumentFormat.Json))] + /* + * We do not expect that we ever have to create any of these, + * but in case something goes wrong, we need to be able to create them, + * in order for the actors to empty their queues + */ + public async Task Can_create_no_series_notifyValidatedMeasureData_document(string documentFormat) + { + // Arrange + var messageBuilder = _meteredDateForMeasurementPointBuilder; + + // Act + var document = await WriteDocument( + messageBuilder.BuildHeader(), + null, + DocumentFormat.FromName(documentFormat)); + + // Assert + using var assertionScope = new AssertionScope(); + await AssertDocument(document, DocumentFormat.FromName(documentFormat)) + .MessageIdExists() .HasBusinessReason(SampleData.BusinessReason.Code) .HasSenderId(SampleData.SenderActorNumber, "A10") .HasSenderRole(SampleData.SenderActorRole) .HasReceiverId(SampleData.ReceiverActorNumber, "A10") .HasReceiverRole(SampleData.ReceiverActorRole) .HasTimestamp(SampleData.TimeStamp.ToString()) - .HasTransactionId(SampleData.TransactionId) - .HasMeteringPointNumber(SampleData.MeteringPointNumber, "A10") - .HasMeteringPointType(SampleData.MeteringPointType) - .HasOriginalTransactionIdReferenceId(SampleData.OriginalTransactionIdReferenceId?.Value) - .HasProduct(SampleData.Product) - .HasQuantityMeasureUnit(SampleData.QuantityMeasureUnit.Code) - .HasRegistrationDateTime(SampleData.RegistrationDateTime.ToString()) - .HasResolution(SampleData.Resolution.Code) - .HasStartedDateTime(SampleData.StartedDateTime.ToString("yyyy-MM-dd'T'HH:mm'Z'", CultureInfo.InvariantCulture)) - .HasEndedDateTime(SampleData.EndedDateTime.ToString("yyyy-MM-dd'T'HH:mm'Z'", CultureInfo.InvariantCulture)) - .HasPoints(SampleData.Points) .DocumentIsValidAsync(); } private Task WriteDocument( OutgoingMessageHeader header, - MeteredDateForMeasurementPointMarketActivityRecord meteredDateForMeasurementPointMarketActivityRecord, + MeteredDateForMeasurementPointMarketActivityRecord? meteredDateForMeasurementPointMarketActivityRecord, DocumentFormat documentFormat) { - var records = _parser.From(meteredDateForMeasurementPointMarketActivityRecord); + var records = meteredDateForMeasurementPointMarketActivityRecord is null + ? null + : _parser.From(meteredDateForMeasurementPointMarketActivityRecord); if (documentFormat == DocumentFormat.Xml) { - return new MeteredDateForMeasurementPointCimXmlDocumentWriter(_parser).WriteAsync(header, new[] { records }); + return new MeteredDateForMeasurementPointCimXmlDocumentWriter(_parser) + .WriteAsync(header, records is null ? [] : [records]); } var serviceProvider = new ServiceCollection().AddJavaScriptEncoder().BuildServiceProvider(); return new MeteredDateForMeasurementPointCimJsonDocumentWriter( _parser, serviceProvider.GetRequiredService()) - .WriteAsync(header, [records], CancellationToken.None); + .WriteAsync(header, records is null ? [] : [records], CancellationToken.None); } private IAssertMeteredDateForMeasurementPointDocumentDocument AssertDocument( diff --git a/source/Tests/Infrastructure/OutgoingMessages/RSM012/OptionalPointDocumentFields.cs b/source/Tests/Infrastructure/OutgoingMessages/RSM012/OptionalPointDocumentFields.cs new file mode 100644 index 0000000000..294d093be5 --- /dev/null +++ b/source/Tests/Infrastructure/OutgoingMessages/RSM012/OptionalPointDocumentFields.cs @@ -0,0 +1,20 @@ +// Copyright 2020 Energinet DataHub A/S +// +// Licensed under the Apache License, Version 2.0 (the "License2"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Energinet.DataHub.EDI.Tests.Infrastructure.OutgoingMessages.RSM012; + +public sealed record OptionalPointDocumentFields(string? Quality, decimal? Quantity) +{ + public static OptionalPointDocumentFields NoOptionalFields() => new(null, null); +} diff --git a/source/Tests/Infrastructure/OutgoingMessages/RSM012/RequiredPointDocumentFields.cs b/source/Tests/Infrastructure/OutgoingMessages/RSM012/RequiredPointDocumentFields.cs new file mode 100644 index 0000000000..d511caa3c4 --- /dev/null +++ b/source/Tests/Infrastructure/OutgoingMessages/RSM012/RequiredPointDocumentFields.cs @@ -0,0 +1,17 @@ +// Copyright 2020 Energinet DataHub A/S +// +// Licensed under the Apache License, Version 2.0 (the "License2"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Energinet.DataHub.EDI.Tests.Infrastructure.OutgoingMessages.RSM012; + +public readonly record struct RequiredPointDocumentFields(int Position); diff --git a/source/Tests/Infrastructure/OutgoingMessages/RSM012/SampleData.cs b/source/Tests/Infrastructure/OutgoingMessages/RSM012/SampleData.cs index 140da37d15..06a118b446 100644 --- a/source/Tests/Infrastructure/OutgoingMessages/RSM012/SampleData.cs +++ b/source/Tests/Infrastructure/OutgoingMessages/RSM012/SampleData.cs @@ -71,5 +71,7 @@ internal static class SampleData new(6, "A02", null), }; + public static IReadOnlyList MinimalPoints => [new(2, null, null)]; + #endregion } From 9e0e5c661aab28c781af977a7d8d37c55d1e8b3d Mon Sep 17 00:00:00 2001 From: Mads Magnus Due Date: Fri, 13 Dec 2024 12:20:28 +0100 Subject: [PATCH 4/5] fix: improve perf for checking duplicated transactionId in incoming message (#1419) * fix: improve perf for checking duplicated transactionId in incoming message --- .../UseCases/ValidateIncomingMessage.cs | 33 ++++++--------- .../TransactionId/ITransactionIdRepository.cs | 6 +-- .../TransactionId/TransactionIdRepository.cs | 33 +++++++++------ ...ngMeteredDataForMeasurementMessageTests.cs | 42 +++++++++++++++++++ 4 files changed, 78 insertions(+), 36 deletions(-) diff --git a/source/IncomingMessages.Application/UseCases/ValidateIncomingMessage.cs b/source/IncomingMessages.Application/UseCases/ValidateIncomingMessage.cs index f54feb791b..6eccc9c000 100644 --- a/source/IncomingMessages.Application/UseCases/ValidateIncomingMessage.cs +++ b/source/IncomingMessages.Application/UseCases/ValidateIncomingMessage.cs @@ -171,12 +171,9 @@ private async Task> CheckTransactionIdsAsyn { var transactionId = series.TransactionId; - var errorsForSeries = await CheckTransactionIdAsync( + var errorsForSeries = CheckTransactionId( transactionId, - message.SenderNumber, - transactionIdsToBeStored, - cancellationToken) - .ConfigureAwait(false); + transactionIdsToBeStored); if (errorsForSeries is null) { @@ -188,34 +185,28 @@ private async Task> CheckTransactionIdsAsyn } } + var duplicatedTransactionIds = await transactionIdRepository + .GetDuplicatedTransactionIdsAsync(message.SenderNumber, transactionIdsToBeStored, cancellationToken) + .ConfigureAwait(false); + foreach (var duplicatedTransactionId in duplicatedTransactionIds) + { + errors.Add(new DuplicateTransactionIdDetected(duplicatedTransactionId)); + } + return errors; } - private async Task CheckTransactionIdAsync( + private ValidationError? CheckTransactionId( string transactionId, - string senderNumber, - IReadOnlyCollection transactionIdsToBeStored, - CancellationToken cancellationToken) + IReadOnlyCollection transactionIdsToBeStored) { return transactionId switch { _ when string.IsNullOrEmpty(transactionId) => new EmptyTransactionId(), _ when transactionId.Length > MaxTransactionIdLength => new InvalidTransactionIdSize(transactionId), - _ when await TransactionIdIsDuplicatedAsync(senderNumber, transactionId, cancellationToken) - .ConfigureAwait(false) => new DuplicateTransactionIdDetected(transactionId), _ when transactionIdsToBeStored.Contains(transactionId) => new DuplicateTransactionIdDetected(transactionId), _ => null, }; } - - private async Task TransactionIdIsDuplicatedAsync( - string senderNumber, - string transactionId, - CancellationToken cancellationToken) - { - return await transactionIdRepository - .TransactionIdExistsAsync(senderNumber, transactionId, cancellationToken) - .ConfigureAwait(false); - } } diff --git a/source/IncomingMessages.Infrastructure/Repositories/TransactionId/ITransactionIdRepository.cs b/source/IncomingMessages.Infrastructure/Repositories/TransactionId/ITransactionIdRepository.cs index 584da8d517..5c48bc8098 100644 --- a/source/IncomingMessages.Infrastructure/Repositories/TransactionId/ITransactionIdRepository.cs +++ b/source/IncomingMessages.Infrastructure/Repositories/TransactionId/ITransactionIdRepository.cs @@ -20,12 +20,12 @@ namespace Energinet.DataHub.EDI.IncomingMessages.Infrastructure.Repositories.Tra public interface ITransactionIdRepository { /// - /// Checks if is already registered by the sender + /// Returns a list of existing if they already is registered by the sender /// /// - /// + /// /// - Task TransactionIdExistsAsync(string senderId, string transactionId, CancellationToken cancellationToken); + Task> GetDuplicatedTransactionIdsAsync(string senderId, IReadOnlyCollection transactionIds, CancellationToken cancellationToken); /// /// Store transaction ids for the specified sender diff --git a/source/IncomingMessages.Infrastructure/Repositories/TransactionId/TransactionIdRepository.cs b/source/IncomingMessages.Infrastructure/Repositories/TransactionId/TransactionIdRepository.cs index e5785c196c..5cb215e0f4 100644 --- a/source/IncomingMessages.Infrastructure/Repositories/TransactionId/TransactionIdRepository.cs +++ b/source/IncomingMessages.Infrastructure/Repositories/TransactionId/TransactionIdRepository.cs @@ -26,15 +26,17 @@ public TransactionIdRepository(IncomingMessagesContext incomingMessagesContext) _incomingMessagesContext = incomingMessagesContext; } - public async Task TransactionIdExistsAsync( + public async Task> GetDuplicatedTransactionIdsAsync( string senderId, - string transactionId, + IReadOnlyCollection transactionIds, CancellationToken cancellationToken) { - var transaction = await GetTransactionFromDbAsync(senderId, transactionId, cancellationToken).ConfigureAwait(false) - ?? GetTransactionFromInMemoryCollection(senderId, transactionId); + var duplicatedTransactionIdsForSender = await GetDuplicatedTransactionsFromDbAsync(senderId, transactionIds, cancellationToken) + .ConfigureAwait(false); + if (!transactionIds.Any()) + duplicatedTransactionIdsForSender = GetDuplicatedTransactionsFromInMemoryCollection(senderId, transactionIds); - return transaction != null; + return duplicatedTransactionIdsForSender.Select(x => x.TransactionId).ToList(); } public async Task AddAsync( @@ -50,19 +52,26 @@ public async Task AddAsync( } } - private TransactionIdForSender? GetTransactionFromInMemoryCollection(string senderId, string transactionId) + private IReadOnlyList GetDuplicatedTransactionsFromInMemoryCollection( + string senderId, + IReadOnlyCollection transactionIds) { return _incomingMessagesContext.TransactionIdForSenders.Local - .FirstOrDefault(x => x.TransactionId == transactionId && x.SenderId == senderId); + .Where( + transactionIdForSender => transactionIds.Contains(transactionIdForSender.TransactionId) + && transactionIdForSender.SenderId == senderId) + .ToList(); } - private async Task GetTransactionFromDbAsync(string senderId, string transactionId, CancellationToken cancellationToken) + private async Task> GetDuplicatedTransactionsFromDbAsync( + string senderId, + IReadOnlyCollection transactionIds, + CancellationToken cancellationToken) { return await _incomingMessagesContext.TransactionIdForSenders - .FirstOrDefaultAsync( - transactionIdForSender => transactionIdForSender.TransactionId == transactionId - && transactionIdForSender.SenderId == senderId, - cancellationToken) + .Where(transactionIdForSender => transactionIds.Contains(transactionIdForSender.TransactionId) + && transactionIdForSender.SenderId == senderId) + .ToListAsync(cancellationToken) .ConfigureAwait(false); } } diff --git a/source/IncomingMessages.IntegrationTests/IncomingMessages/GivenIncomingMeteredDataForMeasurementMessageTests.cs b/source/IncomingMessages.IntegrationTests/IncomingMessages/GivenIncomingMeteredDataForMeasurementMessageTests.cs index 009ed970ec..b8a72fc628 100644 --- a/source/IncomingMessages.IntegrationTests/IncomingMessages/GivenIncomingMeteredDataForMeasurementMessageTests.cs +++ b/source/IncomingMessages.IntegrationTests/IncomingMessages/GivenIncomingMeteredDataForMeasurementMessageTests.cs @@ -157,6 +157,37 @@ public async Task When_MultipleTransactionsWithSameId_Then_ResultContainExcepted result.Errors.Should().Contain(error => error is DuplicateTransactionIdDetected); } + [Fact] + public async Task When_MultipleTransactionsWithSameIdAsExisting_Then_ResultContainExceptedValidationError() + { + var documentFormat = DocumentFormat.Ebix; + var existingTransactionIdForSender = "123456"; + var newTransactionIdForSender = "654321"; + await StoreTransactionIdForActorAsync(existingTransactionIdForSender, _actorIdentity.ActorNumber.Value); + var message = MeteredDataForMeasurementPointBuilder.CreateIncomingMessage( + documentFormat, + _actorIdentity.ActorNumber, + [ + (existingTransactionIdForSender, + Instant.FromUtc(2024, 1, 1, 0, 0), + Instant.FromUtc(2024, 1, 2, 0, 0), + Resolution.QuarterHourly), + (newTransactionIdForSender, + Instant.FromUtc(2024, 1, 1, 0, 0), + Instant.FromUtc(2024, 1, 2, 0, 0), + Resolution.QuarterHourly), + ]); + + var (incomingMessage, _) = await ParseMessageAsync(message.Stream, documentFormat); + var result = await _validateIncomingMessage.ValidateAsync( + incomingMessage!, + documentFormat, + CancellationToken.None); + + result.Success.Should().BeFalse(); + result.Errors.Should().Contain(error => error is DuplicateTransactionIdDetected); + } + [Fact] public async Task When_TransactionIdIsEmpty_Then_ResultContainExceptedValidationError() { @@ -618,6 +649,17 @@ public async Task When_BusinessTypeIsNotAllowed_Then_ExpectedValidationError() throw new NotSupportedException($"No message parser found for message format '{documentFormat}' and document type '{IncomingDocumentType.NotifyValidatedMeasureData}'"); } + private async Task StoreTransactionIdForActorAsync(string existingTransactionIdForSender, string senderActorNumber) + { + var databaseConnectionFactory = GetService(); + using var dbConnection = await databaseConnectionFactory.GetConnectionAndOpenAsync(CancellationToken.None).ConfigureAwait(false); + + await dbConnection.ExecuteAsync( + "INSERT INTO [dbo].[TransactionRegistry] ([TransactionId], [SenderId]) VALUES (@TransactionId, @SenderId)", + new { TransactionId = existingTransactionIdForSender, SenderId = senderActorNumber }) + .ConfigureAwait(false); + } + private async Task StoreMessageIdForActorAsync(string messageId, string senderActorNumber) { var databaseConnectionFactory = GetService(); From 2bb71be5cf8f26574d2f257268211768a7f612ef Mon Sep 17 00:00:00 2001 From: Mads Magnus Due Date: Fri, 13 Dec 2024 13:14:11 +0100 Subject: [PATCH 5/5] test: forward metered data load test (#1421) --- ...ataForMeasurementPointMessageProcessDto.cs | 3 +- ...zeMeteredDataForMeasurementPointHandler.cs | 1 + .../Drivers/EdiDatabaseDriver.cs | 37 +++++++++++ .../LoadTest/ForwardMeteredData.cs | 65 +++++++++++++++++++ .../LoadTest/LoadTestFixture.cs | 3 +- 5 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 source/SubsystemTests/LoadTest/ForwardMeteredData.cs diff --git a/source/OutgoingMessages.Interfaces/Models/MeteredDataForMeasurementPoint/MeteredDataForMeasurementPointMessageProcessDto.cs b/source/OutgoingMessages.Interfaces/Models/MeteredDataForMeasurementPoint/MeteredDataForMeasurementPointMessageProcessDto.cs index 97827a51ce..8197576c72 100644 --- a/source/OutgoingMessages.Interfaces/Models/MeteredDataForMeasurementPoint/MeteredDataForMeasurementPointMessageProcessDto.cs +++ b/source/OutgoingMessages.Interfaces/Models/MeteredDataForMeasurementPoint/MeteredDataForMeasurementPointMessageProcessDto.cs @@ -20,6 +20,7 @@ public sealed class MeteredDataForMeasurementPointMessageProcessDto( EventId eventId, Actor receiver, BusinessReason businessReason, + MessageId relatedToMessageId, MeteredDataForMeasurementPointMessageSeriesDto series) : OutgoingMessageDto( DocumentType.NotifyValidatedMeasureData, @@ -29,7 +30,7 @@ public sealed class MeteredDataForMeasurementPointMessageProcessDto( businessReason.Name, receiver.ActorRole, new ExternalId(Guid.NewGuid()), - null) + relatedToMessageId) { public MeteredDataForMeasurementPointMessageSeriesDto Series { get; } = series; } diff --git a/source/Process.Application/ProcessInitializationHandlers/InitializeMeteredDataForMeasurementPointHandler.cs b/source/Process.Application/ProcessInitializationHandlers/InitializeMeteredDataForMeasurementPointHandler.cs index 1b4b14e1e3..ca92d02645 100644 --- a/source/Process.Application/ProcessInitializationHandlers/InitializeMeteredDataForMeasurementPointHandler.cs +++ b/source/Process.Application/ProcessInitializationHandlers/InitializeMeteredDataForMeasurementPointHandler.cs @@ -61,6 +61,7 @@ await _outgoingMessagesClient.EnqueueAndCommitAsync( EventId.From(Guid.NewGuid()), new Actor(ActorNumber.Create("8100000000115"), ActorRole.EnergySupplier), BusinessReason.FromCode(marketMessage.BusinessReason), + MessageId.Create(marketMessage.MessageId), new MeteredDataForMeasurementPointMessageSeriesDto( TransactionId.From(string.Join(string.Empty, series.TransactionId.Reverse())), series.MeteringPointLocationId!, diff --git a/source/SubsystemTests/Drivers/EdiDatabaseDriver.cs b/source/SubsystemTests/Drivers/EdiDatabaseDriver.cs index 89c62b73ac..ff1f107d1f 100644 --- a/source/SubsystemTests/Drivers/EdiDatabaseDriver.cs +++ b/source/SubsystemTests/Drivers/EdiDatabaseDriver.cs @@ -216,6 +216,29 @@ internal async Task DeleteOutgoingMessagesForCalculationAsync(Guid calculationId } } + /// + /// Delete outgoing messages for previuse performance test. + /// + internal async Task DeleteOutgoingMessagesForFromLoadTestAsync() + { + await using var connection = new SqlConnection(_connectionString); + + await connection.OpenAsync().ConfigureAwait(false); + await using (var deleteOutgoingMessagesCommand = new SqlCommand()) + { + deleteOutgoingMessagesCommand.CommandText = @" + DELETE FROM [MarketDocuments] WHERE BundleId IN (SELECT Id FROM [Bundles] WHERE RelatedToMessageId like 'perf_test_%'); + DELETE FROM [OutgoingMessages] WHERE [AssignedBundleId] = (SELECT Id FROM [Bundles] WHERE RelatedToMessageId like 'perf_test_%'); + DELETE FROM [Bundles] WHERE RelatedToMessageId like 'perf_test_%'; + "; + + deleteOutgoingMessagesCommand.Connection = connection; + deleteOutgoingMessagesCommand.CommandTimeout = (int)TimeSpan.FromMinutes(2).TotalSeconds; + + await deleteOutgoingMessagesCommand.ExecuteNonQueryAsync().ConfigureAwait(false); + } + } + internal async Task<(bool Success, string? Payload)> GetOutboxMessageAsync( Instant createdAfter, @@ -289,6 +312,20 @@ internal async Task CountEnqueuedMessagesForCalculationAsync(Guid calculati return enqueuedMessagesCount; } + internal async Task CountEnqueuedNotifyValidatedMeasureDataMessagesFromLoadTestAsync() + { + await using var connection = new SqlConnection(_connectionString); + + await connection.OpenAsync(); + + var enqueuedMessagesCount = await connection.ExecuteScalarAsync( + sql: @"SELECT COUNT(B.[Id]) FROM [Bundles] + WHERE [DocumentTypeInBundle] = 'NotifyValidatedMeasureData' + AND RelatedToMessageId like 'perf_test_%'"); + + return enqueuedMessagesCount; + } + private async Task GetProcessIdAsync(SqlCommand command, CancellationToken cancellationToken) { await using var connection = new SqlConnection(_connectionString); diff --git a/source/SubsystemTests/LoadTest/ForwardMeteredData.cs b/source/SubsystemTests/LoadTest/ForwardMeteredData.cs new file mode 100644 index 0000000000..f7899ebf91 --- /dev/null +++ b/source/SubsystemTests/LoadTest/ForwardMeteredData.cs @@ -0,0 +1,65 @@ +// Copyright 2020 Energinet DataHub A/S +// +// Licensed under the Apache License, Version 2.0 (the "License2"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Diagnostics.CodeAnalysis; +using Energinet.DataHub.EDI.SubsystemTests.Drivers; +using FluentAssertions; +using FluentAssertions.Execution; +using Xunit.Abstractions; + +namespace Energinet.DataHub.EDI.SubsystemTests.LoadTest; + +/// +/// Test class used in the CI to trigger a calculation completed event, used for load testing on t001. +/// GitHub action should be as following: +/// 1. Run Before_load_test() test +/// 2. Start Azure Load Test +/// 3. Wait for the Azure Load Test to finish +/// 4. Run After_load_test() test +/// +[SuppressMessage("Style", "VSTHRD200:Use \"Async\" suffix for async methods", Justification = "Test class")] +public sealed class ForwardMeteredData : IClassFixture +{ + private const string ForwardMeteredEnqueuedAmountMetric = "ForwardMeteredEnqueuedAmount"; + private readonly LoadTestFixture _fixture; + private readonly ITestOutputHelper _logger; + private readonly EdiDatabaseDriver _ediDatabaseDriver; + + public ForwardMeteredData(LoadTestFixture fixture, ITestOutputHelper logger) + { + _fixture = fixture; + _logger = logger; + _ediDatabaseDriver = new EdiDatabaseDriver(_fixture.DatabaseConnectionString); + } + + [Fact] + public async Task Before_load_test() + { + await _ediDatabaseDriver.DeleteOutgoingMessagesForFromLoadTestAsync(); + } + + [Fact] + public async Task After_load_test() + { + var enqueuedMessagesCount = await _ediDatabaseDriver.CountEnqueuedNotifyValidatedMeasureDataMessagesFromLoadTestAsync(); + _logger.WriteLine($"Enqueued messages count: {enqueuedMessagesCount} (CalculationId={_fixture.LoadTestCalculationId})"); + + _fixture.TelemetryClient.GetMetric(ForwardMeteredEnqueuedAmountMetric).TrackValue(enqueuedMessagesCount); + + using var scope = new AssertionScope(); + enqueuedMessagesCount.Should().BeGreaterThanOrEqualTo( + _fixture.MinimumEnqueuedMessagesCount, + $"because the system should be performant enough to enqueue at least {_fixture.MinimumEnqueuedMessagesCount} messages during the load test"); + } +} diff --git a/source/SubsystemTests/LoadTest/LoadTestFixture.cs b/source/SubsystemTests/LoadTest/LoadTestFixture.cs index 9514c0803c..6f2673ce7c 100644 --- a/source/SubsystemTests/LoadTest/LoadTestFixture.cs +++ b/source/SubsystemTests/LoadTest/LoadTestFixture.cs @@ -61,7 +61,8 @@ public LoadTestFixture() LoadTestCalculationId = GetConfigurationValue( configuration, - "LOAD_TEST_CALCULATION_ID"); + "LOAD_TEST_CALCULATION_ID", + defaultValue: Guid.Empty); MinimumEnqueuedMessagesCount = GetConfigurationValue( configuration,