From b4aa07577f9db8db2224dab79c518459fcc31fb4 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Wed, 10 Jun 2026 08:54:50 -0400 Subject: [PATCH] fix(friend): add BaseRequest body param to 6 body-less actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ShadowverseTranslationMiddleware throws InvalidOperationException when a Unity client posts an encrypted msgpack body to an action with zero [FromBody] parameters — it has no target type for the deserializer. Tests pass because they post JSON directly with no UnityPlayer UA and the middleware short-circuits. Same defect already fixed on /replay/info in 216dcab; this catches up the friend system shipped 2026-06-09. Fixed actions: info, receive_apply_info, send_apply_info, played_together_info, reject_apply_all, cancel_apply_all. Tests updated to post the BaseRequest auth fields so [ApiController] model validation passes (BaseRequest.ViewerId is non-nullable string). Co-Authored-By: Claude Opus 4.7 --- .../Controllers/FriendController.cs | 13 +++++----- .../Controllers/FriendControllerTests.cs | 25 ++++++++++++------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/SVSim.EmulatedEntrypoint/Controllers/FriendController.cs b/SVSim.EmulatedEntrypoint/Controllers/FriendController.cs index 527718c..59560f2 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/FriendController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/FriendController.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using SVSim.Database.Services.Friend; using SVSim.EmulatedEntrypoint.Models.Dtos.Friend; +using SVSim.EmulatedEntrypoint.Models.Dtos.Requests; namespace SVSim.EmulatedEntrypoint.Controllers; @@ -17,7 +18,7 @@ public sealed class FriendController : SVSimController public FriendController(IFriendService friend) => _friend = friend; [HttpPost("info")] - public async Task> Info(CancellationToken ct) + public async Task> Info([FromBody] BaseRequest _, CancellationToken ct) { if (!TryGetViewerId(out var viewerId)) return Unauthorized(); var result = await _friend.GetFriendsAsync(viewerId, ct); @@ -30,7 +31,7 @@ public sealed class FriendController : SVSimController } [HttpPost("receive_apply_info")] - public async Task> ReceiveApplyInfo(CancellationToken ct) + public async Task> ReceiveApplyInfo([FromBody] BaseRequest _, CancellationToken ct) { if (!TryGetViewerId(out var viewerId)) return Unauthorized(); var result = await _friend.GetReceiveAppliesAsync(viewerId, ct); @@ -42,7 +43,7 @@ public sealed class FriendController : SVSimController } [HttpPost("send_apply_info")] - public async Task> SendApplyInfo(CancellationToken ct) + public async Task> SendApplyInfo([FromBody] BaseRequest _, CancellationToken ct) { if (!TryGetViewerId(out var viewerId)) return Unauthorized(); var result = await _friend.GetSendAppliesAsync(viewerId, ct); @@ -55,7 +56,7 @@ public sealed class FriendController : SVSimController } [HttpPost("played_together_info")] - public async Task> PlayedTogetherInfo(CancellationToken ct) + public async Task> PlayedTogetherInfo([FromBody] BaseRequest _, CancellationToken ct) { if (!TryGetViewerId(out var viewerId)) return Unauthorized(); var result = await _friend.GetPlayedTogetherAsync(viewerId, ct); @@ -109,7 +110,7 @@ public sealed class FriendController : SVSimController } [HttpPost("reject_apply_all")] - public async Task RejectApplyAll(CancellationToken ct) + public async Task RejectApplyAll([FromBody] BaseRequest _, CancellationToken ct) { if (!TryGetViewerId(out var viewerId)) return Unauthorized(); await _friend.RejectAllAppliesAsync(viewerId, ct); @@ -117,7 +118,7 @@ public sealed class FriendController : SVSimController } [HttpPost("cancel_apply_all")] - public async Task CancelApplyAll(CancellationToken ct) + public async Task CancelApplyAll([FromBody] BaseRequest _, CancellationToken ct) { if (!TryGetViewerId(out var viewerId)) return Unauthorized(); await _friend.CancelAllAppliesAsync(viewerId, ct); diff --git a/SVSim.UnitTests/Controllers/FriendControllerTests.cs b/SVSim.UnitTests/Controllers/FriendControllerTests.cs index e16c696..9efb286 100644 --- a/SVSim.UnitTests/Controllers/FriendControllerTests.cs +++ b/SVSim.UnitTests/Controllers/FriendControllerTests.cs @@ -13,6 +13,13 @@ public class FriendControllerTests { private static StringContent JsonBody(string json) => new(json, Encoding.UTF8, "application/json"); + // Minimal BaseRequest-shaped payload. Body-less /friend/* actions now declare + // [FromBody] BaseRequest _ so the prod translation middleware can deserialize + // the encrypted msgpack body (it requires at least one parameter). Tests post + // these auth fields so [ApiController] model validation passes — the actual + // viewer_id comes from the session claim, not the body. + private const string EmptyBody = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":""}"""; + private static async Task SeedViewer(SVSimTestFactory factory, ulong steamId, string name = "Test Viewer") => await factory.SeedViewerAsync(steamId: steamId, displayName: name); @@ -23,7 +30,7 @@ public class FriendControllerTests long viewerId = await SeedViewer(factory, 76_561_198_000_010_001UL); using var client = factory.CreateAuthenticatedClient(viewerId); - var response = await client.PostAsync("/friend/info", JsonBody("{}")); + var response = await client.PostAsync("/friend/info", JsonBody(EmptyBody)); var raw = await response.Content.ReadAsStringAsync(); Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), raw); @@ -40,7 +47,7 @@ public class FriendControllerTests long viewerId = await SeedViewer(factory, 76_561_198_000_010_002UL); using var client = factory.CreateAuthenticatedClient(viewerId); - var response = await client.PostAsync("/friend/receive_apply_info", JsonBody("{}")); + var response = await client.PostAsync("/friend/receive_apply_info", JsonBody(EmptyBody)); var raw = await response.Content.ReadAsStringAsync(); Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), raw); @@ -56,7 +63,7 @@ public class FriendControllerTests long viewerId = await SeedViewer(factory, 76_561_198_000_010_003UL); using var client = factory.CreateAuthenticatedClient(viewerId); - var response = await client.PostAsync("/friend/send_apply_info", JsonBody("{}")); + var response = await client.PostAsync("/friend/send_apply_info", JsonBody(EmptyBody)); var raw = await response.Content.ReadAsStringAsync(); Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), raw); @@ -73,7 +80,7 @@ public class FriendControllerTests long viewerId = await SeedViewer(factory, 76_561_198_000_010_004UL); using var client = factory.CreateAuthenticatedClient(viewerId); - var response = await client.PostAsync("/friend/played_together_info", JsonBody("{}")); + var response = await client.PostAsync("/friend/played_together_info", JsonBody(EmptyBody)); var raw = await response.Content.ReadAsStringAsync(); Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), raw); @@ -223,7 +230,7 @@ public class FriendControllerTests } using var client = factory.CreateAuthenticatedClient(me); - var response = await client.PostAsync("/friend/reject_apply_all", JsonBody("{}")); + var response = await client.PostAsync("/friend/reject_apply_all", JsonBody(EmptyBody)); Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); using var verifyScope = factory.Services.CreateScope(); @@ -245,7 +252,7 @@ public class FriendControllerTests } using var client = factory.CreateAuthenticatedClient(me); - var response = await client.PostAsync("/friend/cancel_apply_all", JsonBody("{}")); + var response = await client.PostAsync("/friend/cancel_apply_all", JsonBody(EmptyBody)); Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); using var verifyScope = factory.Services.CreateScope(); @@ -281,7 +288,7 @@ public class FriendControllerTests using var factory = new SVSimTestFactory(); var client = factory.CreateClient(); - var response = await client.PostAsync("/friend/info", JsonBody("{}")); + var response = await client.PostAsync("/friend/info", JsonBody(EmptyBody)); Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized)); } @@ -303,7 +310,7 @@ public class FriendControllerTests int applyId; using (var clientB = factory.CreateAuthenticatedClient(viewerB)) { - var resp = await clientB.PostAsync("/friend/receive_apply_info", JsonBody("{}")); + var resp = await clientB.PostAsync("/friend/receive_apply_info", JsonBody(EmptyBody)); var raw = await resp.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(raw); var applies = doc.RootElement.GetProperty("receive_applies"); @@ -323,7 +330,7 @@ public class FriendControllerTests async Task GetFriendName(long ownerId) { using var client = factory.CreateAuthenticatedClient(ownerId); - var resp = await client.PostAsync("/friend/info", JsonBody("{}")); + var resp = await client.PostAsync("/friend/info", JsonBody(EmptyBody)); var raw = await resp.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(raw); var friends = doc.RootElement.GetProperty("friends");