From bac10b91ff0fd41537adeb895da88049d75da14d Mon Sep 17 00:00:00 2001 From: gamer147 Date: Thu, 28 May 2026 01:38:42 -0400 Subject: [PATCH] test(card): /card/create controller integration Adds CreateBody helper and 15 test runs (7 methods + 8 parametrized cases) covering happy path, 401 unauthenticated, malformed inner JSON, empty object, unknown card, not_craftable, would_exceed_max_copies, and insufficient_vials error paths for POST /card/create. Co-Authored-By: Claude Sonnet 4.6 --- .../Controllers/CardControllerTests.cs | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/SVSim.UnitTests/Controllers/CardControllerTests.cs b/SVSim.UnitTests/Controllers/CardControllerTests.cs index 2f60f35..cab8e7f 100644 --- a/SVSim.UnitTests/Controllers/CardControllerTests.cs +++ b/SVSim.UnitTests/Controllers/CardControllerTests.cs @@ -19,6 +19,12 @@ public class CardControllerTests Encoding.UTF8, "application/json"); + private static StringContent CreateBody(string innerJson) => + new( + $$"""{"card_id_number_array":{{JsonSerializer.Serialize(innerJson)}},"viewer_id":"0","steam_id":0,"steam_session_ticket":""}""", + Encoding.UTF8, + "application/json"); + [Test] public async Task Destruct_happy_path_returns_redether_and_card_post_totals() { @@ -192,4 +198,149 @@ public class CardControllerTests Assert.That(entries, Has.Member((Type: 1, Id: 0L, Num: 50))); Assert.That(entries, Has.Member((Type: 5, Id: 10001001L, Num: 2))); } + + [Test] + public async Task Create_happy_path_returns_redether_and_card_post_totals() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + await factory.SeedOwnedCardAsync(viewerId, cardId: 10001001L, count: 0, craftCost: 200); + await factory.SetRedEtherAsync(viewerId, 1_000UL); + + using var client = factory.CreateAuthenticatedClient(viewerId); + + var response = await client.PostAsync("/card/create", + CreateBody("{\"10001001\":\"2,0\"}")); + + 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(); + + // 1000 - (2 * 200) = 600 + Assert.That(entries, Has.Member((Type: 1, Id: 0L, Num: 600)), + "RedEther post-state total = 1000 - 400 = 600"); + Assert.That(entries, Has.Member((Type: 5, Id: 10001001L, Num: 2)), + "Card post-state owned count = 0 + 2 = 2"); + } + + [Test] + public async Task Create_without_auth_header_returns_401() + { + using var factory = new SVSimTestFactory(); + using var client = factory.CreateClient(); + + var response = await client.PostAsync("/card/create", + CreateBody("{\"10001001\":\"1,0\"}")); + + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized)); + } + + [TestCase("", Description = "empty string")] + [TestCase("not json", Description = "non-JSON garbage")] + [TestCase("{\"10001001\":\"1\"}", Description = "value missing snapshot")] + [TestCase("{\"10001001\":\"0,0\"}", Description = "num=0 not allowed")] + [TestCase("{\"10001001\":\"-1,0\"}", Description = "negative num")] + [TestCase("{\"abc\":\"1,0\"}", Description = "non-numeric cardId")] + [TestCase("{\"10001001\":5}", Description = "value not a string")] + [TestCase("[]", Description = "root must be object, not array")] + public async Task Create_with_malformed_inner_json_returns_400(string innerJson) + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + using var client = factory.CreateAuthenticatedClient(viewerId); + + var response = await client.PostAsync("/card/create", CreateBody(innerJson)); + var body = await response.Content.ReadAsStringAsync(); + + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest), body); + Assert.That(body, Does.Contain("malformed_request")); + } + + [Test] + public async Task Create_with_empty_inner_object_returns_400() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + using var client = factory.CreateAuthenticatedClient(viewerId); + + var response = await client.PostAsync("/card/create", CreateBody("{}")); + var body = await response.Content.ReadAsStringAsync(); + + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest), body); + Assert.That(body, Does.Contain("malformed_request")); + } + + [Test] + public async Task Create_unknown_card_returns_400_unknown_card() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + await factory.SetRedEtherAsync(viewerId, 1_000UL); + using var client = factory.CreateAuthenticatedClient(viewerId); + + var response = await client.PostAsync("/card/create", + CreateBody("{\"99999999\":\"1,0\"}")); + var body = await response.Content.ReadAsStringAsync(); + + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest), body); + Assert.That(body, Does.Contain("unknown_card")); + } + + [Test] + public async Task Create_not_craftable_returns_400_not_craftable() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + await factory.SeedOwnedCardAsync(viewerId, cardId: 10001001L, count: 0, craftCost: 0, dustReward: 0); + await factory.SetRedEtherAsync(viewerId, 1_000UL); + using var client = factory.CreateAuthenticatedClient(viewerId); + + var response = await client.PostAsync("/card/create", + CreateBody("{\"10001001\":\"1,0\"}")); + var body = await response.Content.ReadAsStringAsync(); + + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest), body); + Assert.That(body, Does.Contain("not_craftable")); + } + + [Test] + public async Task Create_would_exceed_max_copies_returns_400() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + await factory.SeedOwnedCardAsync(viewerId, cardId: 10001001L, count: 3, craftCost: 200); + await factory.SetRedEtherAsync(viewerId, 1_000UL); + using var client = factory.CreateAuthenticatedClient(viewerId); + + var response = await client.PostAsync("/card/create", + CreateBody("{\"10001001\":\"1,3\"}")); + var body = await response.Content.ReadAsStringAsync(); + + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest), body); + Assert.That(body, Does.Contain("would_exceed_max_copies")); + } + + [Test] + public async Task Create_insufficient_vials_returns_400() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + await factory.SeedOwnedCardAsync(viewerId, cardId: 10001001L, count: 0, craftCost: 200); + await factory.SetRedEtherAsync(viewerId, 100UL); // half of needed + using var client = factory.CreateAuthenticatedClient(viewerId); + + var response = await client.PostAsync("/card/create", + CreateBody("{\"10001001\":\"1,0\"}")); + var body = await response.Content.ReadAsStringAsync(); + + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest), body); + Assert.That(body, Does.Contain("insufficient_vials")); + } }