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:
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user