Radicade Public Code

This commit is contained in:
JogmansonReal
2026-10-16 23:33:13 -07:00
commit 55175acdf6
832 changed files with 363261 additions and 0 deletions
+160
View File
@@ -0,0 +1,160 @@
using System.Globalization;
using System.Text.Json;
using RadicadeServer.API.Notifications;
using RadicadeServer.Clubs;
using RadicadeServer.Extensions;
using RadicadeServer.RecNet.Enums.Messages;
using RadicadeServer.RecNet.Notifications;
using Xunit;
namespace RadicadeServer.Tests;
public class WatchWireCompatibilityTests
{
// Notifs and NotificationType carry the same values as the client's own enum
// (RelationshipChanged = 1, MessageReceived = 2), so both must reach the wire the same way.
[Theory]
[InlineData(Notifs.RelationshipChanged, "1")]
[InlineData(Notifs.MessageReceived, "2")]
public void FriendNotificationUsesNumericStringIdWithNumericPayload(Notifs eventType, string expectedId)
{
var wire = new Notification
{
Id = eventType,
Msg = new { Type = MessageType.FriendInvite, AccountId = 2 }
}.ToJson();
using var json = JsonDocument.Parse(wire);
Assert.Equal(expectedId, json.RootElement.GetProperty("Id").GetString());
Assert.Equal(4, json.RootElement.GetProperty("Msg").GetProperty("Type").GetInt32());
Assert.Equal(2, json.RootElement.GetProperty("Msg").GetProperty("AccountId").GetInt32());
}
// The 2023 client requires a STRING at the notification Id field AND dispatches on the
// number inside it. A quoted number is the only form that satisfies both: a bare number
// throws in the client's deserializer ("expected:'String Begin Token', actual:'1'"), and
// the enum's name parses but matches no registered handler, so the frame is ignored.
[Theory]
[InlineData(NotificationType.GiftPackageReceivedImmediate, 31)]
[InlineData(NotificationType.SubscriptionUpdatePresence, 12)]
[InlineData(NotificationType.ModerationKick, 22)]
[InlineData(NotificationType.MessageReceived, 2)]
[InlineData(NotificationType.RefreshLogin, 5)]
public void NotificationIdIsTheClientEnumNumberAsAString(NotificationType eventType, int expectedId)
{
using var json = JsonDocument.Parse(new Notification { Id = eventType, Msg = new { AccountId = 2 } }.ToJson());
var id = json.RootElement.GetProperty("Id");
Assert.Equal(JsonValueKind.String, id.ValueKind);
Assert.Equal(expectedId.ToString(CultureInfo.InvariantCulture), id.GetString());
Assert.Equal((int)eventType, expectedId);
Assert.Equal(2, json.RootElement.GetProperty("Msg").GetProperty("AccountId").GetInt32());
}
// Both halves of the contract, stated as the client would experience them: the name is
// never what goes out, and the number is never sent bare.
[Theory]
[InlineData(NotificationType.MessageReceived, "2", "MessageReceived")]
[InlineData(NotificationType.ModerationKick, "22", "ModerationKick")]
public void NotificationIdIsNeitherTheNameNorABareNumber(NotificationType eventType, string expectedNumber, string name)
{
var notification = new Notification { Id = eventType, Msg = new { } };
var wire = notification.ToJson();
using var json = JsonDocument.Parse(wire);
Assert.NotEqual(JsonValueKind.Number, json.RootElement.GetProperty("Id").ValueKind);
// Through the property, exactly as the push log does it: the log has to show the token
// the client will parse, not what a boxed enum's ToString() happens to be.
Assert.Equal($"\"{expectedNumber}\"", Notify.WireId(notification.Id));
Assert.DoesNotContain($"\"{name}\"", wire);
}
// An explicit string id set by a caller is passed through untouched.
[Fact]
public void ExplicitStringNotificationIdIsUnchanged()
{
using var json = JsonDocument.Parse(new Notification { Id = "ReputationUpdate", Msg = new { } }.ToJson());
Assert.Equal("ReputationUpdate", json.RootElement.GetProperty("Id").GetString());
}
// These ids are NOT members of the client's enum, so they are named events and must keep
// travelling as strings (the reference server sends exactly these spellings).
[Theory]
[InlineData("PresenceUpdate")]
[InlineData("AccountUpdate")]
[InlineData("SelfAccountUpdate")]
[InlineData("ReputationUpdate")]
public void NonEnumNotificationIdStaysAString(string eventName)
{
using var json = JsonDocument.Parse(new Notification { Id = eventName, Msg = new { AccountId = 2 } }.ToJson());
Assert.Equal(JsonValueKind.String, json.RootElement.GetProperty("Id").ValueKind);
Assert.Equal(eventName, json.RootElement.GetProperty("Id").GetString());
}
// The push log prints the id with the same token the client will parse, so the server log
// shows the frame's real shape. A boxed enum prints its NAME through ToString() no matter
// how the frame was serialized, which is exactly why a log line reading "ModerationKick"
// looked healthy while every frame went out unparseable.
[Theory]
[InlineData(NotificationType.ModerationKick, "22")]
[InlineData(NotificationType.MessageReceived, "2")]
[InlineData(NotificationType.RefreshLogin, "5")]
public void PushLogShowsTheWireId(NotificationType eventType, string expected)
=> Assert.Equal(expected, Notify.WireId(eventType));
[Theory]
[InlineData("PresenceUpdate")]
[InlineData("AccountUpdate")]
[InlineData("SelfAccountUpdate")]
public void PushLogQuotesANamedId(string eventName)
=> Assert.Equal($"\"{eventName}\"", Notify.WireId(eventName));
// The diagnostic A/B (/diag/notify) has to be able to produce all three forms, because the
// point of it is to show the live client accepting one and rejecting the other two.
[Fact]
public void IdFormJsonProducesTheCanonicalQuotedNumberByDefault()
{
Assert.Equal("\"22\"", Notify.IdFormJson(null, NotificationType.ModerationKick));
Assert.Equal("\"22\"", Notify.IdFormJson("string", NotificationType.ModerationKick));
}
[Theory]
[InlineData("number", "22")]
[InlineData("name", "\"ModerationKick\"")]
public void IdFormJsonCanProduceTheFormsTheClientRejects(string form, string expected)
=> Assert.Equal(expected, Notify.IdFormJson(form, NotificationType.ModerationKick));
// The A/B envelope keeps the real Msg bytes and only swaps the Id token, so what the client
// is reacting to is genuinely just the id's form.
[Fact]
public void DiagnosticEnvelopeSwapsOnlyTheIdToken()
{
var message = new Notification
{
Id = NotificationType.MessageReceived,
Msg = new { Type = MessageType.CoachMessage, Data = "hello" }
};
using var json = JsonDocument.Parse(Notify.EnvelopeWithIdForm(message, "\"2\""));
Assert.Equal("2", json.RootElement.GetProperty("Id").GetString());
Assert.Equal(100, json.RootElement.GetProperty("Msg").GetProperty("Type").GetInt32());
Assert.Equal("hello", json.RootElement.GetProperty("Msg").GetProperty("Data").GetString());
}
[Theory]
[InlineData(2, 100)]
[InlineData(3, 10)]
[InlineData(0, 0)]
public void ClubMembershipUsesClientEnumValues(long viewerId, int expected)
{
var club = new ClubRecord
{
clubId = 1,
ownerPlayerId = 2,
members = viewerId == 3
? [new ClubMemberRecord { accountId = viewerId, membershipType = ClubMembershipTypes.Member }]
: []
};
using var details = JsonDocument.Parse(JsonSerializer.Serialize(ClubStore.BuildClubDetails(club, viewerId)));
using var membership = JsonDocument.Parse(JsonSerializer.Serialize(ClubStore.BuildMembership(club, viewerId)));
Assert.Equal(expected, details.RootElement.GetProperty("MyMembershipType").GetInt32());
Assert.Equal(expected, membership.RootElement.GetProperty("membershipType").GetInt32());
}
}