initial commit v2
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
using NUnit.Framework;
|
||||
using OrderBookDomain;
|
||||
|
||||
namespace OrderBookTests;
|
||||
|
||||
public class OrderBookLifecycleTests
|
||||
{
|
||||
private static readonly DateTimeOffset Day0 = new(2025, 6, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
private static DateTimeOffset At(int day) => Day0.AddDays(day);
|
||||
|
||||
[Test]
|
||||
public void FinalizeThenPay_MovesOrderThroughTwoStates()
|
||||
{
|
||||
var book = new OrderBook();
|
||||
var order = book.OpenOrder("anna");
|
||||
|
||||
book.FinalizeOrder(order.Id, At(1));
|
||||
Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.WaitingForPayment));
|
||||
|
||||
book.PayOrder(order.Id, At(2));
|
||||
Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.Fulfilling));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PayThenShip_DeliversTheOrder()
|
||||
{
|
||||
var book = new OrderBook();
|
||||
var order = book.OpenOrder("anna");
|
||||
|
||||
book.FinalizeOrder(order.Id, At(1));
|
||||
Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.WaitingForPayment));
|
||||
|
||||
book.PayOrder(order.Id, At(2));
|
||||
book.ShipOrder(order.Id);
|
||||
Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.Delivered));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PayWithinWindow_ThenLatePayOnSecondOrder_CancelsOnlyThatOrder()
|
||||
{
|
||||
var book = new OrderBook();
|
||||
var first = book.OpenOrder("anna");
|
||||
var second = book.OpenOrder("bram");
|
||||
|
||||
book.FinalizeOrder(first.Id, At(1));
|
||||
book.FinalizeOrder(second.Id, At(1));
|
||||
Assert.That(book.Orders, Has.Count.EqualTo(2));
|
||||
|
||||
book.PayOrder(first.Id, At(5));
|
||||
Assert.That(book.GetOrder(first.Id).Status, Is.EqualTo(OrderStatus.Fulfilling));
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => book.PayOrder(second.Id, At(30)));
|
||||
Assert.That(book.GetOrder(second.Id).Status, Is.EqualTo(OrderStatus.Cancelled));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PayBeforeFinalize_Throws_ThenFinalizeStillWorks()
|
||||
{
|
||||
var book = new OrderBook();
|
||||
var order = book.OpenOrder("anna");
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => book.PayOrder(order.Id, At(1)));
|
||||
Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.Pending));
|
||||
|
||||
book.FinalizeOrder(order.Id, At(2));
|
||||
Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.WaitingForPayment));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CancelPendingOrder_ThenPayAfterCancel_ThrowsAgain()
|
||||
{
|
||||
var book = new OrderBook();
|
||||
var order = book.OpenOrder("anna");
|
||||
|
||||
book.CancelOrder(order.Id);
|
||||
Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.Cancelled));
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => book.PayOrder(order.Id, At(1)));
|
||||
Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.Cancelled));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ShippedOrder_CannotBeCancelled()
|
||||
{
|
||||
var book = new OrderBook();
|
||||
var order = book.OpenOrder("anna");
|
||||
|
||||
book.FinalizeOrder(order.Id, At(1));
|
||||
book.PayOrder(order.Id, At(2));
|
||||
book.ShipOrder(order.Id);
|
||||
Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.Delivered));
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => book.CancelOrder(order.Id));
|
||||
Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.Delivered));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TwoCustomers_InterleaveTransitions_Independently()
|
||||
{
|
||||
var book = new OrderBook();
|
||||
var annas = book.OpenOrder("anna");
|
||||
var brams = book.OpenOrder("bram");
|
||||
|
||||
book.FinalizeOrder(brams.Id, At(1));
|
||||
Assert.That(book.GetOrder(annas.Id).Status, Is.EqualTo(OrderStatus.Pending));
|
||||
Assert.That(book.GetOrder(brams.Id).Status, Is.EqualTo(OrderStatus.WaitingForPayment));
|
||||
|
||||
book.PayOrder(brams.Id, At(2));
|
||||
book.CancelOrder(annas.Id);
|
||||
Assert.That(book.GetOrder(annas.Id).Status, Is.EqualTo(OrderStatus.Cancelled));
|
||||
Assert.That(book.GetOrder(brams.Id).Status, Is.EqualTo(OrderStatus.Fulfilling));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NUnit" Version="4.6.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="6.3.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\OrderBookDomain\OrderBookDomain.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,74 @@
|
||||
using System.Linq;
|
||||
using NUnit.Framework.Constraints;
|
||||
using OrderDomain;
|
||||
|
||||
namespace OrderTests;
|
||||
|
||||
/// <summary>
|
||||
/// Custom NUnit matcher that bundles multiple order assertions into one.
|
||||
/// Participants: you do not need to understand how this works —
|
||||
/// just recognize the pattern and use it as an example for the agent.
|
||||
/// </summary>
|
||||
public static class Has
|
||||
{
|
||||
public static IResolveConstraint OrderState(decimal expectedTotal, decimal[] expectedLineValues)
|
||||
=> new OrderStateConstraint(expectedTotal, expectedLineValues);
|
||||
}
|
||||
|
||||
public class OrderStateConstraint : Constraint
|
||||
{
|
||||
private readonly decimal _expectedTotal;
|
||||
private readonly decimal[] _expectedLineValues;
|
||||
|
||||
public OrderStateConstraint(decimal expectedTotal, decimal[] expectedLineValues)
|
||||
{
|
||||
_expectedTotal = expectedTotal;
|
||||
_expectedLineValues = expectedLineValues;
|
||||
}
|
||||
|
||||
public override string Description =>
|
||||
$"order with total {_expectedTotal} and line values [{string.Join(", ", _expectedLineValues)}]";
|
||||
|
||||
public override ConstraintResult ApplyTo<TActual>(TActual actual)
|
||||
{
|
||||
var order = (Order)(object)actual!;
|
||||
var actualTotal = order.Total;
|
||||
var actualLineValues = order.Lines.Select(l => l.Value).ToArray();
|
||||
|
||||
var details = new List<string>();
|
||||
if (actualTotal != _expectedTotal)
|
||||
details.Add($"Total: expected {_expectedTotal}, but was {actualTotal}");
|
||||
if (!actualLineValues.SequenceEqual(_expectedLineValues))
|
||||
details.Add(
|
||||
$"Line values: expected [{string.Join(", ", _expectedLineValues)}], " +
|
||||
$"but was [{string.Join(", ", actualLineValues)}]");
|
||||
|
||||
return new OrderStateResult(this, actual!, details.Count == 0, details);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A ConstraintResult that carries the per-field mismatch details and
|
||||
/// writes them as additional lines in the failure message, e.g.
|
||||
///
|
||||
/// Expected: order with total 210 and line values [160, 50]
|
||||
/// But was: <Order>
|
||||
/// Total: expected 210, but was 250
|
||||
/// Line values: expected [160, 50], but was [200, 50]
|
||||
/// </summary>
|
||||
private sealed class OrderStateResult : ConstraintResult
|
||||
{
|
||||
private readonly IReadOnlyList<string> _details;
|
||||
|
||||
public OrderStateResult(Constraint constraint, object? actual, bool isSuccess, IReadOnlyList<string> details)
|
||||
: base(constraint, actual, isSuccess)
|
||||
{
|
||||
_details = details;
|
||||
}
|
||||
|
||||
public override void WriteAdditionalLinesTo(MessageWriter writer)
|
||||
{
|
||||
foreach (var detail in _details)
|
||||
writer.WriteMessageLine(detail);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NUnit" Version="4.6.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="6.3.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\OrderDomain\OrderDomain.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,92 @@
|
||||
using NUnit.Framework;
|
||||
using OrderDomain;
|
||||
|
||||
namespace OrderTests;
|
||||
|
||||
public class OrderTotalTests
|
||||
{
|
||||
// This test was refactored — notice the custom matcher.
|
||||
// The other tests still need the same treatment.
|
||||
[Test]
|
||||
public void DiscountOnFirstLine_TotalAndLineValues()
|
||||
{
|
||||
var order = new Order();
|
||||
order.AddLine(100m, 2); // line 0: 200 → 160 with discount
|
||||
order.AddLine(50m, 1); // line 1: 50
|
||||
|
||||
order.ApplyDiscount(0);
|
||||
|
||||
Assert.That(order, Has.OrderState(
|
||||
expectedTotal: 210m,
|
||||
expectedLineValues: [160m, 50m]));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DiscountOnSecondLine_TotalAndLineValues()
|
||||
{
|
||||
var order = new Order();
|
||||
order.AddLine(100m, 2); // line 0: 200
|
||||
order.AddLine(50m, 1); // line 1: 50 → 40 with discount
|
||||
|
||||
order.ApplyDiscount(1);
|
||||
|
||||
Assert.That(order.Total, Is.EqualTo(240m));
|
||||
Assert.That(order.Lines[0].Value, Is.EqualTo(200m));
|
||||
Assert.That(order.Lines[1].Value, Is.EqualTo(40m));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DiscountOnBothLines_TotalAndLineValues()
|
||||
{
|
||||
var order = new Order();
|
||||
order.AddLine(100m, 2); // line 0: 200 → 160
|
||||
order.AddLine(50m, 1); // line 1: 50 → 40
|
||||
|
||||
order.ApplyDiscount(0);
|
||||
order.ApplyDiscount(1);
|
||||
|
||||
Assert.That(order.Total, Is.EqualTo(200m));
|
||||
Assert.That(order.Lines[0].Value, Is.EqualTo(160m));
|
||||
Assert.That(order.Lines[1].Value, Is.EqualTo(40m));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NoDiscount_TotalAndLineValues()
|
||||
{
|
||||
var order = new Order();
|
||||
order.AddLine(100m, 2); // line 0: 200
|
||||
order.AddLine(50m, 1); // line 1: 50
|
||||
|
||||
Assert.That(order.Total, Is.EqualTo(250m));
|
||||
Assert.That(order.Lines[0].Value, Is.EqualTo(200m));
|
||||
Assert.That(order.Lines[1].Value, Is.EqualTo(50m));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SingleLineWithDiscount_TotalAndValue()
|
||||
{
|
||||
var order = new Order();
|
||||
order.AddLine(75m, 4); // line 0: 300 → 240
|
||||
|
||||
order.ApplyDiscount(0);
|
||||
|
||||
Assert.That(order.Total, Is.EqualTo(240m));
|
||||
Assert.That(order.Lines[0].Value, Is.EqualTo(240m));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ThreeLinesOneDiscount_TotalAndLineValues()
|
||||
{
|
||||
var order = new Order();
|
||||
order.AddLine(20m, 1); // line 0: 20
|
||||
order.AddLine(30m, 2); // line 1: 60 → 48
|
||||
order.AddLine(10m, 3); // line 2: 30
|
||||
|
||||
order.ApplyDiscount(1);
|
||||
|
||||
Assert.That(order.Total, Is.EqualTo(98m));
|
||||
Assert.That(order.Lines[0].Value, Is.EqualTo(20m));
|
||||
Assert.That(order.Lines[1].Value, Is.EqualTo(48m));
|
||||
Assert.That(order.Lines[2].Value, Is.EqualTo(30m));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user