737 lines
20 KiB
C#
737 lines
20 KiB
C#
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<ExpectedError>();
|
|
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<ExpectedError>();
|
|
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<ExceptionalError>();
|
|
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<ExceptionalError>();
|
|
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<ManyErrors>();
|
|
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<ExceptionalError>();
|
|
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<ExceptionalError>();
|
|
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<ManyErrors>();
|
|
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<EmptyError>();
|
|
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<ExpectedError>();
|
|
}
|
|
|
|
[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<ExpectedError>();
|
|
}
|
|
|
|
[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<ManyErrors>();
|
|
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<Error>());
|
|
|
|
// Then
|
|
result.ShouldBeOfType<EmptyError>();
|
|
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<ExpectedError>();
|
|
}
|
|
|
|
[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<ManyErrors>();
|
|
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<Error>());
|
|
|
|
// 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<Error>());
|
|
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>());
|
|
error.ShouldBeOfType<EmptyError>();
|
|
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<ExceptionalError>(json)!;
|
|
|
|
// When
|
|
var result = error.ToException();
|
|
|
|
// Then
|
|
result.ShouldNotBeOfType<InvalidOperationException>();
|
|
result.ShouldBeOfType<ErrorException>();
|
|
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<AggregateException>();
|
|
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<EmptyError>();
|
|
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<ErrorException>();
|
|
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>());
|
|
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>());
|
|
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<ExpectedError>();
|
|
result.ShouldBe(real);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetHashCode_is_stable()
|
|
{
|
|
Error.Empty.GetHashCode().ShouldBe(0);
|
|
EmptyError.Instance.GetHashCode().ShouldBe(0);
|
|
}
|
|
}
|
|
}
|