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_empty_ManyErrors() { // 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_empty_ManyErrors() { // 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 ManyErrors_IsEmpty_when_empty() { var error = Error.Many(Array.Empty()); 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"); } } }