Pack opening

This commit is contained in:
gamer147
2026-05-24 02:03:13 -04:00
parent bdff142d16
commit 79209bd70b
41 changed files with 37320 additions and 0 deletions

View File

@@ -0,0 +1,133 @@
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 PackControllerInfoTests
{
private const string EmptyEnvelope = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":""}""";
private static StringContent JsonBody(string json) => new(json, Encoding.UTF8, "application/json");
private static async Task SeedActivePack(SVSimTestFactory f, int parentId, int baseId, PackCategory cat)
{
using var scope = f.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
db.Packs.Add(new PackConfigEntry
{
Id = parentId, BasePackId = baseId, PackCategory = cat,
CommenceDate = DateTime.UtcNow.AddDays(-1), CompleteDate = DateTime.UtcNow.AddDays(30),
GachaType = 1, GachaDetail = "test",
ChildGachas = { new PackChildGachaEntry { GachaId = parentId * 10 + 7, TypeDetail = 7, Cost = 100, CardCount = 8 } },
});
await db.SaveChangesAsync();
}
[Test]
public async Task Info_returns_active_packs_only()
{
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync();
await SeedActivePack(factory, 10001, 10001, PackCategory.None);
using var client = factory.CreateAuthenticatedClient(viewerId);
var response = await client.PostAsync("/pack/info", JsonBody(EmptyEnvelope));
var body = await response.Content.ReadAsStringAsync();
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
using var doc = JsonDocument.Parse(body);
var list = doc.RootElement.GetProperty("pack_config_list");
Assert.That(list.GetArrayLength(), Is.EqualTo(1));
Assert.That(list[0].GetProperty("parent_gacha_id").GetInt32(), Is.EqualTo(10001));
}
[Test]
public async Task Info_overlays_viewer_open_count()
{
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync();
await SeedActivePack(factory, 10001, 10001, PackCategory.None);
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
var v = await db.Viewers.Include(x => x.PackOpenCounts).FirstAsync(x => x.Id == viewerId);
v.PackOpenCounts.Add(new ViewerPackOpenCount { PackId = 10001, OpenCount = 7 });
await db.SaveChangesAsync();
}
using var client = factory.CreateAuthenticatedClient(viewerId);
var response = await client.PostAsync("/pack/info", JsonBody(EmptyEnvelope));
var body = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(body);
var p = doc.RootElement.GetProperty("pack_config_list")[0];
Assert.That(p.GetProperty("open_count").GetInt32(), Is.EqualTo(7));
}
[Test]
public async Task Info_emits_child_gacha_info_with_correct_wire_keys()
{
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync();
await SeedActivePack(factory, 10001, 10001, PackCategory.None);
using var client = factory.CreateAuthenticatedClient(viewerId);
var response = await client.PostAsync("/pack/info", JsonBody(EmptyEnvelope));
var body = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(body);
var children = doc.RootElement.GetProperty("pack_config_list")[0].GetProperty("child_gacha_info");
Assert.That(children.GetArrayLength(), Is.EqualTo(1));
Assert.That(children[0].GetProperty("type_detail").GetInt32(), Is.EqualTo(7));
Assert.That(children[0].GetProperty("cost").GetInt32(), Is.EqualTo(100));
}
[Test]
public async Task Info_emits_gacha_point_key_as_null_when_pack_has_no_gacha_point_config()
{
// PackInfoTask.cs:126 does `if (jsonData2["gacha_point"] != null)`. LitJson's JsonData
// indexer throws KeyNotFoundException on missing keys — the null check protects against
// null *value*, not missing *key*. With Program.cs's global WhenWritingNull, a null
// PackGachaPointDto would be omitted entirely and crash the client. Override per
// [[project_wire_null_policy]].
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync();
// Seed a pack WITHOUT GachaPointConfig — matches packs 80047, 92001, 99047 in prod
// (legendary specials whose `gacha_point` is null).
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
db.Packs.Add(new PackConfigEntry
{
Id = 92001, BasePackId = 90001, PackCategory = PackCategory.LegendCardPack,
CommenceDate = DateTime.UtcNow.AddDays(-1), CompleteDate = DateTime.UtcNow.AddDays(30),
GachaType = 1, GachaDetail = "legendary special", SleeveId = 5090001,
GachaPointConfig = null,
ChildGachas = { new PackChildGachaEntry { GachaId = 920002, TypeDetail = 5, Cost = 1, CardCount = 8, ItemId = 92001 } },
});
await db.SaveChangesAsync();
}
using var client = factory.CreateAuthenticatedClient(viewerId);
var response = await client.PostAsync("/pack/info", JsonBody(EmptyEnvelope));
var body = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(body);
var pack = doc.RootElement.GetProperty("pack_config_list")[0];
// The key MUST be present, even though its value is null.
Assert.That(pack.TryGetProperty("gacha_point", out var gachaPoint), Is.True,
"gacha_point key must always be present in /pack/info — client at PackInfoTask.cs:126 does a direct key access guarded only by a null check, not Keys.Contains.");
Assert.That(gachaPoint.ValueKind, Is.EqualTo(JsonValueKind.Null),
"gacha_point should serialize as explicit null when no GachaPointConfig is set.");
}
}

View File

@@ -0,0 +1,357 @@
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 PackControllerOpenTests
{
private static StringContent JsonBody(string json) => new(json, Encoding.UTF8, "application/json");
/// <summary>
/// Seeds an active pack (parent 10001, base = first seeded card set) with one rupee child
/// gacha (gacha_id=400002, cost=100), then gives the viewer enough rupees to buy once.
/// </summary>
private static async Task<int> SeedOpenablePack(SVSimTestFactory f, long viewerId, ulong rupees = 200)
{
int baseId;
using (var scope = f.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
baseId = await db.CardSets.Where(s => s.Cards.Count > 0).Select(s => s.Id).FirstAsync();
db.Packs.Add(new PackConfigEntry
{
Id = 10001, BasePackId = baseId, PackCategory = PackCategory.None,
CommenceDate = DateTime.UtcNow.AddDays(-1), CompleteDate = DateTime.UtcNow.AddDays(30),
GachaType = 1, GachaDetail = "test", SleeveId = 3000011,
ChildGachas = {
new PackChildGachaEntry { GachaId = 400002, TypeDetail = 7, Cost = 100, CardCount = 8 },
},
});
var v = await db.Viewers.FirstAsync(x => x.Id == viewerId);
v.Currency.Rupees = rupees;
await db.SaveChangesAsync();
}
return baseId;
}
[Test]
public async Task Open_with_rupees_returns_8_cards_and_deducts_currency()
{
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync();
await SeedOpenablePack(factory, viewerId);
using var client = factory.CreateAuthenticatedClient(viewerId);
var json = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","parent_gacha_id":10001,"gacha_id":400002,"gacha_type":1,"pack_number":1,"exclude_card_ids":[]}""";
var response = await client.PostAsync("/pack/open", JsonBody(json));
var body = await response.Content.ReadAsStringAsync();
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
using var doc = JsonDocument.Parse(body);
var pack = doc.RootElement.GetProperty("pack_list");
Assert.That(pack.GetArrayLength(), Is.EqualTo(8));
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.Rupees, Is.EqualTo(100UL), "200 starting - 100 cost");
}
[Test]
public async Task Open_persists_drawn_cards_to_viewer_collection()
{
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync();
await SeedOpenablePack(factory, viewerId);
using var client = factory.CreateAuthenticatedClient(viewerId);
var json = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","parent_gacha_id":10001,"gacha_id":400002,"gacha_type":1,"pack_number":1,"exclude_card_ids":[]}""";
await client.PostAsync("/pack/open", JsonBody(json));
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
var v = await db.Viewers.Include(x => x.Cards).ThenInclude(c => c.Card).FirstAsync(x => x.Id == viewerId);
var totalGranted = v.Cards.Sum(c => c.Count);
Assert.That(totalGranted, Is.EqualTo(8), "8 cards drawn, all persisted (Count sums to 8 even when duplicates collapse).");
}
[Test]
public async Task Open_increments_viewer_open_count()
{
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync();
await SeedOpenablePack(factory, viewerId);
using var client = factory.CreateAuthenticatedClient(viewerId);
var json = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","parent_gacha_id":10001,"gacha_id":400002,"gacha_type":1,"pack_number":1,"exclude_card_ids":[]}""";
await client.PostAsync("/pack/open", JsonBody(json));
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
var v = await db.Viewers.Include(x => x.PackOpenCounts).FirstAsync(x => x.Id == viewerId);
Assert.That(v.PackOpenCounts.Single(p => p.PackId == 10001).OpenCount, Is.EqualTo(1));
}
[Test]
public async Task Open_rejects_when_class_id_present()
{
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync();
await SeedOpenablePack(factory, viewerId);
using var client = factory.CreateAuthenticatedClient(viewerId);
var json = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","parent_gacha_id":10001,"gacha_id":400002,"gacha_type":1,"pack_number":1,"exclude_card_ids":[],"class_id":3}""";
var response = await client.PostAsync("/pack/open", JsonBody(json));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.NotImplemented));
}
[Test]
public async Task Open_rejects_when_target_card_id_present()
{
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync();
await SeedOpenablePack(factory, viewerId);
using var client = factory.CreateAuthenticatedClient(viewerId);
var json = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","parent_gacha_id":10001,"gacha_id":400002,"gacha_type":1,"pack_number":1,"exclude_card_ids":[],"target_card_id":12345}""";
var response = await client.PostAsync("/pack/open", JsonBody(json));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.NotImplemented));
}
[Test]
public async Task Open_rejects_skin_pack_category()
{
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync();
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
db.Packs.Add(new PackConfigEntry
{
Id = 70001, BasePackId = 70001, PackCategory = PackCategory.LeaderSkinPack,
CommenceDate = DateTime.UtcNow.AddDays(-1), CompleteDate = DateTime.UtcNow.AddDays(30),
GachaType = 1, GachaDetail = "skin pack",
ChildGachas = { new PackChildGachaEntry { GachaId = 700017, TypeDetail = 7, Cost = 100, CardCount = 8 } },
});
await db.SaveChangesAsync();
}
using var client = factory.CreateAuthenticatedClient(viewerId);
var json = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","parent_gacha_id":70001,"gacha_id":700017,"gacha_type":1,"pack_number":1,"exclude_card_ids":[]}""";
var response = await client.PostAsync("/pack/open", JsonBody(json));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.NotImplemented));
}
[Test]
public async Task Open_rejects_ticket_type_detail()
{
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync();
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
db.Packs.Add(new PackConfigEntry
{
Id = 92001, BasePackId = 90001, PackCategory = PackCategory.SpecialCardPack,
CommenceDate = DateTime.UtcNow.AddDays(-1), CompleteDate = DateTime.UtcNow.AddDays(30),
GachaType = 1, GachaDetail = "ticket-only pack", SleeveId = 5090001,
ChildGachas = { new PackChildGachaEntry { GachaId = 920002, TypeDetail = 5, Cost = 1, CardCount = 8, ItemId = 92001 } },
});
await db.SaveChangesAsync();
}
using var client = factory.CreateAuthenticatedClient(viewerId);
var json = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","parent_gacha_id":92001,"gacha_id":920002,"gacha_type":1,"pack_number":1,"exclude_card_ids":[]}""";
var response = await client.PostAsync("/pack/open", JsonBody(json));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.NotImplemented));
}
[Test]
public async Task Open_rejects_insufficient_rupees()
{
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync();
await SeedOpenablePack(factory, viewerId, rupees: 50); // need 100
using var client = factory.CreateAuthenticatedClient(viewerId);
var json = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","parent_gacha_id":10001,"gacha_id":400002,"gacha_type":1,"pack_number":1,"exclude_card_ids":[]}""";
var response = await client.PostAsync("/pack/open", JsonBody(json));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
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.Rupees, Is.EqualTo(50UL), "no deduction on insufficient-funds reject");
}
[Test]
public async Task Open_with_crystals_deducts_crystals()
{
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync();
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
int baseId = await db.CardSets.Where(s => s.Cards.Count > 0).Select(s => s.Id).FirstAsync();
db.Packs.Add(new PackConfigEntry
{
Id = 10001, BasePackId = baseId, PackCategory = PackCategory.None,
CommenceDate = DateTime.UtcNow.AddDays(-1), CompleteDate = DateTime.UtcNow.AddDays(30),
GachaType = 1, GachaDetail = "test",
ChildGachas = { new PackChildGachaEntry { GachaId = 100002, TypeDetail = 2, Cost = 100, CardCount = 8 } },
});
var v = await db.Viewers.FirstAsync(x => x.Id == viewerId);
v.Currency.Crystals = 250;
await db.SaveChangesAsync();
}
using var client = factory.CreateAuthenticatedClient(viewerId);
// gacha_type:1 (parent pack's gacha_type) not :2 (child's type_detail) — see project_wire_pack_gacha_type memory.
var json = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","parent_gacha_id":10001,"gacha_id":100002,"gacha_type":1,"pack_number":1,"exclude_card_ids":[]}""";
var response = await client.PostAsync("/pack/open", JsonBody(json));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
using (var scope2 = factory.Services.CreateScope())
{
var db2 = scope2.ServiceProvider.GetRequiredService<SVSimDbContext>();
var v2 = await db2.Viewers.FirstAsync(x => x.Id == viewerId);
Assert.That(v2.Currency.Crystals, Is.EqualTo(150UL));
}
}
[Test]
public async Task Open_daily_marks_last_daily_free_at_and_rejects_second_attempt()
{
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync();
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
int baseId = await db.CardSets.Where(s => s.Cards.Count > 0).Select(s => s.Id).FirstAsync();
db.Packs.Add(new PackConfigEntry
{
Id = 10001, BasePackId = baseId, PackCategory = PackCategory.None,
CommenceDate = DateTime.UtcNow.AddDays(-1), CompleteDate = DateTime.UtcNow.AddDays(30),
GachaType = 1, GachaDetail = "daily test",
ChildGachas = { new PackChildGachaEntry { GachaId = 200001, TypeDetail = 3, Cost = 0, CardCount = 1, IsDailySingle = true } },
});
await db.SaveChangesAsync();
}
using var client = factory.CreateAuthenticatedClient(viewerId);
// gacha_type:1 (parent pack's gacha_type) not :3 (child's type_detail=DAILY) — prod-correct.
var json = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","parent_gacha_id":10001,"gacha_id":200001,"gacha_type":1,"pack_number":1,"exclude_card_ids":[]}""";
var first = await client.PostAsync("/pack/open", JsonBody(json));
Assert.That(first.StatusCode, Is.EqualTo(HttpStatusCode.OK), await first.Content.ReadAsStringAsync());
var second = await client.PostAsync("/pack/open", JsonBody(json));
Assert.That(second.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest),
"Second daily-free open the same UTC day should be rejected.");
}
// ---------------- Regression tests for wire-shape quirks ----------------
[Test]
public async Task Open_succeeds_when_request_gacha_type_differs_from_child_type_detail()
{
// Prod client sends gacha_type=1 (parent pack's gacha_type) for every buy on a
// gacha_type=1 pack, regardless of which child gacha is being bought (RUPY_MULTI=7,
// DAILY=3, CRYSTAL_MULTI=2, TICKET_MULTI=5, ...). Server must NOT reject on
// `child.TypeDetail != request.GachaType` — gacha_id alone identifies the child.
// See project_wire_pack_gacha_type memory.
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync();
await SeedOpenablePack(factory, viewerId);
using var client = factory.CreateAuthenticatedClient(viewerId);
// gacha_id 400002 is RUPY_MULTI (type_detail=7); request sends gacha_type=1 — should succeed.
var json = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","parent_gacha_id":10001,"gacha_id":400002,"gacha_type":1,"pack_number":1,"exclude_card_ids":[]}""";
var response = await client.PostAsync("/pack/open", JsonBody(json));
var body = await response.Content.ReadAsStringAsync();
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
}
[Test]
public async Task Open_reward_list_includes_post_state_rupee_balance()
{
// Client's PlayerStaticData.UpdateHaveUserGoodsNum does `UserRupyCount = reward_num`
// (direct assignment). Without an entry, the on-screen rupee count stays stale until
// restart / /mypage/refresh. Verify the entry shape: reward_type=9 (Rupy), reward_id=0,
// reward_num=new post-state balance (starting 200 - cost 100 = 100).
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync();
await SeedOpenablePack(factory, viewerId, rupees: 200);
using var client = factory.CreateAuthenticatedClient(viewerId);
var json = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","parent_gacha_id":10001,"gacha_id":400002,"gacha_type":1,"pack_number":1,"exclude_card_ids":[]}""";
var response = await client.PostAsync("/pack/open", JsonBody(json));
var body = await response.Content.ReadAsStringAsync();
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
using var doc = JsonDocument.Parse(body);
var rewardList = doc.RootElement.GetProperty("reward_list");
Assert.That(rewardList.GetArrayLength(), Is.GreaterThan(0),
"reward_list must be populated — empty list leaves client cache stale.");
var rupyEntry = Enumerable.Range(0, rewardList.GetArrayLength())
.Select(i => rewardList[i])
.FirstOrDefault(e => e.GetProperty("reward_type").GetInt32() == 9);
Assert.That(rupyEntry.ValueKind, Is.Not.EqualTo(JsonValueKind.Undefined),
"missing Rupy (type=9) entry — client will keep showing the old rupee balance.");
Assert.That(rupyEntry.GetProperty("reward_id").GetInt64(), Is.EqualTo(0L));
Assert.That(rupyEntry.GetProperty("reward_num").GetInt32(), Is.EqualTo(100),
"reward_num is the new TOTAL balance (200 starting - 100 cost), not a delta.");
}
[Test]
public async Task Open_reward_list_includes_post_state_card_counts()
{
// For each unique drawn card the client expects `{reward_type:5, reward_id:<card_id>,
// reward_num:<new total owned count>}`. Without these the in-session collection cache
// is stale (cards appear in the open animation but the collection view doesn't update).
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync();
await SeedOpenablePack(factory, viewerId);
using var client = factory.CreateAuthenticatedClient(viewerId);
var json = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","parent_gacha_id":10001,"gacha_id":400002,"gacha_type":1,"pack_number":1,"exclude_card_ids":[]}""";
var response = await client.PostAsync("/pack/open", JsonBody(json));
var body = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(body);
var packList = doc.RootElement.GetProperty("pack_list");
var rewardList = doc.RootElement.GetProperty("reward_list");
// Every distinct card_id in pack_list must have a matching type=5 reward_list entry.
var drawnCardIds = Enumerable.Range(0, packList.GetArrayLength())
.Select(i => packList[i].GetProperty("card_id").GetInt64())
.Distinct()
.ToHashSet();
var cardEntryIds = Enumerable.Range(0, rewardList.GetArrayLength())
.Select(i => rewardList[i])
.Where(e => e.GetProperty("reward_type").GetInt32() == 5)
.Select(e => e.GetProperty("reward_id").GetInt64())
.ToHashSet();
Assert.That(cardEntryIds, Is.SupersetOf(drawnCardIds),
"Every unique drawn card_id must appear in reward_list with reward_type=5.");
}
}

View File

@@ -0,0 +1,71 @@
using System.Net;
using System.Text;
using System.Text.Json;
using SVSim.UnitTests.Infrastructure;
namespace SVSim.UnitTests.Controllers;
/// <summary>
/// Drives the importer + controller against the real prod capture (35 packs). Guards against
/// regressions in either layer caused by future capture refreshes.
/// </summary>
public class PackControllerProdCaptureTests
{
[Test]
public async Task Info_returns_full_35_pack_catalog_from_prod_capture()
{
// The default captures dir has both pack-info-fixture.json (3 packs) and
// pack-info-2026-05-23.json (35 packs). LoadCapture sorts by name descending and
// "pack-info-fixture.json" > "pack-info-2026-05-23.json" lexicographically, so the
// fixture would win. Copy captures to a temp dir, drop the fixture, then seed from there.
var sourceDir = Path.Combine(AppContext.BaseDirectory, "Data", "prod-captures");
var tempDir = Path.Combine(Path.GetTempPath(), "svsim-pack-prod-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
try
{
foreach (var src in Directory.EnumerateFiles(sourceDir))
{
if (Path.GetFileName(src).Equals("pack-info-fixture.json", StringComparison.OrdinalIgnoreCase))
continue;
File.Copy(src, Path.Combine(tempDir, Path.GetFileName(src)));
}
using var factory = new SVSimTestFactory();
await factory.SeedGlobalsAsync(tempDir); // imports the 35-pack pack-info-2026-05-23.json
long viewerId = await factory.SeedViewerAsync();
using var client = factory.CreateAuthenticatedClient(viewerId);
var response = await client.PostAsync(
"/pack/info",
new StringContent("""{"viewer_id":"0","steam_id":0,"steam_session_ticket":""}""",
Encoding.UTF8, "application/json"));
var body = await response.Content.ReadAsStringAsync();
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
using var doc = JsonDocument.Parse(body);
var list = doc.RootElement.GetProperty("pack_config_list");
Assert.That(list.GetArrayLength(), Is.EqualTo(35),
"Full prod capture should yield 35 active packs as of 2026-05-23.");
// Spot-check pack 99047 (LegendCardPack throwback, pack_category=1)
bool sawSpecial = false;
for (int i = 0; i < list.GetArrayLength(); i++)
{
var el = list[i];
if (el.GetProperty("parent_gacha_id").GetInt32() == 99047)
{
Assert.That(el.GetProperty("pack_category").GetInt32(), Is.EqualTo(1),
"99047 is a LegendCardPack (category 1) in the prod capture.");
sawSpecial = true;
break;
}
}
Assert.That(sawSpecial, Is.True, "pack 99047 must be in the prod capture output.");
}
finally
{
try { Directory.Delete(tempDir, recursive: true); } catch { /* best-effort cleanup */ }
}
}
}