test(card): snapshot-mismatch + protect-load round-trip

Add two spec-prescribed tests that the implementation plan missed:
- Create_proceeds_when_client_possession_snapshot_disagrees_with_server
- Protect_then_load_index_emits_is_protected_one

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-05-28 02:21:57 -04:00
parent 433408dddb
commit 71b0c66631

View File

@@ -347,6 +347,39 @@ public class CardControllerTests
Assert.That(body, Does.Contain("insufficient_vials"));
}
[Test]
public async Task Create_proceeds_when_client_possession_snapshot_disagrees_with_server()
{
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync();
// Server has 0 owned; client thinks it has 5 (stale snapshot).
await factory.SeedOwnedCardAsync(viewerId, cardId: 10001001L, count: 0, craftCost: 200);
await factory.SetRedEtherAsync(viewerId, 1_000UL);
using var client = factory.CreateAuthenticatedClient(viewerId);
// Inner JSON: create 1, client snapshot=5 (disagrees with server count=0).
// Spec: snapshot mismatch is warn-log only, never blocks the request.
var response = await client.PostAsync("/card/create",
CreateBody("{\"10001001\":\"1,5\"}"));
var body = await response.Content.ReadAsStringAsync();
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
var entries = JsonDocument.Parse(body).RootElement
.GetProperty("reward_list")
.EnumerateArray()
.Select(e => (Type: e.GetProperty("reward_type").GetInt32(),
Id: e.GetProperty("reward_id").GetInt64(),
Num: e.GetProperty("reward_num").GetInt32()))
.ToList();
// RedEther and card count based on actual server state, not client snapshot.
Assert.That(entries, Has.Member((Type: 1, Id: 0L, Num: 800)),
"RedEther post-state total = 1000 - 200 = 800");
Assert.That(entries, Has.Member((Type: 5, Id: 10001001L, Num: 1)),
"Card post-state owned count = 0 + 1 = 1");
}
private static StringContent ProtectBody(long cardId, bool isProtected) =>
new(
$$"""{"card_id":{{cardId}},"is_protected":{{(isProtected ? "true" : "false")}},"viewer_id":"0","steam_id":0,"steam_session_ticket":""}""",
@@ -431,4 +464,39 @@ public class CardControllerTests
// controller payload, which for CardProtectResponse is an empty object.
Assert.That(body.Trim(), Is.EqualTo("{}"));
}
[Test]
public async Task Protect_then_load_index_emits_is_protected_one()
{
// Spec: /load/index user_card_list[].is_protected is an int wire value (0 or 1),
// not a bool. Protect a card then verify /load/index round-trips the flag correctly.
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync();
await factory.SeedOwnedCardAsync(viewerId, cardId: 10001001L, count: 2);
using var client = factory.CreateAuthenticatedClient(viewerId);
// Set the protect flag.
var protectResponse = await client.PostAsync("/card/protect",
ProtectBody(10001001L, isProtected: true));
Assert.That(protectResponse.StatusCode, Is.EqualTo(HttpStatusCode.OK),
await protectResponse.Content.ReadAsStringAsync());
// Call /load/index and parse user_card_list.
const string IndexRequestJson =
"""{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","carrier":"web","card_master_hash":""}""";
var loadResponse = await client.PostAsync("/load/index",
new StringContent(IndexRequestJson, Encoding.UTF8, "application/json"));
var loadBody = await loadResponse.Content.ReadAsStringAsync();
Assert.That(loadResponse.StatusCode, Is.EqualTo(HttpStatusCode.OK), loadBody);
var cardEntry = JsonDocument.Parse(loadBody).RootElement
.GetProperty("user_card_list")
.EnumerateArray()
.FirstOrDefault(e => e.GetProperty("card_id").GetInt64() == 10001001L);
Assert.That(cardEntry.ValueKind, Is.Not.EqualTo(JsonValueKind.Undefined),
"Expected card 10001001 in user_card_list");
Assert.That(cardEntry.GetProperty("is_protected").GetInt32(), Is.EqualTo(1),
"is_protected wire value must be 1 (int) after protect call");
}
}