From c49fd80fa7c4be437efa1a59f7d8cb918baafebc Mon Sep 17 00:00:00 2001 From: Piotr Mazur Date: Mon, 31 Aug 2026 18:58:09 +0200 Subject: [PATCH] feat: add KsefClientRateLimitGroup enum --- .../EffectiveApiRateLimitsExtensions.cs | 70 ++++++++ .../RateLimits/KsefClientRateLimitGroup.cs | 72 ++++++++ .../E2E/Limits/RateLimitsE2ETests.cs | 83 +++------ .../EffectiveApiRateLimitsExtensionsTests.cs | 160 ++++++++++++++++++ 4 files changed, 321 insertions(+), 64 deletions(-) create mode 100644 KSeF.Client.Core/Models/RateLimits/EffectiveApiRateLimitsExtensions.cs create mode 100644 KSeF.Client.Core/Models/RateLimits/KsefClientRateLimitGroup.cs create mode 100644 KSeF.Client.Tests/Models/RateLimits/EffectiveApiRateLimitsExtensionsTests.cs diff --git a/KSeF.Client.Core/Models/RateLimits/EffectiveApiRateLimitsExtensions.cs b/KSeF.Client.Core/Models/RateLimits/EffectiveApiRateLimitsExtensions.cs new file mode 100644 index 0000000..14d5400 --- /dev/null +++ b/KSeF.Client.Core/Models/RateLimits/EffectiveApiRateLimitsExtensions.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; + +namespace KSeF.Client.Core.Models.RateLimits; + +/// +/// Klasa rozszerzająca funkcjonalność poprzez dodanie metod pomocniczych do pobierania limitów dla określonych grup +/// +public static class EffectiveApiRateLimitsExtensions +{ + /// + /// Zwraca wartości limitów dla określonej grupy limitów żądań API KSeF. + /// + /// Obiekt zawierający limity żądań API KSeF. + /// Grupa limitów żądań API KSeF. + /// Obiekt zawierający wartości limitów dla określonej grupy. + /// Wyrzucany, gdy parametr jest null. + /// Wyrzucany, gdy parametr nie jest obsługiwany. + public static EffectiveApiRateLimitValues GetRateLimitValues( + this EffectiveApiRateLimits limits, + KsefClientRateLimitGroup group) + { + if (limits is null) + { + throw new ArgumentNullException(nameof(limits)); + } + + return group switch + { + KsefClientRateLimitGroup.OnlineSession => limits.OnlineSession, + KsefClientRateLimitGroup.BatchSession => limits.BatchSession, + KsefClientRateLimitGroup.InvoiceSend => limits.InvoiceSend, + KsefClientRateLimitGroup.InvoiceStatus => limits.InvoiceStatus, + KsefClientRateLimitGroup.SessionList => limits.SessionList, + KsefClientRateLimitGroup.SessionInvoiceList => limits.SessionInvoiceList, + KsefClientRateLimitGroup.SessionMisc => limits.SessionMisc, + KsefClientRateLimitGroup.InvoiceMetadata => limits.InvoiceMetadata, + KsefClientRateLimitGroup.InvoiceExport => limits.InvoiceExport, + KsefClientRateLimitGroup.InvoiceExportStatus => limits.InvoiceExportStatus, + KsefClientRateLimitGroup.InvoiceDownload => limits.InvoiceDownload, + KsefClientRateLimitGroup.Other => limits.Other, + KsefClientRateLimitGroup.CollectiveIdentifier => limits.CollectiveIdentifier, + _ => throw new ArgumentOutOfRangeException(nameof(group), group, null) + }; + } + + /// + /// Zwraca słownik zawierający wszystkie grupy limitów żądań API KSeF wraz z odpowiadającymi im wartościami limitów. + /// + /// Obiekt zawierający limity żądań API KSeF. + /// Słownik zawierający wszystkie grupy limitów i odpowiadające im wartości. + /// Wyrzucany, gdy parametr jest null. + public static IReadOnlyDictionary GetAllRateLimitValues( + this EffectiveApiRateLimits limits) + { + if (limits is null) + { + throw new ArgumentNullException(nameof(limits)); + } + + Dictionary result = new(); + + foreach (KsefClientRateLimitGroup group in Enum.GetValues(typeof(KsefClientRateLimitGroup))) + { + result[group] = limits.GetRateLimitValues(group); + } + + return result; + } +} diff --git a/KSeF.Client.Core/Models/RateLimits/KsefClientRateLimitGroup.cs b/KSeF.Client.Core/Models/RateLimits/KsefClientRateLimitGroup.cs new file mode 100644 index 0000000..e90c029 --- /dev/null +++ b/KSeF.Client.Core/Models/RateLimits/KsefClientRateLimitGroup.cs @@ -0,0 +1,72 @@ +namespace KSeF.Client.Core.Models.RateLimits; + +/// +/// Enum określający grupy limitów żądań API KSeF. +/// +public enum KsefClientRateLimitGroup +{ + /// + /// Limity otwierania/zamykania sesji interaktywnych. + /// + OnlineSession = 0, + + /// + /// Limity otwierania/zamykania sesji wsadowych. + /// + BatchSession = 1, + + /// + /// Limity wysyłki faktur. + /// + InvoiceSend = 2, + + /// + /// Limity pobierania statusu faktury z sesji. + /// + InvoiceStatus = 3, + + /// + /// Limity pobierania listy sesji. + /// + SessionList = 4, + + /// + /// Limity pobierania listy faktur w sesji. + /// + SessionInvoiceList = 5, + + /// + /// Limity pozostałych operacji w ramach sesji. + /// + SessionMisc = 6, + + /// + /// Limity pobierania metadanych faktur. + /// + InvoiceMetadata = 7, + + /// + /// Limity eksportu paczki faktur. + /// + InvoiceExport = 8, + + /// + /// Limity pobierania statusu eksportu paczki faktur. + /// + InvoiceExportStatus = 9, + + /// + /// Limity pobierania faktur po numerze KSeF. + /// + InvoiceDownload = 10, + + /// + /// Limity pozostałych operacji API. + /// + Other = 11, + + /// + /// Limity generowania identyfikatorów zbiorczych. + /// + CollectiveIdentifier = 12 +} diff --git a/KSeF.Client.Tests.Core/E2E/Limits/RateLimitsE2ETests.cs b/KSeF.Client.Tests.Core/E2E/Limits/RateLimitsE2ETests.cs index 7525771..c1ef2df 100644 --- a/KSeF.Client.Tests.Core/E2E/Limits/RateLimitsE2ETests.cs +++ b/KSeF.Client.Tests.Core/E2E/Limits/RateLimitsE2ETests.cs @@ -661,21 +661,14 @@ private static int Adjust(int current, int delta, int min, int max) private static bool AreRateLimitsEqual(EffectiveApiRateLimits expected, EffectiveApiRateLimits actual) { - return AreRateLimitValuesEqual(expected.OnlineSession, actual.OnlineSession) - && AreRateLimitValuesEqual(expected.BatchSession, actual.BatchSession) - && AreRateLimitValuesEqual(expected.InvoiceSend, actual.InvoiceSend) - && AreRateLimitValuesEqual(expected.InvoiceStatus, actual.InvoiceStatus) - && AreRateLimitValuesEqual(expected.SessionList, actual.SessionList) - && AreRateLimitValuesEqual(expected.SessionInvoiceList, actual.SessionInvoiceList) - && AreRateLimitValuesEqual(expected.SessionMisc, actual.SessionMisc) - && AreRateLimitValuesEqual(expected.InvoiceMetadata, actual.InvoiceMetadata) - && AreRateLimitValuesEqual(expected.InvoiceExport, actual.InvoiceExport) - && AreRateLimitValuesEqual(expected.InvoiceExportStatus, actual.InvoiceExportStatus) - && AreRateLimitValuesEqual(expected.InvoiceDownload, actual.InvoiceDownload) - && AreRateLimitValuesEqual(expected.CollectiveIdentifier, actual.CollectiveIdentifier) - && AreRateLimitValuesEqual(expected.Other, actual.Other); - } + IReadOnlyDictionary expectedGroups = expected.GetAllRateLimitValues(); + IReadOnlyDictionary actualGroups = actual.GetAllRateLimitValues(); + return expectedGroups.Count == actualGroups.Count + && expectedGroups.All(pair => + actualGroups.TryGetValue(pair.Key, out EffectiveApiRateLimitValues? actualValues) + && AreRateLimitValuesEqual(pair.Value, actualValues)); + } private static bool AreRateLimitValuesEqual(EffectiveApiRateLimitValues? expected, EffectiveApiRateLimitValues? actual) { if (expected is null || actual is null) @@ -711,55 +704,17 @@ private static void AssertRateLimitsEqual(EffectiveApiRateLimits expected, Effec Assert.NotNull(expected); Assert.NotNull(actual); - // OnlineSession - Assert.Equal(expected.OnlineSession.PerSecond, actual.OnlineSession.PerSecond); - Assert.Equal(expected.OnlineSession.PerMinute, actual.OnlineSession.PerMinute); - Assert.Equal(expected.OnlineSession.PerHour, actual.OnlineSession.PerHour); - // BatchSession - Assert.Equal(expected.BatchSession.PerSecond, actual.BatchSession.PerSecond); - Assert.Equal(expected.BatchSession.PerMinute, actual.BatchSession.PerMinute); - Assert.Equal(expected.BatchSession.PerHour, actual.BatchSession.PerHour); - // InvoiceSend - Assert.Equal(expected.InvoiceSend.PerSecond, actual.InvoiceSend.PerSecond); - Assert.Equal(expected.InvoiceSend.PerMinute, actual.InvoiceSend.PerMinute); - Assert.Equal(expected.InvoiceSend.PerHour, actual.InvoiceSend.PerHour); - // InvoiceStatus - Assert.Equal(expected.InvoiceStatus.PerSecond, actual.InvoiceStatus.PerSecond); - Assert.Equal(expected.InvoiceStatus.PerMinute, actual.InvoiceStatus.PerMinute); - Assert.Equal(expected.InvoiceStatus.PerHour, actual.InvoiceStatus.PerHour); - // SessionList - Assert.Equal(expected.SessionList.PerSecond, actual.SessionList.PerSecond); - Assert.Equal(expected.SessionList.PerMinute, actual.SessionList.PerMinute); - Assert.Equal(expected.SessionList.PerHour, actual.SessionList.PerHour); - // SessionInvoiceList - Assert.Equal(expected.SessionInvoiceList.PerSecond, actual.SessionInvoiceList.PerSecond); - Assert.Equal(expected.SessionInvoiceList.PerMinute, actual.SessionInvoiceList.PerMinute); - Assert.Equal(expected.SessionInvoiceList.PerHour, actual.SessionInvoiceList.PerHour); - // SessionMisc - Assert.Equal(expected.SessionMisc.PerSecond, actual.SessionMisc.PerSecond); - Assert.Equal(expected.SessionMisc.PerMinute, actual.SessionMisc.PerMinute); - Assert.Equal(expected.SessionMisc.PerHour, actual.SessionMisc.PerHour); - // InvoiceMetadata - Assert.Equal(expected.InvoiceMetadata.PerSecond, actual.InvoiceMetadata.PerSecond); - Assert.Equal(expected.InvoiceMetadata.PerMinute, actual.InvoiceMetadata.PerMinute); - Assert.Equal(expected.InvoiceMetadata.PerHour, actual.InvoiceMetadata.PerHour); - // InvoiceExport - Assert.Equal(expected.InvoiceExport.PerSecond, actual.InvoiceExport.PerSecond); - Assert.Equal(expected.InvoiceExport.PerMinute, actual.InvoiceExport.PerMinute); - Assert.Equal(expected.InvoiceExport.PerHour, actual.InvoiceExport.PerHour); - // InvoiceExportStatus - AssertRateLimitValuesEqual(expected.InvoiceExportStatus, actual.InvoiceExportStatus); - // InvoiceDownload - Assert.Equal(expected.InvoiceDownload.PerSecond, actual.InvoiceDownload.PerSecond); - Assert.Equal(expected.InvoiceDownload.PerMinute, actual.InvoiceDownload.PerMinute); - Assert.Equal(expected.InvoiceDownload.PerHour, actual.InvoiceDownload.PerHour); - // CollectiveIdentifier - Assert.Equal(expected.CollectiveIdentifier.PerSecond, actual.CollectiveIdentifier.PerSecond); - Assert.Equal(expected.CollectiveIdentifier.PerMinute, actual.CollectiveIdentifier.PerMinute); - Assert.Equal(expected.CollectiveIdentifier.PerHour, actual.CollectiveIdentifier.PerHour); - // Other - Assert.Equal(expected.Other.PerSecond, actual.Other.PerSecond); - Assert.Equal(expected.Other.PerMinute, actual.Other.PerMinute); - Assert.Equal(expected.Other.PerHour, actual.Other.PerHour); + IReadOnlyDictionary expectedGroups = expected.GetAllRateLimitValues(); + IReadOnlyDictionary actualGroups = actual.GetAllRateLimitValues(); + + Assert.Equal(expectedGroups.Count, actualGroups.Count); + + foreach (KeyValuePair pair in expectedGroups) + { + Assert.True( + actualGroups.TryGetValue(pair.Key, out EffectiveApiRateLimitValues? actualValues), + $"Brak grupy limitów '{pair.Key}' w aktualnych wartościach."); + AssertRateLimitValuesEqual(pair.Value, actualValues); + } } } diff --git a/KSeF.Client.Tests/Models/RateLimits/EffectiveApiRateLimitsExtensionsTests.cs b/KSeF.Client.Tests/Models/RateLimits/EffectiveApiRateLimitsExtensionsTests.cs new file mode 100644 index 0000000..4bbd623 --- /dev/null +++ b/KSeF.Client.Tests/Models/RateLimits/EffectiveApiRateLimitsExtensionsTests.cs @@ -0,0 +1,160 @@ +using System.Reflection; + +using KSeF.Client.Core.Models.RateLimits; + +namespace KSeF.Client.Tests.Models.RateLimits; + +/// +/// Testy jednostkowe dla rozszerzeń klasy oraz kontraktu grup limitów. +/// +[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2263", Justification = "Projekt wspiera net48, gdzie generyczne przeciążenie nie jest dostępne.")] +public class EffectiveApiRateLimitsExtensionsTests +{ + /// + /// Sprawdza, czy zwraca każdą grupę limitów + /// oraz właściwy obiekt wartości. Lista grup jest pobierana z enuma, więc nowa poprawnie obsłużona + /// grupa jest automatycznie objęta tym testem. + /// + [Fact] + public void GetAll_ShouldContainEveryRateLimitGroup() + { + EffectiveApiRateLimits limits = CreateLimits(); + IReadOnlyDictionary result = limits.GetAllRateLimitValues(); + KsefClientRateLimitGroup[] expectedGroups = GetRateLimitGroups().ToArray(); + + Assert.Equal(expectedGroups.Length, result.Count); + Assert.Equal(expectedGroups.OrderBy(group => group), result.Keys.OrderBy(group => group)); + + foreach (KsefClientRateLimitGroup group in expectedGroups) + { + EffectiveApiRateLimitValues expected = GetPropertyValue(limits, group); + Assert.Same(expected, result[group]); + } + } + + /// + /// Sprawdza, czy każda właściwość limitu ma odpowiadającą wartość w enumie grup. + /// + [Fact] + public void RateLimitProperties_ShouldHaveMatchingRateLimitGroups() + { + string[] propertyNames = GetRateLimitProperties() + .Select(property => property.Name) + .OrderBy(name => name) + .ToArray(); + + string[] groupNames = Enum.GetNames(typeof(KsefClientRateLimitGroup)) + .OrderBy(name => name) + .ToArray(); + + Assert.Equal(propertyNames, groupNames); + } + + /// + /// Sprawdza mapowanie każdej grupy do właściwej właściwości modelu i wszystkich jej wartości. + /// Dane testowe są generowane na podstawie właściwości modelu, dlatego nowa grupa nie wymaga + /// dopisywania osobnego przypadku testowego. + /// + [Fact] + public void Get_ShouldReturnValuesForEveryRateLimitGroup() + { + EffectiveApiRateLimits limits = CreateLimits(); + + foreach (KsefClientRateLimitGroup group in GetRateLimitGroups()) + { + EffectiveApiRateLimitValues expected = GetPropertyValue(limits, group); + EffectiveApiRateLimitValues actual = limits.GetRateLimitValues(group); + + Assert.Same(expected, actual); + Assert.Equal(expected.PerSecond, actual.PerSecond); + Assert.Equal(expected.PerMinute, actual.PerMinute); + Assert.Equal(expected.PerHour, actual.PerHour); + } + } + + [Fact] + public void Get_ShouldThrowWhenLimitsAreNull() + { + Assert.Throws(() => + EffectiveApiRateLimitsExtensions.GetRateLimitValues(null!, KsefClientRateLimitGroup.Other)); + } + + [Fact] + public void Get_ShouldThrowWhenGroupIsUnsupported() + { + Assert.Throws(() => + CreateLimits().GetRateLimitValues((KsefClientRateLimitGroup)999)); + } + + [Fact] + public void GetAll_ShouldThrowWhenLimitsAreNull() + { + Assert.Throws(() => EffectiveApiRateLimitsExtensions.GetAllRateLimitValues(null!)); + } + + /// + /// Zwraca wszystkie grupy zdefiniowane w . + /// Użycie enuma jako źródła danych sprawia, że nowe grupy są automatycznie testowane. + /// + private static IEnumerable GetRateLimitGroups() + { + return Enum.GetValues(typeof(KsefClientRateLimitGroup)) + .Cast(); + } + + /// + /// Zwraca właściwości modelu reprezentujące wartości limitów. + /// Test kontraktowy zakłada, że każda z nich ma właściwość o takiej samej nazwie w enumie grup. + /// + private static IEnumerable GetRateLimitProperties() + { + return typeof(EffectiveApiRateLimits) + .GetProperties() + .Where(property => property.PropertyType == typeof(EffectiveApiRateLimitValues)); + } + + /// + /// Tworzy model z unikalnymi wartościami dla każdej właściwości limitu. + /// Dzięki temu błędne mapowanie grup jest wykrywane bez ręcznego dopisywania danych testowych. + /// + private static EffectiveApiRateLimits CreateLimits() + { + EffectiveApiRateLimits limits = new(); + int seed = 1; + + foreach (PropertyInfo property in GetRateLimitProperties()) + { + property.SetValue(limits, CreateValues(seed)); + seed++; + } + + return limits; + } + + /// + /// Pobiera oczekiwaną właściwość modelu na podstawie nazwy grupy. + /// Brak właściwości oznacza niespójność kontraktu i powoduje jednoznaczne niepowodzenie testu. + /// + private static EffectiveApiRateLimitValues GetPropertyValue( + EffectiveApiRateLimits limits, + KsefClientRateLimitGroup group) + { + PropertyInfo property = typeof(EffectiveApiRateLimits).GetProperty(group.ToString()) + ?? throw new InvalidOperationException($"Brak właściwości dla grupy limitów '{group}'."); + + return Assert.IsType(property.GetValue(limits)); + } + + /// + /// Tworzy wartości limitu z różnymi wartościami dla sekundy, minuty i godziny. + /// + private static EffectiveApiRateLimitValues CreateValues(int seed) + { + return new EffectiveApiRateLimitValues + { + PerSecond = seed, + PerMinute = seed + 100, + PerHour = seed + 1000 + }; + } +}