diff --git a/.claude/agents/reviewer.md b/.claude/agents/reviewer.md index 4ba1542..e89a9e3 100644 --- a/.claude/agents/reviewer.md +++ b/.claude/agents/reviewer.md @@ -15,7 +15,7 @@ First read `CLAUDE.md` and every **accepted** ADR in `docs/adr/` that relates to ## Checklist **ADR compliance** -- ADR-0002: no `catch (Exception)` or bare `catch`. Expected failures (HTTP status, payload, network, timeout) are returned as `OpenUrzednikResult` errors. Only caller cancellation, argument errors and fatal errors are thrown; in provider client methods, a `null` argument or an undefined enum value throws synchronously, before the task is returned (flag an `async` public client method that checks them, or a `ValidationError` for them), while a non-null value of the right type that fails a format or range check is a returned `ValidationError`. Timeouts are distinguished from caller cancellation. Errors implement `CreateException()` returning `OpenUrzednikException` subtypes; exceptions keep their error in `Error`/`Errors`. Several validation errors are thrown as one `OpenUrzednikValidationException` listing all of them, never `AggregateException`; several errors of other kinds as the first error's exception with all of them in `Errors`. Resilience-handler rejections are translated in DI packages, not caught in providers. +- ADR-0002: no `catch (Exception)` or bare `catch`. Expected failures (HTTP status, payload, network, timeout) are returned as `OpenUrzednikResult` errors. Only caller cancellation, argument errors and fatal errors are thrown; in every public method of a public type that returns a `Task` (provider clients, Core's extensions on `Task<…>`), a `null` argument or an undefined enum value throws synchronously, before the task is returned (flag a public `async` method that checks them, or a `ValidationError` for them), while a non-null value of the right type that fails a format or range check is a returned `ValidationError`. Timeouts are distinguished from caller cancellation. Errors implement `CreateException()` returning `OpenUrzednikException` subtypes; exceptions keep their error in `Error`/`Errors`. Several validation errors are thrown as one `OpenUrzednikValidationException` listing all of them, never `AggregateException`; several errors of other kinds as the first error's exception with all of them in `Errors`. Resilience-handler rejections are translated in DI packages, not caught in providers. - ADR-0003: no `ILogger`/`ActivitySource` in Core or provider packages, except Core's internal `Telemetry/Activities` (the .NET fallback trace source, compiled on .NET only). Members of those types that other packages call through `InternalsVisibleTo` are a binary contract: flag a change or removal outside a major. Span and tag naming; HTTP tags use the OpenTelemetry names (`http.request.method`, `url.path`, `http.response.status_code`); errors set `error.type`. Activity sources are named `OpenUrzednik.`. Every log call passes an event id; a new log event has a new id in `RestLogEvents` (HTTP, 1xxx) or the provider's `LogEvents` (2xxx); flag a renumbered, reused or missing id, and a `RestLogEvents` entry not mirrored in `LogEvents`. Flag any change to `IOpenUrzednikLogger`, `IOpenUrzednikTraceSource` or `IOpenUrzednikSpan` after 1.0 (frozen; new capabilities are separate interfaces), and any change to a stable name (source, span, documented tag, `error.type`, logger category, event id) outside a major. `IsEnabled` guards. No secrets (API keys, session ids, tokens) or personal identifiers (PESEL, NIP, REGON, bank account numbers) in logs, span names or tags, error and exception messages, error metadata or `Exception.Data`; at most the last 4 characters. - ADR-0004: no new `PackageReference` in Core or provider projects, apart from netstandard2.0-only official Microsoft BCL packages (currently `System.Text.Json`, `System.Net.Http.Json`, `Microsoft.Bcl.TimeProvider`, `Microsoft.Bcl.HashCode`; any other one needs approval). `System.Diagnostics.DiagnosticSource` is allowed only in `OpenUrzednik.Diagnostics`. No `Version=` on package references (central package management). A dependency of a published library moves only by patch unless the PR says why a minor or major raise is needed; the DI package's `Microsoft.Extensions.*` keep the `net8.0` override (8.0.x), and no package gets two conditional `Include` rows. DI packages don't add a resilience handler themselves (the app chains `AddStandardResilienceHandler()`), and they translate its rejection exceptions into errors. - ADR-0005: code builds for netstandard2.0 and passes `test (net472)`. Package validation stays on with no suppressions: flag a `CompatibilitySuppressions.xml`, a `CPxxxx` in `NoWarn`, or `EnablePackageValidation` turned off (the only exception is ADR-0011's CP0006 for a new client-interface method). `OpenUrzednik.snk`, `SignAssembly`, `PublicKey` and `assemblyVersion.precision` don't change; a project doesn't turn signing off; a third-party `InternalsVisibleTo` friend has its own `Key`. `#if` kept inside helpers (polyfills in `src/Polyfills`, `src/Http/Polyfills`, `Nbp/Polyfills`, `Extensions.Logging/Polyfills`). Every build has the same public signatures for what it shares: flag a public `DateOnly` (parameter, property or return type) and any interface member that exists only on .NET; public dates are `DateTime`, with `DateOnly` overloads only as .NET-only extension methods. Internally, dates are written as `DateOnly` (an alias for `DateTime` on netstandard2.0) and compared by `DayNumber`. A .NET target that isn't the lowest one may be removed in a minor version once Microsoft ends its support; the lowest .NET target and netstandard2.0 are removed only in a major. Targets are netstandard2.0 plus the .NET LTS releases in support. diff --git a/CLAUDE.md b/CLAUDE.md index 9d30259..0c4c43c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,7 +40,7 @@ Before you say a task is done, run `dotnet build`, `dotnet test -f net10.0` and **Errors ([ADR-0002](docs/adr/0002-result-pattern-and-error-handling.md))** - Public async APIs return `Task>`. Failures that can happen during normal execution are returned, not thrown: validation, every non-success HTTP status, bad payloads, network errors, timeouts. -- Throw only for caller cancellation (`OperationCanceledException` when the caller's token is cancelled), programmer errors (`ArgumentNullException`, invalid options) and fatal errors. In provider clients, a `null` argument and an undefined enum value (`ArgumentOutOfRangeException`) throw **synchronously**: the public method checks them and returns the task of a private `…CoreAsync` method. A non-null value of the right type that fails a format or range check is a returned `ValidationError`. +- Throw only for caller cancellation (`OperationCanceledException` when the caller's token is cancelled), programmer errors (`ArgumentNullException`, invalid options) and fatal errors. In every public method of a public type that returns a `Task`, a `null` argument and an undefined enum value (`ArgumentOutOfRangeException`) throw **synchronously**: the method isn't `async` and checks them before it creates the task (typically it returns the task of a private `…CoreAsync` method). A non-null value of the right type that fails a format or range check is a returned `ValidationError`. - **Never write `catch (Exception)` or a bare `catch`.** Catch specific types and convert them to errors. - A timeout is an `OperationCanceledException` while `cancellationToken.IsCancellationRequested == false`. Return it as an error. - New error types derive from `OpenUrzednikError`, define `public const string ErrorCode`, and implement `CreateException()` returning an `OpenUrzednikException` subtype. The public `ToException()` calls it and attaches the error (`OpenUrzednikException.Error`/`Errors`). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f802f0e..068a209 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,7 +15,7 @@ Binding architecture decisions are in [`docs/adr/`](docs/adr/README.md), and current work and priorities are in the [issues](https://github.com/milten89/OpenUrzednik.NET/issues) and the [project](https://github.com/users/milten89/projects/2). A change that contradicts an accepted ADR needs a new ADR in the same PR. The Code conventions, Testing conventions and Performance sections of [`CLAUDE.md`](CLAUDE.md) apply to every contributor; using AI tools is optional. -- **`OpenUrzednikResult` / `OpenUrzednikResult` for every error that can happen during normal operation** (validation, HTTP statuses, bad responses, network errors, timeouts), instead of error handling scattered across the code. We throw exceptions only when the caller cancels, for programmer errors (a `null` argument or an undefined enum value, which a client method throws before it returns its task) and for fatal errors; a value of the right type that fails a format or range check is a returned `ValidationError`. Don't use `catch (Exception)` ([ADR-0002](docs/adr/0002-result-pattern-and-error-handling.md)). Exceptions (`OpenUrzednikException` and its subtypes) are available through `.EnsureSuccess()` / `.EnsureSuccessAsync()` for consumers who prefer the classic style; don't add parallel API variants. +- **`OpenUrzednikResult` / `OpenUrzednikResult` for every error that can happen during normal operation** (validation, HTTP statuses, bad responses, network errors, timeouts), instead of error handling scattered across the code. We throw exceptions only when the caller cancels, for programmer errors (a `null` argument or an undefined enum value, which a public method that returns a task throws before it returns the task) and for fatal errors; a value of the right type that fails a format or range check is a returned `ValidationError`. Don't use `catch (Exception)` ([ADR-0002](docs/adr/0002-result-pattern-and-error-handling.md)). Exceptions (`OpenUrzednikException` and its subtypes) are available through `.EnsureSuccess()` / `.EnsureSuccessAsync()` for consumers who prefer the classic style; don't add parallel API variants. - **Dependencies:** the Core and provider packages have no NuGet dependencies (apart from official BCL packages for netstandard2.0); integrations with `Microsoft.Extensions.*` go into separate packages ([ADR-0004](docs/adr/0004-dependency-policy.md)). - **`OpenUrzednikError`** is the base type for expected errors. A new error is a `sealed` class ([ADR-0011](docs/adr/0011-compatibility-policy.md)) that derives from it ([ADR-0002](docs/adr/0002-result-pattern-and-error-handling.md)), defines `public const string ErrorCode`, passes it to the base constructor and overrides `CreateException()` to return an exception derived from `OpenUrzednikException` (see `NotFoundError`). - **Provider packages** follow the patterns in `OpenUrzednik.Core`: type names, error naming and the way results are returned stay consistent. diff --git a/docs/adr/0002-result-pattern-and-error-handling.md b/docs/adr/0002-result-pattern-and-error-handling.md index 9bd625e..1a60551 100644 --- a/docs/adr/0002-result-pattern-and-error-handling.md +++ b/docs/adr/0002-result-pattern-and-error-handling.md @@ -57,7 +57,7 @@ The metadata keys are public constants next to the errors that set them: `OpenUr **Thrown:** * Caller cancellation – `OperationCanceledException` when the caller's `CancellationToken` was cancelled. -* Programmer errors – `ArgumentNullException`, `ArgumentException`, invalid configuration detected in constructors/options. In the provider clients' public methods, a `null` argument (`ArgumentNullException`) and a value that isn't a defined enum member (`ArgumentOutOfRangeException`) are thrown **synchronously**, before the method returns its task, as the task-based asynchronous pattern says for usage errors: the method checks them, then returns the task of a private `async` method. +* Programmer errors – `ArgumentNullException`, `ArgumentException`, invalid configuration detected in constructors/options. In every public method of a public type that returns a task (the provider clients' methods, Core's extensions on `Task<…>`), a `null` argument (`ArgumentNullException`) and a value that isn't a defined enum member (`ArgumentOutOfRangeException`) are thrown **synchronously**, before the method returns its task, as the task-based asynchronous pattern says for usage errors: the method isn't `async` and checks them before it creates the task, typically by returning the task of a private `async` method. * Fatal runtime conditions – `OutOfMemoryException`, `StackOverflowException`, etc. are never caught. **Rules for code:** @@ -100,7 +100,7 @@ Supersedes the informal rule in `CONTRIBUTING.md`. Shared HTTP behaviour is impl **2026-10-17 change: one list of error paths** ([#77](https://github.com/milten89/OpenUrzednik.NET/issues/77)). The Confirmation, `CLAUDE.md` and the `/new-endpoint` skill listed different error paths: `CLAUDE.md` left out 401/403, and the skill also left out the connection failure. The Confirmation now lists every path the WireMock tests already cover for each NBP client, including the 200 with an empty array for endpoints that return one, and the other two documents point to it. -**2026-10-29 change: argument errors are thrown** ([#89](https://github.com/milten89/OpenUrzednik.NET/issues/89)). Before, the NBP clients returned a `ValidationError` for a `null` currency code and an undefined `TableType`, while the rules above and `CONTRIBUTING` named `ArgumentNullException` for a `null` argument, so callers couldn't tell from the docs whether to catch an exception or check the result. The maintainer drew the line where the .NET design guidelines do: a usage error (a `null` argument or an undefined enum value) throws, synchronously; a non-null value of the right type that fails a format or range check is a returned `ValidationError`. Core's extensions on `Task<…>` (`EnsureSuccessAsync`, `MapAsync`, `BindAsync`) still check their arguments inside an `async` method; that is a separate change. It affects the 15 methods that take a currency code or a `TableType`; the gold methods take neither. It is a breaking change for preview users. +**2026-10-29 change: argument errors are thrown** ([#89](https://github.com/milten89/OpenUrzednik.NET/issues/89)). Before, the NBP clients returned a `ValidationError` for a `null` currency code and an undefined `TableType`, while the rules above and `CONTRIBUTING` named `ArgumentNullException` for a `null` argument, so callers couldn't tell from the docs whether to catch an exception or check the result. The maintainer drew the line where the .NET design guidelines do: a usage error (a `null` argument or an undefined enum value) throws, synchronously; a non-null value of the right type that fails a format or range check is a returned `ValidationError`. Core's extensions on `Task<…>` (`EnsureSuccessAsync`, `MapAsync`, `BindAsync`) still check their arguments inside an `async` method; that is a separate change (done on 2026-10-30, #159). It affects the 15 methods that take a currency code or a `TableType`; the gold methods take neither. It is a breaking change for preview users. **2026-10-29 change: the 1.0 error contract** ([#90](https://github.com/milten89/OpenUrzednik.NET/issues/90)). The "Returned" list became the mapping table above, and the compatibility rule, the reasons for departing from the design guidelines, the `default(OpenUrzednikResult)` rule and the `Value` rule were added. Changes made with it: @@ -113,3 +113,5 @@ Supersedes the informal rule in `CONTRIBUTING.md`. Shared HTTP behaviour is impl These are breaking changes for preview users. **2026-10-30 change: masked values in validation errors** ([#109](https://github.com/milten89/OpenUrzednik.NET/issues/109)). `ValueValidator.GetValidationErrorResult` stores the validated value under `ValueMetadataKey`, so a validator for a personal identifier would copy the identifier into every error, exception and log that shows the metadata. A validator for a personal identifier must pass a masked value, under the rule in [ADR-0003](0003-telemetry-abstractions.md). NBP validates no such value, so nothing changes there. + +**2026-10-30 change: synchronous argument errors in every task-returning method** ([#159](https://github.com/milten89/OpenUrzednik.NET/issues/159)). The rule from #89 covered only the provider clients. Core's extensions on `Task<…>` (`EnsureSuccessAsync`, `MapAsync`, `BindAsync`) checked their arguments inside an `async` method, so a `null` task or delegate came back as a faulted task. They now check their arguments, then return the task of a private `async` method, and the rule covers every public method of a public type that returns a task. It is a breaking change for callers who observed the exception on the task. diff --git a/src/OpenUrzednik.Core/OpenUrzednikResultExtensions.cs b/src/OpenUrzednik.Core/OpenUrzednikResultExtensions.cs index 28cf363..7813d9b 100644 --- a/src/OpenUrzednik.Core/OpenUrzednikResultExtensions.cs +++ b/src/OpenUrzednik.Core/OpenUrzednikResultExtensions.cs @@ -13,12 +13,13 @@ public static class OpenUrzednikResultExtensions /// /// Result task to check. /// Task representing the asynchronous operation. + /// is ; thrown before the task is returned. /// Thrown when the result is a failure, as in . - public static async Task EnsureSuccessAsync(this Task resultTask) + public static Task EnsureSuccessAsync(this Task resultTask) { ArgumentNullException.ThrowIfNull(resultTask); - (await resultTask.ConfigureAwait(false)).EnsureSuccess(); + return EnsureSuccessCoreAsync(resultTask); } /// @@ -26,12 +27,13 @@ public static async Task EnsureSuccessAsync(this Task result /// /// Result task to check. /// The value of the result if it is in a successful state. + /// is ; thrown before the task is returned. /// Thrown when the result is a failure, as in . - public static async Task EnsureSuccessAsync(this Task> resultTask) + public static Task EnsureSuccessAsync(this Task> resultTask) { ArgumentNullException.ThrowIfNull(resultTask); - return (await resultTask.ConfigureAwait(false)).EnsureSuccess(); + return EnsureSuccessCoreAsync(resultTask); } /// @@ -42,12 +44,13 @@ public static async Task EnsureSuccessAsync(this TaskThe task returning the source result. /// The transformation. /// A successful result with the transformed value, or a failure with the source errors. - public static async Task> MapAsync(this Task> resultTask, Func map) + /// or is ; thrown before the task is returned. + public static Task> MapAsync(this Task> resultTask, Func map) { ArgumentNullException.ThrowIfNull(resultTask); ArgumentNullException.ThrowIfNull(map); - return (await resultTask.ConfigureAwait(false)).Map(map); + return MapCoreAsync(resultTask, map); } /// @@ -58,12 +61,13 @@ public static async Task> MapAsync(this TaskThe task returning the source result. /// The operation to run with the value. /// The result of , or a failure with the source errors. - public static async Task> BindAsync(this Task> resultTask, Func> next) + /// or is ; thrown before the task is returned. + public static Task> BindAsync(this Task> resultTask, Func> next) { ArgumentNullException.ThrowIfNull(resultTask); ArgumentNullException.ThrowIfNull(next); - return (await resultTask.ConfigureAwait(false)).Bind(next); + return BindCoreAsync(resultTask, next); } /// @@ -74,11 +78,27 @@ public static async Task> BindAsync(this Task< /// The task returning the source result. /// The asynchronous operation to run with the value. /// The result of , or a failure with the source errors. - public static async Task> BindAsync(this Task> resultTask, Func>> next) + /// or is ; thrown before the task is returned. + public static Task> BindAsync(this Task> resultTask, Func>> next) { ArgumentNullException.ThrowIfNull(resultTask); ArgumentNullException.ThrowIfNull(next); - return await (await resultTask.ConfigureAwait(false)).BindAsync(next).ConfigureAwait(false); + return BindCoreAsync(resultTask, next); } + + private static async Task EnsureSuccessCoreAsync(Task resultTask) => + (await resultTask.ConfigureAwait(false)).EnsureSuccess(); + + private static async Task EnsureSuccessCoreAsync(Task> resultTask) => + (await resultTask.ConfigureAwait(false)).EnsureSuccess(); + + private static async Task> MapCoreAsync(Task> resultTask, Func map) => + (await resultTask.ConfigureAwait(false)).Map(map); + + private static async Task> BindCoreAsync(Task> resultTask, Func> next) => + (await resultTask.ConfigureAwait(false)).Bind(next); + + private static async Task> BindCoreAsync(Task> resultTask, Func>> next) => + await (await resultTask.ConfigureAwait(false)).BindAsync(next).ConfigureAwait(false); } diff --git a/tests/OpenUrzednik.Core.Tests/OpenUrzednikGenericResultTest.Bind.cs b/tests/OpenUrzednik.Core.Tests/OpenUrzednikGenericResultTest.Bind.cs index 515c79f..8c6308a 100644 --- a/tests/OpenUrzednik.Core.Tests/OpenUrzednikGenericResultTest.Bind.cs +++ b/tests/OpenUrzednik.Core.Tests/OpenUrzednikGenericResultTest.Bind.cs @@ -92,9 +92,10 @@ public void Bind_NullNext_ThrowsArgumentNullException() } [Fact] - public async Task BindAsync_NullNext_ThrowsArgumentNullException() + public void BindAsync_NullNext_ThrowsArgumentNullExceptionSynchronously() { // Act && Assert - await Should.ThrowAsync(() => OpenUrzednikResult.Success(1).BindAsync(null!)); + Should.Throw(() => { _ = OpenUrzednikResult.Success(1).BindAsync(null!); }) + .ParamName.ShouldBe("next"); } } diff --git a/tests/OpenUrzednik.Core.Tests/OpenUrzednikResultExtensionsTest.BindAsync.cs b/tests/OpenUrzednik.Core.Tests/OpenUrzednikResultExtensionsTest.BindAsync.cs index 0fc2fa4..1bae25f 100644 --- a/tests/OpenUrzednik.Core.Tests/OpenUrzednikResultExtensionsTest.BindAsync.cs +++ b/tests/OpenUrzednik.Core.Tests/OpenUrzednikResultExtensionsTest.BindAsync.cs @@ -42,9 +42,46 @@ public async Task BindAsync_FailedTaskWithAsyncNext_DoesNotCallNextAndKeepsError } [Fact] - public async Task BindAsync_NullNext_ThrowsArgumentNullException() + public void BindAsync_NullTask_ThrowsArgumentNullExceptionSynchronously() { + // Arrange + Task> task = null!; + + // Act && Assert + Should.Throw(() => { _ = task.BindAsync(v => OpenUrzednikResult.Success(v)); }) + .ParamName.ShouldBe("resultTask"); + } + + [Fact] + public void BindAsync_NullNext_ThrowsArgumentNullExceptionSynchronously() + { + // Arrange + var task = Task.FromResult(OpenUrzednikResult.Success(1)); + + // Act && Assert + Should.Throw(() => { _ = task.BindAsync((Func>)null!); }) + .ParamName.ShouldBe("next"); + } + + [Fact] + public void BindAsync_NullTaskWithAsyncNext_ThrowsArgumentNullExceptionSynchronously() + { + // Arrange + Task> task = null!; + + // Act && Assert + Should.Throw(() => { _ = task.BindAsync(v => Task.FromResult(OpenUrzednikResult.Success(v))); }) + .ParamName.ShouldBe("resultTask"); + } + + [Fact] + public void BindAsync_NullAsyncNext_ThrowsArgumentNullExceptionSynchronously() + { + // Arrange + var task = Task.FromResult(OpenUrzednikResult.Success(1)); + // Act && Assert - await Should.ThrowAsync(() => Task.FromResult(OpenUrzednikResult.Success(1)).BindAsync((Func>)null!)); + Should.Throw(() => { _ = task.BindAsync((Func>>)null!); }) + .ParamName.ShouldBe("next"); } } diff --git a/tests/OpenUrzednik.Core.Tests/OpenUrzednikResultExtensionsTest.EnsureSuccessAsync.cs b/tests/OpenUrzednik.Core.Tests/OpenUrzednikResultExtensionsTest.EnsureSuccessAsync.cs index c20787e..f955d64 100644 --- a/tests/OpenUrzednik.Core.Tests/OpenUrzednikResultExtensionsTest.EnsureSuccessAsync.cs +++ b/tests/OpenUrzednik.Core.Tests/OpenUrzednikResultExtensionsTest.EnsureSuccessAsync.cs @@ -71,4 +71,26 @@ public async Task EnsureSuccessAsync_WhenGenericResultIsFailureWithMultipleError var exception = await Should.ThrowAsync(async () => await result.EnsureSuccessAsync()); exception.Errors.Count.ShouldBe(2); } + + [Fact] + public void EnsureSuccessAsync_NullTask_ThrowsArgumentNullExceptionSynchronously() + { + // Arrange + Task task = null!; + + // Act && Assert + Should.Throw(() => { _ = task.EnsureSuccessAsync(); }) + .ParamName.ShouldBe("resultTask"); + } + + [Fact] + public void EnsureSuccessAsync_NullGenericTask_ThrowsArgumentNullExceptionSynchronously() + { + // Arrange + Task> task = null!; + + // Act && Assert + Should.Throw(() => { _ = task.EnsureSuccessAsync(); }) + .ParamName.ShouldBe("resultTask"); + } } diff --git a/tests/OpenUrzednik.Core.Tests/OpenUrzednikResultExtensionsTest.MapAsync.cs b/tests/OpenUrzednik.Core.Tests/OpenUrzednikResultExtensionsTest.MapAsync.cs index c822da5..05564bd 100644 --- a/tests/OpenUrzednik.Core.Tests/OpenUrzednikResultExtensionsTest.MapAsync.cs +++ b/tests/OpenUrzednik.Core.Tests/OpenUrzednikResultExtensionsTest.MapAsync.cs @@ -39,12 +39,24 @@ public async Task MapAsync_FailedTask_DoesNotCallMapAndKeepsErrors() } [Fact] - public async Task MapAsync_NullTask_ThrowsArgumentNullException() + public void MapAsync_NullTask_ThrowsArgumentNullExceptionSynchronously() { // Arrange Task> task = null!; // Act && Assert - await Should.ThrowAsync(() => task.MapAsync(v => v)); + Should.Throw(() => { _ = task.MapAsync(v => v); }) + .ParamName.ShouldBe("resultTask"); + } + + [Fact] + public void MapAsync_NullMap_ThrowsArgumentNullExceptionSynchronously() + { + // Arrange + var task = Task.FromResult(OpenUrzednikResult.Success(1)); + + // Act && Assert + Should.Throw(() => { _ = task.MapAsync(null!); }) + .ParamName.ShouldBe("map"); } } diff --git a/tests/OpenUrzednik.Core.Tests/OpenUrzednikResultTest.Bind.cs b/tests/OpenUrzednik.Core.Tests/OpenUrzednikResultTest.Bind.cs index 69561a2..97183fe 100644 --- a/tests/OpenUrzednik.Core.Tests/OpenUrzednikResultTest.Bind.cs +++ b/tests/OpenUrzednik.Core.Tests/OpenUrzednikResultTest.Bind.cs @@ -71,4 +71,12 @@ public void Bind_NullNext_ThrowsArgumentNullException() // Act && Assert Should.Throw(() => OpenUrzednikResult.Success().Bind(null!)); } + + [Fact] + public void BindAsync_NullNext_ThrowsArgumentNullExceptionSynchronously() + { + // Act && Assert + Should.Throw(() => { _ = OpenUrzednikResult.Success().BindAsync(null!); }) + .ParamName.ShouldBe("next"); + } } diff --git a/tests/OpenUrzednik.Nbp.Tests/NbpDateOnlyExtensionsTest.GetAsync.cs b/tests/OpenUrzednik.Nbp.Tests/NbpDateOnlyExtensionsTest.GetAsync.cs index c25f5b7..531d3c1 100644 --- a/tests/OpenUrzednik.Nbp.Tests/NbpDateOnlyExtensionsTest.GetAsync.cs +++ b/tests/OpenUrzednik.Nbp.Tests/NbpDateOnlyExtensionsTest.GetAsync.cs @@ -111,7 +111,7 @@ public async Task GetAsync_TableDateRange_CallsDateTimeMethodWithMidnights() public void GetAsync_NullClient_ThrowsArgumentNullException() { // Act && Assert - Should.Throw(() => ((INbpGoldPriceClient)null!).GetAsync(new DateOnly(2026, 10, 2))) + Should.Throw(() => { _ = ((INbpGoldPriceClient)null!).GetAsync(new DateOnly(2026, 10, 2)); }) .ParamName.ShouldBe("client"); } } diff --git a/tests/OpenUrzednik.Nbp.Tests/NbpDateOnlyExtensionsTest.GetBuySellAsync.cs b/tests/OpenUrzednik.Nbp.Tests/NbpDateOnlyExtensionsTest.GetBuySellAsync.cs index 2e1c177..faf6c94 100644 --- a/tests/OpenUrzednik.Nbp.Tests/NbpDateOnlyExtensionsTest.GetBuySellAsync.cs +++ b/tests/OpenUrzednik.Nbp.Tests/NbpDateOnlyExtensionsTest.GetBuySellAsync.cs @@ -78,7 +78,7 @@ public async Task GetBuySellAsync_TableDateRange_CallsDateTimeMethodWithMidnight public void GetBuySellAsync_NullClient_ThrowsArgumentNullException() { // Act && Assert - Should.Throw(() => ((INbpExchangeRateTableClient)null!).GetBuySellAsync(new DateOnly(2026, 10, 2))) + Should.Throw(() => { _ = ((INbpExchangeRateTableClient)null!).GetBuySellAsync(new DateOnly(2026, 10, 2)); }) .ParamName.ShouldBe("client"); } }