Skip to content

Commit 4eaee71

Browse files
aligneddevaligneddev
andauthored
used the msec:farley-score skill (#66)
Co-authored-by: aligneddev <aligneddev@github.com>
1 parent d4a68b1 commit 4eaee71

23 files changed

Lines changed: 1500 additions & 1385 deletions

docs/agent/testing-and-quality-gates.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,15 @@ Mandatory test and validation requirements before implementation and merge.
2727
- Run all impacted commands above
2828
- `cd src/BikeTracking.Frontend && npm run test:e2e`
2929

30+
## Backend Test Conventions
31+
- No mocking framework (Moq/NSubstitute) is used in `BikeTracking.Api.Tests`. Use real objects,
32+
the EF Core in-memory provider (`TestFactories.CreateDbContext`), and in-process
33+
`WebApplication`/`TestServer` hosts instead. This avoids mock-tautology and over-specified
34+
mock-interaction anti-patterns and keeps tests coupled to behavior, not implementation.
35+
- Inject `TimeProvider` (registered as `TimeProvider.System` in `Program.cs`) into any service
36+
that needs "now"/"today" instead of calling `DateTime.Now`/`DateTime.UtcNow` directly. Tests
37+
pass `TestSupport.FakeTimeProvider` with a fixed instant so calendar-boundary logic (e.g.
38+
"this week"/"this month") never depends on wall-clock time.
39+
3040
## PR Gate
3141
- E2E tests are required for every PR.

src/BikeTracking.Api.Tests/Application/Dashboard/GetAdvancedDashboardServiceTests.cs

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using BikeTracking.Api.Application.Dashboard;
22
using BikeTracking.Api.Infrastructure.Persistence;
33
using BikeTracking.Api.Infrastructure.Persistence.Entities;
4+
using BikeTracking.Api.Tests.TestSupport;
45
using Microsoft.EntityFrameworkCore;
56

67
namespace BikeTracking.Api.Tests.Application.Dashboard;
@@ -37,7 +38,7 @@ public async Task GetAdvancedDashboardService_WithRidesInMultipleYears_ReturnsCo
3738
);
3839
await dbContext.SaveChangesAsync();
3940

40-
var service = new GetAdvancedDashboardService(dbContext);
41+
var service = new GetAdvancedDashboardService(dbContext, TimeProvider.System);
4142
var result = await service.GetAsync(rider.UserId);
4243

4344
// 20 miles / 20 mpg = 1 gallon + 10 miles / 10 mpg = 1 gallon = 2 total
@@ -79,7 +80,7 @@ public async Task GetAdvancedDashboardService_WithRideMissingGasPrice_FlagsFuelC
7980
);
8081
await dbContext.SaveChangesAsync();
8182

82-
var service = new GetAdvancedDashboardService(dbContext);
83+
var service = new GetAdvancedDashboardService(dbContext, TimeProvider.System);
8384
var result = await service.GetAsync(rider.UserId);
8485

8586
Assert.True(result.SavingsWindows.AllTime.FuelCostEstimated);
@@ -104,7 +105,7 @@ public async Task GetAdvancedDashboardService_UserWithNoMpgSetting_ReturnsMpgRem
104105
);
105106
await dbContext.SaveChangesAsync();
106107

107-
var service = new GetAdvancedDashboardService(dbContext);
108+
var service = new GetAdvancedDashboardService(dbContext, TimeProvider.System);
108109
var result = await service.GetAsync(rider.UserId);
109110

110111
Assert.True(result.Reminders.MpgReminderRequired);
@@ -129,7 +130,7 @@ public async Task GetAdvancedDashboardService_UserWithNoMileageRateSetting_Retur
129130
);
130131
await dbContext.SaveChangesAsync();
131132

132-
var service = new GetAdvancedDashboardService(dbContext);
133+
var service = new GetAdvancedDashboardService(dbContext, TimeProvider.System);
133134
var result = await service.GetAsync(rider.UserId);
134135

135136
Assert.False(result.Reminders.MpgReminderRequired);
@@ -171,7 +172,7 @@ public async Task GetAdvancedDashboardService_WithRidesInMultipleWindows_Returns
171172
);
172173
await dbContext.SaveChangesAsync();
173174

174-
var service = new GetAdvancedDashboardService(dbContext);
175+
var service = new GetAdvancedDashboardService(dbContext, TimeProvider.System);
175176
var result = await service.GetAsync(rider.UserId);
176177

177178
// Weekly: only the current-week ride (1 gallon)
@@ -200,7 +201,7 @@ public async Task GetAdvancedDashboardService_PartialMonthRides_HandlesZeroDivis
200201
);
201202
await dbContext.SaveChangesAsync();
202203

203-
var service = new GetAdvancedDashboardService(dbContext);
204+
var service = new GetAdvancedDashboardService(dbContext, TimeProvider.System);
204205
var result = await service.GetAsync(rider.UserId);
205206

206207
// Zero MPG rides should contribute null (not throw)
@@ -216,7 +217,11 @@ public async Task GetAdvancedDashboardService_RideThisWeek_ConsistencySuggestion
216217
using var dbContext = CreateDbContext();
217218
var rider = await CreateRiderAsync(dbContext, "Consistency Rider");
218219

219-
var weekStart = DateTime.Now.Date.AddDays(-(((int)DateTime.Now.DayOfWeek - 1 + 7) % 7));
220+
// Fixed reference "now" (a Wednesday) removes dependency on wall-clock time so this
221+
// test can't flip near a real week boundary.
222+
var fixedNow = new DateTime(2026, 1, 14, 12, 0, 0, DateTimeKind.Utc);
223+
var timeProvider = new FakeTimeProvider(fixedNow);
224+
var weekStart = fixedNow.Date.AddDays(-(((int)fixedNow.DayOfWeek - 1 + 7) % 7));
220225

221226
dbContext.Rides.Add(
222227
new RideEntity
@@ -229,7 +234,7 @@ public async Task GetAdvancedDashboardService_RideThisWeek_ConsistencySuggestion
229234
);
230235
await dbContext.SaveChangesAsync();
231236

232-
var service = new GetAdvancedDashboardService(dbContext);
237+
var service = new GetAdvancedDashboardService(dbContext, timeProvider);
233238
var result = await service.GetAsync(rider.UserId);
234239

235240
var consistency = result.Suggestions.Single(s => s.SuggestionKey == "consistency");
@@ -255,7 +260,7 @@ public async Task GetAdvancedDashboardService_CombinedSavingsExceed50_MilestoneS
255260
);
256261
await dbContext.SaveChangesAsync();
257262

258-
var service = new GetAdvancedDashboardService(dbContext);
263+
var service = new GetAdvancedDashboardService(dbContext, TimeProvider.System);
259264
var result = await service.GetAsync(rider.UserId);
260265

261266
var milestone = result.Suggestions.Single(s => s.SuggestionKey == "milestone");
@@ -279,7 +284,7 @@ public async Task GetAdvancedDashboardService_LastRideMoreThan7DaysAgo_ComebackS
279284
);
280285
await dbContext.SaveChangesAsync();
281286

282-
var service = new GetAdvancedDashboardService(dbContext);
287+
var service = new GetAdvancedDashboardService(dbContext, TimeProvider.System);
283288
var result = await service.GetAsync(rider.UserId);
284289

285290
var comeback = result.Suggestions.Single(s => s.SuggestionKey == "comeback");
@@ -294,7 +299,7 @@ public async Task GetAdvancedDashboardService_UserWithNoRides_ReturnsZeroValuesG
294299
using var dbContext = CreateDbContext();
295300
var rider = await CreateRiderAsync(dbContext, "No Rides Rider");
296301

297-
var service = new GetAdvancedDashboardService(dbContext);
302+
var service = new GetAdvancedDashboardService(dbContext, TimeProvider.System);
298303
var result = await service.GetAsync(rider.UserId);
299304

300305
Assert.Equal(0, result.SavingsWindows.AllTime.RideCount);
@@ -316,7 +321,7 @@ public async Task GetAdvancedDashboardService_NoSettings_BothReminderFlagsSet()
316321
using var dbContext = CreateDbContext();
317322
var rider = await CreateRiderAsync(dbContext, "NoSettings Rider");
318323

319-
var service = new GetAdvancedDashboardService(dbContext);
324+
var service = new GetAdvancedDashboardService(dbContext, TimeProvider.System);
320325
var result = await service.GetAsync(rider.UserId);
321326

322327
// No UserSettings row means both nulls
@@ -330,7 +335,7 @@ public async Task GetAdvancedDashboardService_ResponseIncludesAllThreeSuggestion
330335
using var dbContext = CreateDbContext();
331336
var rider = await CreateRiderAsync(dbContext, "AllSuggestions Rider");
332337

333-
var service = new GetAdvancedDashboardService(dbContext);
338+
var service = new GetAdvancedDashboardService(dbContext, TimeProvider.System);
334339
var result = await service.GetAsync(rider.UserId);
335340

336341
Assert.Equal(3, result.Suggestions.Count);
@@ -357,7 +362,7 @@ public async Task GetAdvancedDashboardService_MileageRateSavings_ComputedCorrect
357362
);
358363
await dbContext.SaveChangesAsync();
359364

360-
var service = new GetAdvancedDashboardService(dbContext);
365+
var service = new GetAdvancedDashboardService(dbContext, TimeProvider.System);
361366
var result = await service.GetAsync(rider.UserId);
362367

363368
// 10 miles × $0.67 = $6.70
@@ -412,7 +417,7 @@ public async Task GetAdvancedDashboardService_WithExpensesInWindow_IncludesExpen
412417
);
413418
await dbContext.SaveChangesAsync();
414419

415-
var service = new GetAdvancedDashboardService(dbContext);
420+
var service = new GetAdvancedDashboardService(dbContext, TimeProvider.System);
416421
var result = await service.GetAsync(rider.UserId);
417422

418423
Assert.Equal(50m, result.SavingsWindows.Monthly.TotalExpenses);
@@ -450,7 +455,7 @@ public async Task GetAdvancedDashboardService_WithExpenses_NetSavingsIsCombinedM
450455
);
451456
await dbContext.SaveChangesAsync();
452457

453-
var service = new GetAdvancedDashboardService(dbContext);
458+
var service = new GetAdvancedDashboardService(dbContext, TimeProvider.System);
454459
var result = await service.GetAsync(rider.UserId);
455460

456461
// 100 miles × $0.67 = $67 combined savings - $30 expenses = $37 net
@@ -489,7 +494,7 @@ public async Task GetAdvancedDashboardService_ExpensesExceedSavings_NetSavingsIs
489494
);
490495
await dbContext.SaveChangesAsync();
491496

492-
var service = new GetAdvancedDashboardService(dbContext);
497+
var service = new GetAdvancedDashboardService(dbContext, TimeProvider.System);
493498
var result = await service.GetAsync(rider.UserId);
494499

495500
// Net savings should be negative: $6.70 - $20 = -$13.30
@@ -535,7 +540,7 @@ public async Task GetAdvancedDashboardService_WithOilChangePrice_IncludesWindowe
535540
);
536541
await dbContext.SaveChangesAsync();
537542

538-
var service = new GetAdvancedDashboardService(dbContext);
543+
var service = new GetAdvancedDashboardService(dbContext, TimeProvider.System);
539544
var result = await service.GetAsync(rider.UserId);
540545

541546
// 3200 miles total — crosses one 3000-mile interval → 1 oil change × $40 = $40
@@ -570,7 +575,7 @@ public async Task GetAdvancedDashboardService_WithNoOilChangePrice_OilChangeSavi
570575
);
571576
await dbContext.SaveChangesAsync();
572577

573-
var service = new GetAdvancedDashboardService(dbContext);
578+
var service = new GetAdvancedDashboardService(dbContext, TimeProvider.System);
574579
var result = await service.GetAsync(rider.UserId);
575580

576581
Assert.Null(result.SavingsWindows.AllTime.OilChangeSavings);

src/BikeTracking.Api.Tests/Application/Dashboard/GetDashboardServiceTests.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public async Task GetDashboardService_UsesRideSnapshotsForSavings_WhenCurrentSet
6868
);
6969
await dbContext.SaveChangesAsync();
7070

71-
var service = new GetDashboardService(dbContext);
71+
var service = new GetDashboardService(dbContext, TimeProvider.System);
7272
var dashboard = await service.GetAsync(rider.UserId);
7373

7474
Assert.Equal(5m, dashboard.Totals.MoneySaved.MileageRateSavings);
@@ -101,7 +101,7 @@ public async Task GetDashboardService_ExcludesLegacyRideWithoutSnapshot_FromSavi
101101
);
102102
await dbContext.SaveChangesAsync();
103103

104-
var service = new GetDashboardService(dbContext);
104+
var service = new GetDashboardService(dbContext, TimeProvider.System);
105105
var dashboard = await service.GetAsync(rider.UserId);
106106

107107
Assert.Null(dashboard.Totals.MoneySaved.MileageRateSavings);
@@ -149,7 +149,7 @@ public async Task GetDashboardService_IncludesOptionalMetricValues_WhenDataIsAva
149149
);
150150
await dbContext.SaveChangesAsync();
151151

152-
var service = new GetDashboardService(dbContext);
152+
var service = new GetDashboardService(dbContext, TimeProvider.System);
153153
var dashboard = await service.GetAsync(rider.UserId);
154154

155155
var gallonsSuggestion = dashboard.Suggestions.Single(metric =>
@@ -178,7 +178,7 @@ public async Task GetDashboardService_ExpenseSummary_WithNoExpenses_ReturnsZeroT
178178
dbContext.Users.Add(rider);
179179
await dbContext.SaveChangesAsync();
180180

181-
var service = new GetDashboardService(dbContext);
181+
var service = new GetDashboardService(dbContext, TimeProvider.System);
182182
var dashboard = await service.GetAsync(rider.UserId);
183183

184184
Assert.NotNull(dashboard.Totals.ExpenseSummary);
@@ -234,7 +234,7 @@ public async Task GetDashboardService_ExpenseSummary_SumsNonDeletedExpenses()
234234
);
235235
await dbContext.SaveChangesAsync();
236236

237-
var service = new GetDashboardService(dbContext);
237+
var service = new GetDashboardService(dbContext, TimeProvider.System);
238238
var dashboard = await service.GetAsync(rider.UserId);
239239

240240
Assert.Equal(40.50m, dashboard.Totals.ExpenseSummary.TotalManualExpenses);
@@ -294,7 +294,7 @@ public async Task GetDashboardService_ExpenseSummary_WithOilChangePrice_Calculat
294294
);
295295
await dbContext.SaveChangesAsync();
296296

297-
var service = new GetDashboardService(dbContext);
297+
var service = new GetDashboardService(dbContext, TimeProvider.System);
298298
var dashboard = await service.GetAsync(rider.UserId);
299299

300300
Assert.Equal(80m, dashboard.Totals.ExpenseSummary.TotalManualExpenses);
@@ -327,7 +327,7 @@ public async Task GetDashboardService_ExpenseSummary_WithNoOilChangePrice_OilSav
327327
);
328328
await dbContext.SaveChangesAsync();
329329

330-
var service = new GetDashboardService(dbContext);
330+
var service = new GetDashboardService(dbContext, TimeProvider.System);
331331
var dashboard = await service.GetAsync(rider.UserId);
332332

333333
Assert.Equal(0m, dashboard.Totals.ExpenseSummary.TotalManualExpenses);

src/BikeTracking.Api.Tests/Application/Dashboard/GetYearStatsDashboardServiceTests.cs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public async Task GetAsync_YearWithFullTwelveMonthsOfRides_ProducesCorrectPerMon
5656
}
5757
await dbContext.SaveChangesAsync();
5858

59-
var service = new GetYearStatsDashboardService(dbContext);
59+
var service = new GetYearStatsDashboardService(dbContext, TimeProvider.System);
6060
var response = await service.GetAsync(rider.UserId, 2025);
6161

6262
Assert.Equal(2025, response.Year);
@@ -94,7 +94,7 @@ public async Task GetAsync_InProgressCurrentYearWithPartialData_ZeroFillsElapsed
9494
);
9595
await dbContext.SaveChangesAsync();
9696

97-
var service = new GetYearStatsDashboardService(dbContext);
97+
var service = new GetYearStatsDashboardService(dbContext, TimeProvider.System);
9898
var response = await service.GetAsync(rider.UserId, currentYear);
9999

100100
Assert.True(response.HasDataForYear);
@@ -116,7 +116,7 @@ public async Task GetAsync_YearWithZeroRides_ReturnsHasDataForYearFalse_NoExcept
116116
using var dbContext = CreateDbContext();
117117
var rider = await CreateRiderAsync(dbContext, "Zero Rides Rider");
118118

119-
var service = new GetYearStatsDashboardService(dbContext);
119+
var service = new GetYearStatsDashboardService(dbContext, TimeProvider.System);
120120
var response = await service.GetAsync(rider.UserId, 2019);
121121

122122
Assert.False(response.HasDataForYear);
@@ -165,7 +165,7 @@ public async Task GetAsync_UsesRideSnapshotsForSavings_WhenCurrentSettingsChange
165165
);
166166
await dbContext.SaveChangesAsync();
167167

168-
var service = new GetYearStatsDashboardService(dbContext);
168+
var service = new GetYearStatsDashboardService(dbContext, TimeProvider.System);
169169
var response = await service.GetAsync(rider.UserId, 2022);
170170

171171
var june = response.SavingsByMonth[5];
@@ -202,7 +202,7 @@ public async Task GetAsync_FiltersDifficultyAndWindResistance_ToRequestedYearOnl
202202
);
203203
await dbContext.SaveChangesAsync();
204204

205-
var service = new GetYearStatsDashboardService(dbContext);
205+
var service = new GetYearStatsDashboardService(dbContext, TimeProvider.System);
206206
var response = await service.GetAsync(rider.UserId, 2025);
207207

208208
Assert.True(response.Difficulty.HasData);
@@ -229,7 +229,7 @@ public async Task GetAsync_RidesWithoutDifficultyOrWindData_YieldsPartialEmptySt
229229
);
230230
await dbContext.SaveChangesAsync();
231231

232-
var service = new GetYearStatsDashboardService(dbContext);
232+
var service = new GetYearStatsDashboardService(dbContext, TimeProvider.System);
233233
var response = await service.GetAsync(rider.UserId, 2025);
234234

235235
Assert.True(response.HasDataForYear);
@@ -288,7 +288,7 @@ public async Task GetAsync_YearWithRidesAndExpenses_ComputesTotalsSection()
288288
);
289289
await dbContext.SaveChangesAsync();
290290

291-
var service = new GetYearStatsDashboardService(dbContext);
291+
var service = new GetYearStatsDashboardService(dbContext, TimeProvider.System);
292292
var response = await service.GetAsync(rider.UserId, 2025);
293293

294294
Assert.Equal(150m, response.Totals.TotalMiles);
@@ -302,7 +302,7 @@ public async Task GetAsync_YearWithZeroRides_TotalsAreZeroOrNull()
302302
using var dbContext = CreateDbContext();
303303
var rider = await CreateRiderAsync(dbContext, "Totals Zero Rider");
304304

305-
var service = new GetYearStatsDashboardService(dbContext);
305+
var service = new GetYearStatsDashboardService(dbContext, TimeProvider.System);
306306
var response = await service.GetAsync(rider.UserId, 2019);
307307

308308
Assert.Equal(0m, response.Totals.TotalMiles);
@@ -348,7 +348,7 @@ public async Task GetAvailableYearsAsync_RiderWithRidesInMultipleYears_ReturnsDe
348348
);
349349
await dbContext.SaveChangesAsync();
350350

351-
var service = new GetYearStatsDashboardService(dbContext);
351+
var service = new GetYearStatsDashboardService(dbContext, TimeProvider.System);
352352
var response = await service.GetAvailableYearsAsync(rider.UserId);
353353

354354
Assert.Equal([2025, 2024, 2023], response.Years);
@@ -360,7 +360,7 @@ public async Task GetAvailableYearsAsync_RiderWithZeroRides_ReturnsCurrentYearFa
360360
using var dbContext = CreateDbContext();
361361
var rider = await CreateRiderAsync(dbContext, "No Rides Rider");
362362

363-
var service = new GetYearStatsDashboardService(dbContext);
363+
var service = new GetYearStatsDashboardService(dbContext, TimeProvider.System);
364364
var response = await service.GetAvailableYearsAsync(rider.UserId);
365365

366366
Assert.Equal([DateTime.Now.Year], response.Years);
@@ -383,7 +383,7 @@ public async Task GetAvailableYearsAsync_RiderWithRidesInOnlyOneYear_ReturnsThat
383383
);
384384
await dbContext.SaveChangesAsync();
385385

386-
var service = new GetYearStatsDashboardService(dbContext);
386+
var service = new GetYearStatsDashboardService(dbContext, TimeProvider.System);
387387
var response = await service.GetAvailableYearsAsync(rider.UserId);
388388

389389
Assert.Equal([2021], response.Years);
@@ -396,7 +396,7 @@ public async Task GetAsync_YearBoundaryValue1900_IsAccepted(int year)
396396
using var dbContext = CreateDbContext();
397397
var rider = await CreateRiderAsync(dbContext, "Boundary Rider 1900");
398398

399-
var service = new GetYearStatsDashboardService(dbContext);
399+
var service = new GetYearStatsDashboardService(dbContext, TimeProvider.System);
400400
var response = await service.GetAsync(rider.UserId, year);
401401

402402
Assert.Equal(year, response.Year);
@@ -408,7 +408,7 @@ public async Task GetAsync_YearBoundaryValueCurrentYearPlusOne_IsAccepted()
408408
using var dbContext = CreateDbContext();
409409
var rider = await CreateRiderAsync(dbContext, "Boundary Rider Next Year");
410410

411-
var service = new GetYearStatsDashboardService(dbContext);
411+
var service = new GetYearStatsDashboardService(dbContext, TimeProvider.System);
412412
var response = await service.GetAsync(rider.UserId, DateTime.Now.Year + 1);
413413

414414
Assert.Equal(DateTime.Now.Year + 1, response.Year);

0 commit comments

Comments
 (0)