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
14 changes: 7 additions & 7 deletions FirstClassErrors.GenDoc.UnitTests/CatalogDiffFormatterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ public sealed class CatalogDiffFormatterTests {
private static CatalogDiff SampleDiff() {
CatalogSnapshot baseline = new() {
Errors = [
new CatalogSnapshotEntry { Code = "PAYMENT.DECLINED", Title = "Payment declined" },
new CatalogSnapshotEntry { Code = "PAYMENT_DECLINED", Title = "Payment declined" },
new CatalogSnapshotEntry { Code = "SOME_CODE", Title = "Old title" }
]
};
CatalogSnapshot current = new() {
Errors = [
new CatalogSnapshotEntry { Code = "PAYMENT.REFUSED", Title = "Payment declined" },
new CatalogSnapshotEntry { Code = "PAYMENT_REFUSED", Title = "Payment declined" },
new CatalogSnapshotEntry { Code = "SOME_CODE", Title = "New title" }
]
};
Expand All @@ -43,9 +43,9 @@ public void TheTextReportGroupsTheChangesByImpact() {
Check.That(report).Contains("Breaking changes (1):");
Check.That(report).Contains("Compatible changes (1):");
Check.That(report).Contains("Documentation changes (1):");
Check.That(report).Contains("[removed] PAYMENT.DECLINED");
Check.That(report).Contains("possibly renamed to 'PAYMENT.REFUSED'");
Check.That(report).Contains("[added] PAYMENT.REFUSED");
Check.That(report).Contains("[removed] PAYMENT_DECLINED");
Check.That(report).Contains("possibly renamed to 'PAYMENT_REFUSED'");
Check.That(report).Contains("[added] PAYMENT_REFUSED");
Check.That(report).Contains("[title] SOME_CODE");
}

Expand All @@ -65,7 +65,7 @@ public void TheMarkdownReportHasOneSectionPerImpact() {
Check.That(report).Contains("### 💥 Breaking (1)");
Check.That(report).Contains("### ✅ Compatible (1)");
Check.That(report).Contains("### ℹ️ Documentation (1)");
Check.That(report).Contains("- **`PAYMENT.DECLINED`**");
Check.That(report).Contains("- **`PAYMENT_DECLINED`**");
}

[Fact(DisplayName = "The Markdown report of an empty diff says so under the heading.")]
Expand Down Expand Up @@ -96,7 +96,7 @@ public void TheJsonReportExposesCountsFlagAndChanges() {
Check.That(changes.GetArrayLength()).IsEqualTo(3);
JsonElement removal = changes.EnumerateArray().Single(change => change.GetProperty("kind").GetString() == "errorRemoved");
Check.That(removal.GetProperty("impact").GetString()).IsEqualTo("breaking");
Check.That(removal.GetProperty("code").GetString()).IsEqualTo("PAYMENT.DECLINED");
Check.That(removal.GetProperty("code").GetString()).IsEqualTo("PAYMENT_DECLINED");
}

[Fact(DisplayName = "The JSON report of an empty diff has zero counts and no changes.")]
Expand Down
26 changes: 13 additions & 13 deletions FirstClassErrors.GenDoc.UnitTests/CatalogDifferTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ private static CatalogSnapshotContextKey Key(string key, string valueType) {
[Fact(DisplayName = "Two identical snapshots produce an empty diff.")]
public void TwoIdenticalSnapshotsProduceAnEmptyDiff() {
// Setup
CatalogSnapshot baseline = Snapshot(Entry("PAYMENT.DECLINED", "Payment declined", "Payment", Key("DealId", "System.String")));
CatalogSnapshot current = Snapshot(Entry("PAYMENT.DECLINED", "Payment declined", "Payment", Key("DealId", "System.String")));
CatalogSnapshot baseline = Snapshot(Entry("PAYMENT_DECLINED", "Payment declined", "Payment", Key("DealId", "System.String")));
CatalogSnapshot current = Snapshot(Entry("PAYMENT_DECLINED", "Payment declined", "Payment", Key("DealId", "System.String")));

// Exercise
CatalogDiff diff = CatalogDiffer.Diff(baseline, current);
Expand All @@ -40,28 +40,28 @@ public void TwoIdenticalSnapshotsProduceAnEmptyDiff() {
[Fact(DisplayName = "A removed error code is a breaking change.")]
public void ARemovedErrorCodeIsABreakingChange() {
// Exercise
CatalogDiff diff = CatalogDiffer.Diff(Snapshot(Entry("PAYMENT.DECLINED")), Snapshot());
CatalogDiff diff = CatalogDiffer.Diff(Snapshot(Entry("PAYMENT_DECLINED")), Snapshot());

// Verify
Check.That(diff.BreakingChanges).HasSize(1);
CatalogChange change = diff.BreakingChanges[0];
Check.That(change.Kind).IsEqualTo(CatalogChangeKind.ErrorRemoved);
Check.That(change.Code).IsEqualTo("PAYMENT.DECLINED");
Check.That(change.Code).IsEqualTo("PAYMENT_DECLINED");
}

[Fact(DisplayName = "A removal whose title matches a single added error carries a 'possibly renamed' hint.")]
public void ARemovalMatchingASingleAdditionCarriesARenameHint() {
// Setup
CatalogSnapshot baseline = Snapshot(Entry("PAYMENT.DECLINED", "Payment declined"));
CatalogSnapshot current = Snapshot(Entry("PAYMENT.REFUSED", "Payment declined"));
CatalogSnapshot baseline = Snapshot(Entry("PAYMENT_DECLINED", "Payment declined"));
CatalogSnapshot current = Snapshot(Entry("PAYMENT_REFUSED", "Payment declined"));

// Exercise
CatalogDiff diff = CatalogDiffer.Diff(baseline, current);

// Verify: the removal stays breaking — consumers know the old code — but the hint points at the new one.
CatalogChange removal = diff.Changes.Single(change => change.Kind == CatalogChangeKind.ErrorRemoved);
Check.That(removal.Impact).IsEqualTo(CatalogChangeImpact.Breaking);
Check.That(removal.Description).Contains("possibly renamed to 'PAYMENT.REFUSED'");
Check.That(removal.Description).Contains("possibly renamed to 'PAYMENT_REFUSED'");
}

[Fact(DisplayName = "No rename hint is emitted when several added errors share the removed error's title.")]
Expand All @@ -81,7 +81,7 @@ public void NoRenameHintIsEmittedWhenSeveralAdditionsShareTheTitle() {
[Fact(DisplayName = "A new error code is a compatible change.")]
public void ANewErrorCodeIsACompatibleChange() {
// Exercise
CatalogDiff diff = CatalogDiffer.Diff(Snapshot(), Snapshot(Entry("INVENTORY.OUT_OF_STOCK", "Out of stock", "Inventory")));
CatalogDiff diff = CatalogDiffer.Diff(Snapshot(), Snapshot(Entry("INVENTORY_OUT_OF_STOCK", "Out of stock", "Inventory")));

// Verify
Check.That(diff.CompatibleChanges).HasSize(1);
Expand Down Expand Up @@ -181,8 +181,8 @@ public void ChangesAreOrderedByErrorCode() {
[Fact(DisplayName = "Identities and titles are compared normalized: whitespace-only differences produce no change.")]
public void WhitespaceOnlyDifferencesProduceNoChange() {
// Setup: same contract, but every identity and the title differ only by surrounding whitespace.
CatalogSnapshot baseline = Snapshot(Entry("PAYMENT.DECLINED", "Payment declined", "Payment", Key("DealId", "System.String")));
CatalogSnapshot current = Snapshot(Entry(" PAYMENT.DECLINED ", " Payment declined ", " Payment ", Key(" DealId ", "System.String")));
CatalogSnapshot baseline = Snapshot(Entry("PAYMENT_DECLINED", "Payment declined", "Payment", Key("DealId", "System.String")));
CatalogSnapshot current = Snapshot(Entry(" PAYMENT_DECLINED ", " Payment declined ", " Payment ", Key(" DealId ", "System.String")));

// Exercise
CatalogDiff diff = CatalogDiffer.Diff(baseline, current);
Expand All @@ -194,15 +194,15 @@ public void WhitespaceOnlyDifferencesProduceNoChange() {
[Fact(DisplayName = "The rename hint matches titles that differ only by whitespace.")]
public void TheRenameHintMatchesTitlesThatDifferOnlyByWhitespace() {
// Setup
CatalogSnapshot baseline = Snapshot(Entry("PAYMENT.DECLINED", "Payment declined"));
CatalogSnapshot current = Snapshot(Entry("PAYMENT.REFUSED", " Payment declined "));
CatalogSnapshot baseline = Snapshot(Entry("PAYMENT_DECLINED", "Payment declined"));
CatalogSnapshot current = Snapshot(Entry("PAYMENT_REFUSED", " Payment declined "));

// Exercise
CatalogDiff diff = CatalogDiffer.Diff(baseline, current);

// Verify
CatalogChange removal = diff.Changes.Single(change => change.Kind == CatalogChangeKind.ErrorRemoved);
Check.That(removal.Description).Contains("possibly renamed to 'PAYMENT.REFUSED'");
Check.That(removal.Description).Contains("possibly renamed to 'PAYMENT_REFUSED'");
}

[Fact(DisplayName = "Null snapshots are rejected.")]
Expand Down
2 changes: 1 addition & 1 deletion FirstClassErrors.Testing/ErrorAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace FirstClassErrors.Testing;
/// <example>
/// <code>
/// outcome.ShouldFail()
/// .WithCode("PAYMENT.DECLINED")
/// .WithCode("PAYMENT_DECLINED")
/// .WithContextEntry("CardNetwork", "VISA");
/// </code>
/// </example>
Expand Down
12 changes: 6 additions & 6 deletions FirstClassErrors.UnitTests/OutcomeAssertionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public void Dispose() {

#region Statics members declarations

private static DomainError AnError(string code = "PAYMENT.DECLINED", string diagnostic = "diagnostic", string @short = "short") {
private static DomainError AnError(string code = "PAYMENT_DECLINED", string diagnostic = "diagnostic", string @short = "short") {
return DomainError.Create(ErrorCode.Create(code), diagnostic).WithPublicMessage(@short);
}

Expand All @@ -46,15 +46,15 @@ public void ShouldSucceedOnFailureThrowsNamingTheCode() {
Outcome<int> outcome = Outcome<int>.Failure(AnError());

OutcomeAssertionException exception = Assert.Throws<OutcomeAssertionException>(() => outcome.ShouldSucceed());
Check.That(exception.Message).Contains("PAYMENT.DECLINED");
Check.That(exception.Message).Contains("PAYMENT_DECLINED");
}

[Fact(DisplayName = "ShouldFail on a failed outcome returns a fluent handle that matches the code.")]
public void ShouldFailReturnsFluentHandleMatchingTheCode() {
Outcome<int> outcome = Outcome<int>.Failure(AnError());

outcome.ShouldFail()
.WithCode("PAYMENT.DECLINED")
.WithCode("PAYMENT_DECLINED")
.WithDiagnosticMessage("diagnostic")
.WithShortMessage("short");
}
Expand All @@ -63,7 +63,7 @@ public void ShouldFailReturnsFluentHandleMatchingTheCode() {
public void ShouldFailWithMismatchingCodeThrows() {
Outcome<int> outcome = Outcome<int>.Failure(AnError());

Assert.Throws<OutcomeAssertionException>(() => outcome.ShouldFail().WithCode("SOMETHING.ELSE"));
Assert.Throws<OutcomeAssertionException>(() => outcome.ShouldFail().WithCode("SOMETHING_ELSE"));
}

[Fact(DisplayName = "ShouldFail on a successful outcome throws.")]
Expand All @@ -76,7 +76,7 @@ public void ShouldFailOnSuccessThrows() {
[Fact(DisplayName = "The non-generic Outcome assertions behave the same way.")]
public void NonGenericOutcomeAssertions() {
Outcome.Success.ShouldSucceed();
Outcome.Failure(AnError()).ShouldFail().WithCode("PAYMENT.DECLINED");
Outcome.Failure(AnError()).ShouldFail().WithCode("PAYMENT_DECLINED");

Assert.Throws<OutcomeAssertionException>(() => Outcome.Success.ShouldFail());
Assert.Throws<OutcomeAssertionException>(() => Outcome.Failure(AnError()).ShouldSucceed());
Expand All @@ -85,7 +85,7 @@ public void NonGenericOutcomeAssertions() {
[Fact(DisplayName = "WithContextEntry checks the presence and value of a context entry.")]
public void WithContextEntryChecksPresenceAndValue() {
ErrorContextKey<string> cardNetwork = ErrorContextKey.Create<string>("CardNetwork", "The card network.");
DomainError error = DomainError.Create(ErrorCode.Create("PAYMENT.DECLINED"), "diagnostic", context => context.Add(cardNetwork, "VISA"))
DomainError error = DomainError.Create(ErrorCode.Create("PAYMENT_DECLINED"), "diagnostic", context => context.Add(cardNetwork, "VISA"))
.WithPublicMessage("short");
Outcome<int> outcome = Outcome<int>.Failure(error);

Expand Down
4 changes: 2 additions & 2 deletions FirstClassErrors/Error.cs
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,8 @@ protected Error(ErrorCode code,
/// </summary>
/// <remarks>
/// Unlike <see cref="InstanceId" />, this value is shared across all occurrences
/// of the same logical error and is intended for grouping, monitoring, and alerting. Examples: <c>PAYMENT.DECLINED</c>
/// or <c>INVENTORY.OUT_OF_STOCK</c>.
/// of the same logical error and is intended for grouping, monitoring, and alerting. Examples: <c>PAYMENT_DECLINED</c>
/// or <c>INVENTORY_OUT_OF_STOCK</c>.
/// </remarks>
public ErrorCode Code { get; }

Expand Down
16 changes: 8 additions & 8 deletions doc/Testing.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ usually means unwrapping it by hand and reaching for the null-forgiving
Outcome<Receipt> outcome = checkout.Pay(order);

Assert.True(outcome.IsFailure);
Assert.Equal("PAYMENT.DECLINED", outcome.Error!.Code.ToString());
Assert.Equal("PAYMENT_DECLINED", outcome.Error!.Code.ToString());
```

With the testing package, the intent comes first and the boilerplate disappears:
Expand All @@ -54,7 +54,7 @@ With the testing package, the intent comes first and the boilerplate disappears:
// 🙂 After
checkout.Pay(order)
.ShouldFail()
.WithCode("PAYMENT.DECLINED");
.WithCode("PAYMENT_DECLINED");
```

### Successes return their value
Expand Down Expand Up @@ -92,7 +92,7 @@ public void Declining_a_payment_reports_a_diagnosable_error()
Outcome<Receipt> outcome = checkout.Pay(declinedCard);

outcome.ShouldFail()
.WithCode("PAYMENT.DECLINED")
.WithCode("PAYMENT_DECLINED")
.WithShortMessage("Your payment was declined.")
.WithDiagnosticMessage("Issuer refused authorization (code 51).")
.WithContextEntry("CardNetwork", "VISA");
Expand All @@ -113,7 +113,7 @@ Need something the fluent surface doesn't cover? `Subject` gives you the raw
`Error` back:

```csharp
Error error = outcome.ShouldFail().WithCode("ORDER.NOT_FOUND").Subject;
Error error = outcome.ShouldFail().WithCode("ORDER_NOT_FOUND").Subject;

Assert.Empty(error.InnerErrors);
```
Expand All @@ -125,11 +125,11 @@ with a message that tells you what actually happened — not the domain's own
exception:

```text
Expected the outcome to succeed, but it failed with [PAYMENT.DECLINED]: Issuer refused authorization (code 51).
Expected the outcome to succeed, but it failed with [PAYMENT_DECLINED]: Issuer refused authorization (code 51).
```

```text
Expected the error to have code "ORDER.NOT_FOUND", but it was "ORDER.LOCKED".
Expected the error to have code "ORDER_NOT_FOUND", but it was "ORDER_LOCKED".
```

---
Expand Down Expand Up @@ -161,7 +161,7 @@ public void An_error_records_when_it_occurred()
using (Clock.UseFixed(instant))
{
DomainError error = DomainError
.Create(ErrorCode.Create("ORDER.NOT_FOUND"), "Order 42 was not found.")
.Create(ErrorCode.Create("ORDER_NOT_FOUND"), "Order 42 was not found.")
.WithPublicMessage("This order does not exist.");

Assert.Equal(instant, error.OccurredAt);
Expand Down Expand Up @@ -268,7 +268,7 @@ public void Looking_up_a_missing_order_fails_deterministically()
Outcome<Order> outcome = orders.Find(missingId);

ErrorAssertion failure = outcome.ShouldFail()
.WithCode("ORDER.NOT_FOUND")
.WithCode("ORDER_NOT_FOUND")
.WithContextEntry("OrderId", missingId);

Assert.Equal(instant, failure.Subject.OccurredAt);
Expand Down
16 changes: 8 additions & 8 deletions doc/Testing.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ C'est la partie que vous utiliserez tous les jours. Tester du code qui renvoie u
Outcome<Receipt> outcome = checkout.Pay(order);

Assert.True(outcome.IsFailure);
Assert.Equal("PAYMENT.DECLINED", outcome.Error!.Code.ToString());
Assert.Equal("PAYMENT_DECLINED", outcome.Error!.Code.ToString());
```

Avec le package de test, l'intention passe en premier et le boilerplate disparaît :
Expand All @@ -57,7 +57,7 @@ Avec le package de test, l'intention passe en premier et le boilerplate dispara
// 🙂 Après
checkout.Pay(order)
.ShouldFail()
.WithCode("PAYMENT.DECLINED");
.WithCode("PAYMENT_DECLINED");
```

### Les succès renvoient leur valeur
Expand Down Expand Up @@ -95,7 +95,7 @@ public void Un_paiement_refuse_remonte_une_erreur_diagnosable()
Outcome<Receipt> outcome = checkout.Pay(declinedCard);

outcome.ShouldFail()
.WithCode("PAYMENT.DECLINED")
.WithCode("PAYMENT_DECLINED")
.WithShortMessage("Votre paiement a été refusé.")
.WithDiagnosticMessage("L'émetteur a refusé l'autorisation (code 51).")
.WithContextEntry("CardNetwork", "VISA");
Expand All @@ -116,7 +116,7 @@ Besoin de quelque chose que la surface fluente ne couvre pas ? `Subject` vous
rend l'`Error` brute :

```csharp
Error error = outcome.ShouldFail().WithCode("ORDER.NOT_FOUND").Subject;
Error error = outcome.ShouldFail().WithCode("ORDER_NOT_FOUND").Subject;

Assert.Empty(error.InnerErrors);
```
Expand All @@ -128,11 +128,11 @@ Quand une attente n'est pas satisfaite, les assertions lèvent une
et non l'exception propre au domaine :

```text
Expected the outcome to succeed, but it failed with [PAYMENT.DECLINED]: L'émetteur a refusé l'autorisation (code 51).
Expected the outcome to succeed, but it failed with [PAYMENT_DECLINED]: L'émetteur a refusé l'autorisation (code 51).
```

```text
Expected the error to have code "ORDER.NOT_FOUND", but it was "ORDER.LOCKED".
Expected the error to have code "ORDER_NOT_FOUND", but it was "ORDER_LOCKED".
```

---
Expand Down Expand Up @@ -164,7 +164,7 @@ public void Une_erreur_enregistre_quand_elle_survient()
using (Clock.UseFixed(instant))
{
DomainError error = DomainError
.Create(ErrorCode.Create("ORDER.NOT_FOUND"), "La commande 42 est introuvable.")
.Create(ErrorCode.Create("ORDER_NOT_FOUND"), "La commande 42 est introuvable.")
.WithPublicMessage("Cette commande n'existe pas.");

Assert.Equal(instant, error.OccurredAt);
Expand Down Expand Up @@ -274,7 +274,7 @@ public void Chercher_une_commande_absente_echoue_de_facon_deterministe()
Outcome<Order> outcome = orders.Find(missingId);

ErrorAssertion failure = outcome.ShouldFail()
.WithCode("ORDER.NOT_FOUND")
.WithCode("ORDER_NOT_FOUND")
.WithContextEntry("OrderId", missingId);

Assert.Equal(instant, failure.Subject.OccurredAt);
Expand Down