From a5fe484775ce83ba82c41321c345c615ded26a0b Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sat, 13 Jun 2026 11:35:41 -0400 Subject: [PATCH] feat(account): /account/update_birth persists viewer birth date MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Strict yyyy-MM-dd parse (matches /load/index round-trip format) — rejects malformed input. data_headers.servertime is emitted by the standard envelope, which is what the client reads into BirthDayUpdateServerTime. Co-Authored-By: Claude Opus 4.7 --- .../Controllers/AccountController.cs | 20 +++++++++ .../Account/AccountUpdateBirthRequest.cs | 13 ++++++ .../Controllers/AccountControllerTests.cs | 44 +++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Requests/Account/AccountUpdateBirthRequest.cs diff --git a/SVSim.EmulatedEntrypoint/Controllers/AccountController.cs b/SVSim.EmulatedEntrypoint/Controllers/AccountController.cs index 91a41b12..6c55362d 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/AccountController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/AccountController.cs @@ -57,4 +57,24 @@ public class AccountController : SVSimController // which is a first-set-vs-update signal we have no use for yet. return new EmptyResponse(); } + + [HttpPost("update_birth")] + public async Task> UpdateBirth([FromBody] AccountUpdateBirthRequest request) + { + if (!TryGetViewerId(out long viewerId)) return Unauthorized(); + // Wire format is "yyyy-MM-dd" (see /load/index UserInfo.Birthday round-trip). + // Parse strict; client only ever submits via its date-picker dialog. + if (!DateTime.TryParseExact(request.Birth, "yyyy-MM-dd", + System.Globalization.CultureInfo.InvariantCulture, + System.Globalization.DateTimeStyles.AssumeUniversal | System.Globalization.DateTimeStyles.AdjustToUniversal, + out var birth)) + return BadRequest(new { error = "birth_invalid" }); + + var viewer = await _db.Viewers.FirstAsync(v => v.Id == viewerId); + viewer.Info.BirthDate = birth; + await _db.SaveChangesAsync(); + // data_headers.servertime drives the client's BirthDayUpdateServerTime — the standard + // envelope already emits it, so an empty data payload is sufficient. + return new EmptyResponse(); + } } diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Requests/Account/AccountUpdateBirthRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Requests/Account/AccountUpdateBirthRequest.cs new file mode 100644 index 00000000..5f499529 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Requests/Account/AccountUpdateBirthRequest.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; +using MessagePack; +using SVSim.EmulatedEntrypoint.Models.Dtos.Requests; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Account; + +[MessagePackObject] +public class AccountUpdateBirthRequest : BaseRequest +{ + [JsonPropertyName("birth")] + [Key("birth")] + public string Birth { get; set; } = string.Empty; +} diff --git a/SVSim.UnitTests/Controllers/AccountControllerTests.cs b/SVSim.UnitTests/Controllers/AccountControllerTests.cs index 2eed8634..8b284ab3 100644 --- a/SVSim.UnitTests/Controllers/AccountControllerTests.cs +++ b/SVSim.UnitTests/Controllers/AccountControllerTests.cs @@ -106,4 +106,48 @@ public class AccountControllerTests var viewer = await db.Viewers.FirstAsync(v => v.Id == viewerId); Assert.That(viewer.DisplayName, Is.EqualTo(name)); } + + [Test] + public async Task UpdateBirth_persists_and_round_trips_through_load_index() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(tutorialState: 0); + using var client = factory.CreateAuthenticatedClient(viewerId); + + var setJson = """{"birth":"1995-07-04","viewer_id":"0","steam_id":0,"steam_session_ticket":""}"""; + var setResp = await client.PostAsync("/account/update_birth", + new StringContent(setJson, Encoding.UTF8, "application/json")); + var setBody = await setResp.Content.ReadAsStringAsync(); + Assert.That(setResp.StatusCode, Is.EqualTo(HttpStatusCode.OK), setBody); + + using var scope = factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var viewer = await db.Viewers.FirstAsync(v => v.Id == viewerId); + Assert.That(viewer.Info.BirthDate.ToString("yyyy-MM-dd"), Is.EqualTo("1995-07-04")); + + var loadJson = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","carrier":"none","card_master_hash":""}"""; + var loadResp = await client.PostAsync("/load/index", + new StringContent(loadJson, Encoding.UTF8, "application/json")); + var body = await loadResp.Content.ReadAsStringAsync(); + using var doc = JsonDocument.Parse(body); + Assert.That(doc.RootElement.GetProperty("user_info").GetProperty("birth").GetString(), + Is.EqualTo("1995-07-04"), body); + } + + [TestCase("not-a-date")] + [TestCase("1995/07/04")] + [TestCase("1995-7-4")] + [TestCase("")] + public async Task UpdateBirth_rejects_malformed_input(string birth) + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(tutorialState: 0); + using var client = factory.CreateAuthenticatedClient(viewerId); + + var json = $$"""{"birth":{{JsonSerializer.Serialize(birth)}},"viewer_id":"0","steam_id":0,"steam_session_ticket":""}"""; + var resp = await client.PostAsync("/account/update_birth", + new StringContent(json, Encoding.UTF8, "application/json")); + + Assert.That(resp.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); + } }