Skip to content
107 changes: 107 additions & 0 deletions FirstClassErrors.GenDoc.UnitTests/CustomErrorTypesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#region Usings declarations

using NFluent;

#endregion

namespace FirstClassErrors.GenDoc.UnitTests;

/// <summary>
/// Proves, from an assembly that has no access to the library's <c>internal</c> members, that the built-in error
/// types can be specialized by a consumer (ADR-0072) and that GenDoc documents such specialized errors like any other.
/// </summary>
/// <remarks>
/// This project is deliberately the vantage point: the library only exposes its internals to
/// <c>FirstClassErrors.UnitTests</c> and <c>FirstClassErrors.Testing</c>, so a fixture that compiles here is a fixture a
/// real consumer can write.
/// </remarks>
public sealed class CustomErrorTypesTests {

#region Statics members declarations

private static ErrorDocumentationExtractionResult Extract() {
return AssemblyErrorDocumentationReader.GetErrorDocumentationFrom(typeof(CustomErrorTypesTests).Assembly);
}

#endregion

[Fact(DisplayName = "A consumer assembly can derive a typed error and its exception from the built-in ones, and build it through its own staged factory.")]
public void AConsumerAssemblyCanDeriveATypedError() {
// Exercise
CustomPersistenceError error = CustomRepositoryErrors.Unavailable();

// Verify
Check.That(error.Code).IsEqualTo(ErrorCode.Create("CUSTOM_REPOSITORY_UNAVAILABLE"));
Check.That(error.ShortMessage).IsEqualTo("Service temporarily unavailable.");
Check.That(error.Direction).IsEqualTo(InteractionDirection.Outgoing);
Check.That(error.Transience).IsEqualTo(Transience.Transient);
Check.That(error.ToException()).IsInstanceOf<CustomPersistenceException>();
}

[Fact(DisplayName = "GenDoc extracts a documented factory that returns a custom error type, running its examples.")]
public void GenDocExtractsADocumentedFactoryReturningACustomErrorType() {
// Exercise
ErrorDocumentationExtractionResult extraction = Extract();

// Verify
ErrorDocumentation? documentation = extraction.Documentation.FirstOrDefault(doc => doc.Code == "CUSTOM_REPOSITORY_UNAVAILABLE");

Check.That(documentation).IsNotNull();
Check.That(documentation!.Title).IsEqualTo("Custom repository unavailable");
Check.That(documentation.Source).IsEqualTo("CustomRepository");
Check.That(documentation.Examples).CountIs(1);
}

[Fact(DisplayName = "Extracting a custom error type records no failure.")]
public void ExtractingACustomErrorTypeRecordsNoFailure() {
// Exercise
ErrorDocumentationExtractionResult extraction = Extract();

// Verify
Check.That(extraction.Failures).IsEmpty();
}

private sealed class CustomPersistenceError : SecondaryPortError {

private CustomPersistenceError(ErrorCode code, string diagnosticMessage, string shortMessage, string? detailedMessage, Transience transience)
: base(code, diagnosticMessage, shortMessage, detailedMessage, transience) { }

public static PublicMessageStage<CustomPersistenceError> Create(ErrorCode code, string diagnosticMessage, Transience transience) {
return new PublicMessageStage<CustomPersistenceError>((shortMessage, detailedMessage) =>
new CustomPersistenceError(code, diagnosticMessage, shortMessage, detailedMessage, transience));
}

public override DiagnosableException ToException() {
return new CustomPersistenceException(this);
}

}

private sealed class CustomPersistenceException : SecondaryPortException {

public CustomPersistenceException(CustomPersistenceError error) : base(error) { }

}

[ProvidesErrorsFor("CustomRepository", Description = "Errors raised by the custom repository fixture.")]
private static class CustomRepositoryErrors {

[DocumentedBy(nameof(UnavailableDocumentation))]
public static CustomPersistenceError Unavailable() {
return CustomPersistenceError.Create(ErrorCode.Create("CUSTOM_REPOSITORY_UNAVAILABLE"),
"The custom repository is unavailable.",
Transience.Transient)
.WithPublicMessage("Service temporarily unavailable.");
}

private static ErrorDocumentation UnavailableDocumentation() {
return DescribeError.WithTitle("Custom repository unavailable")
.WithDescription("This error occurs when the custom repository cannot be reached.")
.WithoutRule()
.WithoutDiagnostic()
.WithExamples(Unavailable);
}

}

}
157 changes: 157 additions & 0 deletions FirstClassErrors.UnitTests/ExtensibleErrorTypesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
#region Usings declarations

using FirstClassErrors.Testing;

using NFluent;

#endregion

namespace FirstClassErrors.UnitTests;

/// <summary>
/// Exercises custom error types as they would be defined in an application referencing FirstClassErrors.
Comment thread
Reefact marked this conversation as resolved.
Resolved
/// </summary>
public sealed class ExtensibleErrorTypesTests {

[Fact(DisplayName = "Derived domain errors retain their type and diagnostic fields.")]
public void DerivedDomainErrorRetainsItsTypeAndDiagnosticFields() {
// Exercise
ErrorCode code = ErrorCodeFactory.Any();
CustomDomainError error = CustomDomainError.Create(code, "Internal diagnostic")
.WithPublicMessage("Public summary", "Public details");

// Verify
Check.That(error.Code).IsEqualTo(code);
Check.That(error.DiagnosticMessage).IsEqualTo("Internal diagnostic");
Check.That(error.ShortMessage).IsEqualTo("Public summary");
Check.That(error.DetailedMessage).IsEqualTo("Public details");
Check.That(error.ToException() is DomainException).IsTrue();
}

[Fact(DisplayName = "Derived infrastructure errors preserve direction and transience.")]
public void DerivedInfrastructureErrorPreservesMetadata() {
// Exercise
CustomInfrastructureError error = CustomInfrastructureError.Create(ErrorCodeFactory.Any(), "Diagnostic")
.WithPublicMessage("Unavailable");

// Verify
Check.That(error.Direction).IsEqualTo(InteractionDirection.Outgoing);
Check.That(error.Transience).IsEqualTo(Transience.Transient);
Check.That(error.ToException() is InfrastructureException).IsTrue();
}

[Fact(DisplayName = "Derived primary port errors aggregate nested errors without losing direction.")]
public void DerivedPrimaryPortErrorAggregatesInnerErrors() {
// Setup
DomainError inner = ErrorFactory.Domain(ErrorCodeFactory.Any(), "Invalid input");
PrimaryPortInnerErrors innerErrors = new PrimaryPortInnerErrors().Add(inner);

// Exercise
CustomPrimaryPortError error = CustomPrimaryPortError.Create(ErrorCodeFactory.Any(), "Diagnostic", innerErrors)
.WithPublicMessage("Invalid request");

// Verify
Check.That(error.Direction).IsEqualTo(InteractionDirection.Incoming);
Check.That(error.Transience).IsEqualTo(Transience.Unknown);
Check.That(error.InnerErrors).CountIs(1);
Check.That(error.InnerErrors[0]).IsSameReferenceAs(inner);
Check.That(error.ToException() is PrimaryPortException).IsTrue();
}

[Fact(DisplayName = "A repository factory can produce a derived secondary port error.")]
public void RepositoryFactoryProducesTypedPersistenceError() {
// Exercise
PersistenceError error = CommandRepositoryErrors.Unavailable();

// Verify
Check.That(error.Code).IsEqualTo(ErrorCode.Create("COMMAND_REPOSITORY_UNAVAILABLE"));
Check.That(error.Direction).IsEqualTo(InteractionDirection.Outgoing);
Check.That(error.Transience).IsEqualTo(Transience.Transient);
Check.That(error.ToException() is SecondaryPortException).IsTrue();
}

[Fact(DisplayName = "Derived secondary port errors preserve nested outgoing errors and computed transience.")]
public void DerivedSecondaryPortErrorAggregatesInnerErrors() {
// Setup
SecondaryPortError inner = ErrorFactory.Secondary(ErrorCodeFactory.Any(), "Dependency failed", Transience.NonTransient);
SecondaryPortInnerErrors innerErrors = new SecondaryPortInnerErrors().Add(inner);

// Exercise
PersistenceError error = PersistenceError.Create(ErrorCodeFactory.Any(), "Repository failed", innerErrors)
.WithPublicMessage("Repository unavailable");

// Verify
Check.That(error.Direction).IsEqualTo(InteractionDirection.Outgoing);
Check.That(error.Transience).IsEqualTo(Transience.NonTransient);
Check.That(error.InnerErrors).CountIs(1);
Check.That(error.InnerErrors[0]).IsSameReferenceAs(inner);
}

private sealed class CustomDomainError : DomainError {

private CustomDomainError(ErrorCode code, string diagnosticMessage, string shortMessage, string? detailedMessage)
: base(code, diagnosticMessage, shortMessage, detailedMessage) { }

public static PublicMessageStage<CustomDomainError> Create(ErrorCode code, string diagnosticMessage) {
return new PublicMessageStage<CustomDomainError>((shortMessage, detailedMessage) =>
new CustomDomainError(code, diagnosticMessage, shortMessage, detailedMessage));
}

}

private sealed class CustomInfrastructureError : InfrastructureError {

private CustomInfrastructureError(ErrorCode code, string diagnosticMessage, string shortMessage, string? detailedMessage)
: base(code, diagnosticMessage, shortMessage, detailedMessage, InteractionDirection.Outgoing, Transience.Transient) { }

public static PublicMessageStage<CustomInfrastructureError> Create(ErrorCode code, string diagnosticMessage) {
return new PublicMessageStage<CustomInfrastructureError>((shortMessage, detailedMessage) =>
new CustomInfrastructureError(code, diagnosticMessage, shortMessage, detailedMessage));
}

}

private sealed class CustomPrimaryPortError : PrimaryPortError {

private CustomPrimaryPortError(ErrorCode code, string diagnosticMessage, string shortMessage, string? detailedMessage, PrimaryPortInnerErrors innerErrors)
: base(code, diagnosticMessage, shortMessage, detailedMessage, innerErrors) { }

public static PublicMessageStage<CustomPrimaryPortError> Create(ErrorCode code, string diagnosticMessage, PrimaryPortInnerErrors innerErrors) {
return new PublicMessageStage<CustomPrimaryPortError>((shortMessage, detailedMessage) =>
new CustomPrimaryPortError(code, diagnosticMessage, shortMessage, detailedMessage, innerErrors));
}

}

private sealed class PersistenceError : SecondaryPortError {

private PersistenceError(ErrorCode code, string diagnosticMessage, string shortMessage, string? detailedMessage, Transience transience)
: base(code, diagnosticMessage, shortMessage, detailedMessage, transience) { }

private PersistenceError(ErrorCode code, string diagnosticMessage, string shortMessage, string? detailedMessage, SecondaryPortInnerErrors innerErrors)
: base(code, diagnosticMessage, shortMessage, detailedMessage, innerErrors) { }

public static PublicMessageStage<PersistenceError> Create(ErrorCode code, string diagnosticMessage, Transience transience) {
return new PublicMessageStage<PersistenceError>((shortMessage, detailedMessage) =>
new PersistenceError(code, diagnosticMessage, shortMessage, detailedMessage, transience));
}

public static PublicMessageStage<PersistenceError> Create(ErrorCode code, string diagnosticMessage, SecondaryPortInnerErrors innerErrors) {
return new PublicMessageStage<PersistenceError>((shortMessage, detailedMessage) =>
new PersistenceError(code, diagnosticMessage, shortMessage, detailedMessage, innerErrors));
}

}

private static class CommandRepositoryErrors {

public static PersistenceError Unavailable() {
return PersistenceError.Create(ErrorCode.Create("COMMAND_REPOSITORY_UNAVAILABLE"),
"The command repository is unavailable.",
Transience.Transient)
.WithPublicMessage("Service temporarily unavailable.");
}

}

}
15 changes: 12 additions & 3 deletions FirstClassErrors/DomainError.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,22 @@ public class DomainError : Error {

#region Constructors declarations

internal DomainError(ErrorCode code, string diagnosticMessage, string shortMessage, string? detailedMessage, Action<ErrorContextBuilder>? configureContext = null)
/// <summary>
/// Initializes a <see cref="DomainError" /> or a derived error type.
/// </summary>
protected internal DomainError(ErrorCode code, string diagnosticMessage, string shortMessage, string? detailedMessage, Action<ErrorContextBuilder>? configureContext = null)
: base(code, diagnosticMessage, shortMessage, detailedMessage, configureContext) { }

internal DomainError(ErrorCode code, string diagnosticMessage, string shortMessage, string? detailedMessage, DomainError innerError, Action<ErrorContextBuilder>? configureContext = null)
/// <summary>
/// Initializes a <see cref="DomainError" /> or a derived error type.
/// </summary>
protected internal DomainError(ErrorCode code, string diagnosticMessage, string shortMessage, string? detailedMessage, DomainError innerError, Action<ErrorContextBuilder>? configureContext = null)
: base(code, diagnosticMessage, shortMessage, detailedMessage, innerError, configureContext) { }

internal DomainError(ErrorCode code, string diagnosticMessage, string shortMessage, string? detailedMessage, IEnumerable<DomainError> innerErrors, Action<ErrorContextBuilder>? configureContext = null)
/// <summary>
/// Initializes a <see cref="DomainError" /> or a derived error type.
/// </summary>
protected internal DomainError(ErrorCode code, string diagnosticMessage, string shortMessage, string? detailedMessage, IEnumerable<DomainError> innerErrors, Action<ErrorContextBuilder>? configureContext = null)
: base(code, diagnosticMessage, shortMessage, detailedMessage, innerErrors, configureContext) { }

#endregion
Expand Down
15 changes: 12 additions & 3 deletions FirstClassErrors/InfrastructureError.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,28 @@ public class InfrastructureError : Error {

#region Constructors declarations

internal InfrastructureError(ErrorCode code, string diagnosticMessage, string shortMessage, string? detailedMessage, InteractionDirection direction, Transience transience, Action<ErrorContextBuilder>? configureContext = null)
/// <summary>
/// Initializes a <see cref="InfrastructureError" /> or a derived error type.
/// </summary>
protected internal InfrastructureError(ErrorCode code, string diagnosticMessage, string shortMessage, string? detailedMessage, InteractionDirection direction, Transience transience, Action<ErrorContextBuilder>? configureContext = null)
: base(code, diagnosticMessage, shortMessage, detailedMessage, configureContext) {
Direction = direction;
Transience = transience;
}

internal InfrastructureError(ErrorCode code, string diagnosticMessage, string shortMessage, string? detailedMessage, InteractionDirection direction, Transience transience, Error innerError, Action<ErrorContextBuilder>? configureContext = null)
/// <summary>
/// Initializes a <see cref="InfrastructureError" /> or a derived error type.
/// </summary>
protected internal InfrastructureError(ErrorCode code, string diagnosticMessage, string shortMessage, string? detailedMessage, InteractionDirection direction, Transience transience, Error innerError, Action<ErrorContextBuilder>? configureContext = null)
: base(code, diagnosticMessage, shortMessage, detailedMessage, innerError, configureContext) {
Direction = direction;
Transience = transience;
}

internal InfrastructureError(ErrorCode code, string diagnosticMessage, string shortMessage, string? detailedMessage, InteractionDirection direction, Transience transience, IEnumerable<Error> innerErrors, Action<ErrorContextBuilder>? configureContext = null)
/// <summary>
/// Initializes a <see cref="InfrastructureError" /> or a derived error type.
/// </summary>
protected internal InfrastructureError(ErrorCode code, string diagnosticMessage, string shortMessage, string? detailedMessage, InteractionDirection direction, Transience transience, IEnumerable<Error> innerErrors, Action<ErrorContextBuilder>? configureContext = null)
: base(code, diagnosticMessage, shortMessage, detailedMessage, innerErrors, configureContext) {
Direction = direction;
Transience = transience;
Expand Down
10 changes: 8 additions & 2 deletions FirstClassErrors/PrimaryPortError.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,16 @@ public class PrimaryPortError : InfrastructureError {

#region Constructors declarations

internal PrimaryPortError(ErrorCode code, string diagnosticMessage, string shortMessage, string? detailedMessage, Transience transience, Action<ErrorContextBuilder>? configureContext = null)
/// <summary>
/// Initializes a <see cref="PrimaryPortError" /> or a derived error type.
/// </summary>
protected internal PrimaryPortError(ErrorCode code, string diagnosticMessage, string shortMessage, string? detailedMessage, Transience transience, Action<ErrorContextBuilder>? configureContext = null)
: base(code, diagnosticMessage, shortMessage, detailedMessage, InteractionDirection.Incoming, transience, configureContext) { }

internal PrimaryPortError(ErrorCode code, string diagnosticMessage, string shortMessage, string? detailedMessage, PrimaryPortInnerErrors innerErrors, Action<ErrorContextBuilder>? configureContext = null)
/// <summary>
/// Initializes a <see cref="PrimaryPortError" /> or a derived error type.
/// </summary>
protected internal PrimaryPortError(ErrorCode code, string diagnosticMessage, string shortMessage, string? detailedMessage, PrimaryPortInnerErrors innerErrors, Action<ErrorContextBuilder>? configureContext = null)
: base(code, diagnosticMessage, shortMessage, detailedMessage, InteractionDirection.Incoming, innerErrors.ComputeTransience(), innerErrors.ToList(), configureContext) { }

#endregion
Expand Down
Loading
Loading