From ecf819ca6126f32fb75dd1205fdd3ffc7ce525fc Mon Sep 17 00:00:00 2001 From: gamer147 Date: Thu, 28 May 2026 01:48:29 -0400 Subject: [PATCH] repo(card): SetProtected with zero-count-row preservation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements ICardInventoryRepository.SetProtected — loads only the owned-cards collection (no decks/currency), guards against the EF owned-nav Card.Id==0 default-init quirk, and accepts Count=0 rows (destruct→re-protect round-trip). Covered by 4 new NUnit tests (flip, unset, zero-count-row, unknown-card error). Full suite: 533/533. Co-Authored-By: Claude Sonnet 4.6 --- .../Card/CardInventoryRepository.cs | 17 +++++ .../CardInventoryRepositoryTests.cs | 75 +++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/SVSim.Database/Repositories/Card/CardInventoryRepository.cs b/SVSim.Database/Repositories/Card/CardInventoryRepository.cs index ab1e02f..fb7690f 100644 --- a/SVSim.Database/Repositories/Card/CardInventoryRepository.cs +++ b/SVSim.Database/Repositories/Card/CardInventoryRepository.cs @@ -154,4 +154,21 @@ public class CardInventoryRepository : ICardInventoryRepository return CreateOutcome.Ok(new CreateResult(viewer.Currency.RedEther, allGrants)); } + + public async Task SetProtected(long viewerId, long cardId, bool isProtected) + { + // Lighter load than create/destruct: only need viewer's owned-cards collection. No decks, + // no currency, no CollectionInfo. + var viewer = await _db.Viewers + .Include(v => v.Cards).ThenInclude(c => c.Card) + .FirstAsync(v => v.Id == viewerId); + + var owned = viewer.Cards.FirstOrDefault(c => c.Card.Id == cardId); + if (owned is null || owned.Card.Id == 0) + return ProtectOutcome.Fail(ProtectError.UnknownCard); + + owned.IsProtected = isProtected; + await _db.SaveChangesAsync(); + return ProtectOutcome.Ok(); + } } diff --git a/SVSim.UnitTests/Repositories/CardInventoryRepositoryTests.cs b/SVSim.UnitTests/Repositories/CardInventoryRepositoryTests.cs index a907b31..38e3d87 100644 --- a/SVSim.UnitTests/Repositories/CardInventoryRepositoryTests.cs +++ b/SVSim.UnitTests/Repositories/CardInventoryRepositoryTests.cs @@ -514,4 +514,79 @@ public class CardInventoryRepositoryTests .FirstAsync(v => v.Id == viewerId); Assert.That(viewer.LeaderSkins.Any(s => s.Id == (int)skinId), Is.True, "cascade actually granted the skin"); } + + [Test] + public async Task SetProtected_flips_flag_on_owned_card() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + await factory.SeedOwnedCardAsync(viewerId, cardId: 10001001L, count: 2, isProtected: false); + + using var scope = factory.Services.CreateScope(); + var repo = scope.ServiceProvider.GetRequiredService(); + + var outcome = await repo.SetProtected(viewerId, 10001001L, isProtected: true); + Assert.That(outcome.IsSuccess, Is.True); + + var db = scope.ServiceProvider.GetRequiredService(); + var viewer = await db.Viewers.Include(v => v.Cards).ThenInclude(c => c.Card).FirstAsync(v => v.Id == viewerId); + Assert.That(viewer.Cards.First(c => c.Card.Id == 10001001L).IsProtected, Is.True); + } + + [Test] + public async Task SetProtected_unsets_flag_when_isProtected_false() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + await factory.SeedOwnedCardAsync(viewerId, cardId: 10001001L, count: 2, isProtected: true); + + using var scope = factory.Services.CreateScope(); + var repo = scope.ServiceProvider.GetRequiredService(); + + var outcome = await repo.SetProtected(viewerId, 10001001L, isProtected: false); + Assert.That(outcome.IsSuccess, Is.True); + + var db = scope.ServiceProvider.GetRequiredService(); + var viewer = await db.Viewers.Include(v => v.Cards).ThenInclude(c => c.Card).FirstAsync(v => v.Id == viewerId); + Assert.That(viewer.Cards.First(c => c.Card.Id == 10001001L).IsProtected, Is.False); + } + + [Test] + public async Task SetProtected_allows_zero_count_row() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + // Round-trip: own 1 → destruct 1 → Count=0 row remains → protect succeeds + await factory.SeedOwnedCardAsync(viewerId, cardId: 10001001L, count: 1, dustReward: 50); + using var setup = factory.Services.CreateScope(); + var setupRepo = setup.ServiceProvider.GetRequiredService(); + var destruct = await setupRepo.DestructCards(viewerId, new Dictionary { { 10001001L, 1 } }); + Assert.That(destruct.IsSuccess, Is.True, "setup precondition: destruct-to-zero"); + + using var scope = factory.Services.CreateScope(); + var repo = scope.ServiceProvider.GetRequiredService(); + var outcome = await repo.SetProtected(viewerId, 10001001L, isProtected: true); + Assert.That(outcome.IsSuccess, Is.True); + + var db = scope.ServiceProvider.GetRequiredService(); + var viewer = await db.Viewers.Include(v => v.Cards).ThenInclude(c => c.Card).FirstAsync(v => v.Id == viewerId); + var owned = viewer.Cards.First(c => c.Card.Id == 10001001L); + Assert.That(owned.Count, Is.EqualTo(0)); + Assert.That(owned.IsProtected, Is.True, "protect on zero-count row must persist"); + } + + [Test] + public async Task SetProtected_unknown_card_returns_error() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + // No OwnedCardEntry row at all + + using var scope = factory.Services.CreateScope(); + var repo = scope.ServiceProvider.GetRequiredService(); + + var outcome = await repo.SetProtected(viewerId, 99_999_999L, isProtected: true); + Assert.That(outcome.IsSuccess, Is.False); + Assert.That(outcome.Error, Is.EqualTo(ProtectError.UnknownCard)); + } }