Stack [HttpPost("/tutorial/pack_open")] alias on PackController.Open. Detect
isTutorialPath via HttpContext.Request.Path; gate the type_detail rejection,
currency switch, open-count tracking, and currency reward_list entries behind
!isTutorialPath so the starter legendary pack (99047/990047, type_detail=5)
bypasses the purchasable-pack code path. After grant, set MissionData.TutorialState=100
and emit tutorial_step=100 in PackOpenResponse — this is the sole END transition,
per live-traffic capture. Add pack 99047 to test-fixture packs.json.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
110 lines
5.2 KiB
C#
110 lines
5.2 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 PackControllerTests
|
|
{
|
|
[Test]
|
|
public async Task TutorialPackInfo_returns_same_list_as_pack_info()
|
|
{
|
|
using var factory = new SVSimTestFactory();
|
|
await factory.SeedGlobalsAsync();
|
|
long viewerId = await factory.SeedViewerAsync(tutorialState: 41);
|
|
using var client = factory.CreateAuthenticatedClient(viewerId);
|
|
|
|
var json = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":""}""";
|
|
var direct = await client.PostAsync("/pack/info", new StringContent(json, Encoding.UTF8, "application/json"));
|
|
var tutorial = await client.PostAsync("/tutorial/pack_info", new StringContent(json, Encoding.UTF8, "application/json"));
|
|
|
|
Assert.That(direct.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
|
Assert.That(tutorial.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
|
|
|
var directBody = await direct.Content.ReadAsStringAsync();
|
|
var tutorialBody = await tutorial.Content.ReadAsStringAsync();
|
|
Assert.That(tutorialBody, Is.EqualTo(directBody),
|
|
"tutorial/pack_info wire shape must match /pack/info exactly (no filtering in v1).");
|
|
}
|
|
|
|
[Test]
|
|
public async Task TutorialPackOpen_grants_pack_and_sets_tutorial_step_100()
|
|
{
|
|
using var factory = new SVSimTestFactory();
|
|
await factory.SeedGlobalsAsync();
|
|
long viewerId = await factory.SeedViewerAsync(tutorialState: 41);
|
|
|
|
// Pack 99047 (starter legendary) has base_pack_id=90001. The minimal card seed only
|
|
// creates set 10001, so we seed set 90001 explicitly for the pool resolver.
|
|
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();
|
|
}
|
|
|
|
using var client = factory.CreateAuthenticatedClient(viewerId);
|
|
|
|
var requestJson = """{"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 response = await client.PostAsync("/tutorial/pack_open",
|
|
new StringContent(requestJson, 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 root = doc.RootElement;
|
|
|
|
Assert.That(root.GetProperty("tutorial_step").GetInt32(), Is.EqualTo(100),
|
|
"tutorial/pack_open must include tutorial_step=100 in data — this is the END transition.");
|
|
Assert.That(root.GetProperty("pack_list").GetArrayLength(), Is.EqualTo(8),
|
|
"Starter pack 99047/990047 delivers 8 cards (child_gacha.card_count=8).");
|
|
|
|
Assert.That(await factory.GetViewerTutorialStateAsync(viewerId), Is.EqualTo(100));
|
|
}
|
|
|
|
[Test]
|
|
public async Task NonTutorial_pack_open_does_not_emit_tutorial_step()
|
|
{
|
|
// Verify that regular /pack/open still works AND does not include tutorial_step in the response.
|
|
// Use the tutorial pack (99047/990047) which has type_detail=5 — the non-tutorial path
|
|
// still hits the currency_path_not_implemented guard and returns 501.
|
|
using var factory = new SVSimTestFactory();
|
|
await factory.SeedGlobalsAsync();
|
|
long viewerId = await factory.SeedViewerAsync(tutorialState: 100);
|
|
using var client = factory.CreateAuthenticatedClient(viewerId);
|
|
|
|
var requestJson = """{"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 response = await client.PostAsync("/pack/open",
|
|
new StringContent(requestJson, Encoding.UTF8, "application/json"));
|
|
|
|
// Non-tutorial pack/open + type_detail=5 STILL returns 501 — that's the established behavior.
|
|
Assert.That((int)response.StatusCode, Is.EqualTo(501),
|
|
"Non-tutorial /pack/open with type_detail=5 should still hit the currency_path_not_implemented guard.");
|
|
|
|
// Even on a 501, no tutorial_step field should appear in the response body.
|
|
var body = await response.Content.ReadAsStringAsync();
|
|
Assert.That(body.Contains("\"tutorial_step\""), Is.False,
|
|
"Regular /pack/open must never emit tutorial_step.");
|
|
}
|
|
}
|