Skip to content

Commit 338cd5d

Browse files
committed
[AI:Coder, HUMAN:-, MODEL: Claude 3.7 Sonnet] (#1) Add unit tests for GetCustomersWithOverdueOrders
1 parent 9754ccb commit 338cd5d

6 files changed

Lines changed: 803 additions & 0 deletions

File tree

.copilot-cli/1-overdue-orders.coder.cli.md

Lines changed: 364 additions & 0 deletions
Large diffs are not rendered by default.

AppInfraDemo.sln

Lines changed: 179 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
using DataAccess;
2+
using FluentAssertions;
3+
using Microsoft.Extensions.Logging;
4+
using NSubstitute;
5+
using Sales.DataModel.SalesLT;
6+
using Sales.DataModel.Values;
7+
using Contracts.Sales;
8+
9+
namespace Sales.Services.UnitTests;
10+
11+
public class CustomerServiceTests
12+
{
13+
[Fact]
14+
public void GetCustomersWithOverdueOrders_WithNoOverdueOrders_ReturnsEmptyArray()
15+
{
16+
var repositoryStub = new FakeRepository(Array.Empty<SalesOrderHeader>());
17+
var target = GetTarget(repositoryStub);
18+
19+
var result = target.GetCustomersWithOverdueOrders();
20+
21+
result.Should().BeEmpty();
22+
}
23+
24+
[Fact]
25+
public void GetCustomersWithOverdueOrders_WithOverdueOrders_ReturnsCustomers()
26+
{
27+
var customer = CreateCustomer(1, "John", "Doe", "Acme Corp");
28+
var overdueOrder = CreateOrder(1, customer, DateTime.Today.AddDays(-5), SalesOrderHeaderStatusValues.InProcess);
29+
var repositoryStub = new FakeRepository(new[] { overdueOrder });
30+
var target = GetTarget(repositoryStub);
31+
32+
var result = target.GetCustomersWithOverdueOrders();
33+
34+
result.Should().ContainSingle(c => c.CustomerName == "Acme Corp" && c.OverdueOrderCount == 1);
35+
}
36+
37+
[Fact]
38+
public void GetCustomersWithOverdueOrders_WithMultipleOverduePerCustomer_AggregatesCorrectly()
39+
{
40+
var customer = CreateCustomer(1, "Jane", "Smith", "Beta Inc");
41+
var order1 = CreateOrder(1, customer, DateTime.Today.AddDays(-10), SalesOrderHeaderStatusValues.InProcess);
42+
var order2 = CreateOrder(2, customer, DateTime.Today.AddDays(-5), SalesOrderHeaderStatusValues.Approved);
43+
var order3 = CreateOrder(3, customer, DateTime.Today.AddDays(-3), SalesOrderHeaderStatusValues.Backordered);
44+
var repositoryStub = new FakeRepository(new[] { order1, order2, order3 });
45+
var target = GetTarget(repositoryStub);
46+
47+
var result = target.GetCustomersWithOverdueOrders();
48+
49+
result.Should().ContainSingle(c =>
50+
c.CustomerName == "Beta Inc" &&
51+
c.OverdueOrderCount == 3 &&
52+
c.OldestOverdueOrderDate == DateTime.Today.AddDays(-10));
53+
}
54+
55+
[Fact]
56+
public void GetCustomersWithOverdueOrders_FiltersByStatusShipped_ExcludesShippedOrders()
57+
{
58+
var customer = CreateCustomer(1, "Bob", "Jones", "Gamma Ltd");
59+
var overdueOrder = CreateOrder(1, customer, DateTime.Today.AddDays(-5), SalesOrderHeaderStatusValues.InProcess);
60+
var shippedOrder = CreateOrder(2, customer, DateTime.Today.AddDays(-10), SalesOrderHeaderStatusValues.Shipped);
61+
var repositoryStub = new FakeRepository(new[] { overdueOrder, shippedOrder });
62+
var target = GetTarget(repositoryStub);
63+
64+
var result = target.GetCustomersWithOverdueOrders();
65+
66+
result.Should().ContainSingle(c =>
67+
c.CustomerName == "Gamma Ltd" &&
68+
c.OverdueOrderCount == 1 &&
69+
c.OldestOverdueOrderDate == DateTime.Today.AddDays(-5));
70+
}
71+
72+
[Fact]
73+
public void GetCustomersWithOverdueOrders_FiltersByStatusCancelled_ExcludesCancelledOrders()
74+
{
75+
var customer = CreateCustomer(1, "Alice", "Brown", "Delta Co");
76+
var overdueOrder = CreateOrder(1, customer, DateTime.Today.AddDays(-7), SalesOrderHeaderStatusValues.InProcess);
77+
var cancelledOrder = CreateOrder(2, customer, DateTime.Today.AddDays(-15), SalesOrderHeaderStatusValues.Cancelled);
78+
var repositoryStub = new FakeRepository(new[] { overdueOrder, cancelledOrder });
79+
var target = GetTarget(repositoryStub);
80+
81+
var result = target.GetCustomersWithOverdueOrders();
82+
83+
result.Should().ContainSingle(c =>
84+
c.CustomerName == "Delta Co" &&
85+
c.OverdueOrderCount == 1 &&
86+
c.OldestOverdueOrderDate == DateTime.Today.AddDays(-7));
87+
}
88+
89+
[Fact]
90+
public void GetCustomersWithOverdueOrders_FiltersByDueDate_ExcludesFutureOrders()
91+
{
92+
var customer = CreateCustomer(1, "Tom", "Green", "Epsilon Corp");
93+
var overdueOrder = CreateOrder(1, customer, DateTime.Today.AddDays(-3), SalesOrderHeaderStatusValues.InProcess);
94+
var futureOrder = CreateOrder(2, customer, DateTime.Today.AddDays(5), SalesOrderHeaderStatusValues.InProcess);
95+
var repositoryStub = new FakeRepository(new[] { overdueOrder, futureOrder });
96+
var target = GetTarget(repositoryStub);
97+
98+
var result = target.GetCustomersWithOverdueOrders();
99+
100+
result.Should().ContainSingle(c =>
101+
c.CustomerName == "Epsilon Corp" &&
102+
c.OverdueOrderCount == 1 &&
103+
c.OldestOverdueOrderDate == DateTime.Today.AddDays(-3));
104+
}
105+
106+
[Fact]
107+
public void GetCustomersWithOverdueOrders_SortsByOldestDueDate_Ascending()
108+
{
109+
var customer1 = CreateCustomer(1, "Alex", "White", "Zeta Inc");
110+
var customer2 = CreateCustomer(2, "Chris", "Black", "Alpha Corp");
111+
var customer3 = CreateCustomer(3, "Dana", "Gray", "Mega Ltd");
112+
113+
var order1 = CreateOrder(1, customer1, DateTime.Today.AddDays(-3), SalesOrderHeaderStatusValues.InProcess);
114+
var order2 = CreateOrder(2, customer2, DateTime.Today.AddDays(-10), SalesOrderHeaderStatusValues.InProcess);
115+
var order3 = CreateOrder(3, customer3, DateTime.Today.AddDays(-5), SalesOrderHeaderStatusValues.InProcess);
116+
117+
var repositoryStub = new FakeRepository(new[] { order1, order2, order3 });
118+
var target = GetTarget(repositoryStub);
119+
120+
var result = target.GetCustomersWithOverdueOrders();
121+
122+
var expected = new[]
123+
{
124+
new CustomerOverdueOrdersData
125+
{
126+
CustomerName = "Alpha Corp",
127+
OverdueOrderCount = 1,
128+
OldestOverdueOrderDate = DateTime.Today.AddDays(-10)
129+
},
130+
new CustomerOverdueOrdersData
131+
{
132+
CustomerName = "Mega Ltd",
133+
OverdueOrderCount = 1,
134+
OldestOverdueOrderDate = DateTime.Today.AddDays(-5)
135+
},
136+
new CustomerOverdueOrdersData
137+
{
138+
CustomerName = "Zeta Inc",
139+
OverdueOrderCount = 1,
140+
OldestOverdueOrderDate = DateTime.Today.AddDays(-3)
141+
}
142+
};
143+
144+
result.Should().BeEquivalentTo(expected, options => options
145+
.Including(c => c.CustomerName)
146+
.Including(c => c.OverdueOrderCount)
147+
.Including(c => c.OldestOverdueOrderDate)
148+
.WithStrictOrdering());
149+
}
150+
151+
[Fact]
152+
public void GetCustomersWithOverdueOrders_HandlesNullCompanyName_UsesFirstLastName()
153+
{
154+
var customer = CreateCustomer(1, "Sarah", "Wilson", null);
155+
var overdueOrder = CreateOrder(1, customer, DateTime.Today.AddDays(-2), SalesOrderHeaderStatusValues.InProcess);
156+
var repositoryStub = new FakeRepository(new[] { overdueOrder });
157+
var target = GetTarget(repositoryStub);
158+
159+
var result = target.GetCustomersWithOverdueOrders();
160+
161+
result.Should().ContainSingle(c => c.CustomerName == "Sarah Wilson");
162+
}
163+
164+
// ── Helpers ────────────────────────────────────────────────
165+
166+
private static CustomerService GetTarget(FakeRepository repositoryStub)
167+
=> new CustomerService(repositoryStub, Substitute.For<ILogger<CustomerService>>());
168+
169+
private static Customer CreateCustomer(int id, string firstName, string lastName, string? companyName)
170+
=> new Customer
171+
{
172+
CustomerID = id,
173+
FirstName = firstName,
174+
LastName = lastName,
175+
CompanyName = companyName,
176+
SalesOrderHeaders = new List<SalesOrderHeader>()
177+
};
178+
179+
private static SalesOrderHeader CreateOrder(int id, Customer customer, DateTime dueDate, byte status)
180+
{
181+
var order = new SalesOrderHeader
182+
{
183+
SalesOrderID = id,
184+
CustomerID = customer.CustomerID,
185+
Customer = customer,
186+
DueDate = dueDate,
187+
Status = status,
188+
OrderDate = DateTime.Today.AddDays(-30),
189+
ShipMethod = "CARGO TRANSPORT 5",
190+
RevisionNumber = 1,
191+
SalesOrderNumber = $"SO{id}"
192+
};
193+
customer.SalesOrderHeaders.Add(order);
194+
return order;
195+
}
196+
197+
private class FakeRepository : IRepository
198+
{
199+
private readonly List<object> data = new();
200+
201+
public FakeRepository(SalesOrderHeader[] orders)
202+
{
203+
data.AddRange(orders);
204+
}
205+
206+
public IQueryable<T> GetEntities<T>() where T : class
207+
{
208+
return data.OfType<T>().AsQueryable();
209+
}
210+
211+
public IUnitOfWork CreateUnitOfWork() => throw new NotImplementedException();
212+
}
213+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<OutputType>Exe</OutputType>
8+
<IsPackable>false</IsPackable>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="coverlet.collector" Version="6.0.4" />
13+
<PackageReference Include="FluentAssertions" Version="8.8.0" />
14+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
15+
<PackageReference Include="NSubstitute" Version="5.3.0" />
16+
<PackageReference Include="xunit.v3" Version="3.2.0" />
17+
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
18+
<PrivateAssets>all</PrivateAssets>
19+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
20+
</PackageReference>
21+
</ItemGroup>
22+
23+
<ItemGroup>
24+
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
25+
</ItemGroup>
26+
27+
<ItemGroup>
28+
<ProjectReference Include="..\Sales.Services\Sales.Services.csproj" />
29+
<ProjectReference Include="..\Sales.DataModel\Sales.DataModel.csproj" />
30+
<ProjectReference Include="..\..\Contracts\Contracts.csproj" />
31+
<ProjectReference Include="..\..\..\Infra\DataAccess\DataAccess.csproj" />
32+
</ItemGroup>
33+
34+
<ItemGroup>
35+
<Using Include="Xunit" />
36+
</ItemGroup>
37+
38+
</Project>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
3+
"methodDisplay": "method"
4+
}

Modules/Sales/Sales.Services/Sales.Services.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,9 @@
1414
<ProjectReference Include="..\Sales.DataModel\Sales.DataModel.csproj" />
1515
</ItemGroup>
1616

17+
<ItemGroup>
18+
<InternalsVisibleTo Include="Sales.Services.UnitTests" />
19+
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" Key="0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7" />
20+
</ItemGroup>
21+
1722
</Project>

0 commit comments

Comments
 (0)