From ad5c9e91ae2c2abe0ef93c6f912de7ea13265326 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Thu, 28 May 2026 18:04:28 -0400 Subject: [PATCH] feat(download_time): stub start/end endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spec at docs/api-spec/endpoints/post-login/download_time-{start,end}.md already documented both endpoints fully against the decompiled Wizard/DownloadStartTask.cs and Wizard/DownloadFinishTask.cs — the controller side was the gap. The client fires /download_time/start before kicking off an Akamai asset bundle download and /download_time/end on completion. Both are pure telemetry from our perspective. When NukeIdentityOnStartup wipes PlayerPrefs broadly (the pre-narrow loader behaviour), the client decides it needs to download tutorial assets, calls /download_time/start, and a 404 there surfaces as an HTTP error popup before the download proceeds. Stubbing with empty data:{} bodies plus result_code:1 is the documented minimum-viable response. Acts as belt-and-suspenders against the narrow IdentityWipe (which preserves the cache index so downloads shouldn't trigger) ever being bypassed by a different code path. Co-Authored-By: Claude Opus 4.7 --- .../Controllers/DownloadTimeController.cs | 36 +++++++++++++++++++ .../DownloadTimeControllerTests.cs | 33 +++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 SVSim.EmulatedEntrypoint/Controllers/DownloadTimeController.cs create mode 100644 SVSim.UnitTests/Controllers/DownloadTimeControllerTests.cs diff --git a/SVSim.EmulatedEntrypoint/Controllers/DownloadTimeController.cs b/SVSim.EmulatedEntrypoint/Controllers/DownloadTimeController.cs new file mode 100644 index 0000000..02ce447 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Controllers/DownloadTimeController.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Mvc; +using SVSim.EmulatedEntrypoint.Models.Dtos.Requests; + +namespace SVSim.EmulatedEntrypoint.Controllers; + +/// +/// /download_time/* — asset-download timing telemetry. The client fires +/// POST /download_time/start right before kicking off an Akamai asset bundle +/// download (Wizard/DownloadStartTask.cs) and POST /download_time/end when +/// it completes (Wizard/DownloadFinishTask.cs). Both are pure telemetry from our +/// perspective — we don't track download timings — but the client surfaces an HTTP error +/// dialog if either 404s, so we ack with empty data: {} bodies. +/// +/// Explicit because the base controller token would +/// resolve to /downloadtime, missing the underscore. +/// +[Route("download_time")] +public class DownloadTimeController : SVSimController +{ + /// + /// Spec: docs/api-spec/endpoints/post-login/download_time-start.md. The client's + /// DownloadStartTask.Parse reads an optional image_type string + /// ("card" → CardDetail loading-screen art, "still" → StoryDetail, anything + /// else → default). We omit it; the client falls through to the default art. + /// + [HttpPost("start")] + public IActionResult Start([FromBody] BaseRequest request) => Ok(new { }); + + /// + /// Spec: docs/api-spec/endpoints/post-login/download_time-end.md. The client's + /// DownloadFinishTask doesn't override Parse at all — only result_code + /// matters. Empty data is the documented minimum-viable response. + /// + [HttpPost("end")] + public IActionResult End([FromBody] BaseRequest request) => Ok(new { }); +} diff --git a/SVSim.UnitTests/Controllers/DownloadTimeControllerTests.cs b/SVSim.UnitTests/Controllers/DownloadTimeControllerTests.cs new file mode 100644 index 0000000..1cf922d --- /dev/null +++ b/SVSim.UnitTests/Controllers/DownloadTimeControllerTests.cs @@ -0,0 +1,33 @@ +using System.Net; +using System.Text; +using System.Text.Json; +using NUnit.Framework; +using SVSim.UnitTests.Infrastructure; + +namespace SVSim.UnitTests.Controllers; + +public class DownloadTimeControllerTests +{ + [TestCase("/download_time/start")] + [TestCase("/download_time/end")] + public async Task Returns_200_with_empty_data_object(string path) + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + using var client = factory.CreateAuthenticatedClient(viewerId); + + var requestJson = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":""}"""; + + var response = await client.PostAsync(path, + new StringContent(requestJson, Encoding.UTF8, "application/json")); + + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), + await response.Content.ReadAsStringAsync()); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + Assert.That(doc.RootElement.ValueKind, Is.EqualTo(JsonValueKind.Object)); + Assert.That(doc.RootElement.EnumerateObject().Count(), Is.EqualTo(0), + "Spec calls for empty `data: {}` — DownloadStartTask's optional image_type stays " + + "absent, DownloadFinishTask doesn't read data at all."); + } +}