Files
SVSimServer/SVSim.UnitTests/Controllers/GiftControllerTests.cs
gamer147 190b50cbaf fix(tutorial): gift_receive reward_list carries post-state totals, not deltas
The client's PlayerStaticData.UpdateHaveUserGoodsNumByJsonData does direct
assignment on each reward_list entry's reward_num, so currency/item totals
must be the new viewer balance — not the gift delta. Fresh accounts were
seeing their cached crystal/rupy balances clobbered down to the gift counts
until the next /load/index. Matches the project_wire_reward_list_post_state
memory and the prod capture (which shows 120 rupy = baseline 20 + gift 100).
2026-05-28 13:16:51 -04:00

145 lines
7.2 KiB
C#

using System.Net;
using System.Text;
using System.Text.Json;
using NUnit.Framework;
using SVSim.UnitTests.Infrastructure;
namespace SVSim.UnitTests.Controllers;
public class GiftControllerTests
{
private const string BaseAuthBlock =
@"""viewer_id"":""0"",""steam_id"":0,""steam_session_ticket"":""""";
[Test]
public async Task GiftTop_returns_five_tutorial_gifts_for_unclaimed_viewer()
{
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync(tutorialState: 31);
using var client = factory.CreateAuthenticatedClient(viewerId);
var response = await client.PostAsync("/tutorial/gift_top",
new StringContent($$"""{"page":1,{{BaseAuthBlock}}}""", Encoding.UTF8, "application/json"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
var root = doc.RootElement;
var presents = root.GetProperty("present_list");
Assert.That(presents.GetArrayLength(), Is.EqualTo(5));
// Expect the legendary pack entry (present_id 71478630) to be present.
bool foundLegendaryGift = false;
foreach (var p in presents.EnumerateArray())
{
if (p.GetProperty("present_id").GetString() == "71478630")
{
foundLegendaryGift = true;
Assert.That(p.GetProperty("reward_type").GetString(), Is.EqualTo("4"));
Assert.That(p.GetProperty("reward_detail_id").GetString(), Is.EqualTo("90001"));
Assert.That(p.GetProperty("reward_count").GetString(), Is.EqualTo("1"));
Assert.That(p.GetProperty("item_type").GetInt32(), Is.EqualTo(2));
Assert.That(p.GetProperty("message").GetString(), Is.EqualTo("For completing the tutorial"));
}
}
Assert.That(foundLegendaryGift, Is.True, "Legendary starter pack gift (71478630) must be in present_list.");
Assert.That(root.GetProperty("present_history_list").GetArrayLength(), Is.EqualTo(0));
Assert.That(root.GetProperty("limit_over_present_list").GetArrayLength(), Is.EqualTo(0));
}
[Test]
public async Task GiftReceive_grants_currency_and_items_then_history_is_populated()
{
using var factory = new SVSimTestFactory();
await factory.SeedGlobalsAsync();
long viewerId = await factory.SeedViewerAsync(tutorialState: 31);
using var client = factory.CreateAuthenticatedClient(viewerId);
var pre = await factory.GetViewerCurrencyAsync(viewerId);
var requestJson = $$"""
{"present_id_array":["71478626","71478627","71478628","71478629","71478630"],"state":1,{{BaseAuthBlock}}}
""";
var response = await client.PostAsync("/tutorial/gift_receive",
new StringContent(requestJson, Encoding.UTF8, "application/json"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
var root = doc.RootElement;
// Five received ids echoed.
var ids = root.GetProperty("received_ids").EnumerateArray()
.Select(e => e.GetString()).ToHashSet();
Assert.That(ids, Is.EquivalentTo(new[] { "71478626", "71478627", "71478628", "71478629", "71478630" }));
// present_list emptied, history populated.
Assert.That(root.GetProperty("present_list").GetArrayLength(), Is.EqualTo(0));
Assert.That(root.GetProperty("present_history_list").GetArrayLength(), Is.EqualTo(5));
// Currency credited: +400 crystals, +100 rupees.
var post = await factory.GetViewerCurrencyAsync(viewerId);
Assert.That(post.Crystals - pre.Crystals, Is.EqualTo(400UL));
Assert.That(post.Rupees - pre.Rupees, Is.EqualTo(100UL));
// reward_list carries post-state TOTALS, not deltas, per project_wire_reward_list_post_state.
// After claiming gifts, the crystal/rupy entries in reward_list should equal viewer's post-grant totals.
var rewardList = root.GetProperty("reward_list").EnumerateArray().ToList();
var crystalEntry = rewardList.First(e => e.GetProperty("reward_type").GetString() == "1");
var rupyEntry = rewardList.First(e => e.GetProperty("reward_type").GetString() == "9");
Assert.That(crystalEntry.GetProperty("reward_num").GetString(),
Is.EqualTo(post.Crystals.ToString()),
"reward_list currency entries must carry POST-STATE TOTALS, not gift deltas (client does direct assignment).");
Assert.That(rupyEntry.GetProperty("reward_num").GetString(),
Is.EqualTo(post.Rupees.ToString()));
}
[Test]
public async Task GiftReceive_advances_tutorial_state_from_31_to_41()
{
using var factory = new SVSimTestFactory();
await factory.SeedGlobalsAsync();
long viewerId = await factory.SeedViewerAsync(tutorialState: 31);
using var client = factory.CreateAuthenticatedClient(viewerId);
var json = $$"""{"present_id_array":["71478626"],"state":1,{{BaseAuthBlock}}}""";
var response = await client.PostAsync("/tutorial/gift_receive",
new StringContent(json, Encoding.UTF8, "application/json"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
var root = doc.RootElement;
// Response carries the new step inline.
Assert.That(root.GetProperty("tutorial_step").GetInt32(), Is.EqualTo(41));
Assert.That(root.GetProperty("is_unreceived_present").GetBoolean(), Is.False);
Assert.That(root.GetProperty("reward_list").GetArrayLength(), Is.EqualTo(1));
// Side effect: viewer state advanced to 41.
Assert.That(await factory.GetViewerTutorialStateAsync(viewerId), Is.EqualTo(41));
}
[Test]
public async Task GiftReceive_second_call_with_same_ids_does_not_double_grant()
{
using var factory = new SVSimTestFactory();
await factory.SeedGlobalsAsync();
long viewerId = await factory.SeedViewerAsync(tutorialState: 31);
using var client = factory.CreateAuthenticatedClient(viewerId);
var preFirst = await factory.GetViewerCurrencyAsync(viewerId);
var json = $$"""{"present_id_array":["71478626","71478627"],"state":1,{{BaseAuthBlock}}}""";
await client.PostAsync("/tutorial/gift_receive", new StringContent(json, Encoding.UTF8, "application/json"));
var midPost = await factory.GetViewerCurrencyAsync(viewerId);
Assert.That(midPost.Crystals - preFirst.Crystals, Is.EqualTo(400UL));
Assert.That(midPost.Rupees - preFirst.Rupees, Is.EqualTo(100UL));
var second = await client.PostAsync("/tutorial/gift_receive", new StringContent(json, Encoding.UTF8, "application/json"));
Assert.That(second.StatusCode, Is.EqualTo(HttpStatusCode.OK));
var finalPost = await factory.GetViewerCurrencyAsync(viewerId);
Assert.That(finalPost.Crystals, Is.EqualTo(midPost.Crystals), "Second claim of same present_ids must not re-grant.");
Assert.That(finalPost.Rupees, Is.EqualTo(midPost.Rupees));
}
}