namespace Raliway.Tests.Errors; public class Construction { public class New_Factory { [Fact] public void With_string_creates_ExpectedError_with_default_type() { // Given // When var error = Error.New("something went wrong"); // Then error.ShouldBeOfType(); error.Type.ShouldBe("error"); error.Message.ShouldBe("something went wrong"); error.IsExpected.ShouldBeTrue(); error.IsExceptional.ShouldBeFalse(); error.IsEmpty.ShouldBeFalse(); error.Count.ShouldBe(1); } [Fact] public void With_type_and_message_creates_ExpectedError_with_custom_type() { // Given // When var error = Error.New("validation_error", "Name is required"); // Then error.ShouldBeOfType(); error.Type.ShouldBe("validation_error"); error.Message.ShouldBe("Name is required"); } [Fact] public void With_Exception_creates_ExceptionalError() { // Given var exception = new InvalidOperationException("bad state"); // When var error = Error.New(exception); // Then error.ShouldBeOfType(); error.Type.ShouldBe("System.InvalidOperationException"); error.Message.ShouldBe("bad state"); error.IsExpected.ShouldBeFalse(); error.IsExceptional.ShouldBeTrue(); error.IsEmpty.ShouldBeFalse(); error.Count.ShouldBe(1); } [Fact] public void With_message_and_Exception_overrides_message_preserves_exception() { // Given var exception = new InvalidOperationException("internal detail"); // When var error = Error.New("sanitized message", exception); // Then error.ShouldBeOfType(); error.Type.ShouldBe("System.InvalidOperationException"); error.Message.ShouldBe("sanitized message"); error.IsExceptional.ShouldBeTrue(); var ex = error.ToException(); ex.ShouldBe(exception); } [Fact] public void With_AggregateException_many_inners_creates_ManyErrors() { // Given var inner1 = new InvalidOperationException("first"); var inner2 = new ArgumentException("second"); var aggregate = new AggregateException(inner1, inner2); // When var error = Error.New(aggregate); // Then error.ShouldBeOfType(); var many = (ManyErrors)error; many.Count.ShouldBe(2); many[0].Type.ShouldBe("System.InvalidOperationException"); many[0].Message.ShouldBe("first"); many[1].Type.ShouldBe("System.ArgumentException"); many[1].Message.ShouldBe("second"); } [Fact] public void With_AggregateException_single_inner_creates_ExceptionalError() { // Given var inner = new InvalidOperationException("only one"); var aggregate = new AggregateException(inner); // When var error = Error.New(aggregate); // Then error.ShouldBeOfType(); error.Type.ShouldBe("System.InvalidOperationException"); error.Message.ShouldBe("only one"); } [Fact] public void With_AggregateException_zero_inners_creates_ExceptionalError() { // Given var aggregate = new AggregateException(); // When var error = Error.New(aggregate); // Then error.ShouldBeOfType(); error.Type.ShouldBe("System.AggregateException"); } [Fact] public void With_nested_AggregateException_recursively_unwraps() { // Given var inner1 = new InvalidOperationException("first"); var inner2 = new ArgumentException("second"); var nestedAggregate = new AggregateException(inner1, inner2); // Outer must have >1 inners to trigger ManyErrors path; one of them is itself an AggregateException var outerAggregate = new AggregateException(nestedAggregate, new InvalidOperationException("extra")); // When var error = Error.New(outerAggregate); // Then error.ShouldBeOfType(); var many = (ManyErrors)error; many.Count.ShouldBe(3); // nested 2-flattened + extra } } public class Many_Factory { [Fact] public void With_both_null_returns_EmptyError() { // Given // When var error = Error.Many(null!, null!); // Then error.ShouldBeOfType(); error.IsEmpty.ShouldBeTrue(); error.Count.ShouldBe(0); } [Fact] public void With_error_and_null_returns_the_error() { // Given var expected = Error.New("test"); // When var result = Error.Many(expected, null!); // Then result.ShouldBe(expected); result.ShouldBeOfType(); } [Fact] public void With_null_and_error_returns_the_error() { // Given var expected = Error.New("test"); // When var result = Error.Many(null!, expected); // Then result.ShouldBe(expected); result.ShouldBeOfType(); } [Fact] public void Params_with_null_element_should_not_throw() { // Given Error?[] errors = [Error.New("first"), null!, Error.New("second")]; // When var exception = Record.Exception(() => Error.Many(errors!)); // Then exception.ShouldBeNull(); } [Fact] public void Params_with_null_element_filters_null() { // Given Error?[] errors = [Error.New("first"), null!, Error.New("second")]; // When var result = Error.Many(errors!); // Then result.ShouldBeOfType(); var many = (ManyErrors)result; many.Count.ShouldBe(2); many[0].Message.ShouldBe("first"); many[1].Message.ShouldBe("second"); } [Fact] public void Params_with_empty_array_returns_EmptyError() { // Given // When var result = Error.Many(Array.Empty()); // Then result.ShouldBeOfType(); result.IsEmpty.ShouldBeTrue(); result.Count.ShouldBe(0); } [Fact] public void With_single_error_returns_it_unwrapped() { // Given var single = Error.New("only one"); // When var result = Error.Many(single); // Then result.ShouldBe(single); result.ShouldBeOfType(); } [Fact] public void With_ManyErrors_and_error_flattens() { // Given var first = Error.New("first"); var second = Error.New("second"); var many = Error.Many(first, second); var third = Error.New("third"); // When var result = Error.Many(many, third); // Then result.ShouldBeOfType(); var resultMany = (ManyErrors)result; resultMany.Count.ShouldBe(3); resultMany[0].Message.ShouldBe("first"); resultMany[1].Message.ShouldBe("second"); resultMany[2].Message.ShouldBe("third"); } } public class Operator_Null_Safety { [Fact] public void Error_plus_null_returns_original_error() { // Given var error = Error.New("test"); // When var result = error + null; // Then result.ShouldBe(error); } [Fact] public void Null_plus_error_returns_the_error() { // Given var error = Error.New("test"); // When var result = null + error; // Then result.ShouldBe(error); } [Fact] public void Null_plus_null_is_null() { // Given // When var result = (Error?)null + (Error?)null; // Then result.ShouldBeNull(); } } public class Append_Edge_Cases { [Fact] public void Append_null_returns_original_error() { // Given var error = Error.New("test"); // When var result = error.Append(null); // Then result.ShouldBe(error); } [Fact] public void Append_empty_ManyErrors_returns_original_error() { // Given var error = Error.New("test"); var empty = Error.Many(Array.Empty()); // When var result = error.Append(empty); // Then result.ShouldBe(error); } [Fact] public void Empty_append_error_returns_the_appended_error() { // Given var empty = Error.Many(Array.Empty()); var error = Error.New("test"); // When var result = empty.Append(error); // Then result.ShouldBe(error); } } public class Property_Correctness { [Fact] public void ExpectedError_IsEmpty_is_false() { var error = Error.New("test"); error.IsEmpty.ShouldBeFalse(); } [Fact] public void ExceptionalError_IsEmpty_is_false() { var error = Error.New(new Exception("test")); error.IsEmpty.ShouldBeFalse(); } [Fact] public void EmptyError_IsEmpty() { var error = Error.Many(Array.Empty()); error.ShouldBeOfType(); error.IsEmpty.ShouldBeTrue(); } [Fact] public void ManyErrors_IsExpected_when_all_are_expected() { // Given var errors = Error.Many( Error.New("err1"), Error.New("err2")); // When / Then errors.IsExpected.ShouldBeTrue(); } [Fact] public void ManyErrors_IsExpected_when_mixed_is_false() { // Given var errors = Error.Many( Error.New("err1"), Error.New(new InvalidOperationException("boom"))); // When / Then errors.IsExpected.ShouldBeFalse(); } [Fact] public void ManyErrors_IsExceptional_when_any_is_exceptional() { // Given var errors = Error.Many( Error.New("err1"), Error.New(new InvalidOperationException("boom"))); // When / Then errors.IsExceptional.ShouldBeTrue(); } [Fact] public void ManyErrors_IsExceptional_when_none_is_false() { // Given var errors = Error.Many( Error.New("err1"), Error.New("err2")); // When / Then errors.IsExceptional.ShouldBeFalse(); } } public class ToException { [Fact] public void ExceptionalError_ToException_returns_original_exception() { // Given var original = new InvalidOperationException("boom"); var error = Error.New(original); // When var result = error.ToException(); // Then result.ShouldBe(original); } [Fact] public void ExceptionalError_without_exception_falls_back_to_ErrorException() { // Given // Use deserialization path — creates ExceptionalError without internal Exception var json = """{"type":"System.Exception","msg":"deserialized error"}"""; var error = JsonSerializer.Deserialize(json)!; // When var result = error.ToException(); // Then result.ShouldNotBeOfType(); result.ShouldBeOfType(); var errEx = (ErrorException)result; errEx.Type.ShouldBe("System.Exception"); errEx.Message.ShouldBe("deserialized error"); } [Fact] public void ManyErrors_ToException_returns_AggregateException() { // Given var errors = Error.Many( Error.New("first"), Error.New("second")); // When var result = errors.ToException(); // Then result.ShouldBeOfType(); var aggregate = (AggregateException)result; aggregate.InnerExceptions.Count.ShouldBe(2); aggregate.InnerExceptions[0].Message.ShouldBe("first"); aggregate.InnerExceptions[1].Message.ShouldBe("second"); } } public class ExtensionData { [Fact] public void Extracted_from_Exception_Data() { // Given var exception = new InvalidOperationException("boom"); exception.Data["key1"] = "value1"; exception.Data["key2"] = 42; // int value → ToString → "42" // When var error = Error.New(exception); // Then error.ExtensionData["key1"].ShouldBe("value1"); error.ExtensionData["key2"].ShouldBe("42"); } [Fact] public void Exception_Data_with_null_value_is_skipped() { // Given var exception = new InvalidOperationException("boom"); exception.Data["key"] = null; // When var error = Error.New(exception); // Then error.ExtensionData.Count.ShouldBe(0); } [Fact] public void Exception_without_Data_has_empty_ExtensionData() { // Given var exception = new InvalidOperationException("boom"); // When var error = Error.New(exception); // Then error.ExtensionData.ShouldNotBeNull(); error.ExtensionData.IsEmpty.ShouldBeTrue(); } } public class Indexer { [Fact] public void Existing_key_returns_value() { // Given var exception = new InvalidOperationException("boom"); exception.Data["mykey"] = "myvalue"; var error = Error.New(exception); // When var value = error["mykey"]; // Then value.ShouldBe("myvalue"); } [Fact] public void Missing_key_returns_null() { // Given var error = Error.New("test"); // When var value = error["nonexistent"]; // Then value.ShouldBeNull(); } } public class Deconstruct { [Fact] public void Produces_Type_and_Message() { // Given var error = Error.New("custom_type", "custom message"); // When var (type, message) = error; // Then type.ShouldBe("custom_type"); message.ShouldBe("custom message"); } } public class EmptyError_Tests { [Fact] public void Error_Empty_returns_EmptyError_singleton() { Error.Empty.ShouldBeOfType(); Error.Empty.ShouldBeSameAs(EmptyError.Instance); } [Fact] public void Properties_are_correct() { var error = Error.Empty; error.Type.ShouldBe("empty"); error.Message.ShouldBe(""); error.Count.ShouldBe(0); error.IsEmpty.ShouldBeTrue(); error.IsExpected.ShouldBeTrue(); error.IsExceptional.ShouldBeFalse(); } [Fact] public void ToEnumerable_returns_empty() { Error.Empty.ToEnumerable().ShouldBeEmpty(); } [Fact] public void ToException_returns_ErrorException() { var ex = Error.Empty.ToException(); ex.ShouldBeOfType(); var errEx = (ErrorException)ex; errEx.Type.ShouldBe("empty"); errEx.Message.ShouldBe(""); } [Fact] public void Deconstruct_produces_type_and_message() { var (type, message) = Error.Empty; type.ShouldBe("empty"); message.ShouldBe(""); } [Fact] public void ExtensionData_is_empty() { Error.Empty.ExtensionData.ShouldBeEmpty(); } [Fact] public void Indexer_returns_null() { Error.Empty["anything"].ShouldBeNull(); } [Fact] public void Equals_self() { Error.Empty.Equals(Error.Empty).ShouldBeTrue(); Error.Empty.Equals(EmptyError.Instance).ShouldBeTrue(); } [Fact] public void Equals_other_empty_error() { // An empty ManyErrors is also IsEmpty=true, so EmptyError equals it var emptyMany = new ManyErrors(Array.Empty()); Error.Empty.Equals(emptyMany).ShouldBeTrue(); } [Fact] public void Equals_non_empty_is_false() { var realError = Error.New("test"); Error.Empty.Equals(realError).ShouldBeFalse(); } [Fact] public void IsSimilarTo_empty_error() { var emptyMany = new ManyErrors(Array.Empty()); Error.Empty.IsSimilarTo(emptyMany).ShouldBeTrue(); } [Fact] public void IsSimilarTo_non_empty_is_false() { Error.Empty.IsSimilarTo(Error.New("test")).ShouldBeFalse(); } [Fact] public void CompareTo_sorts_before_non_empty() { Error.Empty.CompareTo(Error.New("test")).ShouldBeLessThan(0); } [Fact] public void CompareTo_self_is_zero() { Error.Empty.CompareTo(Error.Empty).ShouldBe(0); } [Fact] public void ToString_returns_empty_message() { Error.Empty.ToString().ShouldBe(""); } [Fact] public void Operator_equals() { (Error.Empty == EmptyError.Instance).ShouldBeTrue(); (Error.Empty == Error.New("test")).ShouldBeFalse(); } [Fact] public void Operator_not_equals() { (Error.Empty != Error.New("test")).ShouldBeTrue(); (Error.Empty != EmptyError.Instance).ShouldBeFalse(); } [Fact] public void Append_EmptyError_returns_other() { var real = Error.New("test"); Error.Empty.Append(real).ShouldBe(real); } [Fact] public void Append_to_EmptyError_returns_real() { var real = Error.New("test"); real.Append(Error.Empty).ShouldBe(real); } [Fact] public void Operator_plus_EmptyError() { var real = Error.New("test"); (Error.Empty + real).ShouldBe(real); (real + Error.Empty).ShouldBe(real); } [Fact] public void Many_with_EmptyError_absorbs_it() { var real = Error.New("test"); var result = Error.Many(Error.Empty, real); result.ShouldBeOfType(); result.ShouldBe(real); } [Fact] public void GetHashCode_is_stable() { Error.Empty.GetHashCode().ShouldBe(0); EmptyError.Instance.GetHashCode().ShouldBe(0); } } }