366 lines
18 KiB
C#
366 lines
18 KiB
C#
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");
|
|
}
|
|
}
|