From 3968adfcc401cc54c3669478c6c6f645cf9c1223 Mon Sep 17 00:00:00 2001
From: Alexa <53471801+Alexa-RR@users.noreply.github.com>
Date: Tue, 28 Jul 2026 10:04:54 +0300
Subject: [PATCH] test: cover starting a chat with a player end to end
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Reproduces the two-step handshake the client performs — POST thread/withmembers
to create the thread, then GET thread/{id} to open it — on a conversation that
has never carried a message, which is the case that was 404ing.
The request is sent as a form body rather than a query string, matching the
wire ("ids=9741717&messageCount=50"). That detail is load-bearing for routing,
not just fidelity: POST thread/{chatThreadId} also matches the literal path
thread/withmembers, and the content type is what separates them. Posting with
no body makes the two indistinguishable and the request fails as an ambiguous
match across CreateGroup, WithMembersForm and SendMessageForm.
---
.../ChatStartConversationTests.cs | 83 +++++++++++++++++++
1 file changed, 83 insertions(+)
create mode 100644 DorkNet.Server.Tests/ChatStartConversationTests.cs
diff --git a/DorkNet.Server.Tests/ChatStartConversationTests.cs b/DorkNet.Server.Tests/ChatStartConversationTests.cs
new file mode 100644
index 0000000..f67e8ab
--- /dev/null
+++ b/DorkNet.Server.Tests/ChatStartConversationTests.cs
@@ -0,0 +1,83 @@
+using System.Net;
+using System.Net.Http.Headers;
+using System.Text.Json;
+
+namespace DorkNet.Server.Tests;
+
+///
+/// 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.
+///
+public sealed class ChatStartConversationTests : IClassFixture
+{
+ 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("ids", other.PlayerId.ToString()),
+ new KeyValuePair("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.
+ """);
+ 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;
+ }
+}