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