From c63ecb7e451e262e6562cf3503ae3f83cf9810eb Mon Sep 17 00:00:00 2001 From: TekH Date: Thu, 21 May 2026 09:29:17 +0200 Subject: [PATCH] Add tests for ReCClient static client initialization Introduced `StaticReCClientTests` to validate the behavior of the `ReCClient` static client, ensuring deterministic and non-parallel execution due to process-wide state mutation. Added tests to cover various scenarios: - Null configuration callback throws `ArgumentNullException`. - Missing `BaseAddress` or `ConfigureClient` throws. - Conflicting `BaseAddress` and `ConfigureClient` throws. - Successful static client build and resolution via `Create`. - Subsequent `BuildStaticClient` calls throw exceptions. Included helper types for `ConfigureServices` validation and used `#pragma` directives to suppress warnings for obsolete members. Ensured test order with `[Order]` attributes. --- .../ReC.Tests/Client/StaticReCClientTests.cs | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 tests/ReC.Tests/Client/StaticReCClientTests.cs diff --git a/tests/ReC.Tests/Client/StaticReCClientTests.cs b/tests/ReC.Tests/Client/StaticReCClientTests.cs new file mode 100644 index 0000000..cde99a6 --- /dev/null +++ b/tests/ReC.Tests/Client/StaticReCClientTests.cs @@ -0,0 +1,129 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using ReC.Client; + +namespace ReC.Tests.Client; + +// The static BuildStaticClient / Create entry-point mutates process-wide state and can only be +// initialized once per AppDomain. All assertions must therefore live in a single, ordered, +// non-parallel fixture so that the lifecycle is deterministic. +[TestFixture] +[NonParallelizable] +public class StaticReCClientTests +{ + // Validation tests run first. They all throw BEFORE the static state is mutated, + // so they can run repeatedly without consuming the one-time build slot. + + [Test, Order(10)] + public void BuildStaticClient_with_null_configure_throws_ArgumentNullException() + { +#pragma warning disable CS0618 // Type or member is obsolete + Assert.Throws(() => ReCClient.BuildStaticClient((Action)null!)); +#pragma warning restore CS0618 + } + + [Test, Order(20)] + public void BuildStaticClient_without_base_address_or_configure_client_throws() + { +#pragma warning disable CS0618 + var ex = Assert.Throws( + () => ReCClient.BuildStaticClient(_ => { })); +#pragma warning restore CS0618 + + Assert.That(ex!.Message, Does.Contain(nameof(StaticBuildConfiguration.BaseAddress))); + Assert.That(ex.Message, Does.Contain(nameof(StaticBuildConfiguration.ConfigureClient))); + } + + [Test, Order(30)] + public void BuildStaticClient_with_both_base_address_and_configure_client_throws() + { +#pragma warning disable CS0618 + var ex = Assert.Throws(() => ReCClient.BuildStaticClient(cfg => + { + cfg.BaseAddress = "https://example.invalid/"; + cfg.ConfigureClient = http => http.BaseAddress = new Uri("https://example.invalid/"); + })); +#pragma warning restore CS0618 + + Assert.That(ex!.Message, Does.Contain("mutually exclusive")); + } + + // Successful build — consumes the single allowed BuildStaticClient slot for this test run. + [Test, Order(100)] + public void BuildStaticClient_with_configuration_callback_succeeds_and_Create_resolves_client() + { + var customLogger = NullLogger.Instance; + var configureServicesInvoked = false; + +#pragma warning disable CS0618 + ReCClient.BuildStaticClient(cfg => + { + cfg.BaseAddress = "https://example.invalid/"; + cfg.ConfigureOptions = opt => opt.LogSuccessfulRequests = true; + cfg.Logger = customLogger; + cfg.ConfigureServices = services => + { + configureServicesInvoked = true; + services.AddSingleton(); + }; + }); + + // First Create() triggers the Lazy initialization. + var client = ReCClient.Create(); +#pragma warning restore CS0618 + + Assert.That(client, Is.Not.Null); + Assert.That(client.RecActions, Is.Not.Null); + Assert.That(configureServicesInvoked, Is.True, "ConfigureServices callback should run when the provider is built."); + } + + [Test, Order(110)] + public void Create_returns_a_client_using_the_existing_static_provider() + { +#pragma warning disable CS0618 + var client = ReCClient.Create(); +#pragma warning restore CS0618 + + Assert.That(client, Is.Not.Null); + } + + // Any subsequent BuildStaticClient call (regardless of overload) must fail. + + [Test, Order(200)] + public void Calling_BuildStaticClient_configuration_overload_a_second_time_throws() + { +#pragma warning disable CS0618 + var ex = Assert.Throws(() => + ReCClient.BuildStaticClient(cfg => cfg.BaseAddress = "https://other.invalid/")); +#pragma warning restore CS0618 + + Assert.That(ex!.Message, Does.Contain("already built")); + } + + [Test, Order(210)] + public void Calling_legacy_BuildStaticClient_string_overload_after_build_throws() + { +#pragma warning disable CS0618 + var ex = Assert.Throws(() => + ReCClient.BuildStaticClient("https://other.invalid/")); +#pragma warning restore CS0618 + + Assert.That(ex!.Message, Does.Contain("already built")); + } + + [Test, Order(220)] + public void Calling_legacy_BuildStaticClient_action_overload_after_build_throws() + { +#pragma warning disable CS0618 + var ex = Assert.Throws(() => + ReCClient.BuildStaticClient(http => http.BaseAddress = new Uri("https://other.invalid/"))); +#pragma warning restore CS0618 + + Assert.That(ex!.Message, Does.Contain("already built")); + } + + // Helper types used by the ConfigureServices assertion above. + private interface IMarkerService { } + private sealed class MarkerService : IMarkerService { } +}