test(tk2): routing smoke + end-to-end draft→retire

Adds 8 TestCase entries to Authenticated_route_resolves for all
arena_two_pick and arena_two_pick_battle endpoints, and a full
integration test exercising entry → class_choose → 15×card_choose →
retire, verifying seed reward grants (1 ticket + 100 rupies).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-05-31 11:37:47 -04:00
parent 65e0e0fb09
commit 2aa0bdefec
2 changed files with 161 additions and 0 deletions

View File

@@ -0,0 +1,153 @@
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using SVSim.Bootstrap.Importers;
using SVSim.Database;
using SVSim.Database.Enums;
using SVSim.Database.Models;
using SVSim.UnitTests.Infrastructure;
namespace SVSim.UnitTests.Integration;
public class ArenaTwoPickEndToEndTests
{
[Test]
public async Task Full_draft_then_retire_at_zero_wins_grants_seed_rewards()
{
using var factory = new SVSimTestFactory();
// Load globals: challenge-config (pool_card_set_ids includes 10015), item master
// (ticket 80001), and arena-two-pick rewards.
await factory.SeedGlobalsAsync();
// Seed card set 10015 with one Bronze collectible card per class (1-8) + one neutral.
// The card pool service queries sets in ChallengeConfig.PoolCardSetIds, which already
// includes 10015 via the seeded challenge-config.json.
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
var set = new ShadowverseCardSetEntry { Id = 10015, Name = "TK2PoolSet", IsInRotation = true };
// One card per class id 1-8 (already seeded by ReferenceDataImporter/classes.csv).
for (int classId = 1; classId <= 8; classId++)
{
var cls = await db.Classes.FindAsync(classId);
if (cls is null)
{
cls = new ClassEntry { Id = classId, Name = $"Class{classId}" };
db.Classes.Add(cls);
await db.SaveChangesAsync();
}
set.Cards.Add(new ShadowverseCardEntry
{
Id = 10015_000_00L + classId,
Name = $"TK2ClassCard{classId}",
Rarity = Rarity.Bronze,
Class = cls,
CollectionInfo = new CardCollectionInfo { CraftCost = 200, DustReward = 50 },
});
}
// One neutral card.
set.Cards.Add(new ShadowverseCardEntry
{
Id = 10015_000_09L,
Name = "TK2NeutralCard",
Rarity = Rarity.Bronze,
Class = null,
CollectionInfo = new CardCollectionInfo { CraftCost = 200, DustReward = 50 },
});
db.CardSets.Add(set);
await db.SaveChangesAsync();
// Seed the reward catalog.
await new ArenaTwoPickRewardImporter().ImportAsync(
db, Path.Combine(AppContext.BaseDirectory, "Data", "seeds"));
}
// Seed viewer with 5 TK2 tickets. SeedGlobalsAsync already loaded ItemEntry 80001.
long viewerId = await factory.SeedViewerAsync();
await factory.SeedOwnedItemAsync(viewerId, itemId: 80001, count: 5,
itemName: "TK2 Ticket", itemType: 2);
// Capture starting Rupees so the retire assertion can compute expected post-state
// regardless of the default-grants config value (currently 50 000).
var (_, startRupees, _) = await factory.GetViewerCurrencyAsync(viewerId);
using var client = factory.CreateAuthenticatedClient(viewerId);
// 1) /top → entry_info:null (no active run).
var top = await client.PostAsync("/arena_two_pick/top", JsonContent.Create(new { mode = 0 }));
Assert.That(top.StatusCode, Is.EqualTo(HttpStatusCode.OK));
StringAssert.Contains("\"entry_info\":null", await top.Content.ReadAsStringAsync());
// 2) /entry → deducts 1 ticket (post-state = 4), returns 3 candidate class ids.
var entry = await client.PostAsync("/arena_two_pick/entry",
JsonContent.Create(new { consume_item_type = 3 }));
Assert.That(entry.StatusCode, Is.EqualTo(HttpStatusCode.OK),
$"/entry failed: {await entry.Content.ReadAsStringAsync()}");
using var entryDoc = JsonDocument.Parse(await entry.Content.ReadAsStringAsync());
var candidates = entryDoc.RootElement.GetProperty("candidate_class_ids")
.EnumerateArray().Select(e => e.GetInt32()).ToList();
Assert.That(candidates.Count, Is.EqualTo(3), "Entry must offer exactly 3 candidate classes");
// 3) /class_choose with first candidate → returns candidate_card_list.
var classChoose = await client.PostAsync("/arena_two_pick/class_choose",
JsonContent.Create(new { class_id = candidates[0] }));
Assert.That(classChoose.StatusCode, Is.EqualTo(HttpStatusCode.OK),
$"/class_choose failed: {await classChoose.Content.ReadAsStringAsync()}");
using var classDoc = JsonDocument.Parse(await classChoose.Content.ReadAsStringAsync());
long firstPickId = long.Parse(
classDoc.RootElement.GetProperty("candidate_card_list")[0]
.GetProperty("id").GetString()!);
// 4) 15 rounds of /card_choose, always picking the first candidate set.
long pickId = firstPickId;
for (int turn = 1; turn <= 15; turn++)
{
var cc = await client.PostAsync("/arena_two_pick/card_choose",
JsonContent.Create(new { selected_id = pickId }));
Assert.That(cc.StatusCode, Is.EqualTo(HttpStatusCode.OK),
$"turn {turn} /card_choose failed: {await cc.Content.ReadAsStringAsync()}");
if (turn == 15) break;
// Parse next candidate list for the following turn.
using var ccDoc = JsonDocument.Parse(await cc.Content.ReadAsStringAsync());
pickId = long.Parse(
ccDoc.RootElement.GetProperty("candidate_card_list")[0]
.GetProperty("id").GetString()!);
}
// 5) /retire at 0 wins → 1 ticket + 100 rupies from the seed table.
// Post-state: ticket = 4 (after debit) + 1 (grant) = 5; rupies = 0 + 100 = 100.
var retire = await client.PostAsync("/arena_two_pick/retire", JsonContent.Create(new { }));
Assert.That(retire.StatusCode, Is.EqualTo(HttpStatusCode.OK),
$"/retire failed: {await retire.Content.ReadAsStringAsync()}");
using var retDoc = JsonDocument.Parse(await retire.Content.ReadAsStringAsync());
var rewards = retDoc.RootElement.GetProperty("rewards").EnumerateArray().ToList();
Assert.That(rewards.Count, Is.EqualTo(2), "0-win rewards = 1 ticket + 100 rupy");
var rewardList = retDoc.RootElement.GetProperty("reward_list").EnumerateArray().ToList();
// reward_type 9 = Rupy; post-state = startRupees + 100.
var rupyEntry = rewardList.Single(r => r.GetProperty("reward_type").GetInt32() == 9);
var expectedRupees = (startRupees + 100).ToString();
Assert.That(rupyEntry.GetProperty("reward_num").GetString(), Is.EqualTo(expectedRupees),
$"post-state rupy = {startRupees} + 100");
// reward_type 4 = Item (ticket 80001); post-state = 4 (after debit) + 1 (grant) = 5.
var ticketEntry = rewardList.Single(r => r.GetProperty("reward_type").GetInt32() == 4);
Assert.That(ticketEntry.GetProperty("reward_num").GetString(), Is.EqualTo("5"),
"post-state ticket = 4 (after debit) + 1 (grant) = 5");
// 6) /top → entry_info:null again (run was deleted by /retire).
var topAgain = await client.PostAsync("/arena_two_pick/top", JsonContent.Create(new { mode = 0 }));
Assert.That(topAgain.StatusCode, Is.EqualTo(HttpStatusCode.OK));
StringAssert.Contains("\"entry_info\":null", await topAgain.Content.ReadAsStringAsync());
}
}

View File

@@ -100,6 +100,14 @@ public class RoutingSmokeTests
[TestCase("/deck/delete_deck_list")]
[TestCase("/deck/get_empty_deck_number")]
[TestCase("/deck/set_deck_redis")]
[TestCase("/arena_two_pick/top")]
[TestCase("/arena_two_pick/entry")]
[TestCase("/arena_two_pick/class_choose")]
[TestCase("/arena_two_pick/card_choose")]
[TestCase("/arena_two_pick/retire")]
[TestCase("/arena_two_pick/finish")]
[TestCase("/arena_two_pick_battle/do_matching")]
[TestCase("/arena_two_pick_battle/finish")]
public async Task Authenticated_route_resolves(string path)
{
using var factory = new TestFactory();