using CartWise.Application.Services; using CartWise.Infrastructure.Data; using Microsoft.EntityFrameworkCore; namespace CartWise.Application.Tests; public class ShoppingListServiceTests : IDisposable { private readonly CartWiseDbContext _db; private readonly ShoppingListService _sut; public ShoppingListServiceTests() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; _db = new CartWiseDbContext(options); _sut = new ShoppingListService(_db, TimeProvider.System); } public void Dispose() { _db.Dispose(); GC.SuppressFinalize(this); } [Fact] public async Task CreateListAsync_CreatesActiveList() { var householdId = Guid.NewGuid(); var result = await _sut.CreateListAsync(householdId, "Groceries", "user-1"); Assert.True(result.IsSuccess); Assert.Equal("Groceries", result.Value!.Name); Assert.Equal(householdId, result.Value.HouseholdId); } [Fact] public async Task CreateListAsync_FailsWhenHouseholdAlreadyHasAnActiveList() { var householdId = Guid.NewGuid(); await _sut.CreateListAsync(householdId, "Groceries", "user-1"); var result = await _sut.CreateListAsync(householdId, "Second List", "user-1"); Assert.False(result.IsSuccess); Assert.NotNull(result.Error); } [Fact] public async Task CreateListAsync_FailsWhenNameIsMissing() { var result = await _sut.CreateListAsync(Guid.NewGuid(), " ", "user-1"); Assert.False(result.IsSuccess); } [Fact] public async Task GetActiveListAsync_ReturnsNullWhenHouseholdHasNoList() { var list = await _sut.GetActiveListAsync(Guid.NewGuid()); Assert.Null(list); } [Fact] public async Task GetActiveListAsync_ReturnsTheActiveList() { var householdId = Guid.NewGuid(); var created = await _sut.CreateListAsync(householdId, "Groceries", "user-1"); var list = await _sut.GetActiveListAsync(householdId); Assert.NotNull(list); Assert.Equal(created.Value!.ShoppingListId, list!.ShoppingListId); } [Fact] public async Task GetActiveListAsync_DoesNotReturnAnotherHouseholdsList() { var householdA = Guid.NewGuid(); var householdB = Guid.NewGuid(); await _sut.CreateListAsync(householdA, "Household A's List", "user-1"); var list = await _sut.GetActiveListAsync(householdB); Assert.Null(list); } [Fact] public async Task AddItemAsync_AddsItemAndPersistsIt() { var created = await _sut.CreateListAsync(Guid.NewGuid(), "Groceries", "user-1"); var result = await _sut.AddItemAsync(created.Value!.ShoppingListId, "Milk", 2m, "gal", "user-1"); Assert.True(result.IsSuccess); Assert.Equal("Milk", result.Value!.DisplayName); var reloaded = await _sut.GetActiveListAsync(created.Value.HouseholdId); Assert.Single(reloaded!.Items); Assert.Equal("Milk", reloaded.Items.Single().DisplayName); } [Fact] public async Task AddItemAsync_FailsWhenListDoesNotExist() { var result = await _sut.AddItemAsync(Guid.NewGuid(), "Milk", null, null, "user-1"); Assert.False(result.IsSuccess); } [Fact] public async Task AddItemAsync_FailsWhenDisplayNameIsMissing() { var created = await _sut.CreateListAsync(Guid.NewGuid(), "Groceries", "user-1"); var result = await _sut.AddItemAsync(created.Value!.ShoppingListId, " ", null, null, "user-1"); Assert.False(result.IsSuccess); } [Fact] public async Task TogglePurchasedAsync_MarksItemPurchased() { var (householdId, item) = await CreateItemAsync(); var result = await _sut.TogglePurchasedAsync(householdId, item.ShoppingListItemId, item.RowVersion); Assert.True(result.IsSuccess); Assert.Equal(Domain.Enums.ShoppingListItemStatus.Purchased, result.Value!.Status); } [Fact] public async Task TogglePurchasedAsync_FailsForItemInAnotherHousehold() { var (_, item) = await CreateItemAsync(); var result = await _sut.TogglePurchasedAsync(Guid.NewGuid(), item.ShoppingListItemId, item.RowVersion); Assert.False(result.IsSuccess); } [Fact] public async Task TogglePurchasedAsync_FailsOnConcurrencyConflict() { var (householdId, item) = await CreateItemAsync(); var staleRowVersion = Guid.NewGuid(); var result = await _sut.TogglePurchasedAsync(householdId, item.ShoppingListItemId, staleRowVersion); Assert.False(result.IsSuccess); Assert.NotNull(result.Error); } [Fact] public async Task ToggleSkippedAsync_MarksItemSkipped() { var (householdId, item) = await CreateItemAsync(); var result = await _sut.ToggleSkippedAsync(householdId, item.ShoppingListItemId, item.RowVersion); Assert.True(result.IsSuccess); Assert.Equal(Domain.Enums.ShoppingListItemStatus.Skipped, result.Value!.Status); } [Fact] public async Task ToggleSkippedAsync_UnskipsOnSecondCall() { var (householdId, item) = await CreateItemAsync(); var firstResult = await _sut.ToggleSkippedAsync(householdId, item.ShoppingListItemId, item.RowVersion); var secondResult = await _sut.ToggleSkippedAsync(householdId, item.ShoppingListItemId, firstResult.Value!.RowVersion); Assert.True(secondResult.IsSuccess); Assert.Equal(Domain.Enums.ShoppingListItemStatus.Needed, secondResult.Value!.Status); } [Fact] public async Task DeleteItemAsync_RemovesItemFromList() { var (householdId, item) = await CreateItemAsync(); var result = await _sut.DeleteItemAsync(householdId, item.ShoppingListItemId, item.RowVersion); Assert.True(result.IsSuccess); var list = await _sut.GetActiveListAsync(householdId); Assert.Empty(list!.Items); } [Fact] public async Task DeleteItemAsync_FailsForItemInAnotherHousehold() { var (_, item) = await CreateItemAsync(); var result = await _sut.DeleteItemAsync(Guid.NewGuid(), item.ShoppingListItemId, item.RowVersion); Assert.False(result.IsSuccess); } [Fact] public async Task DeleteItemAsync_FailsOnConcurrencyConflict() { var (householdId, item) = await CreateItemAsync(); var staleRowVersion = Guid.NewGuid(); var result = await _sut.DeleteItemAsync(householdId, item.ShoppingListItemId, staleRowVersion); Assert.False(result.IsSuccess); var list = await _sut.GetActiveListAsync(householdId); Assert.Single(list!.Items); } private async Task<(Guid HouseholdId, Domain.Entities.ShoppingListItem Item)> CreateItemAsync() { var householdId = Guid.NewGuid(); var list = await _sut.CreateListAsync(householdId, "Groceries", "user-1"); var item = await _sut.AddItemAsync(list.Value!.ShoppingListId, "Milk", null, null, "user-1"); return (householdId, item.Value!); } }