From 703f7ff3d750822c2835327bafdb2fd12acce6a2 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Thu, 28 May 2026 11:37:24 -0400 Subject: [PATCH] feat(tutorial): add /tutorial/update_action fire-and-forget endpoint Returns an empty data object (result_code=1 from middleware envelope). Client uses SkipAllNetworkChecks so the response body is never read. Co-Authored-By: Claude Sonnet 4.6 --- .../Controllers/TutorialController.cs | 19 ++++++++++ .../Tutorial/TutorialUpdateActionRequest.cs | 22 ++++++++++++ .../Controllers/TutorialControllerTests.cs | 36 +++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 SVSim.EmulatedEntrypoint/Controllers/TutorialController.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Requests/Tutorial/TutorialUpdateActionRequest.cs create mode 100644 SVSim.UnitTests/Controllers/TutorialControllerTests.cs diff --git a/SVSim.EmulatedEntrypoint/Controllers/TutorialController.cs b/SVSim.EmulatedEntrypoint/Controllers/TutorialController.cs new file mode 100644 index 0000000..7106d15 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Controllers/TutorialController.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Mvc; +using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Tutorial; + +namespace SVSim.EmulatedEntrypoint.Controllers; + +/// +/// Tutorial step bookkeeping. The tutorial itself runs entirely client-side +/// (StoryTutorial*BattleMgr per class); the server only persists step transitions. +/// +public class TutorialController : SVSimController +{ + [HttpPost("update_action")] + public IActionResult UpdateAction([FromBody] TutorialUpdateActionRequest request) + { + // Fire-and-forget. Client uses SkipAllNetworkChecks; response body is ignored. + // We still emit an empty object so the translation middleware has a `data` payload to wrap. + return new JsonResult(new { }); + } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Requests/Tutorial/TutorialUpdateActionRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Requests/Tutorial/TutorialUpdateActionRequest.cs new file mode 100644 index 0000000..7ef1cc5 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Requests/Tutorial/TutorialUpdateActionRequest.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; +using MessagePack; +using SVSim.EmulatedEntrypoint.Models.Dtos.Requests; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Tutorial; + +/// +/// POST /tutorial/update_action — fire-and-forget sub-step tracking. +/// Client task: Wizard/TutorialUpdateActionTask.cs. SkipAllNetworkChecks is on, +/// so any return value (including failures) is silently ignored. +/// +[MessagePackObject] +public class TutorialUpdateActionRequest : BaseRequest +{ + [JsonPropertyName("tutorial_step")] + [Key("tutorial_step")] + public int TutorialStep { get; set; } + + [JsonPropertyName("tutorial_action_number")] + [Key("tutorial_action_number")] + public int TutorialActionNumber { get; set; } +} diff --git a/SVSim.UnitTests/Controllers/TutorialControllerTests.cs b/SVSim.UnitTests/Controllers/TutorialControllerTests.cs new file mode 100644 index 0000000..ad11db4 --- /dev/null +++ b/SVSim.UnitTests/Controllers/TutorialControllerTests.cs @@ -0,0 +1,36 @@ +using System.Net; +using System.Text; +using System.Text.Json; +using NUnit.Framework; +using SVSim.UnitTests.Infrastructure; + +namespace SVSim.UnitTests.Controllers; + +public class TutorialControllerTests +{ + [Test] + public async Task UpdateAction_returns_result_code_1_with_empty_data() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(tutorialState: 0); + using var client = factory.CreateAuthenticatedClient(viewerId); + + // tutorial_step and tutorial_action_number are fire-and-forget bookkeeping fields; + // send representative values from the live capture (step=1, action=2). + var requestJson = + """{"tutorial_step":1,"tutorial_action_number":2,"viewer_id":"0","steam_id":0,"steam_session_ticket":""}"""; + + var response = await client.PostAsync("/tutorial/update_action", + new StringContent(requestJson, Encoding.UTF8, "application/json")); + + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + var body = await response.Content.ReadAsStringAsync(); + + // Controllers return the INNER data payload; envelope is middleware's job. + // For the no-op shape the action returns an empty object. + using var doc = JsonDocument.Parse(body); + Assert.That(doc.RootElement.ValueKind, Is.EqualTo(JsonValueKind.Object)); + Assert.That(doc.RootElement.EnumerateObject().Count(), Is.EqualTo(0), + "update_action returns empty data — client uses SkipAllNetworkChecks and reads nothing."); + } +}