using System.Net; using System.Text; using System.Text.Json; using SVSim.UnitTests.Infrastructure; namespace SVSim.UnitTests.Controllers; /// /// Coverage for /check/* — the first two endpoints the client hits on boot. The /// SpecialTitle smoke is duplicated in RoutingSmokeTests for routing-prefix coverage; this /// test layers shape assertions over the deeper boot-path concern. /// public class CheckControllerTests { private const string BaseRequestJson = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":""}"""; private const string GameStartRequestJson = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","app_type":0,"campaign_data":"","campaign_sign":"","campaign_user":0}"""; [Test] public async Task SpecialTitle_returns_default_title_id() { using var factory = new SVSimTestFactory(); using var client = factory.CreateClient(); var response = await client.PostAsync("/check/special_title", new StringContent(BaseRequestJson, Encoding.UTF8, "application/json")); Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); var body = await response.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(body); Assert.That(doc.RootElement.GetProperty("title_image_id").GetString(), Is.EqualTo("0")); } [Test] public async Task GameStart_with_authed_viewer_returns_spec_shape() { using var factory = new SVSimTestFactory(); long viewerId = await factory.SeedViewerAsync(); using var client = factory.CreateAuthenticatedClient(viewerId); var response = await client.PostAsync("/check/game_start", new StringContent(GameStartRequestJson, Encoding.UTF8, "application/json")); var body = await response.Content.ReadAsStringAsync(); Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body); using var doc = JsonDocument.Parse(body); var root = doc.RootElement; // now_tutorial_step is a STRING on the wire (prod sends "100"); client calls .ToInt(). Assert.That(root.GetProperty("now_tutorial_step").GetString(), Is.EqualTo("100"), "RegisterViewer's seed-config default sets tutorial_state=100 (tutorial complete)."); Assert.That(root.GetProperty("tos_state").GetInt32(), Is.EqualTo(1)); Assert.That(root.GetProperty("policy_state").GetInt32(), Is.EqualTo(1)); Assert.That(root.GetProperty("kor_authority_state").GetInt32(), Is.EqualTo(0)); Assert.That(root.GetProperty("tos_id").GetInt32(), Is.EqualTo(1)); Assert.That(root.GetProperty("policy_id").GetInt32(), Is.EqualTo(1)); Assert.That(root.GetProperty("kor_authority_id").GetInt32(), Is.EqualTo(0)); // Prod-shape fields (not strictly read by GameStartCheckTask.Parse but sent by prod). Assert.That(root.GetProperty("now_viewer_id").GetInt64(), Is.GreaterThan(0)); Assert.That(root.GetProperty("now_name").GetString(), Is.Not.Empty); Assert.That(root.GetProperty("now_rank").ValueKind, Is.EqualTo(JsonValueKind.Object)); // Steam connection should round-trip into transition_account_data — all three fields // serialized as strings (matches prod wire shape). var transitions = root.GetProperty("transition_account_data"); Assert.That(transitions.ValueKind, Is.EqualTo(JsonValueKind.Array)); Assert.That(transitions.GetArrayLength(), Is.EqualTo(1), "Seeded viewer has exactly one Steam social account connection."); Assert.That(transitions[0].GetProperty("social_account_type").GetString(), Is.EqualTo(((int)SVSim.Database.Enums.SocialAccountType.Steam).ToString())); Assert.That(transitions[0].GetProperty("social_account_id").GetString(), Is.Not.Empty); Assert.That(transitions[0].GetProperty("connected_viewer_id").GetString(), Is.Not.Empty); } [Test] public async Task GameStart_does_not_expose_unsettable_optional_fields() { // GameStartCheckTask.Parse uses `Keys.Contains("rewrite_viewer_id")` + `.ToInt()` with // no null guard, and same for `account_delete_reservation_status` (presence-only check). // We can't omit nullable properties on the encrypted MessagePack path — the [Key] // formatter writes them as Nil unconditionally. So these keys must not exist on // GameStartResponse at all. If a future change re-adds them, this test breaks the build. using var factory = new SVSimTestFactory(); long viewerId = await factory.SeedViewerAsync(); using var client = factory.CreateAuthenticatedClient(viewerId); var response = await client.PostAsync("/check/game_start", new StringContent(GameStartRequestJson, Encoding.UTF8, "application/json")); var body = await response.Content.ReadAsStringAsync(); Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body); using var doc = JsonDocument.Parse(body); var root = doc.RootElement; Assert.That(root.TryGetProperty("rewrite_viewer_id", out _), Is.False, "rewrite_viewer_id must NOT be present in the response — client NREs on null .ToInt()."); Assert.That(root.TryGetProperty("account_delete_reservation_status", out _), Is.False, "account_delete_reservation_status must NOT be present — presence triggers client behavior."); } [Test] public async Task GameStart_with_no_viewer_returns_401() { using var factory = new SVSimTestFactory(); using var client = factory.CreateClient(); var response = await client.PostAsync("/check/game_start", new StringContent(GameStartRequestJson, Encoding.UTF8, "application/json")); Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized)); } }