Files
SVSimServer/SVSim.UnitTests/Services/Guild/GuildChatServicePostTests.cs
gamer147 ab1e23b7cb feat(guild_chat): post text + stamp
Implements PostTextOrStampAsync: validates membership (NotInGuild),
validates stamp id against UsableStampList (PermissionDenied), rejects
type other than 0 or 1, allocates next per-guild message_id, appends row.
Wires /guild_chat/post controller action mapping failures to result_code=2.
Response is GuildChatPostResponse (empty body per spec). 7 integration tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 14:58:18 -04:00

301 lines
14 KiB
C#

using System.Net.Http.Json;
using System.Text.Json;
using SVSim.Database.Entities.Guild;
using SVSim.UnitTests.Infrastructure;
namespace SVSim.UnitTests.Services.Guild;
/// <summary>
/// Integration tests for POST /guild_chat/post — NORMAL text and STAMP messages.
/// </summary>
public class GuildChatServicePostTests
{
private const string Vid = "0";
private const int Sid = 0;
private const string Stk = "";
private static object BaseBody() => new { viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk };
private static int GetStringifiedInt(JsonElement el, string prop)
=> int.Parse(el.GetProperty(prop).GetString()!);
private static long GetStringifiedLong(JsonElement el, string prop)
=> long.Parse(el.GetProperty(prop).GetString()!);
// ─── Helper: create a guild and return its guild_id ──────────────────────
private static async Task<int> CreateGuildAsync(HttpClient client, string name, int joinCondition = 1)
{
var createResp = await client.PostAsync("/guild/create",
JsonContent.Create(new
{
guild_name = name,
activity = 1,
join_condition = joinCondition,
viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk
}));
Assert.That(createResp.IsSuccessStatusCode, Is.True,
$"create failed: {await createResp.Content.ReadAsStringAsync()}");
var infoResp = await client.PostAsync("/guild/info",
JsonContent.Create(BaseBody()));
var infoJson = await infoResp.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(infoJson);
return GetStringifiedInt(doc.RootElement.GetProperty("guild").GetProperty("detail"), "guild_id");
}
// ─── Helper: POST /guild_chat/post ────────────────────────────────────────
private static async Task<(bool ok, string body)> PostAsync(
HttpClient client,
int type,
string message)
{
var resp = await client.PostAsync("/guild_chat/post",
JsonContent.Create(new
{
type,
message,
viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk
}));
var body = await resp.Content.ReadAsStringAsync();
return (resp.IsSuccessStatusCode, body);
}
// ─── Helper: poll messages ────────────────────────────────────────────────
private static async Task<JsonElement> PollAsync(HttpClient client, int startMessageId = 0, int direction = 1)
{
var resp = await client.PostAsync("/guild_chat/messages",
JsonContent.Create(new
{
start_message_id = startMessageId,
direction = direction,
wait_interval = 3,
viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk
}));
Assert.That(resp.IsSuccessStatusCode, Is.True,
$"messages HTTP failed: {await resp.Content.ReadAsStringAsync()}");
var json = await resp.Content.ReadAsStringAsync();
return JsonDocument.Parse(json).RootElement.Clone();
}
// =========================================================================
// Test 1: Member posts NORMAL — message appears with next message_id
// =========================================================================
[Test]
public async Task Post_normal_message_appears_in_chat()
{
using var factory = new SVSimTestFactory();
long leaderId = await factory.SeedViewerAsync(76_561_198_600_000_001UL, "PostLeader1");
using var leaderClient = factory.CreateAuthenticatedClient(leaderId);
await CreateGuildAsync(leaderClient, "PostGuild1");
var (ok, _) = await PostAsync(leaderClient, type: 0, message: "hello world");
Assert.That(ok, Is.True, "POST /guild_chat/post should return 200");
var root = await PollAsync(leaderClient, startMessageId: 0, direction: 1 /* OLD */);
var messages = root.GetProperty("chat_message");
// At least: CreateGuild + our Normal message
Assert.That(messages.GetArrayLength(), Is.GreaterThanOrEqualTo(2), "Expected at least CreateGuild + Normal msg");
bool foundNormal = false;
for (int i = 0; i < messages.GetArrayLength(); i++)
{
int msgType = GetStringifiedInt(messages[i], "message_type");
if (msgType == (int)GuildChatMessageType.Normal)
{
foundNormal = true;
Assert.That(messages[i].GetProperty("message").GetString(), Is.EqualTo("hello world"),
"Normal message body must match");
Assert.That(GetStringifiedLong(messages[i], "message_id"), Is.GreaterThan(0),
"message_id must be positive");
}
}
Assert.That(foundNormal, Is.True, "A Normal message must appear in the chat window");
}
// =========================================================================
// Test 2: Member posts STAMP with valid id — type=1, body is stringified stamp id
// =========================================================================
[Test]
public async Task Post_stamp_with_valid_id_appears_with_type1()
{
using var factory = new SVSimTestFactory();
long leaderId = await factory.SeedViewerAsync(76_561_198_600_000_002UL, "PostLeader2");
using var leaderClient = factory.CreateAuthenticatedClient(leaderId);
await CreateGuildAsync(leaderClient, "PostGuild2");
// ShippedDefaults has stamps 100001..100020; use the first one
var (ok, _) = await PostAsync(leaderClient, type: 1, message: "100001");
Assert.That(ok, Is.True, "POST stamp should return 200");
var root = await PollAsync(leaderClient, startMessageId: 0, direction: 1);
var messages = root.GetProperty("chat_message");
bool foundStamp = false;
for (int i = 0; i < messages.GetArrayLength(); i++)
{
int msgType = GetStringifiedInt(messages[i], "message_type");
if (msgType == (int)GuildChatMessageType.Stamp)
{
foundStamp = true;
Assert.That(messages[i].GetProperty("message").GetString(), Is.EqualTo("100001"),
"Stamp body must be the stringified stamp id");
}
}
Assert.That(foundStamp, Is.True, "A Stamp message (type=1) must appear in the chat window");
}
// =========================================================================
// Test 3: STAMP id NOT in UsableStampList → result_code=2
// =========================================================================
[Test]
public async Task Post_stamp_invalid_id_returns_error()
{
using var factory = new SVSimTestFactory();
long leaderId = await factory.SeedViewerAsync(76_561_198_600_000_003UL, "PostLeader3");
using var leaderClient = factory.CreateAuthenticatedClient(leaderId);
await CreateGuildAsync(leaderClient, "PostGuild3");
var (ok, body) = await PostAsync(leaderClient, type: 1, message: "999999");
Assert.That(ok, Is.True, "HTTP should be 200 even for logic errors");
using var doc = JsonDocument.Parse(body);
Assert.That(doc.RootElement.TryGetProperty("result_code", out var rc), Is.True, "result_code must be present");
Assert.That(rc.GetInt32(), Is.EqualTo(2), "result_code must be 2 for invalid stamp");
}
// =========================================================================
// Test 4: Non-member tries to post → result_code=2 (NotInGuild)
// =========================================================================
[Test]
public async Task Post_non_member_returns_error()
{
using var factory = new SVSimTestFactory();
long leaderId = await factory.SeedViewerAsync(76_561_198_600_000_004UL, "PostLeader4");
long outsiderId = await factory.SeedViewerAsync(76_561_198_600_000_005UL, "PostOutsider4");
using var leaderClient = factory.CreateAuthenticatedClient(leaderId);
using var outsiderClient = factory.CreateAuthenticatedClient(outsiderId);
await CreateGuildAsync(leaderClient, "PostGuild4");
var (ok, body) = await PostAsync(outsiderClient, type: 0, message: "hi from outside");
Assert.That(ok, Is.True, "HTTP 200 even for non-member");
using var doc = JsonDocument.Parse(body);
Assert.That(doc.RootElement.TryGetProperty("result_code", out var rc), Is.True, "result_code must be present");
Assert.That(rc.GetInt32(), Is.EqualTo(2), "result_code must be 2 for non-member");
}
// =========================================================================
// Test 5: Invalid type (e.g., 5 = REPLAY) → result_code=2
// =========================================================================
[Test]
public async Task Post_invalid_type_returns_error()
{
using var factory = new SVSimTestFactory();
long leaderId = await factory.SeedViewerAsync(76_561_198_600_000_006UL, "PostLeader5");
using var leaderClient = factory.CreateAuthenticatedClient(leaderId);
await CreateGuildAsync(leaderClient, "PostGuild5");
var (ok, body) = await PostAsync(leaderClient, type: 5, message: "some replay payload");
Assert.That(ok, Is.True, "HTTP 200 even for invalid type");
using var doc = JsonDocument.Parse(body);
Assert.That(doc.RootElement.TryGetProperty("result_code", out var rc), Is.True, "result_code must be present");
Assert.That(rc.GetInt32(), Is.EqualTo(2), "result_code must be 2 for type=5");
}
// =========================================================================
// Test 6: Post-then-poll — direction=NEW, start_message_id=<last known> returns posted msg
// =========================================================================
[Test]
public async Task Post_then_poll_NEW_returns_just_posted_message()
{
using var factory = new SVSimTestFactory();
long leaderId = await factory.SeedViewerAsync(76_561_198_600_000_007UL, "PostLeader6");
using var leaderClient = factory.CreateAuthenticatedClient(leaderId);
await CreateGuildAsync(leaderClient, "PostGuild6");
// Get the current max message_id (the CreateGuild system event)
var beforePoll = await PollAsync(leaderClient, startMessageId: 0, direction: 1);
var beforeMsgs = beforePoll.GetProperty("chat_message");
int lastKnownId = 0;
for (int i = 0; i < beforeMsgs.GetArrayLength(); i++)
{
int msgId = (int)GetStringifiedLong(beforeMsgs[i], "message_id");
if (msgId > lastKnownId) lastKnownId = msgId;
}
// Post a Normal message
var (ok, _) = await PostAsync(leaderClient, type: 0, message: "post-then-poll test");
Assert.That(ok, Is.True);
// Poll NEW from lastKnownId — should return exactly our posted message
var newPoll = await PollAsync(leaderClient, startMessageId: lastKnownId, direction: 2 /* NEW */);
var newMsgs = newPoll.GetProperty("chat_message");
Assert.That(newMsgs.GetArrayLength(), Is.GreaterThanOrEqualTo(1),
"direction=NEW must return at least the just-posted message");
bool foundPosted = false;
for (int i = 0; i < newMsgs.GetArrayLength(); i++)
{
int msgType = GetStringifiedInt(newMsgs[i], "message_type");
string? body = newMsgs[i].GetProperty("message").GetString();
long msgId = GetStringifiedLong(newMsgs[i], "message_id");
Assert.That(msgId, Is.GreaterThan(lastKnownId),
"direction=NEW must return only messages after lastKnownId");
if (msgType == (int)GuildChatMessageType.Normal && body == "post-then-poll test")
foundPosted = true;
}
Assert.That(foundPosted, Is.True, "The just-posted Normal message must appear in the NEW poll");
}
// =========================================================================
// Test 7: Second post increments message_id
// =========================================================================
[Test]
public async Task Post_twice_second_message_has_higher_message_id()
{
using var factory = new SVSimTestFactory();
long leaderId = await factory.SeedViewerAsync(76_561_198_600_000_008UL, "PostLeader7");
using var leaderClient = factory.CreateAuthenticatedClient(leaderId);
await CreateGuildAsync(leaderClient, "PostGuild7");
await PostAsync(leaderClient, type: 0, message: "first");
await PostAsync(leaderClient, type: 0, message: "second");
var root = await PollAsync(leaderClient, startMessageId: 0, direction: 1);
var messages = root.GetProperty("chat_message");
// Collect message_ids for Normal messages only
var normalIds = new List<long>();
for (int i = 0; i < messages.GetArrayLength(); i++)
{
if (GetStringifiedInt(messages[i], "message_type") == (int)GuildChatMessageType.Normal)
normalIds.Add(GetStringifiedLong(messages[i], "message_id"));
}
Assert.That(normalIds.Count, Is.EqualTo(2), "Exactly 2 Normal messages expected");
Assert.That(normalIds[0], Is.LessThan(normalIds[1]),
"Second message_id must be greater than first");
}
}