feat(services): ViewerEntitlements (freeplay-aware ownership/balance authority)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-05-29 13:40:24 -04:00
parent be19c0ad8d
commit 91c539fb8d
2 changed files with 206 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
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);
}
}