refactor(inventory): delete old primitives after InventoryService cutover
Removed RewardGrantService, CurrencySpendService, ICurrencySpendService, ViewerEntitlements, IViewerEntitlements, CardAcquisitionService, ICardAcquisitionService, CardGrantResult and their tests (RewardGrantServiceTests, CurrencySpendServiceTests, CardAcquisitionServiceTests, ViewerEntitlementsTests). Removed four DI registrations from Program.cs. No caller references any deleted type; GrantedReward and EffectiveCosmetics were pre-moved to InventoryGrantTypes.cs in the prior commit. Build clean, 712/712 tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,365 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Enums;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.EmulatedEntrypoint.Services;
|
||||
using SVSim.UnitTests.Infrastructure;
|
||||
|
||||
namespace SVSim.UnitTests.Services;
|
||||
|
||||
public class CardAcquisitionServiceTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Seeds a viewer (via the factory's real RegisterViewer-backed helper) and gives it the
|
||||
/// given owned cards (key = card_id, value = count). Card rows are created on-demand if
|
||||
/// the test's card_id isn't already in the minimal seeded card set (matches the pattern
|
||||
/// used by SVSimTestFactory.SeedOwnedCardAsync, but inlined so multiple cards can be
|
||||
/// seeded in one viewer in one call). Returns the viewer's Id.
|
||||
/// </summary>
|
||||
private static async Task<long> SeedViewerWithCards(
|
||||
SVSimTestFactory factory,
|
||||
Dictionary<long, int> ownedCards,
|
||||
IEnumerable<long>? grantableCardIds = null)
|
||||
{
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
var viewer = await db.Viewers.Include(v => v.Cards).ThenInclude(c => c.Card)
|
||||
.FirstAsync(v => v.Id == viewerId);
|
||||
|
||||
foreach (var (cardId, count) in ownedCards)
|
||||
{
|
||||
var card = await EnsureCardAsync(db, cardId);
|
||||
viewer.Cards.Add(new OwnedCardEntry { Card = card, Count = count, IsProtected = false });
|
||||
}
|
||||
// Pre-seed bare Cards rows (no ownership) for any cardIds the test plans to grant via
|
||||
// the service. RewardGrantService.ApplyAsync does FirstOrDefaultAsync on _db.Cards;
|
||||
// without the row the grant throws InvalidOperationException("Card {id} not in catalog").
|
||||
if (grantableCardIds is not null)
|
||||
{
|
||||
foreach (var cardId in grantableCardIds)
|
||||
{
|
||||
await EnsureCardAsync(db, cardId);
|
||||
}
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
return viewerId;
|
||||
}
|
||||
|
||||
private static async Task<ShadowverseCardEntry> EnsureCardAsync(SVSimDbContext db, long cardId)
|
||||
{
|
||||
var card = await db.Cards.FirstOrDefaultAsync(c => c.Id == cardId);
|
||||
if (card is null)
|
||||
{
|
||||
// Foil twins follow the universal +1 convention (card_id ends in 1). Marking
|
||||
// IsFoil here keeps test setup tidy so foil-resolution tests don't have to
|
||||
// hand-patch the card row.
|
||||
var isFoil = cardId % 10 == 1;
|
||||
card = new ShadowverseCardEntry { Id = cardId, Name = $"SeededCard{cardId}", Rarity = Database.Enums.Rarity.Bronze, IsFoil = isFoil };
|
||||
db.Cards.Add(card);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
return card;
|
||||
}
|
||||
|
||||
private static ICardAcquisitionService GetService(SVSimTestFactory factory)
|
||||
{
|
||||
var scope = factory.Services.CreateScope();
|
||||
return scope.ServiceProvider.GetRequiredService<ICardAcquisitionService>();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GrantManyAsync_NewBronzeCard_GrantsCardOnly()
|
||||
{
|
||||
// 101111010 is a synthetic test card (inserted ad-hoc via grantableCardIds) with no
|
||||
// CardCosmeticReward associations. Expectation: grant returns only the type=5 entry.
|
||||
using var factory = new SVSimTestFactory();
|
||||
var viewerId = await SeedViewerWithCards(factory, new() { }, grantableCardIds: new[] { 101111010L });
|
||||
|
||||
var service = GetService(factory);
|
||||
var result = await service.GrantManyAsync(viewerId, new[] { 101111010L });
|
||||
|
||||
Assert.That(result.RewardList, Has.Count.EqualTo(1));
|
||||
Assert.That(result.RewardList[0].RewardType, Is.EqualTo(5)); // Card
|
||||
Assert.That(result.RewardList[0].RewardId, Is.EqualTo(101111010L));
|
||||
Assert.That(result.RewardList[0].RewardNum, Is.EqualTo(1)); // post-state count
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GrantManyAsync_LeaderCard_GrantsCardAndSkin()
|
||||
{
|
||||
// Card 704741010 (Aria leader-card variant) has 3 cosmetic associations in the seed:
|
||||
// skin 407, sleeve 704741010, emblem 704741010.
|
||||
using var factory = new SVSimTestFactory();
|
||||
var viewerId = await SeedViewerWithCards(factory, new() { }, grantableCardIds: new[] { 704741010L });
|
||||
|
||||
// Since SqliteFriendlyModelCustomizer strips CardCosmeticReward seed in tests, insert
|
||||
// the specific mappings we need for this test.
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
db.CardCosmeticRewards.AddRange(
|
||||
new CardCosmeticReward { CardId = 704741010L, Type = CosmeticType.Skin, CosmeticId = 407L, Quantity = 1 },
|
||||
new CardCosmeticReward { CardId = 704741010L, Type = CosmeticType.Sleeve, CosmeticId = 704741010L, Quantity = 1 },
|
||||
new CardCosmeticReward { CardId = 704741010L, Type = CosmeticType.Emblem, CosmeticId = 704741010L, Quantity = 1 }
|
||||
);
|
||||
// Ensure master rows exist for the cosmetics we'll grant
|
||||
if (await db.LeaderSkins.FindAsync(407) is null)
|
||||
db.LeaderSkins.Add(new LeaderSkinEntry { Id = 407, Name = "TestSkin407" });
|
||||
if (await db.Sleeves.FindAsync(704741010) is null)
|
||||
db.Sleeves.Add(new SleeveEntry { Id = 704741010 });
|
||||
if (await db.Emblems.FindAsync(704741010) is null)
|
||||
db.Emblems.Add(new EmblemEntry { Id = 704741010 });
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var service = GetService(factory);
|
||||
var result = await service.GrantManyAsync(viewerId, new[] { 704741010L });
|
||||
|
||||
var skinEntry = result.RewardList.SingleOrDefault(r => r.RewardType == 10);
|
||||
Assert.That(skinEntry, Is.Not.Null, "expected a Skin reward entry");
|
||||
Assert.That(skinEntry!.RewardId, Is.EqualTo(407L));
|
||||
|
||||
// Verify viewer ownership was actually written to DB
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
var viewer = await db.Viewers
|
||||
.Include(v => v.LeaderSkins)
|
||||
.FirstAsync(v => v.Id == viewerId);
|
||||
Assert.That(viewer.LeaderSkins.Any(s => s.Id == 407), Is.True);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GrantManyAsync_AlreadyOwnedSkin_OmitsFromRewardList()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
var viewerId = await SeedViewerWithCards(factory, new() { }, grantableCardIds: new[] { 704741010L });
|
||||
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
// Pre-grant the skin to this viewer
|
||||
var viewer = await db.Viewers.Include(v => v.LeaderSkins).FirstAsync(v => v.Id == viewerId);
|
||||
var skin = await db.LeaderSkins.FindAsync(407) ?? db.LeaderSkins.Add(new LeaderSkinEntry { Id = 407, Name = "TestSkin407" }).Entity;
|
||||
if (!viewer.LeaderSkins.Any(s => s.Id == 407))
|
||||
viewer.LeaderSkins.Add(skin);
|
||||
// Seed the card→skin mapping
|
||||
db.CardCosmeticRewards.Add(new CardCosmeticReward { CardId = 704741010L, Type = CosmeticType.Skin, CosmeticId = 407L, Quantity = 1 });
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var service = GetService(factory);
|
||||
var result = await service.GrantManyAsync(viewerId, new[] { 704741010L });
|
||||
|
||||
Assert.That(result.RewardList.Any(r => r.RewardType == 10), Is.False,
|
||||
"skin entry should be omitted since viewer already owns it");
|
||||
Assert.That(result.RewardList.Any(r => r.RewardType == 5 && r.RewardId == 704741010L), Is.True,
|
||||
"card grant entry should still be emitted");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GrantManyAsync_FoilLeaderCard_ResolvesToNonFoilCosmetics()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
var viewerId = await SeedViewerWithCards(factory, new() { }, grantableCardIds: new[] { 704741011L });
|
||||
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
// CardCosmeticReward.CardId has a FK→Cards.Id; ensure the non-foil row exists
|
||||
// even though we never grant it directly (the foil twin is the granted card).
|
||||
await EnsureCardAsync(db, 704741010L);
|
||||
// Map cosmetics to the NON-FOIL card_id (704741010), as the seed convention requires
|
||||
db.CardCosmeticRewards.Add(new CardCosmeticReward { CardId = 704741010L, Type = CosmeticType.Skin, CosmeticId = 407L, Quantity = 1 });
|
||||
if (await db.LeaderSkins.FindAsync(407) is null)
|
||||
db.LeaderSkins.Add(new LeaderSkinEntry { Id = 407, Name = "TestSkin407" });
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var service = GetService(factory);
|
||||
var result = await service.GrantManyAsync(viewerId, new[] { 704741011L });
|
||||
|
||||
var skinEntry = result.RewardList.SingleOrDefault(r => r.RewardType == 10);
|
||||
Assert.That(skinEntry, Is.Not.Null, "expected skin entry via foil resolution");
|
||||
Assert.That(skinEntry!.RewardId, Is.EqualTo(407L));
|
||||
|
||||
using var scope2 = factory.Services.CreateScope();
|
||||
var db2 = scope2.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
var viewer = await db2.Viewers.Include(v => v.Cards).ThenInclude(c => c.Card).Include(v => v.LeaderSkins).FirstAsync(v => v.Id == viewerId);
|
||||
Assert.That(viewer.Cards.Any(c => c.Card.Id == 704741011L), Is.True, "card is the foil");
|
||||
Assert.That(viewer.LeaderSkins.Any(s => s.Id == 407), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GrantManyAsync_MultipleCopiesOfSameLeader_GrantsCosmeticOnce()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
var viewerId = await SeedViewerWithCards(factory, new() { }, grantableCardIds: new[] { 704741010L });
|
||||
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
db.CardCosmeticRewards.Add(new CardCosmeticReward { CardId = 704741010L, Type = CosmeticType.Skin, CosmeticId = 407L, Quantity = 1 });
|
||||
if (await db.LeaderSkins.FindAsync(407) is null)
|
||||
db.LeaderSkins.Add(new LeaderSkinEntry { Id = 407, Name = "TestSkin407" });
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var service = GetService(factory);
|
||||
var result = await service.GrantManyAsync(viewerId, new[] { 704741010L, 704741010L, 704741010L });
|
||||
|
||||
Assert.That(result.RewardList.Count(r => r.RewardType == 10), Is.EqualTo(1),
|
||||
"skin should appear exactly once in reward_list");
|
||||
var cardEntry = result.RewardList.Single(r => r.RewardType == 5 && r.RewardId == 704741010L);
|
||||
Assert.That(cardEntry.RewardNum, Is.EqualTo(3), "card count should reflect all 3 copies");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GrantManyAsync_RecentLeaderCard_GrantsAllFiveCosmeticTypes()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
var viewerId = await SeedViewerWithCards(factory, new() { }, grantableCardIds: new[] { 721141010L });
|
||||
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
// All 5 cosmetic types for this card. Exact ids: from data_dumps captures.
|
||||
db.CardCosmeticRewards.AddRange(
|
||||
new CardCosmeticReward { CardId = 721141010L, Type = CosmeticType.Sleeve, CosmeticId = 721141010L, Quantity = 1 },
|
||||
new CardCosmeticReward { CardId = 721141010L, Type = CosmeticType.Emblem, CosmeticId = 721141010L, Quantity = 1 },
|
||||
new CardCosmeticReward { CardId = 721141010L, Type = CosmeticType.Degree, CosmeticId = 120021L, Quantity = 1 },
|
||||
new CardCosmeticReward { CardId = 721141010L, Type = CosmeticType.Skin, CosmeticId = 4601L, Quantity = 1 },
|
||||
new CardCosmeticReward { CardId = 721141010L, Type = CosmeticType.MyPageBG, CosmeticId = 721141010L, Quantity = 1 }
|
||||
);
|
||||
// Ensure master rows
|
||||
if (await db.LeaderSkins.FindAsync(4601) is null)
|
||||
db.LeaderSkins.Add(new LeaderSkinEntry { Id = 4601, Name = "TestSkin4601" });
|
||||
if (await db.Sleeves.FindAsync(721141010) is null)
|
||||
db.Sleeves.Add(new SleeveEntry { Id = 721141010 });
|
||||
if (await db.Emblems.FindAsync(721141010) is null)
|
||||
db.Emblems.Add(new EmblemEntry { Id = 721141010 });
|
||||
if (await db.Degrees.FindAsync(120021) is null)
|
||||
db.Degrees.Add(new DegreeEntry { Id = 120021 });
|
||||
if (await db.MyPageBackgrounds.FindAsync(721141010) is null)
|
||||
db.MyPageBackgrounds.Add(new MyPageBackgroundEntry { Id = 721141010 });
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var service = GetService(factory);
|
||||
var result = await service.GrantManyAsync(viewerId, new[] { 721141010L });
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(result.RewardList.Any(r => r.RewardType == 6), Is.True, "Sleeve");
|
||||
Assert.That(result.RewardList.Any(r => r.RewardType == 7), Is.True, "Emblem");
|
||||
Assert.That(result.RewardList.Any(r => r.RewardType == 8), Is.True, "Degree");
|
||||
Assert.That(result.RewardList.Any(r => r.RewardType == 10), Is.True, "Skin");
|
||||
Assert.That(result.RewardList.Any(r => r.RewardType == 15), Is.True, "MyPageBG");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task BackfillCosmeticsAsync_DoesNotIncrementCardCount()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
// Pre-seed viewer with card 704741010 count=5, no skin
|
||||
var viewerId = await SeedViewerWithCards(factory, new() { [704741010L] = 5 });
|
||||
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
db.CardCosmeticRewards.Add(new CardCosmeticReward { CardId = 704741010L, Type = CosmeticType.Skin, CosmeticId = 407L, Quantity = 1 });
|
||||
if (await db.LeaderSkins.FindAsync(407) is null)
|
||||
db.LeaderSkins.Add(new LeaderSkinEntry { Id = 407, Name = "TestSkin407" });
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var service = GetService(factory);
|
||||
var result = await service.BackfillCosmeticsAsync(viewerId);
|
||||
|
||||
using var scope2 = factory.Services.CreateScope();
|
||||
var db2 = scope2.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
var viewer = await db2.Viewers.Include(v => v.Cards).ThenInclude(c => c.Card).Include(v => v.LeaderSkins).FirstAsync(v => v.Id == viewerId);
|
||||
var owned = viewer.Cards.Single(c => c.Card.Id == 704741010L);
|
||||
|
||||
Assert.That(owned.Count, Is.EqualTo(5), "card count should be unchanged in backfill mode");
|
||||
Assert.That(viewer.LeaderSkins.Any(s => s.Id == 407), Is.True, "skin should be backfilled");
|
||||
Assert.That(result.RewardList.Any(r => r.RewardType == 10 && r.RewardId == 407L), Is.True,
|
||||
"skin entry returned even in backfill mode");
|
||||
Assert.That(result.RewardList.Any(r => r.RewardType == 5), Is.False,
|
||||
"no type=5 card entries in backfill mode");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task BackfillCosmeticsAsync_CalledTwice_SecondCallIsNoOp()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
var viewerId = await SeedViewerWithCards(factory, new() { [704741010L] = 1 });
|
||||
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
db.CardCosmeticRewards.Add(new CardCosmeticReward { CardId = 704741010L, Type = CosmeticType.Skin, CosmeticId = 407L, Quantity = 1 });
|
||||
if (await db.LeaderSkins.FindAsync(407) is null)
|
||||
db.LeaderSkins.Add(new LeaderSkinEntry { Id = 407, Name = "TestSkin407" });
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var service = GetService(factory);
|
||||
var first = await service.BackfillCosmeticsAsync(viewerId);
|
||||
var second = await service.BackfillCosmeticsAsync(viewerId);
|
||||
|
||||
Assert.That(first.RewardList, Is.Not.Empty, "first call should grant cosmetics");
|
||||
Assert.That(second.RewardList, Is.Empty, "second call should be a no-op");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GrantManyAsync_LeaderCardWithMissingMapping_GrantsCardSilently()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
var viewerId = await SeedViewerWithCards(factory, new() { }, grantableCardIds: new[] { 701141010L });
|
||||
|
||||
// NO CardCosmeticReward rows inserted for this card — simulates the 83 missing-mapping cases.
|
||||
|
||||
var service = GetService(factory);
|
||||
var result = await service.GrantManyAsync(viewerId, new[] { 701141010L });
|
||||
|
||||
Assert.That(result.RewardList.Any(r => r.RewardType == 5 && r.RewardId == 701141010L), Is.True);
|
||||
Assert.That(result.RewardList.Any(r => r.RewardType == 10), Is.False);
|
||||
// No exception means it handled the missing mapping gracefully.
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GrantManyAsync_OrphanCosmeticReward_LogsWarningAndSkips()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
var viewerId = await SeedViewerWithCards(factory, new() { }, grantableCardIds: new[] { 704741010L });
|
||||
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
// Real skin association
|
||||
db.CardCosmeticRewards.Add(new CardCosmeticReward { CardId = 704741010L, Type = CosmeticType.Skin, CosmeticId = 407L, Quantity = 1 });
|
||||
if (await db.LeaderSkins.FindAsync(407) is null)
|
||||
db.LeaderSkins.Add(new LeaderSkinEntry { Id = 407, Name = "TestSkin407" });
|
||||
|
||||
// ORPHAN: points to non-existent skin_id
|
||||
db.CardCosmeticRewards.Add(new CardCosmeticReward { CardId = 704741010L, Type = CosmeticType.Skin, CosmeticId = 9999999L, Quantity = 1 });
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var service = GetService(factory);
|
||||
var result = await service.GrantManyAsync(viewerId, new[] { 704741010L });
|
||||
|
||||
Assert.That(result.RewardList.Any(r => r.RewardType == 5 && r.RewardId == 704741010L), Is.True);
|
||||
Assert.That(result.RewardList.Any(r => r.RewardType == 10 && r.RewardId == 407L), Is.True,
|
||||
"real skin should still be granted");
|
||||
Assert.That(result.RewardList.Any(r => r.RewardType == 10 && r.RewardId == 9999999L), Is.False,
|
||||
"orphan cosmetic should not appear in reward_list");
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
using SVSim.Database.Enums;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.Database.Services;
|
||||
|
||||
namespace SVSim.UnitTests.Services;
|
||||
|
||||
public class CurrencySpendServiceTests
|
||||
{
|
||||
private sealed class FakeEntitlements : IViewerEntitlements
|
||||
{
|
||||
public bool IsFreeplay { get; init; }
|
||||
public long FreeplayAmount { get; init; } = 99999;
|
||||
|
||||
public long EffectiveBalance(Viewer viewer, SpendCurrency currency)
|
||||
{
|
||||
if (IsFreeplay && currency != SpendCurrency.SpotPoint) return FreeplayAmount;
|
||||
return currency switch
|
||||
{
|
||||
SpendCurrency.Crystal => (long)viewer.Currency.Crystals,
|
||||
SpendCurrency.Rupee => (long)viewer.Currency.Rupees,
|
||||
SpendCurrency.RedEther => (long)viewer.Currency.RedEther,
|
||||
SpendCurrency.SpotPoint => (long)viewer.Currency.SpotPoints,
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
public bool OwnsCard(Viewer viewer, long cardId) => IsFreeplay;
|
||||
public bool OwnsCosmetic(Viewer viewer, CosmeticType type, int id) => IsFreeplay;
|
||||
public Task<IReadOnlyList<OwnedCardEntry>> EffectiveOwnedCardsAsync(Viewer viewer, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<OwnedCardEntry>>(new List<OwnedCardEntry>());
|
||||
public Task<EffectiveCosmetics> EffectiveCosmeticsAsync(Viewer viewer, CancellationToken ct = default)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
|
||||
private static Viewer NewViewer() => new() { Currency = new ViewerCurrency() };
|
||||
|
||||
[Test]
|
||||
public async Task Normal_deducts_and_returns_post_state()
|
||||
{
|
||||
var v = NewViewer();
|
||||
v.Currency.Crystals = 250;
|
||||
var svc = new CurrencySpendService(new FakeEntitlements { IsFreeplay = false });
|
||||
|
||||
var r = await svc.TrySpendAsync(v, SpendCurrency.Crystal, 100);
|
||||
|
||||
Assert.That(r.Success, Is.True);
|
||||
Assert.That(r.PostStateTotal, Is.EqualTo(150));
|
||||
Assert.That(v.Currency.Crystals, Is.EqualTo(150UL));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Normal_insufficient_does_not_deduct()
|
||||
{
|
||||
var v = NewViewer();
|
||||
v.Currency.Rupees = 50;
|
||||
var svc = new CurrencySpendService(new FakeEntitlements { IsFreeplay = false });
|
||||
|
||||
var r = await svc.TrySpendAsync(v, SpendCurrency.Rupee, 100);
|
||||
|
||||
Assert.That(r.Success, Is.False);
|
||||
Assert.That(r.Outcome, Is.EqualTo(SpendOutcome.Insufficient));
|
||||
Assert.That(v.Currency.Rupees, Is.EqualTo(50UL), "no deduction on insufficient funds");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Freeplay_main_currency_succeeds_without_deducting()
|
||||
{
|
||||
var v = NewViewer();
|
||||
v.Currency.Crystals = 10;
|
||||
var svc = new CurrencySpendService(new FakeEntitlements { IsFreeplay = true });
|
||||
|
||||
var r = await svc.TrySpendAsync(v, SpendCurrency.Crystal, 100000);
|
||||
|
||||
Assert.That(r.Success, Is.True, "freeplay never blocks on affordability");
|
||||
Assert.That(r.PostStateTotal, Is.EqualTo(99999), "post-state shows the freeplay balance");
|
||||
Assert.That(v.Currency.Crystals, Is.EqualTo(10UL), "DB balance untouched in freeplay");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Freeplay_spot_points_still_deduct()
|
||||
{
|
||||
var v = NewViewer();
|
||||
v.Currency.SpotPoints = 300;
|
||||
var svc = new CurrencySpendService(new FakeEntitlements { IsFreeplay = true });
|
||||
|
||||
var r = await svc.TrySpendAsync(v, SpendCurrency.SpotPoint, 100);
|
||||
|
||||
Assert.That(r.Success, Is.True);
|
||||
Assert.That(r.PostStateTotal, Is.EqualTo(200));
|
||||
Assert.That(v.Currency.SpotPoints, Is.EqualTo(200UL), "spot points are real even in freeplay");
|
||||
}
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Enums;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.Database.Services;
|
||||
using SVSim.UnitTests.Infrastructure;
|
||||
|
||||
namespace SVSim.UnitTests.Services;
|
||||
|
||||
public class RewardGrantServiceTests
|
||||
{
|
||||
[Test]
|
||||
public async Task Sleeve_added_to_viewer_collection()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
const int testSleeveId = 2_000_000_000;
|
||||
var sleeve = new SleeveEntry { Id = testSleeveId };
|
||||
ctx.Sleeves.Add(sleeve);
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
var viewer = await ctx.Viewers.Include(v => v.Sleeves).FirstAsync(v => v.Id == viewerId);
|
||||
var svc = scope.ServiceProvider.GetRequiredService<RewardGrantService>();
|
||||
|
||||
var result = await svc.ApplyAsync(viewer, UserGoodsType.Sleeve, detailId: testSleeveId, num: 1);
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
Assert.That(result, Has.Count.EqualTo(1));
|
||||
Assert.That(viewer.Sleeves.Any(s => s.Id == testSleeveId), Is.True);
|
||||
Assert.That(result[0].RewardType, Is.EqualTo((int)UserGoodsType.Sleeve));
|
||||
Assert.That(result[0].RewardId, Is.EqualTo((long)testSleeveId));
|
||||
Assert.That(result[0].RewardNum, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Rupy_sets_currency_post_state_total()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
var viewer = await ctx.Viewers.FirstAsync(v => v.Id == viewerId);
|
||||
viewer.Currency.Rupees = 100UL;
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
var svc = scope.ServiceProvider.GetRequiredService<RewardGrantService>();
|
||||
|
||||
var result = await svc.ApplyAsync(viewer, UserGoodsType.Rupy, detailId: 0, num: 50);
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
Assert.That(viewer.Currency.Rupees, Is.EqualTo(150UL));
|
||||
Assert.That(result, Has.Count.EqualTo(1));
|
||||
Assert.That(result[0].RewardNum, Is.EqualTo(150));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task LeaderSkin_added_idempotently()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
const int testSkinId = 9_999_999;
|
||||
ctx.LeaderSkins.Add(new LeaderSkinEntry { Id = testSkinId, Name = "Round1Reward" });
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
var viewer = await ctx.Viewers.Include(v => v.LeaderSkins).FirstAsync(v => v.Id == viewerId);
|
||||
var svc = scope.ServiceProvider.GetRequiredService<RewardGrantService>();
|
||||
|
||||
await svc.ApplyAsync(viewer, UserGoodsType.Skin, testSkinId, 1);
|
||||
await svc.ApplyAsync(viewer, UserGoodsType.Skin, testSkinId, 1);
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
Assert.That(viewer.LeaderSkins.Count(s => s.Id == testSkinId), Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Card_fresh_grant_inserts_owned_entry_and_returns_post_state_count()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
const long testCardId = 999_001_001L;
|
||||
ctx.Cards.Add(new ShadowverseCardEntry { Id = testCardId, Name = "RGSTestCard", Rarity = Rarity.Bronze });
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
var viewer = await ctx.Viewers.Include(v => v.Cards).ThenInclude(c => c.Card).FirstAsync(v => v.Id == viewerId);
|
||||
var svc = scope.ServiceProvider.GetRequiredService<RewardGrantService>();
|
||||
|
||||
var result = await svc.ApplyAsync(viewer, UserGoodsType.Card, testCardId, 1);
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
Assert.That(result, Has.Count.EqualTo(1));
|
||||
Assert.That(result[0].RewardType, Is.EqualTo((int)UserGoodsType.Card));
|
||||
Assert.That(result[0].RewardId, Is.EqualTo(testCardId));
|
||||
Assert.That(result[0].RewardNum, Is.EqualTo(1));
|
||||
Assert.That(viewer.Cards.Single(c => c.Card.Id == testCardId).Count, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Card_existing_grant_increments_count()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
const long testCardId = 999_001_002L;
|
||||
var card = new ShadowverseCardEntry { Id = testCardId, Name = "RGSTestCard2", Rarity = Rarity.Bronze };
|
||||
ctx.Cards.Add(card);
|
||||
var viewer = await ctx.Viewers.Include(v => v.Cards).ThenInclude(c => c.Card).FirstAsync(v => v.Id == viewerId);
|
||||
viewer.Cards.Add(new OwnedCardEntry { Card = card, Count = 2, IsProtected = false });
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
var svc = scope.ServiceProvider.GetRequiredService<RewardGrantService>();
|
||||
var result = await svc.ApplyAsync(viewer, UserGoodsType.Card, testCardId, 1);
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
Assert.That(result, Has.Count.EqualTo(1));
|
||||
Assert.That(result[0].RewardNum, Is.EqualTo(3));
|
||||
Assert.That(viewer.Cards.Single(c => c.Card.Id == testCardId).Count, Is.EqualTo(3));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Card_with_cascade_rows_emits_card_plus_cosmetics()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
const long testCardId = 999_002_010L;
|
||||
const int testSkinId = 999_002_011;
|
||||
ctx.Cards.Add(new ShadowverseCardEntry { Id = testCardId, Name = "CascadeTestCard", Rarity = Rarity.Gold });
|
||||
ctx.LeaderSkins.Add(new LeaderSkinEntry { Id = testSkinId, Name = "CascadeTestSkin" });
|
||||
ctx.CardCosmeticRewards.Add(new CardCosmeticReward
|
||||
{
|
||||
CardId = testCardId, Type = CosmeticType.Skin, CosmeticId = testSkinId, Quantity = 1,
|
||||
});
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
var viewer = await ctx.Viewers
|
||||
.Include(v => v.Cards).ThenInclude(c => c.Card)
|
||||
.Include(v => v.LeaderSkins)
|
||||
.AsSplitQuery()
|
||||
.FirstAsync(v => v.Id == viewerId);
|
||||
|
||||
var svc = scope.ServiceProvider.GetRequiredService<RewardGrantService>();
|
||||
var result = await svc.ApplyAsync(viewer, UserGoodsType.Card, testCardId, 1);
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
Assert.That(result, Has.Count.EqualTo(2));
|
||||
Assert.That(result.Any(r => r.RewardType == (int)UserGoodsType.Card && r.RewardId == testCardId), Is.True);
|
||||
Assert.That(result.Any(r => r.RewardType == (int)UserGoodsType.Skin && r.RewardId == testSkinId), Is.True);
|
||||
Assert.That(viewer.LeaderSkins.Any(s => s.Id == testSkinId), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Card_cascade_skips_already_owned_cosmetic()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
const long testCardId = 999_002_020L;
|
||||
const int testSkinId = 999_002_021;
|
||||
ctx.Cards.Add(new ShadowverseCardEntry { Id = testCardId, Name = "CascadeOwnedTestCard", Rarity = Rarity.Gold });
|
||||
var skin = new LeaderSkinEntry { Id = testSkinId, Name = "CascadeOwnedTestSkin" };
|
||||
ctx.LeaderSkins.Add(skin);
|
||||
ctx.CardCosmeticRewards.Add(new CardCosmeticReward
|
||||
{
|
||||
CardId = testCardId, Type = CosmeticType.Skin, CosmeticId = testSkinId, Quantity = 1,
|
||||
});
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
var viewer = await ctx.Viewers
|
||||
.Include(v => v.Cards).ThenInclude(c => c.Card)
|
||||
.Include(v => v.LeaderSkins)
|
||||
.AsSplitQuery()
|
||||
.FirstAsync(v => v.Id == viewerId);
|
||||
viewer.LeaderSkins.Add(skin);
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
var svc = scope.ServiceProvider.GetRequiredService<RewardGrantService>();
|
||||
var result = await svc.ApplyAsync(viewer, UserGoodsType.Card, testCardId, 1);
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
Assert.That(result, Has.Count.EqualTo(1));
|
||||
Assert.That(result[0].RewardType, Is.EqualTo((int)UserGoodsType.Card));
|
||||
Assert.That(result[0].RewardId, Is.EqualTo(testCardId));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Card_foil_grant_resolves_cascade_to_non_foil_id()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
const long nonFoilId = 999_002_030L;
|
||||
const long foilId = 999_002_031L;
|
||||
const int testSkinId = 999_002_032;
|
||||
|
||||
ctx.Cards.Add(new ShadowverseCardEntry { Id = nonFoilId, Name = "FoilCascadeBase", Rarity = Rarity.Gold });
|
||||
ctx.Cards.Add(new ShadowverseCardEntry { Id = foilId, Name = "FoilCascadeFoil", Rarity = Rarity.Gold, IsFoil = true });
|
||||
ctx.LeaderSkins.Add(new LeaderSkinEntry { Id = testSkinId, Name = "FoilCascadeSkin" });
|
||||
ctx.CardCosmeticRewards.Add(new CardCosmeticReward
|
||||
{
|
||||
CardId = nonFoilId, Type = CosmeticType.Skin, CosmeticId = testSkinId, Quantity = 1,
|
||||
});
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
var viewer = await ctx.Viewers
|
||||
.Include(v => v.Cards).ThenInclude(c => c.Card)
|
||||
.Include(v => v.LeaderSkins)
|
||||
.AsSplitQuery()
|
||||
.FirstAsync(v => v.Id == viewerId);
|
||||
|
||||
var svc = scope.ServiceProvider.GetRequiredService<RewardGrantService>();
|
||||
var result = await svc.ApplyAsync(viewer, UserGoodsType.Card, foilId, 1);
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
Assert.That(result.Any(r => r.RewardType == (int)UserGoodsType.Card && r.RewardId == foilId), Is.True);
|
||||
Assert.That(result.Any(r => r.RewardType == (int)UserGoodsType.Skin && r.RewardId == testSkinId), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SpotCard_still_throws_NotSupported()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
var viewer = await ctx.Viewers.FirstAsync(v => v.Id == viewerId);
|
||||
var svc = scope.ServiceProvider.GetRequiredService<RewardGrantService>();
|
||||
|
||||
Assert.ThrowsAsync<NotSupportedException>(async () =>
|
||||
await svc.ApplyAsync(viewer, UserGoodsType.SpotCard, 1L, 1));
|
||||
Assert.ThrowsAsync<NotSupportedException>(async () =>
|
||||
await svc.ApplyAsync(viewer, UserGoodsType.SpotCardOnlyLatestCardPack, 1L, 1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task OwnedCardEntry_unique_index_blocks_duplicate_viewer_card_row()
|
||||
{
|
||||
// Schema-level safety net: any code that forgets to .Include(v => v.Cards) before doing
|
||||
// a find-or-add OwnedCardEntry would silently insert a duplicate row otherwise. The
|
||||
// unique index on (ViewerId, CardId) makes that crash loudly at SaveChanges instead.
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
const long testCardId = 999_003_001L;
|
||||
var card = new ShadowverseCardEntry { Id = testCardId, Name = "UniqueIdxTest", Rarity = Rarity.Bronze };
|
||||
ctx.Cards.Add(card);
|
||||
var viewer = await ctx.Viewers.Include(v => v.Cards).ThenInclude(c => c.Card).FirstAsync(v => v.Id == viewerId);
|
||||
viewer.Cards.Add(new OwnedCardEntry { Card = card, Count = 1, IsProtected = false });
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
// Simulate the bug: a fresh viewer load WITHOUT .Include(v => v.Cards), then a manual
|
||||
// Add of a second row for the same (Viewer, Card). The unique index must reject this.
|
||||
using var scope2 = factory.Services.CreateScope();
|
||||
var ctx2 = scope2.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
var unloadedViewer = await ctx2.Viewers.FirstAsync(v => v.Id == viewerId);
|
||||
var sameCard = await ctx2.Cards.FirstAsync(c => c.Id == testCardId);
|
||||
unloadedViewer.Cards.Add(new OwnedCardEntry { Card = sameCard, Count = 1, IsProtected = false });
|
||||
|
||||
Assert.ThrowsAsync<DbUpdateException>(async () => await ctx2.SaveChangesAsync());
|
||||
}
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Enums;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.Database.Models.Config;
|
||||
using SVSim.Database.Repositories.Card;
|
||||
using SVSim.Database.Repositories.Collectibles;
|
||||
using SVSim.Database.Services;
|
||||
using SVSim.EmulatedEntrypoint.Services;
|
||||
using SVSim.UnitTests.Infrastructure;
|
||||
|
||||
namespace SVSim.UnitTests.Services;
|
||||
|
||||
public class ViewerEntitlementsTests
|
||||
{
|
||||
/// <summary>
|
||||
/// FreeplayConfig is in SVSim.Database so EnsureSeedDataAsync seeds a DB row with
|
||||
/// Enabled=false (ShippedDefaults). Since tier 1 (DB) wins, we mutate the seeded row
|
||||
/// to activate freeplay rather than relying on an IConfiguration override.
|
||||
/// </summary>
|
||||
private static void SetFreeplayEnabled(SVSimDbContext db, bool enabled, ulong currencyAmount = 99999, int cardCopies = 3)
|
||||
{
|
||||
var row = db.GameConfigs.First(s => s.SectionName == "Freeplay");
|
||||
var cfg = JsonSerializer.Deserialize<FreeplayConfig>(row.ValueJson)!;
|
||||
cfg.Enabled = enabled;
|
||||
cfg.CurrencyAmount = currencyAmount;
|
||||
cfg.CardCopies = cardCopies;
|
||||
row.ValueJson = JsonSerializer.Serialize(cfg);
|
||||
db.SaveChanges();
|
||||
}
|
||||
|
||||
private static ViewerEntitlements Build(IServiceScope scope)
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
return new ViewerEntitlements(
|
||||
new GameConfigService(db, new ConfigurationBuilder().Build()),
|
||||
new CardRepository(db),
|
||||
new CollectionRepository(db));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Freeplay_off_reflects_real_balance_and_ownership()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
// Freeplay is seeded as Enabled=false by default — no mutation needed.
|
||||
var viewer = await db.Viewers.Include(v => v.Cards).ThenInclude(c => c.Card).FirstAsync(v => v.Id == viewerId);
|
||||
viewer.Currency.Crystals = 7;
|
||||
|
||||
var ent = Build(scope);
|
||||
|
||||
Assert.That(ent.IsFreeplay, Is.False);
|
||||
Assert.That(ent.EffectiveBalance(viewer, SpendCurrency.Crystal), Is.EqualTo(7));
|
||||
Assert.That(ent.OwnsCard(viewer, 99_999_999L), Is.False);
|
||||
Assert.That(ent.OwnsCosmetic(viewer, CosmeticType.Skin, 99_999), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Freeplay_on_inflates_main_currencies_but_not_spot_points()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
SetFreeplayEnabled(db, enabled: true, currencyAmount: 99999);
|
||||
var viewer = await db.Viewers.FirstAsync(v => v.Id == viewerId);
|
||||
viewer.Currency.SpotPoints = 5;
|
||||
|
||||
var ent = Build(scope);
|
||||
|
||||
Assert.That(ent.IsFreeplay, Is.True);
|
||||
Assert.That(ent.EffectiveBalance(viewer, SpendCurrency.Crystal), Is.EqualTo(99999));
|
||||
Assert.That(ent.EffectiveBalance(viewer, SpendCurrency.Rupee), Is.EqualTo(99999));
|
||||
Assert.That(ent.EffectiveBalance(viewer, SpendCurrency.RedEther), Is.EqualTo(99999));
|
||||
Assert.That(ent.EffectiveBalance(viewer, SpendCurrency.SpotPoint), Is.EqualTo(5),
|
||||
"spot points are not a freeplay-inflated currency");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Freeplay_on_treats_all_cards_and_cosmetics_as_owned()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
SetFreeplayEnabled(db, enabled: true);
|
||||
var viewer = await db.Viewers.Include(v => v.Cards).ThenInclude(c => c.Card).FirstAsync(v => v.Id == viewerId);
|
||||
|
||||
var ent = Build(scope);
|
||||
|
||||
Assert.That(ent.OwnsCard(viewer, 99_999_999L), Is.True);
|
||||
Assert.That(ent.OwnsCosmetic(viewer, CosmeticType.Skin, 99_999), Is.True);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// EffectiveOwnedCardsAsync
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Test]
|
||||
public async Task EffectiveOwnedCards_freeplay_on_returns_all_collectible_cards_at_card_copies()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
// Seed one collectible card owned by this viewer (gives it a CollectionInfo).
|
||||
await factory.SeedOwnedCardAsync(viewerId, 50001001L, count: 1);
|
||||
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
SetFreeplayEnabled(db, enabled: true, cardCopies: 3);
|
||||
|
||||
var viewer = await db.Viewers
|
||||
.Include(v => v.Cards).ThenInclude(c => c.Card)
|
||||
.FirstAsync(v => v.Id == viewerId);
|
||||
|
||||
var ent = Build(scope);
|
||||
var result = await ent.EffectiveOwnedCardsAsync(viewer);
|
||||
|
||||
// Freeplay returns the whole collectible catalog — card 50001001 must be present.
|
||||
Assert.That(result.Any(e => e.Card.Id == 50001001L), Is.True,
|
||||
"seeded collectible card must appear in freeplay result");
|
||||
|
||||
// Every returned entry must have Count == CardCopies (3).
|
||||
Assert.That(result.All(e => e.Count == 3), Is.True,
|
||||
"every freeplay entry should have Count == CardCopies (3)");
|
||||
|
||||
// The full set == every collectible card in the DB.
|
||||
int collectibleCount = db.Cards.Count(c => c.CollectionInfo != null);
|
||||
Assert.That(result.Count, Is.EqualTo(collectibleCount),
|
||||
"freeplay result should contain exactly all collectible cards");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task EffectiveOwnedCards_freeplay_off_returns_only_owned()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
// Seed card 50001002 owned at count 2.
|
||||
await factory.SeedOwnedCardAsync(viewerId, 50001002L, count: 2);
|
||||
|
||||
// Seed a second collectible card (50001003) NOT owned by the viewer — insert card row
|
||||
// only (with CollectionInfo so it's collectible) but do not link it to the viewer.
|
||||
using (var setupScope = factory.Services.CreateScope())
|
||||
{
|
||||
var setupDb = setupScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
if (!setupDb.Cards.Any(c => c.Id == 50001003L))
|
||||
{
|
||||
setupDb.Cards.Add(new ShadowverseCardEntry
|
||||
{
|
||||
Id = 50001003L,
|
||||
Name = "UnownedCollectible",
|
||||
Rarity = SVSim.Database.Enums.Rarity.Bronze,
|
||||
CollectionInfo = new SVSim.Database.Models.CardCollectionInfo { CraftCost = 200, DustReward = 50 },
|
||||
});
|
||||
await setupDb.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
// Freeplay is off by default — no mutation needed.
|
||||
|
||||
var viewer = await db.Viewers
|
||||
.Include(v => v.Cards).ThenInclude(c => c.Card)
|
||||
.FirstAsync(v => v.Id == viewerId);
|
||||
|
||||
var ent = Build(scope);
|
||||
var result = await ent.EffectiveOwnedCardsAsync(viewer);
|
||||
|
||||
// The owned card must be present at the right count.
|
||||
var owned = result.FirstOrDefault(e => e.Card.Id == 50001002L);
|
||||
Assert.That(owned, Is.Not.Null, "owned card should appear in result");
|
||||
Assert.That(owned!.Count, Is.EqualTo(2));
|
||||
|
||||
// The unowned collectible card must NOT appear.
|
||||
Assert.That(result.Any(e => e.Card.Id == 50001003L), Is.False,
|
||||
"card not owned by viewer must not appear when freeplay is off");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// EffectiveCosmeticsAsync
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Test]
|
||||
public async Task EffectiveCosmetics_leader_skins_always_full_catalog_owned_set_differs()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
|
||||
// --- freeplay OFF: fresh viewer owns no cosmetics ---
|
||||
{
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
// Freeplay off by default.
|
||||
|
||||
var viewer = await db.Viewers
|
||||
.Include(v => v.LeaderSkins)
|
||||
.Include(v => v.Sleeves)
|
||||
.Include(v => v.Emblems)
|
||||
.Include(v => v.Degrees)
|
||||
.Include(v => v.MyPageBackgrounds)
|
||||
.FirstAsync(v => v.Id == viewerId);
|
||||
|
||||
var ent = Build(scope);
|
||||
var cosmetics = await ent.EffectiveCosmeticsAsync(viewer);
|
||||
|
||||
int masterSkinCount = db.LeaderSkins.Count();
|
||||
Assert.That(masterSkinCount, Is.GreaterThan(0),
|
||||
"leaderskins.csv must have been imported — master table must be non-empty");
|
||||
Assert.That(cosmetics.AllLeaderSkins.Count, Is.EqualTo(masterSkinCount),
|
||||
"AllLeaderSkins should always be the full catalog regardless of freeplay");
|
||||
|
||||
// A fresh viewer owns one default skin per class (granted at registration).
|
||||
// Assert the owned set matches what the viewer actually has — don't assume empty.
|
||||
var expectedOwnedSkinIds = viewer.LeaderSkins.Select(s => s.Id).ToHashSet();
|
||||
Assert.That(cosmetics.OwnedLeaderSkinIds, Is.EquivalentTo(expectedOwnedSkinIds),
|
||||
"OwnedLeaderSkinIds should match the viewer's actual owned skins when freeplay is off");
|
||||
|
||||
// OwnedLeaderSkinIds must be a strict subset of AllLeaderSkins (not all of them).
|
||||
Assert.That(cosmetics.OwnedLeaderSkinIds.Count, Is.LessThan(masterSkinCount),
|
||||
"fresh viewer should own fewer skins than the full catalog");
|
||||
|
||||
// The four id-lists reflect what the viewer actually owns.
|
||||
Assert.That(cosmetics.SleeveIds.Count, Is.EqualTo(viewer.Sleeves.Count));
|
||||
Assert.That(cosmetics.EmblemIds.Count, Is.EqualTo(viewer.Emblems.Count));
|
||||
Assert.That(cosmetics.DegreeIds.Count, Is.EqualTo(viewer.Degrees.Count));
|
||||
Assert.That(cosmetics.MyPageBackgroundIds.Count, Is.EqualTo(viewer.MyPageBackgrounds.Count));
|
||||
}
|
||||
|
||||
// --- freeplay ON: all catalogs become owned ---
|
||||
{
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
SetFreeplayEnabled(db, enabled: true);
|
||||
|
||||
var viewer = await db.Viewers
|
||||
.Include(v => v.LeaderSkins)
|
||||
.Include(v => v.Sleeves)
|
||||
.Include(v => v.Emblems)
|
||||
.Include(v => v.Degrees)
|
||||
.Include(v => v.MyPageBackgrounds)
|
||||
.FirstAsync(v => v.Id == viewerId);
|
||||
|
||||
var ent = Build(scope);
|
||||
var cosmetics = await ent.EffectiveCosmeticsAsync(viewer);
|
||||
|
||||
int masterSkinCount = db.LeaderSkins.Count();
|
||||
Assert.That(cosmetics.AllLeaderSkins.Count, Is.EqualTo(masterSkinCount),
|
||||
"AllLeaderSkins count unchanged when freeplay is on");
|
||||
Assert.That(cosmetics.OwnedLeaderSkinIds.Count, Is.EqualTo(masterSkinCount),
|
||||
"freeplay: every skin id must be in OwnedLeaderSkinIds");
|
||||
|
||||
// All four id-lists should equal the full catalog counts.
|
||||
Assert.That(cosmetics.SleeveIds.Count, Is.EqualTo(db.Sleeves.Count()),
|
||||
"freeplay: SleeveIds should equal full sleeve catalog");
|
||||
Assert.That(cosmetics.EmblemIds.Count, Is.EqualTo(db.Emblems.Count()),
|
||||
"freeplay: EmblemIds should equal full emblem catalog");
|
||||
Assert.That(cosmetics.DegreeIds.Count, Is.EqualTo(db.Degrees.Count()),
|
||||
"freeplay: DegreeIds should equal full degree catalog");
|
||||
Assert.That(cosmetics.MyPageBackgroundIds.Count, Is.EqualTo(db.MyPageBackgrounds.Count()),
|
||||
"freeplay: MyPageBackgroundIds should equal full my-page-background catalog");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user