diff --git a/Runtime/Game/Requests/LootLockerBanRequest.cs b/Runtime/Game/Requests/LootLockerBanRequest.cs
index ff98da223..0c47c97e0 100644
--- a/Runtime/Game/Requests/LootLockerBanRequest.cs
+++ b/Runtime/Game/Requests/LootLockerBanRequest.cs
@@ -12,6 +12,11 @@ public class LootLockerBanInfo
///
public string ban_reason { get; set; }
+ ///
+ /// A player-facing message describing the ban. Empty when no detail was provided.
+ ///
+ public string ban_detail { get; set; }
+
///
/// The time the ban was issued, as an ISO 8601 timestamp.
///
diff --git a/Tests/LootLockerTestUtils/LootLockerTestConfigurationBan.cs b/Tests/LootLockerTestUtils/LootLockerTestConfigurationBan.cs
index 2a07ee457..3d02d6c9a 100644
--- a/Tests/LootLockerTestUtils/LootLockerTestConfigurationBan.cs
+++ b/Tests/LootLockerTestUtils/LootLockerTestConfigurationBan.cs
@@ -5,12 +5,21 @@ namespace LootLockerTestConfigurationUtils
{
public static class LootLockerTestPlayerBan
{
+ ///
+ /// Request body for the admin ban endpoint.
+ ///
+ private class BanPlayerRequest
+ {
+ public string ban_detail { get; set; }
+ }
+
///
/// Bans a player using the admin API. Creates a permanent manual ban.
///
/// The ULID of the player to ban.
/// Called with the raw response when the request completes.
- public static void BanPlayer(string playerUlid, Action onComplete)
+ /// Optional player-facing ban message to store with the ban.
+ public static void BanPlayer(string playerUlid, Action onComplete, string banDetail = null)
{
if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken))
{
@@ -21,7 +30,11 @@ public static void BanPlayer(string playerUlid, Action onCom
var endpoint = LootLockerTestConfigurationEndpoints.banPlayer;
string formattedEndpoint = string.Format(endpoint.endPoint, playerUlid);
- LootLockerAdminRequest.Send(formattedEndpoint, endpoint.httpMethod, "{}", serverResponse =>
+ string json = string.IsNullOrEmpty(banDetail)
+ ? "{}"
+ : LootLockerJson.SerializeObject(new BanPlayerRequest { ban_detail = banDetail });
+
+ LootLockerAdminRequest.Send(formattedEndpoint, endpoint.httpMethod, json, serverResponse =>
{
onComplete?.Invoke(serverResponse);
}, true);
diff --git a/Tests/LootLockerTests/PlayMode/BanTest.cs b/Tests/LootLockerTests/PlayMode/BanTest.cs
index 51607ac95..08ebc0a97 100644
--- a/Tests/LootLockerTests/PlayMode/BanTest.cs
+++ b/Tests/LootLockerTests/PlayMode/BanTest.cs
@@ -131,13 +131,15 @@ public IEnumerator StartSession_ForBannedPlayer_Returns403WithBanInfo()
string playerUlid = sessionResponse.player_ulid;
LootLockerStateData.ClearAllSavedStates();
+ const string banDetail = "Banned by the integration test";
+
bool banCompleted = false;
LootLockerResponse banResponse = null;
LootLockerTestPlayerBan.BanPlayer(playerUlid, response =>
{
banResponse = response;
banCompleted = true;
- });
+ }, banDetail);
yield return new WaitUntil(() => banCompleted);
Assert.IsTrue(banResponse?.success, "Failed to ban player via admin API");
@@ -157,6 +159,7 @@ public IEnumerator StartSession_ForBannedPlayer_Returns403WithBanInfo()
Assert.AreEqual("player_banned", bannedSessionResponse.errorData?.code, "Expected player_banned error code");
Assert.IsNotNull(bannedSessionResponse.errorData?.ban, "Expected ban info to be present in the error data");
Assert.IsFalse(string.IsNullOrEmpty(bannedSessionResponse.errorData?.ban?.ban_reason), "Expected ban_reason to be populated");
+ Assert.AreEqual(banDetail, bannedSessionResponse.errorData?.ban?.ban_detail, "Expected ban_detail to round-trip through the session start error data");
}
[UnityTest, Category("LootLocker"), Category("LootLockerCI")]
@@ -178,13 +181,15 @@ public IEnumerator GetPlayerBanStatus_ForBannedPlayer_ReturnsIsBannedWithDetails
string playerUlid = sessionResponse.player_ulid;
LootLockerStateData.ClearAllSavedStates();
+ const string banDetail = "Banned by the integration test";
+
bool banCompleted = false;
LootLockerResponse banResponse = null;
LootLockerTestPlayerBan.BanPlayer(playerUlid, response =>
{
banResponse = response;
banCompleted = true;
- });
+ }, banDetail);
yield return new WaitUntil(() => banCompleted);
Assert.IsTrue(banResponse?.success, "Failed to ban player via admin API");
@@ -203,6 +208,7 @@ public IEnumerator GetPlayerBanStatus_ForBannedPlayer_ReturnsIsBannedWithDetails
Assert.IsTrue(banStatusResponse.is_banned, "Expected player to be banned");
Assert.IsNotNull(banStatusResponse.ban, "Expected ban details to be populated");
Assert.IsFalse(string.IsNullOrEmpty(banStatusResponse.ban?.ban_reason), "Expected ban_reason to be populated");
+ Assert.AreEqual(banDetail, banStatusResponse.ban?.ban_detail, "Expected ban_detail to round-trip through the ban status response");
Assert.IsFalse(string.IsNullOrEmpty(banStatusResponse.ban?.banned_on), "Expected banned_on to be populated");
Assert.IsTrue(banStatusResponse.ban?.permanent ?? false, "Expected permanent ban (no banned_until)");
}