feat(packs): accept all currently-supported currencies on /pack/open
Extends /pack/open beyond the v1 CRYSTAL_MULTI=2 / DAILY=3 / RUPY_MULTI=7
trio to cover every type_detail whose payment primitive already exists:
1 CRYSTAL -> ICurrencySpendService crystal debit
6 RUPY -> ICurrencySpendService rupee debit
4 TICKET / 5 TICKET_MULTI -> debit child.ItemId from OwnedItemEntry
(ticketsNeeded = cost * packNumber), 400 on
missing/short balance; reward_list gets a
RewardType=4 post-state Item entry to mirror
project_wire_reward_list_post_state
Skin-overload type_details (8/9/13) and free-pack overlays (10/11/12)
stay 501 — they need selection / banner plumbing the current code
doesn't have.
Tutorial alias unchanged: it still consumes the gating ticket post-draw
and stamps tutorial_step=100. The two ticket flows diverge by intent
(tutorial = free server-grant; normal = paid by ticket inventory).
Removed Open_rejects_ticket_type_detail (asserted the old 501 path);
covered by Open_rejects_insufficient_tickets. Updated
NonTutorial_pack_open_does_not_emit_tutorial_step to assert the new
200-on-ticket-success behavior — same invariant under test.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -271,11 +271,14 @@ public class PackController : SVSimController
|
||||
// when buying a RUPY_MULTI (type_detail=7) child. The gacha_id alone disambiguates the
|
||||
// child; gacha_type validation against child.TypeDetail would falsely reject every buy.
|
||||
|
||||
// Supported currency types in v1: CRYSTAL_MULTI=2, DAILY=3, RUPY_MULTI=7. Ticket flows
|
||||
// (TICKET=4, TICKET_MULTI=5) and the rest are explicitly out of scope for the normal path.
|
||||
// The tutorial path (type_detail=5, TICKET_MULTI) bypasses this guard — the starter pack
|
||||
// is a free server-granted bonus, not a purchasable pack.
|
||||
if (!isTutorialPath && child.TypeDetail is not (2 or 3 or 7))
|
||||
// Supported type_details on the normal path:
|
||||
// 1 CRYSTAL / 2 CRYSTAL_MULTI -> spend crystals
|
||||
// 6 RUPY / 7 RUPY_MULTI -> spend rupees
|
||||
// 3 DAILY -> spend rupees, once per UTC day
|
||||
// 4 TICKET / 5 TICKET_MULTI -> consume child.ItemId from OwnedItemEntry
|
||||
// Skin-overload types (8/9/13) and free-pack overlays (10/11/12) need extra
|
||||
// selection / banner plumbing — kept 501 until the relevant flows land.
|
||||
if (!isTutorialPath && child.TypeDetail is not (1 or 2 or 3 or 4 or 5 or 6 or 7))
|
||||
return StatusCode(StatusCodes.Status501NotImplemented, new { error = "currency_path_not_implemented" });
|
||||
|
||||
var viewer = await _db.Viewers
|
||||
@@ -301,14 +304,16 @@ public class PackController : SVSimController
|
||||
{
|
||||
switch (child.TypeDetail)
|
||||
{
|
||||
case 2: // CRYSTAL_MULTI
|
||||
case 1: // CRYSTAL (single)
|
||||
case 2: // CRYSTAL_MULTI (10-pack)
|
||||
{
|
||||
long cost = (long)child.Cost * packNumber;
|
||||
var r = await _spend.TrySpendAsync(viewer, SpendCurrency.Crystal, cost);
|
||||
if (!r.Success) return BadRequest(new { error = "insufficient_crystals" });
|
||||
break;
|
||||
}
|
||||
case 7: // RUPY_MULTI
|
||||
case 6: // RUPY (single)
|
||||
case 7: // RUPY_MULTI (10-pack)
|
||||
{
|
||||
long cost = (long)child.Cost * packNumber;
|
||||
var r = await _spend.TrySpendAsync(viewer, SpendCurrency.Rupee, cost);
|
||||
@@ -329,6 +334,20 @@ public class PackController : SVSimController
|
||||
if (!r.Success) return BadRequest(new { error = "insufficient_rupees" });
|
||||
break;
|
||||
}
|
||||
case 4: // TICKET (single)
|
||||
case 5: // TICKET_MULTI (10-pack)
|
||||
{
|
||||
if (child.ItemId is not long ticketItemId)
|
||||
return StatusCode(StatusCodes.Status501NotImplemented, new { error = "ticket_pack_missing_item_id" });
|
||||
|
||||
int ticketsNeeded = child.Cost * packNumber;
|
||||
var owned = viewer.Items.FirstOrDefault(i => i.Item.Id == (int)ticketItemId);
|
||||
if (owned is null || owned.Count < ticketsNeeded)
|
||||
return BadRequest(new { error = "insufficient_tickets" });
|
||||
|
||||
owned.Count -= ticketsNeeded;
|
||||
break;
|
||||
}
|
||||
}
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
@@ -388,14 +407,26 @@ public class PackController : SVSimController
|
||||
// Currency reward entries only apply to purchasable packs; tutorial path omits them.
|
||||
if (!isTutorialPath)
|
||||
{
|
||||
if (child.TypeDetail == 2)
|
||||
if (child.TypeDetail is 1 or 2)
|
||||
{
|
||||
rewardList.Add(new RewardListEntry { RewardType = 2, RewardId = 0, RewardNum = (int)_entitlements.EffectiveBalance(viewer, SpendCurrency.Crystal) });
|
||||
}
|
||||
else if (child.TypeDetail == 7 || child.TypeDetail == 3)
|
||||
else if (child.TypeDetail is 3 or 6 or 7)
|
||||
{
|
||||
rewardList.Add(new RewardListEntry { RewardType = 9, RewardId = 0, RewardNum = (int)_entitlements.EffectiveBalance(viewer, SpendCurrency.Rupee) });
|
||||
}
|
||||
else if (child.TypeDetail is 4 or 5 && child.ItemId is long ticketItemId)
|
||||
{
|
||||
// Item post-state count for the ticket we just consumed — client direct-assigns
|
||||
// _userItemDict, so this must be the new total (project_wire_reward_list_post_state).
|
||||
var owned = viewer.Items.FirstOrDefault(i => i.Item.Id == (int)ticketItemId);
|
||||
rewardList.Add(new RewardListEntry
|
||||
{
|
||||
RewardType = 4, // Item
|
||||
RewardId = ticketItemId,
|
||||
RewardNum = owned?.Count ?? 0, // post-state total
|
||||
});
|
||||
}
|
||||
}
|
||||
rewardList.AddRange(grant.RewardList);
|
||||
|
||||
|
||||
@@ -153,30 +153,6 @@ public class PackControllerOpenTests
|
||||
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()
|
||||
{
|
||||
@@ -232,6 +208,195 @@ public class PackControllerOpenTests
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Open_with_crystal_single_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 = "crystal single",
|
||||
// type_detail=1 (CRYSTAL single-pack) — new path added 2026-05-31.
|
||||
ChildGachas = { new PackChildGachaEntry { GachaId = 100012, TypeDetail = 1, Cost = 120, CardCount = 8 } },
|
||||
});
|
||||
var v = await db.Viewers.FirstAsync(x => x.Id == viewerId);
|
||||
v.Currency.Crystals = 200;
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
var json = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","parent_gacha_id":10001,"gacha_id":100012,"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), await response.Content.ReadAsStringAsync());
|
||||
using var scope2 = factory.Services.CreateScope();
|
||||
var db2 = scope2.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
Assert.That((await db2.Viewers.FirstAsync(x => x.Id == viewerId)).Currency.Crystals, Is.EqualTo(80UL));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Open_with_rupy_single_deducts_rupees()
|
||||
{
|
||||
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 = "rupy single",
|
||||
ChildGachas = { new PackChildGachaEntry { GachaId = 600006, TypeDetail = 6, Cost = 100, CardCount = 8 } },
|
||||
});
|
||||
var v = await db.Viewers.FirstAsync(x => x.Id == viewerId);
|
||||
v.Currency.Rupees = 250;
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
var json = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","parent_gacha_id":10001,"gacha_id":600006,"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), await response.Content.ReadAsStringAsync());
|
||||
using var scope2 = factory.Services.CreateScope();
|
||||
var db2 = scope2.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
Assert.That((await db2.Viewers.FirstAsync(x => x.Id == viewerId)).Currency.Rupees, Is.EqualTo(150UL));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Open_with_ticket_consumes_ticket_and_emits_post_state_item_count()
|
||||
{
|
||||
const int TicketItemId = 90001;
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
await factory.SeedOwnedItemAsync(viewerId, itemId: TicketItemId, count: 3, itemName: "Legendary Card Pack Ticket");
|
||||
|
||||
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 = "ticket pack",
|
||||
// type_detail=4 (TICKET single) — 1 ticket per pack.
|
||||
ChildGachas = { new PackChildGachaEntry { GachaId = 100014, TypeDetail = 4, Cost = 1, CardCount = 8, ItemId = TicketItemId } },
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
var json = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","parent_gacha_id":10001,"gacha_id":100014,"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);
|
||||
|
||||
// Ticket count should be 2 (started at 3, consumed 1).
|
||||
using var scope2 = factory.Services.CreateScope();
|
||||
var db2 = scope2.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
var owned = await db2.Viewers
|
||||
.Where(v => v.Id == viewerId)
|
||||
.SelectMany(v => v.Items)
|
||||
.Where(i => i.Item.Id == TicketItemId)
|
||||
.Select(i => i.Count)
|
||||
.FirstAsync();
|
||||
Assert.That(owned, Is.EqualTo(2));
|
||||
|
||||
// reward_list must include the post-state Item entry (RewardType=4) so the client
|
||||
// direct-assigns _userItemDict — same post-state convention as the tutorial path.
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var rewardList = doc.RootElement.GetProperty("reward_list");
|
||||
var ticketEntry = rewardList.EnumerateArray()
|
||||
.FirstOrDefault(e => e.GetProperty("reward_type").GetInt32() == 4
|
||||
&& e.GetProperty("reward_id").GetInt64() == TicketItemId);
|
||||
Assert.That(ticketEntry.ValueKind, Is.Not.EqualTo(JsonValueKind.Undefined),
|
||||
"ticket reward entry missing from reward_list");
|
||||
Assert.That(ticketEntry.GetProperty("reward_num").GetInt32(), Is.EqualTo(2),
|
||||
"RewardNum must be post-state total, not delta");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Open_with_ticket_multi_consumes_one_ticket_per_pack()
|
||||
{
|
||||
const int TicketItemId = 90001;
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
await factory.SeedOwnedItemAsync(viewerId, itemId: TicketItemId, count: 10, itemName: "Bulk Ticket");
|
||||
|
||||
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 = "10-pack ticket",
|
||||
// type_detail=5 (TICKET_MULTI 10-pack) — cost=1 ticket per pack, packNumber=10
|
||||
// => 10 tickets consumed total.
|
||||
ChildGachas = { new PackChildGachaEntry { GachaId = 100015, TypeDetail = 5, Cost = 1, CardCount = 8, ItemId = TicketItemId } },
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
var json = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","parent_gacha_id":10001,"gacha_id":100015,"gacha_type":1,"pack_number":10,"exclude_card_ids":[]}""";
|
||||
var response = await client.PostAsync("/pack/open", JsonBody(json));
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), await response.Content.ReadAsStringAsync());
|
||||
|
||||
using var scope2 = factory.Services.CreateScope();
|
||||
var db2 = scope2.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
var owned = await db2.Viewers
|
||||
.Where(v => v.Id == viewerId)
|
||||
.SelectMany(v => v.Items)
|
||||
.Where(i => i.Item.Id == TicketItemId)
|
||||
.Select(i => i.Count)
|
||||
.FirstAsync();
|
||||
Assert.That(owned, Is.EqualTo(0), "10 tickets started, 10-pack buy consumes 10");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Open_rejects_insufficient_tickets()
|
||||
{
|
||||
const int TicketItemId = 90001;
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
await factory.SeedOwnedItemAsync(viewerId, itemId: TicketItemId, count: 0, itemName: "Ticket");
|
||||
|
||||
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 = "ticket pack",
|
||||
ChildGachas = { new PackChildGachaEntry { GachaId = 100014, TypeDetail = 4, Cost = 1, CardCount = 8, ItemId = TicketItemId } },
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
var json = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","parent_gacha_id":10001,"gacha_id":100014,"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));
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.That(body, Does.Contain("insufficient_tickets"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Open_daily_marks_last_daily_free_at_and_rejects_second_attempt()
|
||||
{
|
||||
|
||||
@@ -141,26 +141,44 @@ public class PackControllerTests
|
||||
[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.
|
||||
// Verify that regular /pack/open works on a ticket-funded pack AND does not include
|
||||
// tutorial_step in the response. Pack 99047 uses type_detail=5 (TICKET_MULTI), which
|
||||
// the non-tutorial path now accepts: a normal viewer with a ticket buys a normal pack
|
||||
// — only the /tutorial/pack_open alias attaches tutorial_step.
|
||||
using var factory = new SVSimTestFactory();
|
||||
await factory.SeedGlobalsAsync();
|
||||
long viewerId = await factory.SeedViewerAsync(tutorialState: 100);
|
||||
|
||||
// Pack 99047 has BasePackId=90001 (Throwback). The minimal card seed only creates set
|
||||
// 10001, so seed set 90001 explicitly + install a draw table pointing at its cards.
|
||||
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();
|
||||
}
|
||||
await factory.SeedPackDrawTableAsync(99047, 90001001L, 90001002L, 90001003L);
|
||||
await factory.SeedOwnedItemAsync(viewerId, itemId: 90001, count: 1, itemName: "Starter Legendary Ticket");
|
||||
|
||||
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(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
|
||||
Assert.That(body.Contains("\"tutorial_step\""), Is.False,
|
||||
"Regular /pack/open must never emit tutorial_step.");
|
||||
"Regular /pack/open must never emit tutorial_step — only /tutorial/pack_open does.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
Reference in New Issue
Block a user