Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#region Usings declarations

using NFluent;

#endregion

namespace FirstClassErrors.RequestBinder.UnitTests;

/// <summary>
/// Unit tests for the two configuration value types the structural-error definitions introduce:
/// <see cref="BindingMessage" /> (a plain public-message carrier) and <see cref="BinderErrorDefinition" /> (a code
/// bundled with its message builder, immutable, derivable through WithCode / WithMessage).
/// </summary>
public sealed class BinderErrorDefinitionTests {

private static readonly ErrorCode SampleCode = ErrorCode.Create("SOME_CODE");

private static BindingMessage Message(string argumentPath) {
return new BindingMessage($"short {argumentPath}", $"detailed {argumentPath}");
}

// ── BindingMessage ────────────────────────────────────────────────────────────────────────────────────

[Fact(DisplayName = "BindingMessage carries the short and detailed messages; the detail defaults to null.")]
public void BindingMessageCarriesBothMessages() {
var full = new BindingMessage("short", "detailed");
Check.That(full.ShortMessage).IsEqualTo("short");
Check.That(full.DetailedMessage).IsEqualTo("detailed");

var shortOnly = new BindingMessage("short");
Check.That(shortOnly.DetailedMessage).IsNull();
}

[Fact(DisplayName = "BindingMessage never throws on a null short message — coalescing is deferred to the error factory.")]
public void BindingMessageToleratesNullShortMessage() {
Check.ThatCode(() => new BindingMessage(null!)).DoesNotThrow();
}

// ── BinderErrorDefinition: construction and accessors ─────────────────────────────────────────────────

[Fact(DisplayName = "GetMessage invokes the builder with the argument path; Code exposes the code.")]
public void GetMessageInvokesTheBuilder() {
var definition = new BinderErrorDefinition(SampleCode, Message);

BindingMessage message = definition.GetMessage("Guests[1].FirstName");

Check.That(definition.Code).IsEqualTo(SampleCode);
Check.That(message.ShortMessage).IsEqualTo("short Guests[1].FirstName");
Check.That(message.DetailedMessage).IsEqualTo("detailed Guests[1].FirstName");
}

[Fact(DisplayName = "A null code or a null message builder is rejected at construction.")]
public void ConstructorRejectsNulls() {
Check.ThatCode(() => new BinderErrorDefinition(null!, Message)).Throws<ArgumentNullException>();
Check.ThatCode(() => new BinderErrorDefinition(SampleCode, null!)).Throws<ArgumentNullException>();
}

// ── Immutable derivation: WithCode / WithMessage ──────────────────────────────────────────────────────

[Fact(DisplayName = "WithCode swaps the code and keeps the message builder, without mutating the original.")]
public void WithCodeSwapsCodeKeepsMessage() {
var original = new BinderErrorDefinition(SampleCode, Message);
var other = ErrorCode.Create("OTHER_CODE");

BinderErrorDefinition derived = original.WithCode(other);

Check.That(derived.Code).IsEqualTo(other);
Check.That(derived.GetMessage("X").ShortMessage).IsEqualTo("short X"); // same builder
Check.That(original.Code).IsEqualTo(SampleCode); // original untouched
}

[Fact(DisplayName = "WithMessage swaps the message builder and keeps the code, without mutating the original.")]
public void WithMessageSwapsMessageKeepsCode() {
var original = new BinderErrorDefinition(SampleCode, Message);

BinderErrorDefinition derived = original.WithMessage(argumentPath => new BindingMessage($"new {argumentPath}"));

Check.That(derived.Code).IsEqualTo(SampleCode); // same code
Check.That(derived.GetMessage("X").ShortMessage).IsEqualTo("new X");
Check.That(original.GetMessage("X").ShortMessage).IsEqualTo("short X"); // original untouched
}

[Fact(DisplayName = "WithCode and WithMessage reject nulls.")]
public void WithersRejectNulls() {
var definition = new BinderErrorDefinition(SampleCode, Message);

Check.ThatCode(() => definition.WithCode(null!)).Throws<ArgumentNullException>();
Check.ThatCode(() => definition.WithMessage(null!)).Throws<ArgumentNullException>();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ public void CrossBinderReadMessageNamesTheCause() {

[Fact(DisplayName = "REQUEST_ARGUMENT_REQUIRED carries its public summary, and its detailed and diagnostic messages name the argument path.")]
public void RequiredArgumentErrorCarriesItsMessages() {
PrimaryPortError error = RequestBindingError.ArgumentRequired(RequestBindingError.DefaultArgumentRequiredCode, "GuestEmail");
PrimaryPortError error = RequestBindingError.ArgumentRequired(RequestBindingError.DefaultArgumentRequired, "GuestEmail");

Check.That(error.Code.ToString()).IsEqualTo("REQUEST_ARGUMENT_REQUIRED");
Check.That(error.ShortMessage).IsEqualTo("A required argument is missing.");
Expand All @@ -336,7 +336,7 @@ public void RequiredArgumentErrorCarriesItsMessages() {

[Fact(DisplayName = "REQUEST_ARGUMENT_INVALID carries its public summary, its detailed message names the path, and it wraps the cause.")]
public void InvalidArgumentErrorCarriesItsMessages() {
PrimaryPortError error = RequestBindingError.ArgumentInvalid(RequestBindingError.DefaultArgumentInvalidCode, "GuestEmail", BookingDomainError.EmailInvalid("x"));
PrimaryPortError error = RequestBindingError.ArgumentInvalid(RequestBindingError.DefaultArgumentInvalid, "GuestEmail", BookingDomainError.EmailInvalid("x"));

Check.That(error.Code.ToString()).IsEqualTo("REQUEST_ARGUMENT_INVALID");
Check.That(error.ShortMessage).IsEqualTo("An argument is invalid.");
Expand Down
16 changes: 8 additions & 8 deletions FirstClassErrors.RequestBinder.UnitTests/DefaultOptionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ private static Error BindMissingEmail(RequestBinderEnvelopeStage<BookingRequest>
[Fact(DisplayName = "Bind.PropertiesOf binds with the configured default options — naming and structural codes — without WithOptions.")]
public void BindPropertiesOfUsesTheConfiguredDefault() {
var configured = new RequestBinderOptions(new SnakeCaseNameProvider(),
ErrorCode.Create("ACME_ARGUMENT_REQUIRED"),
ErrorCode.Create("ACME_ARGUMENT_INVALID"));
RequestBindingError.DefaultArgumentRequired.WithCode(ErrorCode.Create("ACME_ARGUMENT_REQUIRED")),
RequestBindingError.DefaultArgumentInvalid.WithCode(ErrorCode.Create("ACME_ARGUMENT_INVALID")));

using (RequestBinderOptions.OverrideDefaultForTests(configured)) {
Error error = BindMissingEmail(Bind.PropertiesOf(MissingEmail()));
Expand All @@ -54,11 +54,11 @@ public void OutsideScopeFallsBackToBuiltIn() {
[Fact(DisplayName = "A per-call Bind.WithOptions overrides the configured default.")]
public void WithOptionsWinsOverTheConfiguredDefault() {
var appDefault = new RequestBinderOptions(new SnakeCaseNameProvider(),
ErrorCode.Create("DEFAULT_REQUIRED"),
ErrorCode.Create("DEFAULT_INVALID"));
RequestBindingError.DefaultArgumentRequired.WithCode(ErrorCode.Create("DEFAULT_REQUIRED")),
RequestBindingError.DefaultArgumentInvalid.WithCode(ErrorCode.Create("DEFAULT_INVALID")));
var perCall = new RequestBinderOptions(new SnakeCaseNameProvider(),
ErrorCode.Create("PERCALL_REQUIRED"),
ErrorCode.Create("PERCALL_INVALID"));
RequestBindingError.DefaultArgumentRequired.WithCode(ErrorCode.Create("PERCALL_REQUIRED")),
RequestBindingError.DefaultArgumentInvalid.WithCode(ErrorCode.Create("PERCALL_INVALID")));

using (RequestBinderOptions.OverrideDefaultForTests(appDefault)) {
Error error = BindMissingEmail(Bind.WithOptions(perCall).PropertiesOf(MissingEmail()));
Expand All @@ -71,8 +71,8 @@ public void WithOptionsWinsOverTheConfiguredDefault() {

[Fact(DisplayName = "Unconfigured, RequestBinderOptions.Default is the built-in default (default structural codes).")]
public void DefaultIsBuiltInWhenUnconfigured() {
Check.That(RequestBinderOptions.Default.ArgumentRequiredCode == RequestBindingError.DefaultArgumentRequiredCode).IsTrue();
Check.That(RequestBinderOptions.Default.ArgumentInvalidCode == RequestBindingError.DefaultArgumentInvalidCode).IsTrue();
Check.That(RequestBinderOptions.Default.ArgumentRequired.Code == RequestBindingError.DefaultArgumentRequiredCode).IsTrue();
Check.That(RequestBinderOptions.Default.ArgumentInvalid.Code == RequestBindingError.DefaultArgumentInvalidCode).IsTrue();
}

// ── Setter contract: null-rejecting and frozen-after-first-use ────────────────────────────────────────
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ public sealed class StructuralCodeOverrideTests {
private static readonly ErrorCode AcmeInvalid = ErrorCode.Create("ACME_ARGUMENT_INVALID");

private static RequestBinderOptions CustomCodes() {
return new RequestBinderOptions(RequestBinderOptions.Default.ArgumentNameProvider, AcmeRequired, AcmeInvalid);
return new RequestBinderOptions(
RequestBinderOptions.Default.ArgumentNameProvider,
RequestBindingError.DefaultArgumentRequired.WithCode(AcmeRequired),
RequestBindingError.DefaultArgumentInvalid.WithCode(AcmeInvalid));
}

private static BookingRequest Request(string? guestEmail = "a@b.c", StayDto? stay = null,
Expand All @@ -37,12 +40,12 @@ public void PublicDefaultCodesAreExposed() {

[Fact(DisplayName = "Options without custom codes keep the default structural codes.")]
public void OptionsDefaultToTheStructuralCodes() {
Check.That(RequestBinderOptions.Default.ArgumentRequiredCode == RequestBindingError.DefaultArgumentRequiredCode).IsTrue();
Check.That(RequestBinderOptions.Default.ArgumentInvalidCode == RequestBindingError.DefaultArgumentInvalidCode).IsTrue();
Check.That(RequestBinderOptions.Default.ArgumentRequired.Code == RequestBindingError.DefaultArgumentRequiredCode).IsTrue();
Check.That(RequestBinderOptions.Default.ArgumentInvalid.Code == RequestBindingError.DefaultArgumentInvalidCode).IsTrue();

var namesOnly = new RequestBinderOptions(RequestBinderOptions.Default.ArgumentNameProvider);
Check.That(namesOnly.ArgumentRequiredCode == RequestBindingError.DefaultArgumentRequiredCode).IsTrue();
Check.That(namesOnly.ArgumentInvalidCode == RequestBindingError.DefaultArgumentInvalidCode).IsTrue();
Check.That(namesOnly.ArgumentRequired.Code == RequestBindingError.DefaultArgumentRequiredCode).IsTrue();
Check.That(namesOnly.ArgumentInvalid.Code == RequestBindingError.DefaultArgumentInvalidCode).IsTrue();
}

[Fact(DisplayName = "By default, a missing required argument still records REQUEST_ARGUMENT_REQUIRED.")]
Expand Down
Loading