Card liquefication
This commit is contained in:
195
SVSim.UnitTests/Controllers/CardControllerTests.cs
Normal file
195
SVSim.UnitTests/Controllers/CardControllerTests.cs
Normal file
@@ -0,0 +1,195 @@
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using SVSim.UnitTests.Infrastructure;
|
||||
|
||||
namespace SVSim.UnitTests.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// End-to-end coverage for POST /card/destruct. Exercises the controller's double-decode of
|
||||
/// the JSON-string-in-JSON payload and the reward_list post-state-totals shape. The
|
||||
/// validate/mutate logic itself is covered by <c>CardInventoryRepositoryTests</c>; the tests
|
||||
/// here just confirm the wire contract.
|
||||
/// </summary>
|
||||
public class CardControllerTests
|
||||
{
|
||||
private static StringContent DestructBody(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()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
await factory.SeedOwnedCardAsync(viewerId, cardId: 10001001L, count: 5, dustReward: 50);
|
||||
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
|
||||
// Inner JSON: cardId -> "<num>,<client_snapshot>". The snapshot is informational only.
|
||||
var response = await client.PostAsync("/card/destruct",
|
||||
DestructBody("{\"10001001\":\"2,5\"}"));
|
||||
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
|
||||
|
||||
// The ShadowverseTranslationMiddleware only fires for UnityPlayer UA requests; test
|
||||
// clients send plain HTTP so the controller's JSON is returned unwrapped.
|
||||
var rewardList = JsonDocument.Parse(body).RootElement
|
||||
.GetProperty("reward_list");
|
||||
|
||||
// Two entries — one RedEther (type 1), one Card (type 5).
|
||||
var entries = rewardList.EnumerateArray()
|
||||
.Select(e => (Type: e.GetProperty("reward_type").GetInt32(),
|
||||
Id: e.GetProperty("reward_id").GetInt64(),
|
||||
Num: e.GetProperty("reward_num").GetInt32()))
|
||||
.ToList();
|
||||
|
||||
Assert.That(entries, Has.Member((Type: 1, Id: 0L, Num: 100)),
|
||||
"RedEther post-state total = 2 * 50 = 100");
|
||||
Assert.That(entries, Has.Member((Type: 5, Id: 10001001L, Num: 3)),
|
||||
"Card post-state owned count = 5 - 2 = 3");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Destruct_without_auth_header_returns_401()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
using var client = factory.CreateClient(); // no auth header
|
||||
|
||||
var response = await client.PostAsync("/card/destruct",
|
||||
DestructBody("{\"10001001\":\"1,1\"}"));
|
||||
|
||||
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,5\"}", Description = "num=0 not allowed")]
|
||||
[TestCase("{\"10001001\":\"-1,5\"}", Description = "negative num")]
|
||||
[TestCase("{\"abc\":\"1,5\"}", Description = "non-numeric cardId")]
|
||||
[TestCase("{\"10001001\":5}", Description = "value not a string")]
|
||||
[TestCase("[]", Description = "root must be object, not array")]
|
||||
public async Task Destruct_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/destruct", DestructBody(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 Destruct_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/destruct", DestructBody("{}"));
|
||||
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 Destruct_unknown_card_returns_400_unknown_card()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
|
||||
var response = await client.PostAsync("/card/destruct",
|
||||
DestructBody("{\"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 Destruct_not_destructible_returns_400_not_destructible()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
await factory.SeedOwnedCardAsync(viewerId, cardId: 10001001L, count: 3, dustReward: 0, craftCost: 0);
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
|
||||
var response = await client.PostAsync("/card/destruct",
|
||||
DestructBody("{\"10001001\":\"1,3\"}"));
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest), body);
|
||||
Assert.That(body, Does.Contain("not_destructible"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Destruct_protected_returns_400_card_protected()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
await factory.SeedOwnedCardAsync(viewerId, cardId: 10001001L, count: 3, dustReward: 50, isProtected: true);
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
|
||||
var response = await client.PostAsync("/card/destruct",
|
||||
DestructBody("{\"10001001\":\"1,3\"}"));
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest), body);
|
||||
Assert.That(body, Does.Contain("card_protected"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Destruct_insufficient_cards_returns_400_insufficient_cards()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
await factory.SeedOwnedCardAsync(viewerId, cardId: 10001001L, count: 2, dustReward: 50);
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
|
||||
var response = await client.PostAsync("/card/destruct",
|
||||
DestructBody("{\"10001001\":\"3,2\"}"));
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest), body);
|
||||
Assert.That(body, Does.Contain("insufficient_cards"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Destruct_proceeds_when_client_possession_snapshot_disagrees_with_server()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
// Server has 3 owned; client thinks it has 5 (stale snapshot).
|
||||
await factory.SeedOwnedCardAsync(viewerId, cardId: 10001001L, count: 3, dustReward: 50);
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
|
||||
// Inner JSON: destruct 1, client snapshot=5 (disagrees with server count=3).
|
||||
// Spec: snapshot mismatch is warn-log only, never blocks the request.
|
||||
var response = await client.PostAsync("/card/destruct",
|
||||
DestructBody("{\"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();
|
||||
|
||||
// Vials awarded based on actual server count, not client snapshot.
|
||||
Assert.That(entries, Has.Member((Type: 1, Id: 0L, Num: 50)));
|
||||
Assert.That(entries, Has.Member((Type: 5, Id: 10001001L, Num: 2)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user