mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
feat: Add regression tests for room cloning functionality; ensure cloned rooms return full details
This commit is contained in:
@@ -130,6 +130,76 @@ public sealed class RoomSave2023Tests : IClassFixture<DorkNetServerFactory>
|
|||||||
$"expected a RoomDataBlobs row for {filename}");
|
$"expected a RoomDataBlobs row for {filename}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Regression: cloning a room — and then cloning the CLONE — must both
|
||||||
|
/// succeed and return the full room-details shape (FGCPNAACHIK). Previously
|
||||||
|
/// the handler relied on DB auto-increment for the room id AND the copied
|
||||||
|
/// scene ids, which collide with the seeded id range once the identity
|
||||||
|
/// sequence lags → the clone POST 500s and the client boots the player to
|
||||||
|
/// their dorm ("Failed to copy room: Failed to clone room").
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task Cloning_a_room_and_then_the_clone_both_succeed()
|
||||||
|
{
|
||||||
|
using var setupClient = ApiClient();
|
||||||
|
var owner = await GameClientSessionFactory.CreateAsync(setupClient, _factory.ApexDomain);
|
||||||
|
|
||||||
|
var roomId = 9_300_777L;
|
||||||
|
await using (var scope = _factory.Services.CreateAsyncScope())
|
||||||
|
{
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<DorkNetDbContext>();
|
||||||
|
if (!await db.Rooms.AnyAsync(r => r.Id == roomId))
|
||||||
|
{
|
||||||
|
db.Rooms.Add(new RoomEntity
|
||||||
|
{
|
||||||
|
Id = roomId,
|
||||||
|
Name = $"CloneSrc_{Guid.NewGuid():N}"[..20],
|
||||||
|
Description = "clone source",
|
||||||
|
CreatorPlayerId = owner.PlayerId,
|
||||||
|
ImageName = RoomService.DefaultRoomImageName,
|
||||||
|
Accessibility = 1,
|
||||||
|
IsAGRoom = false,
|
||||||
|
IsDormRoom = false,
|
||||||
|
CloningAllowed = true,
|
||||||
|
CurrentDataBlobName = $"room_{roomId}_v1.dat",
|
||||||
|
});
|
||||||
|
db.RoomScenes.Add(new RoomSceneEntity
|
||||||
|
{
|
||||||
|
RoomId = roomId,
|
||||||
|
OrderIndex = 0,
|
||||||
|
Name = "Main",
|
||||||
|
DataBlobName = $"room_{roomId}_v1.dat",
|
||||||
|
});
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using var roomsClient = ApiClient(owner, subdomain: "rooms");
|
||||||
|
|
||||||
|
var firstCloneId = await CloneAndAssertAsync(roomsClient, roomId, "FirstClone");
|
||||||
|
Assert.NotEqual(roomId, firstCloneId);
|
||||||
|
|
||||||
|
// The regression: cloning the clone.
|
||||||
|
var secondCloneId = await CloneAndAssertAsync(roomsClient, firstCloneId, "SecondClone");
|
||||||
|
Assert.NotEqual(firstCloneId, secondCloneId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<long> CloneAndAssertAsync(HttpClient client, long sourceId, string name)
|
||||||
|
{
|
||||||
|
using var resp = await client.PostAsync(
|
||||||
|
$"/rooms/{sourceId}/clone",
|
||||||
|
new StringContent(JsonSerializer.Serialize(new { name }), Encoding.UTF8, "application/json"));
|
||||||
|
var text = await resp.Content.ReadAsStringAsync();
|
||||||
|
Assert.True(resp.IsSuccessStatusCode, $"clone of {sourceId} -> {(int)resp.StatusCode}: {text}");
|
||||||
|
|
||||||
|
using var json = JsonDocument.Parse(text);
|
||||||
|
var root = json.RootElement;
|
||||||
|
// Full room-details shape (FGCPNAACHIK), not a status wrapper.
|
||||||
|
foreach (var key in new[] { "RoomId", "SubRooms", "Roles" })
|
||||||
|
Assert.True(root.TryGetProperty(key, out _), $"clone response missing '{key}': {text}");
|
||||||
|
return root.GetProperty("RoomId").GetInt64();
|
||||||
|
}
|
||||||
|
|
||||||
private HttpClient ApiClient(GameClientSession? session = null, string subdomain = "api")
|
private HttpClient ApiClient(GameClientSession? session = null, string subdomain = "api")
|
||||||
{
|
{
|
||||||
var client = _factory.CreateClient(new() { AllowAutoRedirect = false });
|
var client = _factory.CreateClient(new() { AllowAutoRedirect = false });
|
||||||
|
|||||||
@@ -1046,8 +1046,14 @@ public class RoomsModerationController(DorkNetDbContext db) : ControllerBase
|
|||||||
.Where(s => s.RoomId == source.Id)
|
.Where(s => s.RoomId == source.Id)
|
||||||
.OrderBy(s => s.OrderIndex)
|
.OrderBy(s => s.OrderIndex)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
// Scene ids need to be explicit for the same reason room ids do —
|
||||||
|
// RoomSceneEntity.Id is an auto/identity column whose Postgres sequence
|
||||||
|
// lags behind the manually-seeded rows, so relying on DB generation
|
||||||
|
// collides and the whole clone SaveChanges 500s.
|
||||||
|
var nextSceneId = await db.RoomScenes.MaxAsync(s => (long?)s.Id) ?? 0L;
|
||||||
var clonedScenes = sourceScenes.Select(s => new RoomSceneEntity
|
var clonedScenes = sourceScenes.Select(s => new RoomSceneEntity
|
||||||
{
|
{
|
||||||
|
Id = ++nextSceneId,
|
||||||
RoomId = clone.Id,
|
RoomId = clone.Id,
|
||||||
OrderIndex = s.OrderIndex,
|
OrderIndex = s.OrderIndex,
|
||||||
Name = s.Name,
|
Name = s.Name,
|
||||||
@@ -1069,10 +1075,14 @@ public class RoomsModerationController(DorkNetDbContext db) : ControllerBase
|
|||||||
|
|
||||||
// The 2023 client's clone (RecNet.Runtime NLDBPDCNNCF.GDHIIAHCBMN)
|
// The 2023 client's clone (RecNet.Runtime NLDBPDCNNCF.GDHIIAHCBMN)
|
||||||
// deserializes the response as the FULL room-details object
|
// deserializes the response as the FULL room-details object
|
||||||
// (FGCPNAACHIK — the same type get-by-id / rename return), NOT the
|
// (FGCPNAACHIK — the same type get-by-id / rename return on the
|
||||||
// slim list-room shape. Returning ToWireRoom here made the strict
|
// roomserver host), NOT the slim list-room shape. This is the
|
||||||
// reader fail → "Failed to copy room: Failed to clone room".
|
// roomserver-family path (rooms/{id}/clone), so use
|
||||||
return Ok(RoomsController.BuildRoomDetails(clone, clonedScenes));
|
// BuildRoomServerDetails — the exact shape GET rooms/{id} returns for
|
||||||
|
// the 2023 client. Returning ToWireRoom (or the wrong details variant)
|
||||||
|
// made the strict reader fail → "Failed to copy room: Failed to clone
|
||||||
|
// room".
|
||||||
|
return Ok(RoomsController.BuildRoomServerDetails(clone, clonedScenes));
|
||||||
}
|
}
|
||||||
public sealed class CloneBareRequest { public string? Name { get; set; } }
|
public sealed class CloneBareRequest { public string? Name { get; set; } }
|
||||||
|
|
||||||
|
|||||||
@@ -564,11 +564,23 @@ public class CdnController(
|
|||||||
r.IsAGRoom && r.CurrentDataBlobName == fileName))
|
r.IsAGRoom && r.CurrentDataBlobName == fileName))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
if (fileName.StartsWith("room_", StringComparison.OrdinalIgnoreCase) &&
|
// Synthetic/default room-data blob names for rooms with no saved blob:
|
||||||
fileName.EndsWith("_v1.dat", StringComparison.OrdinalIgnoreCase) &&
|
// the CURRENT form is room_{id}_dorknet_v8.dat
|
||||||
long.TryParse(fileName[5..^7], out var roomId) &&
|
// (RoomService.SyntheticDefaultRoomDataBlobName) and the legacy form is
|
||||||
|
// room_{id}_v1.dat. Baked RRO rooms (empty CurrentDataBlobName) load
|
||||||
|
// their permissions from this synthetic blob, so it MUST overlay too —
|
||||||
|
// otherwise MakerPen only appears in RRO rooms that happen to have a
|
||||||
|
// real saved blob (which is why it worked for Rec Center / Stunt Runner
|
||||||
|
// but not the rest). Parse the id from any room_{id}_* form.
|
||||||
|
if (fileName.StartsWith("room_", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var rest = fileName["room_".Length..];
|
||||||
|
var underscore = rest.IndexOf('_');
|
||||||
|
if (underscore > 0 &&
|
||||||
|
long.TryParse(rest[..underscore], out var roomId) &&
|
||||||
await db.Rooms.AsNoTracking().AnyAsync(r => r.Id == roomId && r.IsAGRoom))
|
await db.Rooms.AsNoTracking().AnyAsync(r => r.Id == roomId && r.IsAGRoom))
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return await (
|
return await (
|
||||||
from scene in db.RoomScenes.AsNoTracking()
|
from scene in db.RoomScenes.AsNoTracking()
|
||||||
|
|||||||
Reference in New Issue
Block a user