589 lines
29 KiB
C#
589 lines
29 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 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.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task OpenPack_ResponseIncludesSkinEntry_WhenLeaderCardDrawn()
|
|
{
|
|
// Verifies the C.1 refactor surfaces cosmetic grants from ICardAcquisitionService into
|
|
// the /pack/open response. Seeds a pack whose pool contains ONLY a known leader card
|
|
// (so every slot is forced to that card via PickCardOfRarity's fallback walk), plus the
|
|
// CardCosmeticReward mapping + LeaderSkinEntry master row. Expectation: response's
|
|
// reward_list contains a type=10 (Skin) entry with the mapped skin_id.
|
|
const long LeaderCardId = 704741010L;
|
|
const int SkinId = 407;
|
|
|
|
using var factory = new SVSimTestFactory();
|
|
long viewerId = await factory.SeedViewerAsync();
|
|
int parentGachaId = await SeedSingleLeaderCardPack(factory, LeaderCardId);
|
|
await SeedCosmeticMapping(factory, LeaderCardId, SkinId);
|
|
|
|
using (var scope = factory.Services.CreateScope())
|
|
{
|
|
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
|
var v = await db.Viewers.FirstAsync(x => x.Id == viewerId);
|
|
v.Currency.Rupees = 200;
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
using var client = factory.CreateAuthenticatedClient(viewerId);
|
|
var json = $$"""{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","parent_gacha_id":{{parentGachaId}},"gacha_id":{{parentGachaId * 100 + 2}},"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");
|
|
var skinEntry = Enumerable.Range(0, rewardList.GetArrayLength())
|
|
.Select(i => rewardList[i])
|
|
.FirstOrDefault(e => e.GetProperty("reward_type").GetInt32() == 10);
|
|
|
|
Assert.That(skinEntry.ValueKind, Is.Not.EqualTo(JsonValueKind.Undefined),
|
|
"expected a Skin (type=10) entry in /pack/open's reward_list when a leader card is drawn");
|
|
Assert.That(skinEntry.GetProperty("reward_id").GetInt64(), Is.EqualTo((long)SkinId));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new CardSet whose ONLY non-foil card is <paramref name="leaderCardId"/>, plus
|
|
/// a PackConfigEntry pointing at that set. PackOpenService's PickCardOfRarity fallback walks
|
|
/// every rarity and lands on the sole available card, so every draw from this pack is
|
|
/// guaranteed to produce <paramref name="leaderCardId"/>. Returns the parent gacha id.
|
|
/// </summary>
|
|
private static async Task<int> SeedSingleLeaderCardPack(SVSimTestFactory f, long leaderCardId)
|
|
{
|
|
const int CardSetId = 99001;
|
|
const int ParentGachaId = 99001;
|
|
|
|
using var scope = f.Services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
|
|
|
// Reuse an existing card row if one exists at this id (e.g. seeded by globals); else
|
|
// insert a Legendary stub. Rarity=Legendary so SV Classic slot-8 (which forbids Bronze)
|
|
// resolves cleanly without falling through.
|
|
var card = await db.Cards.FirstOrDefaultAsync(c => c.Id == leaderCardId);
|
|
if (card is null)
|
|
{
|
|
card = new ShadowverseCardEntry { Id = leaderCardId, Name = $"SeededLeader{leaderCardId}", Rarity = Rarity.Legendary };
|
|
db.Cards.Add(card);
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
// Attach to a dedicated CardSet so the pool resolver (PackCategory.None branch) sees
|
|
// exactly this one card.
|
|
var set = new ShadowverseCardSetEntry
|
|
{
|
|
Id = CardSetId, Name = "SingleLeaderTestSet",
|
|
IsInRotation = false, IsBasic = false,
|
|
Cards = new List<ShadowverseCardEntry> { card },
|
|
};
|
|
db.CardSets.Add(set);
|
|
|
|
db.Packs.Add(new PackConfigEntry
|
|
{
|
|
Id = ParentGachaId, BasePackId = CardSetId, PackCategory = PackCategory.None,
|
|
CommenceDate = DateTime.UtcNow.AddDays(-1), CompleteDate = DateTime.UtcNow.AddDays(30),
|
|
GachaType = 1, GachaDetail = "single-leader test", SleeveId = 3000099,
|
|
ChildGachas = {
|
|
new PackChildGachaEntry { GachaId = ParentGachaId * 100 + 2, TypeDetail = 7, Cost = 100, CardCount = 8 },
|
|
},
|
|
});
|
|
await db.SaveChangesAsync();
|
|
return ParentGachaId;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inserts a CardCosmeticReward(Skin) row mapping <paramref name="cardId"/> → <paramref name="skinId"/>,
|
|
/// and ensures the LeaderSkinEntry master row exists. Production seed for CardCosmeticReward
|
|
/// is stripped in tests by SqliteFriendlyModelCustomizer, so the test must insert its own.
|
|
/// </summary>
|
|
private static async Task SeedCosmeticMapping(SVSimTestFactory f, long cardId, int skinId)
|
|
{
|
|
using var scope = f.Services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
|
|
|
if (await db.LeaderSkins.FindAsync(skinId) is null)
|
|
db.LeaderSkins.Add(new LeaderSkinEntry { Id = skinId, Name = $"TestSkin{skinId}" });
|
|
|
|
db.CardCosmeticRewards.Add(new CardCosmeticReward
|
|
{
|
|
CardId = cardId, Type = CosmeticType.Skin, CosmeticId = skinId, Quantity = 1,
|
|
});
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
[Test]
|
|
public async Task PackOpen_accrues_gacha_points_per_pack_drawn()
|
|
{
|
|
using var factory = new SVSimTestFactory();
|
|
long viewerId = await factory.SeedViewerAsync();
|
|
using (var scope = factory.Services.CreateScope())
|
|
{
|
|
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
|
db.Classes.Add(new ClassEntry { Id = 0, Name = "Neutral" });
|
|
var set = new ShadowverseCardSetEntry { Id = 10008, IsInRotation = true };
|
|
db.CardSets.Add(set);
|
|
// Need a pool that can draw 8 cards across the 4 default rarities.
|
|
for (int i = 0; i < 30; i++)
|
|
{
|
|
set.Cards.Add(new ShadowverseCardEntry
|
|
{
|
|
Id = 10804_1010 + i,
|
|
Name = $"c{i}",
|
|
Rarity = (Rarity)((i % 4) + 1), // Bronze..Legendary
|
|
Class = db.Classes.Local.First(),
|
|
IsFoil = false,
|
|
});
|
|
}
|
|
db.Packs.Add(new PackConfigEntry
|
|
{
|
|
Id = 10008, BasePackId = 10008, PackCategory = PackCategory.LegendCardPack,
|
|
CommenceDate = DateTime.UtcNow.AddDays(-1), CompleteDate = DateTime.UtcNow.AddDays(30),
|
|
GachaType = 1,
|
|
GachaPointConfig = new PackGachaPointConfig { ExchangeablePoint = 400, IncreaseGachaPoint = 1 },
|
|
ChildGachas =
|
|
{
|
|
new PackChildGachaEntry
|
|
{
|
|
GachaId = 100087, TypeDetail = 7, Cost = 100, CardCount = 8,
|
|
OverrideIncreaseGachaPoint = 0,
|
|
},
|
|
},
|
|
});
|
|
var viewer = await db.Viewers.FirstAsync(v => v.Id == viewerId);
|
|
viewer.Currency.Rupees = 10000;
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
using var client = factory.CreateAuthenticatedClient(viewerId);
|
|
var body = new StringContent(
|
|
"""{"parent_gacha_id":10008,"gacha_id":100087,"gacha_type":1,"pack_number":3,"exclude_card_ids":[],"viewer_id":"0","steam_id":0,"steam_session_ticket":""}""",
|
|
System.Text.Encoding.UTF8, "application/json");
|
|
var response = await client.PostAsync("/pack/open", body);
|
|
var text = await response.Content.ReadAsStringAsync();
|
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), text);
|
|
|
|
using var scope2 = factory.Services.CreateScope();
|
|
var db2 = scope2.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
|
var v = await db2.Viewers
|
|
.Include(x => x.GachaPointBalances)
|
|
.FirstAsync(x => x.Id == viewerId);
|
|
Assert.That(v.GachaPointBalances.Single().Points, Is.EqualTo(3));
|
|
}
|
|
|
|
[Test]
|
|
public async Task TutorialPackOpen_does_not_accrue_gacha_points()
|
|
{
|
|
using var factory = new SVSimTestFactory();
|
|
long viewerId = await factory.SeedViewerAsync();
|
|
// Seed the starter pack 99047 with a GachaPointConfig set — the tutorial-path skip
|
|
// must hold even when the pack is technically point-eligible.
|
|
using (var scope = factory.Services.CreateScope())
|
|
{
|
|
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
|
db.Classes.Add(new ClassEntry { Id = 0, Name = "Neutral" });
|
|
var set = new ShadowverseCardSetEntry { Id = 99047, IsInRotation = true };
|
|
db.CardSets.Add(set);
|
|
for (int i = 0; i < 30; i++)
|
|
{
|
|
set.Cards.Add(new ShadowverseCardEntry
|
|
{
|
|
Id = 99047_1010 + i, Name = $"c{i}",
|
|
Rarity = (Rarity)((i % 4) + 1),
|
|
Class = db.Classes.Local.First(), IsFoil = false,
|
|
});
|
|
}
|
|
db.Items.Add(new ItemEntry { Id = 90001, Name = "starter-ticket" });
|
|
db.Packs.Add(new PackConfigEntry
|
|
{
|
|
Id = 99047, BasePackId = 99047, PackCategory = PackCategory.LegendCardPack,
|
|
CommenceDate = DateTime.UtcNow.AddDays(-1), CompleteDate = DateTime.UtcNow.AddDays(30),
|
|
GachaType = 1,
|
|
GachaPointConfig = new PackGachaPointConfig { ExchangeablePoint = 400, IncreaseGachaPoint = 1 },
|
|
ChildGachas =
|
|
{
|
|
new PackChildGachaEntry
|
|
{
|
|
GachaId = 990475, TypeDetail = 5, Cost = 0, CardCount = 8,
|
|
ItemId = 90001,
|
|
},
|
|
},
|
|
});
|
|
var viewer = await db.Viewers
|
|
.Include(v => v.Items).ThenInclude(i => i.Item)
|
|
.FirstAsync(v => v.Id == viewerId);
|
|
viewer.Items.Add(new OwnedItemEntry { Item = db.Items.Local.First(), Count = 1, Viewer = viewer });
|
|
viewer.MissionData.TutorialState = 41; // pre-END so the tutorial path is allowed
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
using var client = factory.CreateAuthenticatedClient(viewerId);
|
|
var body = new StringContent(
|
|
"""{"parent_gacha_id":99047,"gacha_id":990475,"gacha_type":1,"pack_number":1,"exclude_card_ids":[],"viewer_id":"0","steam_id":0,"steam_session_ticket":""}""",
|
|
System.Text.Encoding.UTF8, "application/json");
|
|
var response = await client.PostAsync("/tutorial/pack_open", body);
|
|
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 v = await db2.Viewers
|
|
.Include(x => x.GachaPointBalances)
|
|
.FirstAsync(x => x.Id == viewerId);
|
|
Assert.That(v.GachaPointBalances, Is.Empty, "tutorial path must not accrue gacha points");
|
|
}
|
|
}
|