feat: Update room cloning API to accept multiple content types for name input; enhance compatibility with 2023 client

This commit is contained in:
Alexa
2026-07-13 01:00:00 +03:00
parent 5460cc3601
commit 1152d9ba48
2 changed files with 41 additions and 6 deletions
+4 -1
View File
@@ -186,9 +186,12 @@ public sealed class RoomSave2023Tests : IClassFixture<DorkNetServerFactory>
private static async Task<long> CloneAndAssertAsync(HttpClient client, long sourceId, string name) private static async Task<long> CloneAndAssertAsync(HttpClient client, long sourceId, string name)
{ {
// The real 2023 client posts the name as x-www-form-urlencoded
// (`name=...`), NOT JSON — testing with JSON would miss the 415 that
// a [FromBody] handler returns for a form POST.
using var resp = await client.PostAsync( using var resp = await client.PostAsync(
$"/rooms/{sourceId}/clone", $"/rooms/{sourceId}/clone",
new StringContent(JsonSerializer.Serialize(new { name }), Encoding.UTF8, "application/json")); new FormUrlEncodedContent(new Dictionary<string, string> { ["name"] = name }));
var text = await resp.Content.ReadAsStringAsync(); var text = await resp.Content.ReadAsStringAsync();
Assert.True(resp.IsSuccessStatusCode, $"clone of {sourceId} -> {(int)resp.StatusCode}: {text}"); Assert.True(resp.IsSuccessStatusCode, $"clone of {sourceId} -> {(int)resp.StatusCode}: {text}");
@@ -959,12 +959,17 @@ public class RoomsModerationController(DorkNetDbContext db) : ControllerBase
/// <see cref="DorkNet.Server.Controllers.API.Rooms.V2.RoomsController"/>; /// <see cref="DorkNet.Server.Controllers.API.Rooms.V2.RoomsController"/>;
/// this handler proxies via the service so we don't duplicate the /// this handler proxies via the service so we don't duplicate the
/// substantial clone-rooms logic.</summary> /// substantial clone-rooms logic.</summary>
// The 2023 client posts the new name as an x-www-form-urlencoded field
// (`name=...`), NOT JSON. A `[FromBody]` parameter demands a JSON
// content-type and makes ASP.NET reject the form POST with 415 BEFORE the
// handler runs → the client reports "Failed to clone room" and boots the
// player to their dorm. Accept all three content types and read the name
// manually from form / JSON body / query.
[HttpPost("rooms/{roomId:long}/clone")] [HttpPost("rooms/{roomId:long}/clone")]
public async Task<IActionResult> BareClone(long roomId, [Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
[FromBody] CloneBareRequest? body, public async Task<IActionResult> BareClone(long roomId)
[FromForm(Name = "Name")] string? formName)
{ {
var newName = body?.Name ?? formName ?? string.Empty; var newName = await ReadCloneNameAsync();
var source = await db.Rooms.FirstOrDefaultAsync(r => r.Id == roomId); var source = await db.Rooms.FirstOrDefaultAsync(r => r.Id == roomId);
if (source is null) return NotFound(); if (source is null) return NotFound();
if (!source.CloningAllowed && source.CreatorPlayerId != Me) if (!source.CloningAllowed && source.CreatorPlayerId != Me)
@@ -1084,7 +1089,34 @@ public class RoomsModerationController(DorkNetDbContext db) : ControllerBase
// room". // room".
return Ok(RoomsController.BuildRoomServerDetails(clone, clonedScenes)); return Ok(RoomsController.BuildRoomServerDetails(clone, clonedScenes));
} }
public sealed class CloneBareRequest { public string? Name { get; set; } } private async Task<string> ReadCloneNameAsync()
{
// Form: name=... (what the 2023 client sends).
if (Request.HasFormContentType)
{
foreach (var k in new[] { "name", "Name" })
{
var v = Request.Form[k].ToString();
if (!string.IsNullOrWhiteSpace(v)) return v;
}
}
else
{
// JSON body: { "name": "..." }.
try
{
Request.EnableBuffering();
Request.Body.Position = 0;
using var doc = await JsonDocument.ParseAsync(Request.Body);
if (doc.RootElement.ValueKind == JsonValueKind.Object)
foreach (var k in new[] { "name", "Name" })
if (doc.RootElement.TryGetProperty(k, out var v) && v.ValueKind == JsonValueKind.String)
return v.GetString() ?? string.Empty;
}
catch { /* non-JSON / empty body */ }
}
return Request.Query["name"].ToString();
}
// ── Bare-path bans aliases ───────────────────────────────────────── // ── Bare-path bans aliases ─────────────────────────────────────────