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
Expand Up @@ -32,7 +32,7 @@
<PackageRequireLicenseAcceptance>False</PackageRequireLicenseAcceptance>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageTags>OneBitSoftware; OperationResult;</PackageTags>
<Version>2.0.0</Version>
<Version>2.1.0</Version>
</PropertyGroup>

</Project>
11 changes: 11 additions & 0 deletions src/OneBitSoftware.Utilities.OperationResult/OperationResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,17 @@
/// </summary>
public TResult? ResultObject { get; set; }

/// <summary>
/// Appends error from <paramref name="otherOperationResult"/> to the current instance.
/// </summary>
/// <param name="otherOperationResult">The <see cref="OperationResult"/> to append from.</param>
/// <returns>The original <see cref="OperationResult"/> with the appended messages from <paramref name="otherOperationResult"/>.</returns>
public new OperationResult<TResult> AppendErrors(OperationResult? otherOperationResult)
{
base.AppendErrors(otherOperationResult);

Check warning on line 305 in src/OneBitSoftware.Utilities.OperationResult/OperationResult.cs

View workflow job for this annotation

GitHub Actions / Build and test

Possible null reference argument for parameter 'otherOperationResult' in 'OperationResult OperationResult.AppendErrors(OperationResult otherOperationResult)'.
return this;
}

/// <summary>
/// This method will append an error with a specific `user-friendly` message to this operation result instance.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,318 @@ public void AppendErrors_ShouldLogWhenCreatedWithNoLogger()
// Assert
Assert.Equal(1, testLogger.LogMessages.Count);
}

[Fact]
public void AppendErrors_NonGenericSource_To_GenericTarget_Retains_Generic_Type_And_MergesErrors()
{
// Arrange
var source = new OperationResult();
source.AppendError("E1", 101, LogLevel.Warning, "D1");

var target = new OperationResult<string>();
target.AppendError("E0", 100, LogLevel.Information, "D0");

// Act
var returned = target.AppendErrors(source);

// Assert
Assert.Same(target, returned);
Assert.True(target.Fail);
Assert.Equal(2, target.Errors.Count);
Assert.NotNull(target.Errors.Single(e => e is { Code: 100, Message: "E0" }));
Assert.NotNull(target.Errors.Single(e => e is { Code: 101, Message: "E1" }));
}

[Fact]
public void AppendErrors_GenericSource_To_GenericTarget_Retains_Generic_Type_And_MergesErrors()
{
// Arrange
var source = new OperationResult<double>();
source.AppendError("E1", 101, LogLevel.Warning, "D1");

var target = new OperationResult<string>();
target.AppendError("E0", 100, LogLevel.Information, "D0");

// Act
var returned = target.AppendErrors(source);

// Assert
Assert.Same(target, returned);
Assert.True(target.Fail);
Assert.Equal(2, target.Errors.Count);
Assert.NotNull(target.Errors.Single(e => e is { Code: 100, Message: "E0" }));
Assert.NotNull(target.Errors.Single(e => e is { Code: 101, Message: "E1" }));
}

[Fact]
public void AppendErrors_WithNullSource_On_NonGenericTarget_Returns_Same_Instance_And_NoChange()
{
// Arrange
var target = new OperationResult();
target.AppendError("E0", 100, LogLevel.Information, "D0");
var beforeCount = target.Errors.Count;

// Act
var returned = target.AppendErrors(null);

// Assert
Assert.Same(target, returned);
Assert.Equal(beforeCount, target.Errors.Count);
}

[Fact]
public void AppendErrors_WithNullSource_On_GenericTarget_Returns_Same_Instance_And_NoChange()
{
// Arrange
var target = new OperationResult<object>();
target.AppendError("E0", 100, LogLevel.Information, "D0");
var beforeCount = target.Errors.Count;

// Act
var returned = target.AppendErrors((OperationResult)null);

// Assert
Assert.Same(target, returned);
Assert.Equal(beforeCount, target.Errors.Count);
}

[Fact]
public void AppendErrors_PreservesResultObject_WhenMergingErrors()
{
// Arrange
var originalResult = new { Id = 42, Name = "Test" };
var target = new OperationResult<object>(originalResult);

var source = new OperationResult<int>();
source.AppendError("Source error", 500);

// Act
var returned = target.AppendErrors(source);

// Assert
Assert.Same(target, returned);
Assert.Same(originalResult, target.ResultObject);
Assert.Single(target.Errors);
Assert.True(target.Fail);
}

[Fact]
public void AppendErrors_MaintainsFluentChaining_WithGenericType()
{
// Arrange
var source1 = new OperationResult<double>();
source1.AppendError("E1", 1);

var source2 = new OperationResult<int>();
source2.AppendError("E2", 2);

var target = new OperationResult<string>();

// Act - Chain multiple AppendErrors calls
var returned = target
.AppendErrors(source1)
.AppendErrors(source2)
.AppendError("E3", 3);

// Assert
Assert.Same(target, returned);
Assert.IsType<OperationResult<string>>(returned);
Assert.Equal(3, target.Errors.Count);
}

[Fact]
public void AppendErrors_PreservesInitialException_FromSource()
{
// Arrange
var sourceException = new InvalidOperationException("Source exception");
var source = new OperationResult<int>();
source.AppendException(sourceException, 999);

var target = new OperationResult<double>();

// Act
target.AppendErrors(source);

// Assert
Assert.Null(target.InitialException); // Target doesn't inherit source's InitialException
Assert.Single(target.Errors);
Assert.Contains(sourceException.ToString(), target.Errors[0].Message);
}

[Fact]
public void AppendErrors_HandlesSourceWithMultipleErrorTypes()
{
// Arrange
var source = new OperationResult<List<string>>();
source.AppendError("Standard error", 100);
source.AppendException(new ArgumentNullException("param"), 200);
source.AppendError("Another error", 300, LogLevel.Critical, "Critical details");

var target = new OperationResult<int>();

// Act
target.AppendErrors(source);

// Assert
Assert.Equal(3, target.Errors.Count);
Assert.NotNull(target.Errors.Single(e => e.Code == 100));
Assert.NotNull(target.Errors.Single(e => e.Code == 200));
Assert.NotNull(target.Errors.Single(e => e is { Code: 300, Details: "Critical details" }));
}

[Fact]
public void AppendErrors_MergesSuccessMessages_AreNotTransferred()
{
// Arrange
var source = new OperationResult<double>();
source.AddSuccessMessage("Success from source");
source.AppendError("But has error", 1);

var target = new OperationResult<string>();
target.AddSuccessMessage("Success from target");

// Act
target.AppendErrors(source);

// Assert - SuccessMessages are not merged in AppendErrors
Assert.Single(target.SuccessMessages);
Assert.Contains("Success from target", target.SuccessMessages);
}

[Fact]
public void AppendErrors_EmptySource_DoesNotAffectTarget()
{
// Arrange
var source = new OperationResult<double>();
var target = new OperationResult<string>();
target.AppendError("Target error", 1);

// Act
var returned = target.AppendErrors(source);

// Assert
Assert.Same(target, returned);
Assert.Single(target.Errors);
Assert.True(target.Fail);
}

[Fact]
public void AppendErrors_EmptyTarget_AcceptsSourceErrors()
{
// Arrange
var source = new OperationResult<object>();
source.AppendError("Source error", 100);

var target = new OperationResult<string>();

// Act
target.AppendErrors(source);

// Assert
Assert.True(target.Fail);
Assert.Single(target.Errors);
Assert.Equal("Source error", target.Errors[0].Message);
}

[Fact]
public void AppendErrors_WithDifferentGenericTypes_MaintainsTargetType()
{
// Arrange
var sourceInt = new OperationResult<int> { ResultObject = 42 };
sourceInt.AppendError("Int error", 1);

var targetString = new OperationResult<string> { ResultObject = "test" };

// Act
var returned = targetString.AppendErrors(sourceInt);

// Assert
Assert.IsType<OperationResult<string>>(returned);
Assert.Equal("test", targetString.ResultObject);
Assert.Single(targetString.Errors);
}

[Fact]
public void AppendErrors_ChainedCalls_AccumulatesAllErrors()
{
// Arrange
var s1 = new OperationResult();
s1.AppendError("E1", 1);

var s2 = new OperationResult<int>();
s2.AppendError("E2", 2);

var s3 = new OperationResult();
s3.AppendError("E3", 3);

var target = new OperationResult<string>();

// Act
target.AppendErrors(s1).AppendErrors(s2).AppendErrors(s3);

// Assert
Assert.Equal(3, target.Errors.Count);
Assert.Contains(target.Errors, e => e.Code == 1);
Assert.Contains(target.Errors, e => e.Code == 2);
Assert.Contains(target.Errors, e => e.Code == 3);
}

[Fact]
public void AppendErrors_WithComplexGenericType_PreservesTypeIntegrity()
{
// Arrange
var complexObject = new Dictionary<string, List<int>>
{
["key1"] = [1, 2, 3]
};

var target = new OperationResult<Dictionary<string, List<int>>>(complexObject);
var source = new OperationResult<int>();
source.AppendError("Complex type error", 999);

// Act
var returned = target.AppendErrors(source);

// Assert
Assert.Same(complexObject, target.ResultObject);
Assert.IsType<OperationResult<Dictionary<string, List<int>>>>(returned);
Assert.Single(target.Errors);
}

[Fact]
public void AppendErrors_ReturnsCorrectType_AfterMultipleOperations()
{
// Arrange
var target = new OperationResult<int> { ResultObject = 100 };
var source1 = new OperationResult<double>();
source1.AppendError("E1", 1);

// Act
var step1 = target.AppendErrors(source1);
var step2 = step1.AppendError("E2", 2);
var step3 = step2.AppendException(new Exception("E3"), 3);

// Assert
Assert.IsType<OperationResult<int>>(step1);
Assert.IsType<OperationResult<int>>(step2);
Assert.IsType<OperationResult<int>>(step3);
Assert.Same(target, step3);
Assert.Equal(3, target.Errors.Count);
}

[Fact]
public void AppendErrors_GenericTarget_ShouldLogWhenCreatedWithALogger()
{
// Arrange
var testLogger = new TestLogger();
var operationResultNoLogger = new OperationResult();
var operationResultWithLogger = new OperationResult<string>(testLogger);

// Act
operationResultNoLogger.AppendError("test");
operationResultWithLogger.AppendErrors(operationResultNoLogger);

// Assert
Assert.Single(testLogger.LogMessages);
}
}
Loading