Files
SVSimServer/SVSim.UnitTests/Controllers/TutorialFlowEndToEndTests.cs
gamer147 b2a5b69423 fix(gift): wire reward_type is UserGoodsType integer, not legacy 1/4/9 encoding
Replace WireRewardTypeToUserGoodsType switch with a validating identity cast backed
by GiftRewardTypes.IsSupported. Wire type 1 is RedEther (UserGoodsType.RedEther),
not Crystal (UserGoodsType.Crystal=2); the old switch silently granted the wrong
wallet for every tutorial-completion claim. Update all 5 GiftControllerTests assertions
and 1 TutorialFlowEndToEndTests assertion to expect RedEther instead of Crystals.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 20:45:49 -04:00

123 lines
6.3 KiB
C#

using System.Net;
using System.Text;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using SVSim.Database;
using SVSim.Database.Enums;
using SVSim.Database.Models;
using SVSim.UnitTests.Infrastructure;
namespace SVSim.UnitTests.Controllers;
public class TutorialFlowEndToEndTests
{
[Test]
public async Task FreshSignup_through_pack_open_reaches_tutorial_step_100()
{
using var factory = new SVSimTestFactory();
await factory.SeedGlobalsAsync();
// Fresh viewer at PRE_TUTORIAL_STEP (the real prod default after Task 1).
// SeedViewerAsync goes through RegisterViewer (admin/social path) which does NOT
// auto-seed tutorial presents — only the prod /tool/signup -> RegisterAnonymousViewer
// flow does. The end-to-end simulation here needs the inbox populated explicitly.
long viewerId = await factory.SeedViewerAsync(tutorialState: 0);
await factory.SeedTutorialPresentsAsync(viewerId);
// Pack 99047 (starter legendary) has base_pack_id=90001. Seed the card set used by
// the tutorial pack pool resolver — mirrors the pattern in PackControllerTests.
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
db.CardSets.Add(new ShadowverseCardSetEntry
{
Id = 90001,
Name = "TutorialStarterSet",
IsInRotation = true,
IsBasic = false,
Cards =
[
new ShadowverseCardEntry { Id = 90001001L, Name = "StarterCard1", Rarity = Rarity.Bronze },
new ShadowverseCardEntry { Id = 90001002L, Name = "StarterCard2", Rarity = Rarity.Gold },
new ShadowverseCardEntry { Id = 90001003L, Name = "StarterCard3", Rarity = Rarity.Legendary },
],
});
await db.SaveChangesAsync();
}
// Install a draw table for 99047 pointing at the seeded starter cards.
await factory.SeedPackDrawTableAsync(99047, 90001001L, 90001002L, 90001003L);
using var client = factory.CreateAuthenticatedClient(viewerId);
var preCurrency = await factory.GetViewerCurrencyAsync(viewerId);
// 1. /account/update_name (after name-entry screen).
var nameResp = await Post(client, "/account/update_name",
"""{"name":"e2e_test_user","viewer_id":"0","steam_id":0,"steam_session_ticket":""}""");
Assert.That(nameResp.StatusCode, Is.EqualTo(HttpStatusCode.OK));
// 2. Step transitions observed in capture: 11 → 21 → 31.
foreach (var step in new[] { 11, 21, 31 })
{
var json = $$"""{"tutorial_step":{{step}},"is_skip":0,"viewer_id":"0","steam_id":0,"steam_session_ticket":""}""";
var resp = await Post(client, "/tutorial/update", json);
using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync());
Assert.That(doc.RootElement.GetProperty("tutorial_step").GetInt32(), Is.EqualTo(step));
}
// 3. /tutorial/update_action — a couple of representative sub-step calls.
await Post(client, "/tutorial/update_action",
"""{"tutorial_step":1,"tutorial_action_number":2,"viewer_id":"0","steam_id":0,"steam_session_ticket":""}""");
// 4. /tutorial/gift_top — surface the bundle.
var topResp = await Post(client, "/tutorial/gift_top",
"""{"page":1,"viewer_id":"0","steam_id":0,"steam_session_ticket":""}""");
using (var doc = JsonDocument.Parse(await topResp.Content.ReadAsStringAsync()))
{
Assert.That(doc.RootElement.GetProperty("present_list").GetArrayLength(), Is.EqualTo(5));
}
// 5. /tutorial/gift_receive — claim them.
var receiveResp = await Post(client, "/tutorial/gift_receive",
"""{"present_id_array":["71478630","71478629","71478628","71478627","71478626"],"state":1,"viewer_id":"0","steam_id":0,"steam_session_ticket":""}""");
Assert.That(receiveResp.StatusCode, Is.EqualTo(HttpStatusCode.OK));
var midCurrency = await factory.GetViewerCurrencyAsync(viewerId);
// Tutorial gift 71478626 has reward_type=1 — that's RedEther per UserGoods.Type, not Crystal.
Assert.That(midCurrency.RedEther - preCurrency.RedEther, Is.EqualTo(400UL));
Assert.That(midCurrency.Rupees - preCurrency.Rupees, Is.EqualTo(100UL));
// gift_receive should also have advanced the tutorial step to 41 server-side.
Assert.That(await factory.GetViewerTutorialStateAsync(viewerId), Is.EqualTo(41));
// 6. /tutorial/pack_info — show the 3 active packs.
var packInfoResp = await Post(client, "/tutorial/pack_info",
"""{"viewer_id":"0","steam_id":0,"steam_session_ticket":""}""");
Assert.That(packInfoResp.StatusCode, Is.EqualTo(HttpStatusCode.OK));
// 7. /tutorial/pack_open of the starter legendary pack — END transition.
var openBody = """{"parent_gacha_id":99047,"gacha_id":990047,"gacha_type":1,"pack_number":1,"exclude_card_ids":[],"viewer_id":"0","steam_id":0,"steam_session_ticket":""}""";
var openResp = await Post(client, "/tutorial/pack_open", openBody);
var openRespBody = await openResp.Content.ReadAsStringAsync();
Assert.That(openResp.StatusCode, Is.EqualTo(HttpStatusCode.OK), openRespBody);
using (var doc = JsonDocument.Parse(openRespBody))
{
Assert.That(doc.RootElement.GetProperty("tutorial_step").GetInt32(), Is.EqualTo(100));
}
Assert.That(await factory.GetViewerTutorialStateAsync(viewerId), Is.EqualTo(100),
"Viewer reaches TUTORIAL_END after the full flow.");
// The gift granted item 90001 count=1 (via /tutorial/gift_receive entry 71478630).
// /tutorial/pack_open consumes it; assert the ticket is gone post-flow.
Assert.That(await factory.GetOwnedItemCountAsync(viewerId, 90001), Is.EqualTo(0),
"Starter legendary ticket must be consumed by /tutorial/pack_open.");
}
private static Task<HttpResponseMessage> Post(HttpClient client, string url, string body)
=> client.PostAsync(url, new StringContent(body, Encoding.UTF8, "application/json"));
}