test(card): CreateCards validation matrix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Enums;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.Database.Repositories.Card;
|
||||
using SVSim.UnitTests.Infrastructure;
|
||||
|
||||
@@ -311,4 +312,206 @@ public class CardInventoryRepositoryTests
|
||||
Assert.That(viewer.Currency.RedEther, Is.EqualTo(800UL));
|
||||
Assert.That(viewer.Cards.First(c => c.Card.Id == 10001001L).Count, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Create_batch_charges_sum_and_grants_each_card()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
await factory.SeedOwnedCardAsync(viewerId, cardId: 10001001L, count: 0, craftCost: 200);
|
||||
await factory.SeedOwnedCardAsync(viewerId, cardId: 10001002L, count: 0, craftCost: 800);
|
||||
await factory.SetRedEtherAsync(viewerId, 5_000UL);
|
||||
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var repo = scope.ServiceProvider.GetRequiredService<ICardInventoryRepository>();
|
||||
|
||||
var outcome = await repo.CreateCards(viewerId, new Dictionary<long, int>
|
||||
{
|
||||
{ 10001001L, 2 },
|
||||
{ 10001002L, 1 },
|
||||
});
|
||||
|
||||
Assert.That(outcome.IsSuccess, Is.True);
|
||||
// 2 * 200 + 1 * 800 = 1200 → 5000 - 1200 = 3800
|
||||
Assert.That(outcome.Result!.NewRedEtherTotal, Is.EqualTo(3_800UL));
|
||||
|
||||
var grants = outcome.Result!.Grants;
|
||||
Assert.That(grants.Any(g => g.RewardId == 10001001L && g.RewardNum == 2), Is.True);
|
||||
Assert.That(grants.Any(g => g.RewardId == 10001002L && g.RewardNum == 1), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Create_rejects_unknown_card_without_mutation()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
await factory.SetRedEtherAsync(viewerId, 1_000UL);
|
||||
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var repo = scope.ServiceProvider.GetRequiredService<ICardInventoryRepository>();
|
||||
|
||||
var outcome = await repo.CreateCards(viewerId, new Dictionary<long, int> { { 99_999_999L, 1 } });
|
||||
|
||||
Assert.That(outcome.IsSuccess, Is.False);
|
||||
Assert.That(outcome.Error, Is.EqualTo(CreateError.UnknownCard));
|
||||
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
var viewer = await db.Viewers.FirstAsync(v => v.Id == viewerId);
|
||||
Assert.That(viewer.Currency.RedEther, Is.EqualTo(1_000UL), "no debit on rejection");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Create_rejects_not_craftable_card_without_mutation()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
// craftCost=0 mirrors IsNotCraftDestruct on basic/token cards
|
||||
await factory.SeedOwnedCardAsync(viewerId, cardId: 10001001L, count: 0, craftCost: 0, dustReward: 0);
|
||||
await factory.SetRedEtherAsync(viewerId, 1_000UL);
|
||||
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var repo = scope.ServiceProvider.GetRequiredService<ICardInventoryRepository>();
|
||||
|
||||
var outcome = await repo.CreateCards(viewerId, new Dictionary<long, int> { { 10001001L, 1 } });
|
||||
|
||||
Assert.That(outcome.IsSuccess, Is.False);
|
||||
Assert.That(outcome.Error, Is.EqualTo(CreateError.NotCraftable));
|
||||
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
var viewer = await db.Viewers.FirstAsync(v => v.Id == viewerId);
|
||||
Assert.That(viewer.Currency.RedEther, Is.EqualTo(1_000UL));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Create_rejects_when_would_exceed_max_copies()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
// Viewer already owns 2 copies — crafting 2 more would push to 4, exceeding MaxCopies=3.
|
||||
await factory.SeedOwnedCardAsync(viewerId, cardId: 10001001L, count: 2, craftCost: 200);
|
||||
await factory.SetRedEtherAsync(viewerId, 1_000UL);
|
||||
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var repo = scope.ServiceProvider.GetRequiredService<ICardInventoryRepository>();
|
||||
|
||||
var outcome = await repo.CreateCards(viewerId, new Dictionary<long, int> { { 10001001L, 2 } });
|
||||
|
||||
Assert.That(outcome.IsSuccess, Is.False);
|
||||
Assert.That(outcome.Error, Is.EqualTo(CreateError.WouldExceedMaxCopies));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Create_at_boundary_2_to_3_succeeds()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
await factory.SeedOwnedCardAsync(viewerId, cardId: 10001001L, count: 2, craftCost: 200);
|
||||
await factory.SetRedEtherAsync(viewerId, 1_000UL);
|
||||
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var repo = scope.ServiceProvider.GetRequiredService<ICardInventoryRepository>();
|
||||
|
||||
// 2 + 1 = 3 = MaxCopies — must succeed
|
||||
var outcome = await repo.CreateCards(viewerId, new Dictionary<long, int> { { 10001001L, 1 } });
|
||||
|
||||
Assert.That(outcome.IsSuccess, Is.True);
|
||||
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
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).Count, Is.EqualTo(3));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Create_rejects_insufficient_vials_without_mutation()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
await factory.SeedOwnedCardAsync(viewerId, cardId: 10001001L, count: 0, craftCost: 200);
|
||||
await factory.SetRedEtherAsync(viewerId, 199UL); // one less than needed
|
||||
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var repo = scope.ServiceProvider.GetRequiredService<ICardInventoryRepository>();
|
||||
|
||||
var outcome = await repo.CreateCards(viewerId, new Dictionary<long, int> { { 10001001L, 1 } });
|
||||
|
||||
Assert.That(outcome.IsSuccess, Is.False);
|
||||
Assert.That(outcome.Error, Is.EqualTo(CreateError.InsufficientVials));
|
||||
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
var viewer = await db.Viewers.Include(v => v.Cards).ThenInclude(c => c.Card).FirstAsync(v => v.Id == viewerId);
|
||||
Assert.That(viewer.Currency.RedEther, Is.EqualTo(199UL), "no debit on rejection");
|
||||
Assert.That(viewer.Cards.First(c => c.Card.Id == 10001001L).Count, Is.EqualTo(0), "no grant on rejection");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Create_validates_full_batch_before_mutating()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
await factory.SeedOwnedCardAsync(viewerId, cardId: 10001001L, count: 0, craftCost: 200);
|
||||
// Second card has count=3 already — adding any would exceed MaxCopies
|
||||
await factory.SeedOwnedCardAsync(viewerId, cardId: 10001002L, count: 3, craftCost: 200);
|
||||
await factory.SetRedEtherAsync(viewerId, 1_000UL);
|
||||
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var repo = scope.ServiceProvider.GetRequiredService<ICardInventoryRepository>();
|
||||
|
||||
var outcome = await repo.CreateCards(viewerId, new Dictionary<long, int>
|
||||
{
|
||||
{ 10001001L, 1 }, // would-be valid
|
||||
{ 10001002L, 1 }, // would push to 4 — fails validation
|
||||
});
|
||||
|
||||
Assert.That(outcome.IsSuccess, Is.False);
|
||||
Assert.That(outcome.Error, Is.EqualTo(CreateError.WouldExceedMaxCopies));
|
||||
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
var viewer = await db.Viewers.Include(v => v.Cards).ThenInclude(c => c.Card).FirstAsync(v => v.Id == viewerId);
|
||||
Assert.That(viewer.Currency.RedEther, Is.EqualTo(1_000UL), "no debit when batch fails");
|
||||
Assert.That(viewer.Cards.First(c => c.Card.Id == 10001001L).Count, Is.EqualTo(0), "valid card untouched");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Create_first_time_owner_triggers_cosmetic_cascade()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
// Set up a card with a cosmetic-cascade row pointing at a Skin the viewer doesn't own.
|
||||
// Use ids outside the seeded 10001001–10001003 range so the cascade can't accidentally
|
||||
// pick up unrelated rows.
|
||||
const long cardId = 999_003_010L;
|
||||
const long skinId = 999_003_011L;
|
||||
ctx.Cards.Add(new ShadowverseCardEntry
|
||||
{
|
||||
Id = cardId, Name = "CreateCascadeCard", Rarity = Rarity.Gold,
|
||||
CollectionInfo = new CardCollectionInfo { CraftCost = 800, DustReward = 200 },
|
||||
});
|
||||
ctx.LeaderSkins.Add(new LeaderSkinEntry { Id = (int)skinId, Name = "CreateCascadeSkin" });
|
||||
ctx.CardCosmeticRewards.Add(new CardCosmeticReward
|
||||
{
|
||||
CardId = cardId, Type = CosmeticType.Skin, CosmeticId = skinId, Quantity = 1,
|
||||
});
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
// Give the viewer enough RedEther in a separate scope so the helper's reset doesn't fire.
|
||||
await factory.SetRedEtherAsync(viewerId, 1_000UL);
|
||||
|
||||
var repo = scope.ServiceProvider.GetRequiredService<ICardInventoryRepository>();
|
||||
var outcome = await repo.CreateCards(viewerId, new Dictionary<long, int> { { cardId, 1 } });
|
||||
|
||||
Assert.That(outcome.IsSuccess, Is.True);
|
||||
|
||||
var grants = outcome.Result!.Grants;
|
||||
// One Card grant + one Skin cascade grant
|
||||
Assert.That(grants.Any(g => g.RewardType == (int)UserGoodsType.Card && g.RewardId == cardId), Is.True);
|
||||
Assert.That(grants.Any(g => g.RewardType == (int)UserGoodsType.Skin && g.RewardId == skinId), Is.True);
|
||||
|
||||
var viewer = await ctx.Viewers
|
||||
.Include(v => v.LeaderSkins)
|
||||
.FirstAsync(v => v.Id == viewerId);
|
||||
Assert.That(viewer.LeaderSkins.Any(s => s.Id == (int)skinId), Is.True, "cascade actually granted the skin");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user