diff --git a/src/FluentResults.Extensions.AwesomeAssertions.Test/FluentResults.Extensions.AwesomeAssertions.Test.csproj b/src/FluentResults.Extensions.AwesomeAssertions.Test/FluentResults.Extensions.AwesomeAssertions.Test.csproj new file mode 100644 index 0000000..bb668ff --- /dev/null +++ b/src/FluentResults.Extensions.AwesomeAssertions.Test/FluentResults.Extensions.AwesomeAssertions.Test.csproj @@ -0,0 +1,26 @@ + + + + net48;net8.0;net9.0 + latest + false + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/src/FluentResults.Extensions.AwesomeAssertions.Test/GlobalUsings.cs b/src/FluentResults.Extensions.AwesomeAssertions.Test/GlobalUsings.cs new file mode 100644 index 0000000..dbacb28 --- /dev/null +++ b/src/FluentResults.Extensions.AwesomeAssertions.Test/GlobalUsings.cs @@ -0,0 +1,4 @@ +global using System; +global using AwesomeAssertions; +global using Xunit; +global using Xunit.Sdk; \ No newline at end of file diff --git a/src/FluentResults.Extensions.AwesomeAssertions.Test/ResultTests/BeFailureTests.cs b/src/FluentResults.Extensions.AwesomeAssertions.Test/ResultTests/BeFailureTests.cs new file mode 100644 index 0000000..dd42fa7 --- /dev/null +++ b/src/FluentResults.Extensions.AwesomeAssertions.Test/ResultTests/BeFailureTests.cs @@ -0,0 +1,27 @@ +namespace FluentResults.Extensions.AwesomeAssertions.Test.ResultTests +{ + public class BeFailureTests + { + [Fact] + public void A_failed_result_throw_no_exception() + { + var failedResult = Result.Fail("Error 1"); + + Action action = () => failedResult.Should().BeFailure(); + + action.Should().NotThrow(); + } + + [Fact] + public void A_success_result_throw_a_exception() + { + var successResult = Result.Ok(); + + Action action = () => successResult.Should().BeFailure(); + + action.Should() + .Throw() + .WithMessage("Expected result be failed, but is success"); + } + } +} diff --git a/src/FluentResults.Extensions.AwesomeAssertions.Test/ResultTests/BeSuccessTests.cs b/src/FluentResults.Extensions.AwesomeAssertions.Test/ResultTests/BeSuccessTests.cs new file mode 100644 index 0000000..a3e548b --- /dev/null +++ b/src/FluentResults.Extensions.AwesomeAssertions.Test/ResultTests/BeSuccessTests.cs @@ -0,0 +1,55 @@ +// ReSharper disable once CheckNamespace +namespace FluentResults.Extensions.AwesomeAssertions.Test.ResultTests +{ + public class BeSuccessTests + { + [Fact] + public void A_success_result_throw_no_exception() + { + var successResult = Result.Ok(); + + Action action = () => successResult.Should().BeSuccess(); + + action.Should().NotThrow(); + } + + [Fact] + public void A_failed_result_with_one_error_throw_a_exception() + { + var failedResult = Result.Fail("Error 1"); + + Action action = () => failedResult.Should().BeSuccess(); + + action.Should() + .Throw() + .WithMessage("Expected result be success, but is failed because of '{Error with Message='Error 1'}'"); + } + + [Fact] + public void A_failed_result_with_multiple_errors_throw_a_exception() + { + var failedResult = Result.Fail("Error 1") + .WithError("Error 2"); + + Action action = () => failedResult.Should().BeSuccess(); + + action.Should() + .Throw() + .WithMessage( + "Expected result be success, but is failed because of '{Error with Message='Error 1', Error with Message='Error 2'}'"); + } + + [Fact] + public void A_failed_result_with_a_success_throw_a_exception() + { + var failedResult = Result.Fail("Error 1") + .WithSuccess("Success 1"); + + Action action = () => failedResult.Should().BeSuccess(); + + action.Should() + .Throw() + .WithMessage("Expected result be success, but is failed because of '{Error with Message='Error 1'}'"); + } + } +} \ No newline at end of file diff --git a/src/FluentResults.Extensions.AwesomeAssertions.Test/ResultTests/HaveErrorTests.cs b/src/FluentResults.Extensions.AwesomeAssertions.Test/ResultTests/HaveErrorTests.cs new file mode 100644 index 0000000..b7c53a7 --- /dev/null +++ b/src/FluentResults.Extensions.AwesomeAssertions.Test/ResultTests/HaveErrorTests.cs @@ -0,0 +1,32 @@ +namespace FluentResults.Extensions.AwesomeAssertions.Test.ResultTests +{ + public class HaveErrorTests + { + [Theory] + [InlineData("Error 1")] + [InlineData("Error")] + public void A_result_with_expected_reason_throw_no_exception(string expectedError) + { + var failedResult = Result.Fail("Error 1"); + + Action action = () => + failedResult.Should().BeFailure().And + .HaveError(expectedError, MessageComparisonLogics.ActualContainsExpected); + + action.Should().NotThrow(); + } + + [Fact] + public void A_result_without_expected_reason_throw_a_exception() + { + var successResult = Result.Fail("Error 2"); + + Action action = () => successResult.Should().BeFailure().And.HaveError("Error 1"); + + action.Should() + .Throw() + .WithMessage( + "Expected result to contain error with message containing \"Error 1\", but found error '{Error with Message='Error 2'}'"); + } + } +} \ No newline at end of file diff --git a/src/FluentResults.Extensions.AwesomeAssertions.Test/ResultTests/HaveReasonTests.cs b/src/FluentResults.Extensions.AwesomeAssertions.Test/ResultTests/HaveReasonTests.cs new file mode 100644 index 0000000..b95b998 --- /dev/null +++ b/src/FluentResults.Extensions.AwesomeAssertions.Test/ResultTests/HaveReasonTests.cs @@ -0,0 +1,93 @@ +namespace FluentResults.Extensions.AwesomeAssertions.Test.ResultTests +{ + public class HaveReasonTests + { + [Theory] + [InlineData("Error 1")] + [InlineData("Error")] + public void A_result_with_expected_reason_throw_no_exception(string expectedError) + { + var failedResult = Result.Fail("Error 1"); + + Action action = () => + failedResult.Should().BeFailure().And + .HaveReason(expectedError, MessageComparisonLogics.ActualContainsExpected); + + action.Should().NotThrow(); + } + + [Fact] + public void A_result_without_expected_reason_throw_a_exception() + { + var successResult = Result.Fail("Error 2"); + + Action action = () => successResult.Should().BeFailure().And.HaveReason("Error 1"); + + action.Should() + .Throw() + .WithMessage( + "Expected result to contain reason with message containing \"Error 1\", but found reasons '{Error with Message='Error 2'}'"); + } + + [Theory] + [InlineData("Error")] + public void A_result_with_expected_reason_of_type_throw_no_exception_equal(string expectedError) + { + var successResult = Result.Fail(new SomeReason("Error 1")); + + Action action = () => successResult.Should().BeFailure().And.HaveReason(expectedError); + + action.Should().Throw(); + } + + [Theory] + [InlineData("Error 1")] + [InlineData("Error")] + public void A_result_with_expected_reason_of_type_throw_no_exception(string expectedError) + { + var successResult = Result.Fail(new SomeReason("Error 1")); + + Action action = () => + successResult.Should().BeFailure().And + .HaveReason(expectedError, MessageComparisonLogics.ActualContainsExpected); + + action.Should().NotThrow(); + } + + [Fact] + public void A_result_without_expected_reason_of_type_throw_a_exception() + { + var successResult = Result.Fail("Error 1"); + + Action action = () => successResult.Should().BeFailure().And.HaveReason("Error 1"); + + action.Should() + .Throw() + .WithMessage( + "Expected result to contain reason of type \"SomeReason\" with message containing \"Error 1\", but found reasons '{Error with Message='Error 1'}'"); + } + + [Fact] + public void A_result_with_expected_reason_object_throw_no_exception() + { + var successResult = Result.Fail("Error 1"); + + Action action = () => successResult.Should().BeFailure().And.HaveReason(new Error("Error 1")); + + action.Should().NotThrow(); + } + + [Fact] + public void A_result_without_expected_reason_object_throw_a_exception() + { + var successResult = Result.Fail("Error 1"); + + Action action = () => successResult.Should().BeFailure().And.HaveReason(new Error("Error 2")); + + action.Should() + .Throw() + .WithMessage( + "Expected Subject.Reasons {Error with Message='Error 1'} to contain equivalent of Error with Message='Error 2'*"); + } + } +} \ No newline at end of file diff --git a/src/FluentResults.Extensions.AwesomeAssertions.Test/ResultTests/ReasonHaveMetadataTests.cs b/src/FluentResults.Extensions.AwesomeAssertions.Test/ResultTests/ReasonHaveMetadataTests.cs new file mode 100644 index 0000000..eb7b875 --- /dev/null +++ b/src/FluentResults.Extensions.AwesomeAssertions.Test/ResultTests/ReasonHaveMetadataTests.cs @@ -0,0 +1,59 @@ +namespace FluentResults.Extensions.AwesomeAssertions.Test.ResultTests +{ + public class ReasonHaveMetadataTests + { + [Fact] + public void A_result_with_reason_with_specific_metadata_throw_no_exception() + { + var failedResult = Result.Fail(new SomeReason("Error 1") + .WithMetadata("key", "value")); + + Action action = () => failedResult + .Should() + .BeFailure() + .And.HaveReason("Error 1") + .That.HaveMetadata("key", "value"); + + action.Should().NotThrow(); + } + + [Theory] + [InlineData(null)] + [InlineData("value1")] + [InlineData(1)] + public void A_result_with_reason_with_another_metadata_because_of_value_throw_exception(object expectedMetadataValue) + { + var failedResult = Result.Fail(new SomeReason("Error 1") + .WithMetadata("key", "value")); + + Action action = () => failedResult + .Should() + .BeFailure() + .And.HaveReason("Error 1") + .That.HaveMetadata("key", expectedMetadataValue); + + action.Should() + .Throw() + .WithMessage($"Reason should contain 'key' with '{expectedMetadataValue}', but not contain it"); + } + + [Theory] + [InlineData("ke")] + [InlineData("key1")] + public void A_result_with_reason_with_another_metadata_because_of_key_throw_exception(string expectedMetadataKey) + { + var failedResult = Result.Fail(new SomeReason("Error 1") + .WithMetadata("key", "value")); + + Action action = () => failedResult + .Should() + .BeFailure() + .And.HaveReason("Error 1") + .That.HaveMetadata(expectedMetadataKey, "value"); + + action.Should() + .Throw() + .WithMessage($"Reason should contain '{expectedMetadataKey}' with 'value', but not contain it"); + } + } +} \ No newline at end of file diff --git a/src/FluentResults.Extensions.AwesomeAssertions.Test/ResultTests/ReasonSatisfyTests.cs b/src/FluentResults.Extensions.AwesomeAssertions.Test/ResultTests/ReasonSatisfyTests.cs new file mode 100644 index 0000000..a78cab9 --- /dev/null +++ b/src/FluentResults.Extensions.AwesomeAssertions.Test/ResultTests/ReasonSatisfyTests.cs @@ -0,0 +1,59 @@ +namespace FluentResults.Extensions.AwesomeAssertions.Test.ResultTests +{ + public class ReasonSatisfyTests + { + + [Fact] + public void A_reason_with_the_expected_property_throw_no_exception() + { + var failedResult = Result.Fail(new SomeReason("Error 1") + { + Prop = "Prop1" + }); + + Action action = () => failedResult + .Should() + .BeFailure() + .And.HaveReason("Error 1") + .That.Satisfy(r => r.Prop.Should().Be("Prop1")); + + action.Should().NotThrow(); + } + + [Fact] + public void A_reason_with_another_property_throw_exception() + { + var failedResult = Result.Fail(new SomeReason("Error 1") + { + Prop = "Prop1" + }); + + Action action = () => failedResult + .Should() + .BeFailure() + .And.HaveReason("Error 1") + .That.Satisfy(r => r.Prop.Should().Be("Prop2")); + + action.Should() + .Throw(); + } + + [Fact] + public void A_reason_with_another_type_throw_exception() + { + var failedResult = Result.Fail(new SomeReason("Error 1") + { + Prop = "Prop1" + }); + + Action action = () => failedResult + .Should() + .BeFailure() + .And.HaveReason("Error 1") + .That.Satisfy(r => {}); + + action.Should() + .Throw(); + } + } +} \ No newline at end of file diff --git a/src/FluentResults.Extensions.AwesomeAssertions.Test/SomeReason.cs b/src/FluentResults.Extensions.AwesomeAssertions.Test/SomeReason.cs new file mode 100644 index 0000000..c5ee00c --- /dev/null +++ b/src/FluentResults.Extensions.AwesomeAssertions.Test/SomeReason.cs @@ -0,0 +1,20 @@ +namespace FluentResults.Extensions.AwesomeAssertions.Test +{ + internal class SomeReason : Error + { + public string Prop { get; set; } + + public SomeReason(string message) : base(message) + { + + } + } + + internal class AnotherReason : Error + { + public AnotherReason(string message) : base(message) + { + + } + } +} diff --git a/src/FluentResults.Extensions.AwesomeAssertions.Test/ValueResultTests/BeFailureTests.cs b/src/FluentResults.Extensions.AwesomeAssertions.Test/ValueResultTests/BeFailureTests.cs new file mode 100644 index 0000000..e3a3974 --- /dev/null +++ b/src/FluentResults.Extensions.AwesomeAssertions.Test/ValueResultTests/BeFailureTests.cs @@ -0,0 +1,27 @@ +namespace FluentResults.Extensions.AwesomeAssertions.Test.ValueResultTests +{ + public class BeFailureTests + { + [Fact] + public void A_failed_result_throw_no_exception() + { + var failedResult = Result.Fail("Error 1"); + + Action action = () => failedResult.Should().BeFailure(); + + action.Should().NotThrow(); + } + + [Fact] + public void A_success_result_throw_a_exception() + { + var successResult = Result.Ok(1); + + Action action = () => successResult.Should().BeFailure(); + + action.Should() + .Throw() + .WithMessage("Expected result be failed, but is success"); + } + } +} diff --git a/src/FluentResults.Extensions.AwesomeAssertions.Test/ValueResultTests/BeSuccessTests.cs b/src/FluentResults.Extensions.AwesomeAssertions.Test/ValueResultTests/BeSuccessTests.cs new file mode 100644 index 0000000..a14fc64 --- /dev/null +++ b/src/FluentResults.Extensions.AwesomeAssertions.Test/ValueResultTests/BeSuccessTests.cs @@ -0,0 +1,54 @@ +namespace FluentResults.Extensions.AwesomeAssertions.Test.ValueResultTests +{ + public class BeSuccessTests + { + [Fact] + public void A_success_result_throw_no_exception() + { + var successResult = Result.Ok(1); + + Action action = () => successResult.Should().BeSuccess(); + + action.Should().NotThrow(); + } + + [Fact] + public void A_failed_result_with_one_error_throw_a_exception() + { + var failedResult = Result.Fail("Error 1"); + + Action action = () => failedResult.Should().BeSuccess(); + + action.Should() + .Throw() + .WithMessage("Expected result be success, but is failed because of '{Error with Message='Error 1'}'"); + } + + [Fact] + public void A_failed_result_with_multiple_errors_throw_a_exception() + { + var failedResult = Result.Fail("Error 1") + .WithError("Error 2"); + + Action action = () => failedResult.Should().BeSuccess(); + + action.Should() + .Throw() + .WithMessage( + "Expected result be success, but is failed because of '{Error with Message='Error 1', Error with Message='Error 2'}'"); + } + + [Fact] + public void A_failed_result_with_a_success_throw_a_exception() + { + var failedResult = Result.Fail("Error 1") + .WithSuccess("Success 1"); + + Action action = () => failedResult.Should().BeSuccess(); + + action.Should() + .Throw() + .WithMessage("Expected result be success, but is failed because of '{Error with Message='Error 1'}'"); + } + } +} \ No newline at end of file diff --git a/src/FluentResults.Extensions.AwesomeAssertions.Test/ValueResultTests/HaveReasonTests.cs b/src/FluentResults.Extensions.AwesomeAssertions.Test/ValueResultTests/HaveReasonTests.cs new file mode 100644 index 0000000..220e1a3 --- /dev/null +++ b/src/FluentResults.Extensions.AwesomeAssertions.Test/ValueResultTests/HaveReasonTests.cs @@ -0,0 +1,75 @@ +namespace FluentResults.Extensions.AwesomeAssertions.Test.ValueResultTests +{ + public class HaveReasonTests + { + [Theory] + [InlineData("Error 1")] + [InlineData("Error")] + public void A_result_with_expected_reason_throw_no_exception(string expectedError) + { + var failedResult = Result.Fail("Error 1"); + + Action action = () => failedResult.Should().BeFailure().And.HaveReason(expectedError, MessageComparisonLogics.ActualContainsExpected); + + action.Should().NotThrow(); + } + + [Fact] + public void A_result_without_expected_reason_throw_a_exception() + { + var successResult = Result.Fail("Error 2"); + + Action action = () => successResult.Should().BeFailure().And.HaveReason("Error 1"); + + action.Should() + .Throw() + .WithMessage("Expected result to contain reason with message containing \"Error 1\", but found reasons '{Error with Message='Error 2'}'"); + } + + [Theory] + [InlineData("Error 1")] + [InlineData("Error")] + public void A_result_with_expected_reason_of_type_throw_no_exception(string expectedError) + { + var successResult = Result.Fail(new SomeReason("Error 1")); + + Action action = () => successResult.Should().BeFailure().And.HaveReason(expectedError, MessageComparisonLogics.ActualContainsExpected); + + action.Should().NotThrow(); + } + + [Fact] + public void A_result_without_expected_reason_of_type_throw_a_exception() + { + var successResult = Result.Fail("Error 1"); + + Action action = () => successResult.Should().BeFailure().And.HaveReason("Error 1"); + + action.Should() + .Throw() + .WithMessage("Expected result to contain reason of type \"SomeReason\" with message containing \"Error 1\", but found reasons '{Error with Message='Error 1'}'"); + } + + [Fact] + public void A_result_with_expected_reason_object_throw_no_exception() + { + var successResult = Result.Fail("Error 1"); + + Action action = () => successResult.Should().BeFailure().And.HaveReason(new Error("Error 1")); + + action.Should().NotThrow(); + } + + [Fact] + public void A_result_without_expected_reason_object_throw_a_exception() + { + var successResult = Result.Fail("Error 1"); + + Action action = () => successResult.Should().BeFailure().And.HaveReason(new Error("Error 2")); + + action.Should() + .Throw() + .WithMessage("Expected Subject.Reasons {Error with Message='Error 1'} to contain equivalent of Error with Message='Error 2'*"); + } + } +} diff --git a/src/FluentResults.Extensions.AwesomeAssertions.Test/ValueResultTests/HaveValueTests.cs b/src/FluentResults.Extensions.AwesomeAssertions.Test/ValueResultTests/HaveValueTests.cs new file mode 100644 index 0000000..6e13618 --- /dev/null +++ b/src/FluentResults.Extensions.AwesomeAssertions.Test/ValueResultTests/HaveValueTests.cs @@ -0,0 +1,78 @@ +namespace FluentResults.Extensions.AwesomeAssertions.Test.ValueResultTests +{ + public class HaveValueTests + { + [Fact] + public void Asserting_the_correct_value_of_a_success_result_throw_no_exception() + { + var successResult = Result.Ok(1); + + Action action = () => successResult.Should().HaveValue(1); + + action.Should().NotThrow(); + } + + [Fact] + public void Asserting_a_false_value_of_a_success_result_throw_a_exception() + { + var successResult = Result.Ok(1); + + Action action = () => successResult.Should().HaveValue(2); + + action.Should() + .Throw() + .WithMessage("Expected value is '2', but is '1'"); + } + + [Fact] + public void A_failed_result_throw_no_exception() + { + var failedResult = Result.Fail("Error 1"); + + Action action = () => failedResult.Should().HaveValue(1); + + action.Should() + .Throw() + .WithMessage( + "Value can not be asserted because result is failed because of '{Error with Message='Error 1'}'"); + } + + [Fact] + public void Asserting_null_should_not_throw_exceptions_when_type_is_a_primitive() + { + var successResult = Result.Ok(null as int?); + + Action action = () => successResult.Should().HaveValue(null); + + action.Should().NotThrow(); + } + + [Fact] + public void Asserting_null_should_not_throw_exceptions_when_type_is_a_struct() + { + var successResult = Result.Ok(null as SomeStruct?); + + Action action = () => successResult.Should().HaveValue(null); + + action.Should().NotThrow(); + } + + [Fact] + public void Asserting_null_should_not_throw_exceptions_when_type_is_a_class() + { + var successResult = Result.Ok(null as SomeClass); + + Action action = () => successResult.Should().HaveValue(null); + + action.Should().NotThrow(); + } + + internal struct SomeStruct + { + } + + internal class SomeClass + { + } + } +} \ No newline at end of file diff --git a/src/FluentResults.Extensions.AwesomeAssertions.Test/ValueResultTests/ReasonHaveMetadataTests.cs b/src/FluentResults.Extensions.AwesomeAssertions.Test/ValueResultTests/ReasonHaveMetadataTests.cs new file mode 100644 index 0000000..6d93088 --- /dev/null +++ b/src/FluentResults.Extensions.AwesomeAssertions.Test/ValueResultTests/ReasonHaveMetadataTests.cs @@ -0,0 +1,59 @@ +namespace FluentResults.Extensions.AwesomeAssertions.Test.ValueResultTests +{ + public class ReasonHaveMetadataTests + { + [Fact] + public void A_result_with_reason_with_specific_metadata_throw_no_exception() + { + var failedResult = Result.Fail(new SomeReason("Error 1") + .WithMetadata("key", "value")); + + Action action = () => failedResult + .Should() + .BeFailure() + .And.HaveReason("Error 1") + .That.HaveMetadata("key", "value"); + + action.Should().NotThrow(); + } + + [Theory] + [InlineData(null)] + [InlineData("value1")] + [InlineData(1)] + public void A_result_with_reason_with_another_metadata_because_of_value_throw_exception(object expectedMetadataValue) + { + var failedResult = Result.Fail(new SomeReason("Error 1") + .WithMetadata("key", "value")); + + Action action = () => failedResult + .Should() + .BeFailure() + .And.HaveReason("Error 1") + .That.HaveMetadata("key", expectedMetadataValue); + + action.Should() + .Throw() + .WithMessage($"Reason should contain 'key' with '{expectedMetadataValue}', but not contain it"); + } + + [Theory] + [InlineData("ke")] + [InlineData("key1")] + public void A_result_with_reason_with_another_metadata_because_of_key_throw_exception(string expectedMetadataKey) + { + var failedResult = Result.Fail(new SomeReason("Error 1") + .WithMetadata("key", "value")); + + Action action = () => failedResult + .Should() + .BeFailure() + .And.HaveReason("Error 1") + .That.HaveMetadata(expectedMetadataKey, "value"); + + action.Should() + .Throw() + .WithMessage($"Reason should contain '{expectedMetadataKey}' with 'value', but not contain it"); + } + } +} \ No newline at end of file diff --git a/src/FluentResults.Extensions.AwesomeAssertions.Test/ValueResultTests/ReasonSatisfyTests.cs b/src/FluentResults.Extensions.AwesomeAssertions.Test/ValueResultTests/ReasonSatisfyTests.cs new file mode 100644 index 0000000..714a84c --- /dev/null +++ b/src/FluentResults.Extensions.AwesomeAssertions.Test/ValueResultTests/ReasonSatisfyTests.cs @@ -0,0 +1,59 @@ +namespace FluentResults.Extensions.AwesomeAssertions.Test.ValueResultTests +{ + public class ReasonSatisfyTests + { + + [Fact] + public void A_reason_with_the_expected_property_throw_no_exception() + { + var failedResult = Result.Fail(new SomeReason("Error 1") + { + Prop = "Prop1" + }); + + Action action = () => failedResult + .Should() + .BeFailure() + .And.HaveReason("Error 1") + .That.Satisfy(r => r.Prop.Should().Be("Prop1")); + + action.Should().NotThrow(); + } + + [Fact] + public void A_reason_with_another_property_throw_exception() + { + var failedResult = Result.Fail(new SomeReason("Error 1") + { + Prop = "Prop1" + }); + + Action action = () => failedResult + .Should() + .BeFailure() + .And.HaveReason("Error 1") + .That.Satisfy(r => r.Prop.Should().Be("Prop2")); + + action.Should() + .Throw(); + } + + [Fact] + public void A_reason_with_another_type_throw_exception() + { + var failedResult = Result.Fail(new SomeReason("Error 1") + { + Prop = "Prop1" + }); + + Action action = () => failedResult + .Should() + .BeFailure() + .And.HaveReason("Error 1") + .That.Satisfy(r => {}); + + action.Should() + .Throw(); + } + } +} \ No newline at end of file diff --git a/src/FluentResults.Extensions.AwesomeAssertions/AndWhichThatConstraint.cs b/src/FluentResults.Extensions.AwesomeAssertions/AndWhichThatConstraint.cs new file mode 100644 index 0000000..c8ef5e3 --- /dev/null +++ b/src/FluentResults.Extensions.AwesomeAssertions/AndWhichThatConstraint.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using AwesomeAssertions; + +namespace FluentResults.Extensions.AwesomeAssertions +{ + public class AndWhichThatConstraint : AndWhichConstraint + { + public AndWhichThatConstraint(TParentConstraint parentConstraint, TMatchedElement matchedConstraint, TThatConstraint thatConstraint) + : base(parentConstraint, matchedConstraint) + { + That = thatConstraint; + } + + public AndWhichThatConstraint(TParentConstraint parentConstraint, TMatchedElement matchedConstraint, IEnumerable matchedElements) + : base(parentConstraint, matchedConstraint) + { + } + + public TThatConstraint That { get; } + } +} \ No newline at end of file diff --git a/src/FluentResults.Extensions.AwesomeAssertions/Assertions/AssertionOperators.cs b/src/FluentResults.Extensions.AwesomeAssertions/Assertions/AssertionOperators.cs new file mode 100644 index 0000000..cae1a9b --- /dev/null +++ b/src/FluentResults.Extensions.AwesomeAssertions/Assertions/AssertionOperators.cs @@ -0,0 +1,107 @@ +using System; +using System.Linq; +using AwesomeAssertions; +using AwesomeAssertions.Execution; + +// ReSharper disable once CheckNamespace +namespace FluentResults.Extensions.AwesomeAssertions +{ + internal class BeFailureAssertionOperator + { + public AndWhichConstraint Do(TResult subject, TResultAssertion parentConstraint, AssertionChain assertionChain, string because, params object[] becauseArgs) + where TResult : ResultBase + { + assertionChain + .BecauseOf(because, becauseArgs) + .Given(() => subject.IsFailed) + .ForCondition(isFailed => isFailed) + .FailWith("Expected result be failed, but is success"); + + return new AndWhichConstraint(parentConstraint, subject); + } + } + + internal class BeSuccessAssertionOperator + { + public AndWhichConstraint Do(TResult subject, TResultAssertion parentConstraint, AssertionChain assertionChain, string because, params object[] becauseArgs) + where TResult : ResultBase + { + assertionChain + .BecauseOf(because, becauseArgs) + .Given(() => subject.IsSuccess) + .ForCondition(isSuccess => isSuccess) + .FailWith("Expected result be success, but is failed because of '{0}'", subject.Errors); + + return new AndWhichConstraint(parentConstraint, subject); + } + } + + internal class HaveReasonAssertionOperator + { + public AndWhichThatConstraint Do(TResult subject, TResultAssertion parentConstraint, AssertionChain assertionChain, string expectedMessage, Func messageComparison, string because, params object[] becauseArgs) + where TResult : ResultBase + { + messageComparison = messageComparison ?? FluentResultAssertionsConfig.MessageComparison; + + assertionChain + .BecauseOf(because, becauseArgs) + .Given(() => subject.Reasons) + .ForCondition(reasons => reasons.Any(reason => messageComparison(reason.Message, expectedMessage))) + .FailWith("Expected result to contain reason with message containing {0}, but found reasons '{1}'", expectedMessage, subject.Reasons); + + return new AndWhichThatConstraint(parentConstraint, subject, new ReasonAssertions(subject.Reasons.SingleOrDefault(reason => messageComparison(reason.Message, expectedMessage)), assertionChain)); + } + } + + internal class HaveErrorAssertionOperator + { + public AndWhichThatConstraint Do(TResult subject, TResultAssertion parentConstraint, AssertionChain assertionChain, string expectedMessage, Func messageComparison, string because, params object[] becauseArgs) + where TResult : ResultBase + { + messageComparison = messageComparison ?? FluentResultAssertionsConfig.MessageComparison; + + assertionChain + .BecauseOf(because, becauseArgs) + .Given(() => subject.Errors) + .ForCondition(errors => errors.Any(reason => messageComparison(reason.Message, expectedMessage))) + .FailWith("Expected result to contain error with message containing {0}, but found error '{1}'", expectedMessage, subject.Errors); + + return new AndWhichThatConstraint(parentConstraint, subject, new ReasonAssertions(subject.Reasons.SingleOrDefault(reason => messageComparison(reason.Message, expectedMessage)), assertionChain)); + } + } + + internal class HaveReasonTAssertionOperator + { + public AndWhichThatConstraint Do(TResult subject, TResultAssertion parentConstraint, AssertionChain assertionChain, string expectedMessage, Func messageComparison, string because, params object[] becauseArgs) + where TResult : ResultBase + where TReason : IReason + { + messageComparison = messageComparison ?? FluentResultAssertionsConfig.MessageComparison; + + assertionChain + .BecauseOf(because, becauseArgs) + .Given(() => subject.Reasons.OfType()) + .ForCondition(reasons => reasons.Any(reason => messageComparison(reason.Message, expectedMessage))) + .FailWith("Expected result to contain reason of type {0} with message containing {1}, but found reasons '{2}'", typeof(TReason).Name, expectedMessage, subject.Reasons); + + return new AndWhichThatConstraint(parentConstraint, subject, new ReasonAssertions(subject.Reasons.SingleOrDefault(reason => messageComparison(reason.Message, expectedMessage)), assertionChain)); + } + } + + internal class HaveValueAssertionOperator + { + public AndConstraint Do(Result subject, TResultAssertion parentConstraint, AssertionChain assertionChain, T expectedValue, string because, params object[] becauseArgs) + { + assertionChain + .BecauseOf(because) + .ForCondition(subject.IsSuccess) + .FailWith("Value can not be asserted because result is failed because of '{0}'", subject.Errors) + .Then + .Given(() => subject.Value) + .ForCondition(actualValue => (actualValue == null && expectedValue == null) || actualValue.Equals(expectedValue)) + .FailWith("Expected value is '{0}', but is '{1}'", expectedValue, subject.Value); + + return new AndConstraint(parentConstraint); + } + } +} diff --git a/src/FluentResults.Extensions.AwesomeAssertions/Assertions/ReasonAssertions.cs b/src/FluentResults.Extensions.AwesomeAssertions/Assertions/ReasonAssertions.cs new file mode 100644 index 0000000..ab9f2c9 --- /dev/null +++ b/src/FluentResults.Extensions.AwesomeAssertions/Assertions/ReasonAssertions.cs @@ -0,0 +1,46 @@ +using System; +using AwesomeAssertions; +using AwesomeAssertions.Execution; +using AwesomeAssertions.Primitives; + +// ReSharper disable once CheckNamespace +namespace FluentResults.Extensions.AwesomeAssertions +{ + public class ReasonAssertions : ReferenceTypeAssertions + { + public ReasonAssertions(IReason subject, AssertionChain assertionChain) + : base(subject, assertionChain) + { } + + protected override string Identifier => nameof(IReason); + + public AndWhichConstraint HaveMetadata(string metadataKey, object metadataValue, string because = "", params object[] becauseArgs) + { + CurrentAssertionChain + .BecauseOf(because, becauseArgs) + .Given(() => Subject.Metadata) + .ForCondition(metadata => + { + metadata.TryGetValue(metadataKey, out var actualMetadataValue); + return Equals(actualMetadataValue, metadataValue); + }) + .FailWith($"Reason should contain '{metadataKey}' with '{metadataValue}', but not contain it"); + + return new AndWhichConstraint(this, Subject); + } + + public AndWhichConstraint Satisfy(Action action) where TReason : class, IReason + { + var specificReason = Subject as TReason; + + CurrentAssertionChain + .Given(() => Subject) + .ForCondition(reason => reason is TReason) + .FailWith($"Reason should be of type '{typeof(TReason)}', but is of type '{Subject.GetType()}'"); + + action(specificReason); + + return new AndWhichConstraint(this, Subject); + } + } +} \ No newline at end of file diff --git a/src/FluentResults.Extensions.AwesomeAssertions/Assertions/ResultAssertions.cs b/src/FluentResults.Extensions.AwesomeAssertions/Assertions/ResultAssertions.cs new file mode 100644 index 0000000..351dbc9 --- /dev/null +++ b/src/FluentResults.Extensions.AwesomeAssertions/Assertions/ResultAssertions.cs @@ -0,0 +1,121 @@ +using System; +using AwesomeAssertions; +using AwesomeAssertions.Execution; +using AwesomeAssertions.Primitives; + +// ReSharper disable once CheckNamespace +namespace FluentResults.Extensions.AwesomeAssertions +{ + public class ResultAssertions : ReferenceTypeAssertions + { + static ResultAssertions() + { + ResultFormatters.Register(); + } + + public ResultAssertions(Result subject, AssertionChain assertionChain) + : base(subject, assertionChain) + { + } + + protected override string Identifier => nameof(Result); + + public AndWhichConstraint BeFailure(string because = "", params object[] becauseArgs) + { + return new BeFailureAssertionOperator().Do(Subject, this, CurrentAssertionChain, because, becauseArgs); + } + + public AndWhichConstraint BeSuccess(string because = "", params object[] becauseArgs) + { + return new BeSuccessAssertionOperator().Do(Subject, this, CurrentAssertionChain, because, becauseArgs); + } + + public AndWhichThatConstraint HaveReason(string message, Func messageComparison = null, string because = "", params object[] becauseArgs) + { + return new HaveReasonAssertionOperator().Do(Subject, this, CurrentAssertionChain, message, messageComparison, because, becauseArgs); + } + + public AndWhichThatConstraint HaveReason(string message, Func messageComparison = null, string because = "", params object[] becauseArgs) where TReason : IReason + { + return new HaveReasonTAssertionOperator().Do(Subject, this, CurrentAssertionChain, message, messageComparison, because, becauseArgs); + } + + public AndWhichThatConstraint HaveError(string message, Func messageComparison = null, string because = "", params object[] becauseArgs) + { + return new HaveErrorAssertionOperator().Do(Subject, this, CurrentAssertionChain, message, messageComparison, because, becauseArgs); + } + + public AndWhichConstraint HaveReason(IReason reason, string because = "", params object[] becauseArgs) + { + Subject.Reasons.Should().ContainEquivalentOf(reason, because, becauseArgs); + + return new AndWhichConstraint(this, Subject); + } + + public AndConstraint Satisfy(Action action) + { + action(Subject); + + return new AndConstraint(this); + } + } + + public class ResultAssertions : ReferenceTypeAssertions, ResultAssertions> + { + static ResultAssertions() + { + ResultFormatters.Register(); + } + + public ResultAssertions(Result subject, AssertionChain assertionChain) + : base(subject, assertionChain) + { + } + + protected override string Identifier => nameof(Result); + + public AndWhichConstraint, Result> BeFailure(string because = "", params object[] becauseArgs) + { + return new BeFailureAssertionOperator().Do(Subject, this, CurrentAssertionChain, because, becauseArgs); + } + + public AndWhichConstraint, Result> BeSuccess(string because = "", params object[] becauseArgs) + { + return new BeSuccessAssertionOperator().Do(Subject, this, CurrentAssertionChain, because, becauseArgs); + } + + public AndWhichThatConstraint, Result, ReasonAssertions> HaveReason(string message, Func messageComparison = null, string because = "", params object[] becauseArgs) + { + return new HaveReasonAssertionOperator().Do(Subject, this, CurrentAssertionChain, message, messageComparison, because, becauseArgs); + } + + public AndWhichThatConstraint, Result, ReasonAssertions> HaveReason(string message, Func messageComparison = null, string because = "", params object[] becauseArgs) where TReason : IReason + { + return new HaveReasonTAssertionOperator().Do, Result, TReason>(Subject, this, CurrentAssertionChain, message, messageComparison, because, becauseArgs); + } + + public AndWhichThatConstraint, Result, ReasonAssertions> HaveError(string message, Func messageComparison = null, string because = "", params object[] becauseArgs) + { + return new HaveErrorAssertionOperator().Do(Subject, this, CurrentAssertionChain, message, messageComparison, because, becauseArgs); + } + + public AndWhichConstraint, Result> HaveReason(IReason reason, string because = "", params object[] becauseArgs) + { + Subject.Reasons.Should().ContainEquivalentOf(reason, because, becauseArgs); + + return new AndWhichConstraint, Result>(this, Subject); + } + + public AndConstraint> HaveValue(T expectedValue, string because = "", params object[] becauseArgs) + { + return new HaveValueAssertionOperator().Do(Subject, this, CurrentAssertionChain, expectedValue, because, becauseArgs); + } + + public AndConstraint> Satisfy(Action> action) + { + action(Subject); + + return new AndConstraint>(this); + } + } +} \ No newline at end of file diff --git a/src/FluentResults.Extensions.AwesomeAssertions/Common/ErrorListValueFormatter.cs b/src/FluentResults.Extensions.AwesomeAssertions/Common/ErrorListValueFormatter.cs new file mode 100644 index 0000000..ac85ac5 --- /dev/null +++ b/src/FluentResults.Extensions.AwesomeAssertions/Common/ErrorListValueFormatter.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Linq; +using AwesomeAssertions.Formatting; + +// ReSharper disable once CheckNamespace +namespace FluentResults.Extensions.AwesomeAssertions +{ + public class ErrorListValueFormatter : IValueFormatter + { + public bool CanHandle(object value) + { + return value is List; + } + + public void Format(object value, FormattedObjectGraph formattedGraph, FormattingContext context, FormatChild formatChild) + { + var errors = (IEnumerable)value; + formattedGraph.AddFragment(string.Join("; ", errors.Select(error => error.Message))); + } + } +} \ No newline at end of file diff --git a/src/FluentResults.Extensions.AwesomeAssertions/Common/FluentResultAssertionsConfig.cs b/src/FluentResults.Extensions.AwesomeAssertions/Common/FluentResultAssertionsConfig.cs new file mode 100644 index 0000000..b3d5a42 --- /dev/null +++ b/src/FluentResults.Extensions.AwesomeAssertions/Common/FluentResultAssertionsConfig.cs @@ -0,0 +1,10 @@ +using System; + +// ReSharper disable once CheckNamespace +namespace FluentResults.Extensions.AwesomeAssertions +{ + public static class FluentResultAssertionsConfig + { + public static Func MessageComparison { get; set; } = MessageComparisonLogics.Equal; + } +} \ No newline at end of file diff --git a/src/FluentResults.Extensions.AwesomeAssertions/Common/MessageComparisonLogics.cs b/src/FluentResults.Extensions.AwesomeAssertions/Common/MessageComparisonLogics.cs new file mode 100644 index 0000000..746e92b --- /dev/null +++ b/src/FluentResults.Extensions.AwesomeAssertions/Common/MessageComparisonLogics.cs @@ -0,0 +1,11 @@ +using System; + +// ReSharper disable once CheckNamespace +namespace FluentResults.Extensions.AwesomeAssertions +{ + public static class MessageComparisonLogics + { + public static Func Equal = (actual, expected) => actual == expected; + public static Func ActualContainsExpected = (actual, expected) => actual.Contains(expected); + } +} \ No newline at end of file diff --git a/src/FluentResults.Extensions.AwesomeAssertions/Common/ResultFormatters.cs b/src/FluentResults.Extensions.AwesomeAssertions/Common/ResultFormatters.cs new file mode 100644 index 0000000..7f8ee82 --- /dev/null +++ b/src/FluentResults.Extensions.AwesomeAssertions/Common/ResultFormatters.cs @@ -0,0 +1,13 @@ +using AwesomeAssertions.Formatting; + +// ReSharper disable once CheckNamespace +namespace FluentResults.Extensions.AwesomeAssertions +{ + public static class ResultFormatters + { + public static void Register() + { + Formatter.AddFormatter(new ErrorListValueFormatter()); + } + } +} \ No newline at end of file diff --git a/src/FluentResults.Extensions.AwesomeAssertions/FluentResults.Extensions.AwesomeAssertions.csproj b/src/FluentResults.Extensions.AwesomeAssertions/FluentResults.Extensions.AwesomeAssertions.csproj new file mode 100644 index 0000000..c7bbfc9 --- /dev/null +++ b/src/FluentResults.Extensions.AwesomeAssertions/FluentResults.Extensions.AwesomeAssertions.csproj @@ -0,0 +1,36 @@ + + + + netstandard2.0;netstandard2.1;net8.0;net9.0 + FluentResults.Extensions.AwesomeAssertions + 2.2.1.0 + Michael Altmann + Asserting FluentResults objects with AwesomeAssertions + false + MIT + + + Copyright 2025 (c) Michael Altmann. All rights reserved. + Result Results exception error handling FluentResults + https://github.com/altmann/FluentResults + https://raw.githubusercontent.com/altmann/FluentResults/master/resources/icons/FluentResults-Icon-128.png + FluentResults-Icon-128.png + + + true + true + true + snupkg + false + + + + + + + + + + + + diff --git a/src/FluentResults.Extensions.AwesomeAssertions/ReasonExtensions.cs b/src/FluentResults.Extensions.AwesomeAssertions/ReasonExtensions.cs new file mode 100644 index 0000000..9636785 --- /dev/null +++ b/src/FluentResults.Extensions.AwesomeAssertions/ReasonExtensions.cs @@ -0,0 +1,14 @@ +using System; +using AwesomeAssertions.Execution; + +namespace FluentResults.Extensions.AwesomeAssertions +{ + public static class ReasonExtensions + { + public static ReasonAssertions Should(this IReason reason) + { + if (reason == null) throw new ArgumentNullException(nameof(reason)); + return new ReasonAssertions(reason, AssertionChain.GetOrCreate()); + } + } +} \ No newline at end of file diff --git a/src/FluentResults.Extensions.AwesomeAssertions/ResultExtensions.cs b/src/FluentResults.Extensions.AwesomeAssertions/ResultExtensions.cs new file mode 100644 index 0000000..19b7162 --- /dev/null +++ b/src/FluentResults.Extensions.AwesomeAssertions/ResultExtensions.cs @@ -0,0 +1,20 @@ +using System; +using AwesomeAssertions.Execution; + +namespace FluentResults.Extensions.AwesomeAssertions +{ + public static class ResultExtensions + { + public static ResultAssertions Should(this Result value) + { + if (value == null) throw new ArgumentNullException(nameof(value)); + return new ResultAssertions(value, AssertionChain.GetOrCreate()); + } + + public static ResultAssertions Should(this Result value) + { + if (value == null) throw new ArgumentNullException(nameof(value)); + return new ResultAssertions(value, AssertionChain.GetOrCreate()); + } + } +} \ No newline at end of file diff --git a/src/FluentResults.sln b/src/FluentResults.sln index b7b7846..4d3a01e 100644 --- a/src/FluentResults.sln +++ b/src/FluentResults.sln @@ -26,6 +26,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentResults.Extensions.As EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentResults.Samples.WebHost", "FluentResults.Samples.WebHost\FluentResults.Samples.WebHost.csproj", "{47A242AF-DB1A-4E2C-A1BE-E3C2840BD9F5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentResults.Extensions.AwesomeAssertions.Test", "FluentResults.Extensions.AwesomeAssertions.Test\FluentResults.Extensions.AwesomeAssertions.Test.csproj", "{7A0052F0-AAC2-D45E-B7DF-1B277D36778C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentResults.Extensions.AwesomeAssertions", "FluentResults.Extensions.AwesomeAssertions\FluentResults.Extensions.AwesomeAssertions.csproj", "{9657E8E2-6B45-58FF-0FD3-CC71E9CD3D26}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -64,6 +68,14 @@ Global {47A242AF-DB1A-4E2C-A1BE-E3C2840BD9F5}.Debug|Any CPU.Build.0 = Debug|Any CPU {47A242AF-DB1A-4E2C-A1BE-E3C2840BD9F5}.Release|Any CPU.ActiveCfg = Release|Any CPU {47A242AF-DB1A-4E2C-A1BE-E3C2840BD9F5}.Release|Any CPU.Build.0 = Release|Any CPU + {7A0052F0-AAC2-D45E-B7DF-1B277D36778C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7A0052F0-AAC2-D45E-B7DF-1B277D36778C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7A0052F0-AAC2-D45E-B7DF-1B277D36778C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7A0052F0-AAC2-D45E-B7DF-1B277D36778C}.Release|Any CPU.Build.0 = Release|Any CPU + {9657E8E2-6B45-58FF-0FD3-CC71E9CD3D26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9657E8E2-6B45-58FF-0FD3-CC71E9CD3D26}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9657E8E2-6B45-58FF-0FD3-CC71E9CD3D26}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9657E8E2-6B45-58FF-0FD3-CC71E9CD3D26}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE