mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 15:08:47 -07:00
9c9f1841d6
Cloning was a shallow copy: the new room's scenes kept pointing at the SOURCE room's blob until someone saved over them. So every clone of a room shared one object, a clone read as "already has content" while owning none of it, and the room a player copied stayed load-bearing for their copy indefinitely. Cloning RecCenter produced rooms whose data blob was literally room_100_v1.dat, which is visible in the join log. Each sub-room's bytes are now copied into the clone's own namespace, keyed by the CLONE's room id so BlobRouter files them under that room's prefix. Sub-room 0 takes the canonical default name, which matters because the details builder falls back to exactly that name when a room has no recorded blob — writing the copy there means both the recorded and the fallback path resolve to real content. Each copy also gets a RoomDataBlobs row so it is tracked like any other save. Two cases deliberately do not clear a reference: - With no object store configured there is nothing to copy, and a blob name is only a reference; clearing it would throw away the player's edits, which is strictly worse than the shared reference. That path keeps the old shallow behaviour, which is also what the existing user-room clone test asserts. - A source object that is missing or unreadable leaves the clone empty rather than failing the request. The room and its sub-rooms are already committed by that point, so a hard failure would strand a half-made room, and an unsaved source clones to an empty room anyway. Everywhere else a reference is either replaced by a real copy or cleared — it is never left pointing into another room.
89 lines
4.0 KiB
C#
89 lines
4.0 KiB
C#
using System.Net;
|
|
using System.Net.Http.Headers;
|
|
using System.Text.Json;
|
|
|
|
namespace DorkNet.Server.Tests;
|
|
|
|
/// <summary>
|
|
/// Starting a chat with a player is a two-step handshake, and both steps have to
|
|
/// work on a thread that has never carried a message:
|
|
///
|
|
/// POST thread/withmembers?ids={playerId} -> creates/returns the thread
|
|
/// GET thread/{id} -> opens it
|
|
///
|
|
/// Observed failing in production: the POST returned 200 at 06:38:20.862 and the
|
|
/// GET 404'd 415ms later, because the handler decided a thread existed by
|
|
/// counting its messages. A brand-new conversation has none, so the client was
|
|
/// handed a thread id it was then told did not exist, and no chat could ever be
|
|
/// opened with someone you had not already messaged.
|
|
/// </summary>
|
|
public sealed class ChatStartConversationTests : IClassFixture<DorkNetServerFactory>
|
|
{
|
|
private readonly DorkNetServerFactory _factory;
|
|
|
|
public ChatStartConversationTests(DorkNetServerFactory factory) => _factory = factory;
|
|
|
|
[Fact]
|
|
public async Task Brand_new_conversation_can_be_opened_before_any_message_is_sent()
|
|
{
|
|
using var starterClient = Client();
|
|
using var otherClient = Client();
|
|
|
|
var starter = await GameClientSessionFactory.CreateAsync(starterClient, _factory.ApexDomain);
|
|
var other = await GameClientSessionFactory.CreateAsync(otherClient, _factory.ApexDomain);
|
|
|
|
starterClient.DefaultRequestHeaders.Authorization =
|
|
new AuthenticationHeaderValue("Bearer", starter.AccessToken);
|
|
|
|
// Step 1 — the client asks for the thread with that player. It sends the
|
|
// ids as a form body (observed on the wire as
|
|
// "ids=9741717&messageCount=50"), which also matters for routing: the
|
|
// content type is what separates this from POST thread/{chatThreadId},
|
|
// whose parameter segment otherwise also matches "withmembers".
|
|
using var form = new FormUrlEncodedContent(
|
|
[
|
|
new KeyValuePair<string, string>("ids", other.PlayerId.ToString()),
|
|
new KeyValuePair<string, string>("messageCount", "50"),
|
|
]);
|
|
using var createResponse = await starterClient.PostAsync("/thread/withmembers", form);
|
|
var createBody = await createResponse.Content.ReadAsStringAsync();
|
|
Assert.True(createResponse.IsSuccessStatusCode,
|
|
$"POST /thread/withmembers -> {(int)createResponse.StatusCode}: {createBody}");
|
|
|
|
var thread = JsonDocument.Parse(createBody).RootElement;
|
|
Assert.True(thread.TryGetProperty("ChatThreadId", out var idElement)
|
|
|| thread.TryGetProperty("chatThreadId", out idElement),
|
|
$"no thread id in the withmembers reply: {createBody}");
|
|
var threadId = idElement.ValueKind == JsonValueKind.String
|
|
? idElement.GetString()
|
|
: idElement.ToString();
|
|
Assert.False(string.IsNullOrWhiteSpace(threadId));
|
|
|
|
// Step 2 — and immediately opens it. No message has been sent yet.
|
|
using var openResponse = await starterClient.GetAsync($"/thread/{threadId}?maxCount=50");
|
|
var openBody = await openResponse.Content.ReadAsStringAsync();
|
|
|
|
Assert.True(openResponse.StatusCode != HttpStatusCode.NotFound,
|
|
$"""
|
|
GET /thread/{threadId} returned 404 for a thread that thread/withmembers
|
|
had just created. An empty thread is not a missing thread — the chat
|
|
cannot be opened until someone has already sent a message, which is
|
|
impossible to do first.
|
|
""");
|
|
Console.WriteLine("=== WITHMEMBERS ===");
|
|
Console.WriteLine(createBody);
|
|
Console.WriteLine("=== GET THREAD ===");
|
|
Console.WriteLine(openBody);
|
|
|
|
Assert.True(openResponse.IsSuccessStatusCode,
|
|
$"GET /thread/{threadId} -> {(int)openResponse.StatusCode}: {openBody}");
|
|
}
|
|
|
|
private HttpClient Client()
|
|
{
|
|
var client = _factory.CreateClient(new() { AllowAutoRedirect = false });
|
|
client.BaseAddress = new Uri($"http://chat.{_factory.ApexDomain}");
|
|
return client;
|
|
}
|
|
}
|