fix(friend): add BaseRequest body param to 6 body-less actions

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 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-10 08:54:50 -04:00
parent b54a47f333
commit b4aa07577f
2 changed files with 23 additions and 15 deletions

View File

@@ -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<ActionResult<FriendInfoResponse>> Info(CancellationToken ct)
public async Task<ActionResult<FriendInfoResponse>> 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<ActionResult<ReceiveApplyInfoResponse>> ReceiveApplyInfo(CancellationToken ct)
public async Task<ActionResult<ReceiveApplyInfoResponse>> 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<ActionResult<SendApplyInfoResponse>> SendApplyInfo(CancellationToken ct)
public async Task<ActionResult<SendApplyInfoResponse>> 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<ActionResult<PlayedTogetherInfoResponse>> PlayedTogetherInfo(CancellationToken ct)
public async Task<ActionResult<PlayedTogetherInfoResponse>> 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<IActionResult> RejectApplyAll(CancellationToken ct)
public async Task<IActionResult> 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<IActionResult> CancelApplyAll(CancellationToken ct)
public async Task<IActionResult> CancelApplyAll([FromBody] BaseRequest _, CancellationToken ct)
{
if (!TryGetViewerId(out var viewerId)) return Unauthorized();
await _friend.CancelAllAppliesAsync(viewerId, ct);

View File

@@ -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<long> 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<string> 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");