Files
SVSimServer/SVSim.UnitTests/Controllers/BattlePassControllerBuyTests.cs
gamer147 05d8169012 refactor: type reward_type columns as UserGoodsType enum
Replace bare `int RewardType` on 12 catalog/reward entities and GrantedReward
with the existing UserGoodsType enum. Verified against the decompiled client:
every wire reward_type decodes through the single Wizard.UserGoods.Type enum, so
one enum is correct across all endpoint families (item_type is a separate
Item.Type axis, left untouched). EF stores the enum as the same int column, so
there is no migration.

- Importers cast seed int -> UserGoodsType at the ingest boundary.
- New GrantedReward.ToRewardList() extension replaces 8 copy-pasted
  GrantedReward -> RewardListEntry projections.
- Fix 3 .ToString() sites that would otherwise emit enum names ("Crystal")
  instead of the int wire value ("2").
- Wire DTOs keep int; the enum is widened to int at the wire boundary only.

Build green; 962/962 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 07:50:49 -04:00

185 lines
8.9 KiB
C#

using System.Net;
using System.Text;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using SVSim.Database;
using SVSim.Database.Enums;
using SVSim.Database.Models;
using SVSim.UnitTests.Infrastructure;
namespace SVSim.UnitTests.Controllers;
public class BattlePassControllerBuyTests
{
private static StringContent JsonBody(string json) => new(json, Encoding.UTF8, "application/json");
// Reward Id formula matches BattlePassRewardImporter.MakeId — keep in sync.
private static long MakeRewardId(int seasonId, BattlePassTrack track, int level)
=> seasonId * 10_000L + (long)track * 1_000 + level;
private static async Task SeedSeason23WithPremiumReward(SVSimTestFactory f, long viewerId, ulong viewerCrystals, int currentPoint)
{
using var scope = f.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
if (await db.BattlePassLevels.CountAsync() == 0)
{
for (int i = 1; i <= 100; i++)
db.BattlePassLevels.Add(new BattlePassLevelEntry { Level = i, RequiredPoint = (i - 1) * 500 });
}
db.BattlePassSeasons.Add(new BattlePassSeasonEntry
{
Id = 23, Name = "Season 23", MaxLevel = 100,
StartDate = DateTimeOffset.UtcNow.AddDays(-30),
EndDate = DateTimeOffset.UtcNow.AddDays(30),
CanPurchase = true, PriceCrystal = 980, Description = "",
});
db.BattlePassRewards.Add(new BattlePassRewardEntry
{
Id = MakeRewardId(23, BattlePassTrack.Premium, 2),
SeasonId = 23, Track = BattlePassTrack.Premium, Level = 2, RewardType = (UserGoodsType)9,
RewardDetailId = 0, RewardNumber = 20, IsAppealExclusion = false,
});
var v = await db.Viewers.FirstAsync(x => x.Id == viewerId);
v.Currency.Crystals = viewerCrystals;
await db.SaveChangesAsync();
if (currentPoint > 0)
{
var p = new ViewerBattlePassProgressEntry
{
ViewerId = viewerId, SeasonId = 23, CurrentPoint = currentPoint,
IsPremium = false, WeeklyPoints = 0,
};
db.ViewerBattlePassProgress.Add(p);
await db.SaveChangesAsync();
}
}
[Test]
public async Task Buy_happy_path_at_level_1_deducts_crystals_and_emits_empty_achieved()
{
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync();
await SeedSeason23WithPremiumReward(factory, viewerId, viewerCrystals: 1000, currentPoint: 0);
using var client = factory.CreateAuthenticatedClient(viewerId);
var req = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","season_id":23,"id":23000}""";
var response = await client.PostAsync("/battle_pass/buy", JsonBody(req));
var body = await response.Content.ReadAsStringAsync();
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
using var doc = JsonDocument.Parse(body);
var achievedList = doc.RootElement.GetProperty("achieved_info").GetProperty("battle_pass_reward_list");
Assert.That(achievedList.GetArrayLength(), Is.EqualTo(0),
"level 1 → no premium rewards crossed");
bool foundCrystal = false;
foreach (var el in doc.RootElement.GetProperty("reward_list").EnumerateArray())
{
if (el.GetProperty("reward_type").GetInt32() == 2)
{
foundCrystal = true;
Assert.That(el.GetProperty("reward_num").GetInt32(), Is.EqualTo(20),
"post-state crystal total = 1000 - 980");
}
}
Assert.That(foundCrystal, Is.True, "reward_list must carry post-state crystal total");
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
var v = await db.Viewers.FirstAsync(x => x.Id == viewerId);
var progress = await db.ViewerBattlePassProgress.SingleAsync(p => p.ViewerId == viewerId);
Assert.That(v.Currency.Crystals, Is.EqualTo(20UL));
Assert.That(progress.IsPremium, Is.True);
}
[Test]
public async Task Buy_at_level_2_includes_retroactive_premium_reward_in_achieved()
{
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync();
await SeedSeason23WithPremiumReward(factory, viewerId, viewerCrystals: 1000, currentPoint: 500); // level 2
using var client = factory.CreateAuthenticatedClient(viewerId);
var req = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","season_id":23,"id":23000}""";
var response = await client.PostAsync("/battle_pass/buy", JsonBody(req));
var body = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(body);
var achievedList = doc.RootElement.GetProperty("achieved_info").GetProperty("battle_pass_reward_list");
Assert.That(achievedList.GetArrayLength(), Is.EqualTo(1), "premium level-2 reward must drop");
Assert.That(achievedList[0].GetProperty("reward_type").GetInt32(), Is.EqualTo(9));
Assert.That(achievedList[0].GetProperty("reward_number").GetInt32(), Is.EqualTo(20));
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
bool claimWritten = await db.ViewerBattlePassClaims.AnyAsync(
c => c.ViewerId == viewerId && c.SeasonId == 23
&& c.Track == BattlePassTrack.Premium && c.Level == 2);
Assert.That(claimWritten, Is.True, "claim row must be persisted");
}
[Test]
public async Task Buy_with_insufficient_crystals_returns_22_and_makes_no_changes()
{
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync();
await SeedSeason23WithPremiumReward(factory, viewerId, viewerCrystals: 100, currentPoint: 0);
using var client = factory.CreateAuthenticatedClient(viewerId);
var req = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","season_id":23,"id":23000}""";
var response = await client.PostAsync("/battle_pass/buy", JsonBody(req));
var body = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(body);
Assert.That(doc.RootElement.GetProperty("result_code").GetInt32(), Is.EqualTo(22));
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
var v = await db.Viewers.FirstAsync(x => x.Id == viewerId);
Assert.That(v.Currency.Crystals, Is.EqualTo(100UL), "no crystal deduction on failure");
bool anyProgress = await db.ViewerBattlePassProgress
.AnyAsync(p => p.ViewerId == viewerId && p.IsPremium);
Assert.That(anyProgress, Is.False);
}
[Test]
public async Task Buy_when_already_premium_returns_23()
{
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync();
await SeedSeason23WithPremiumReward(factory, viewerId, viewerCrystals: 5000, currentPoint: 0);
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
var p = await db.ViewerBattlePassProgress.SingleOrDefaultAsync(x => x.ViewerId == viewerId)
?? new ViewerBattlePassProgressEntry { ViewerId = viewerId, SeasonId = 23 };
p.IsPremium = true;
if (p.Id == 0) db.ViewerBattlePassProgress.Add(p);
await db.SaveChangesAsync();
}
using var client = factory.CreateAuthenticatedClient(viewerId);
var req = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","season_id":23,"id":23000}""";
var response = await client.PostAsync("/battle_pass/buy", JsonBody(req));
var body = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(body);
Assert.That(doc.RootElement.GetProperty("result_code").GetInt32(), Is.EqualTo(23));
}
[Test]
public async Task Buy_outside_period_returns_24()
{
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync();
// No season seeded → active-season lookup returns null.
using var client = factory.CreateAuthenticatedClient(viewerId);
var req = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","season_id":23,"id":23000}""";
var response = await client.PostAsync("/battle_pass/buy", JsonBody(req));
var body = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(body);
Assert.That(doc.RootElement.GetProperty("result_code").GetInt32(), Is.EqualTo(24));
}
}