Files
Radicade/ServerTests/ReputationTests.cs
T
2026-10-16 23:33:13 -07:00

157 lines
5.7 KiB
C#

using RadicadeServer.RecNet.Enums.Moderation;
using RadicadeServer.RecNet.Moderation;
using Xunit;
namespace RadicadeServer.Tests;
/// <summary>
/// Unit tests for the reputation system spec:
/// - every player starts at ReputationScore = 10
/// - Noteriety starts at 0 and grows +0.5 per cheer / subscriber received
/// - bans drop reputation: -0.5 per ban, -1 per cheating ban, -4 per permanent ban (floored at 0)
/// - Trust label: Trusted (8-10), Neutral (6-7), Not Trusted (0-5)
/// - reports increment ReportCount, which never feeds Noteriety (so reports can't lock
/// the client's room-comment / club-creation features)
/// </summary>
public class ReputationTests
{
[Fact]
public void CheerCategory_MatchesTheClientsLadder()
{
// Copied off the client's own enum in dump.cs (and matching the archived 2023
// servers FemRec and Rubidium). Numbering them sequentially silently broke every
// cheer except General: none of 10/20/30/40 matched a switch arm.
Assert.Equal(0, (int)CheerCategory.General);
Assert.Equal(10, (int)CheerCategory.Helpful);
Assert.Equal(20, (int)CheerCategory.Sportsman);
Assert.Equal(30, (int)CheerCategory.GreatHost);
Assert.Equal(40, (int)CheerCategory.Creative);
// The badge only developers can equip. The client asks for it by this value, so
// leaving it out of the enum made the request look like an unknown category.
Assert.Equal(9000, (int)CheerCategory.RecRoomDeveloper);
}
[Fact]
public void NewReputation_StartsAtTen()
{
var rep = new Reputation { AccountId = 42 };
Assert.Equal(Reputation.DefaultReputation, rep.ReputationScore);
Assert.Equal(10.0, rep.ReputationScore);
Assert.Equal(0, rep.Noteriety);
Assert.Equal(0, rep.ReportCount);
}
[Theory]
[InlineData(10.0, "Trusted")]
[InlineData(9.5, "Trusted")]
[InlineData(8.0, "Trusted")]
[InlineData(7.99, "Neutral")]
[InlineData(7.0, "Neutral")]
[InlineData(6.0, "Neutral")]
[InlineData(5.99, "Not Trusted")]
[InlineData(5.0, "Not Trusted")]
[InlineData(1.0, "Not Trusted")]
[InlineData(0.0, "Not Trusted")]
public void Trust_FollowsTheScoreBands(double score, string expected)
{
var rep = new Reputation { AccountId = 1, ReputationScore = score };
Assert.Equal(expected, rep.Trust);
}
[Fact]
public void NormalBan_DropsReputationByHalf()
{
var rep = new Reputation { AccountId = 1, ReputationScore = 10 };
rep.ApplyBanPenalty(permanent: false, cheating: false);
Assert.Equal(9.5, rep.ReputationScore);
}
[Fact]
public void CheatingBan_DropsReputationByOne()
{
var rep = new Reputation { AccountId = 1, ReputationScore = 10 };
rep.ApplyBanPenalty(permanent: false, cheating: true);
Assert.Equal(9.0, rep.ReputationScore);
}
[Fact]
public void PermanentBan_DropsReputationByFour()
{
var rep = new Reputation { AccountId = 1, ReputationScore = 10 };
rep.ApplyBanPenalty(permanent: true, cheating: false);
Assert.Equal(6.0, rep.ReputationScore);
}
[Fact]
public void CheatingPermanentBan_AppliesTheLargerPenalty()
{
// A cheating offense that escalates to permanent takes the permanent (-4) penalty.
var rep = new Reputation { AccountId = 1, ReputationScore = 10 };
rep.ApplyBanPenalty(permanent: true, cheating: true);
Assert.Equal(6.0, rep.ReputationScore);
}
[Fact]
public void RepeatedBans_NeverPushReputationNegative()
{
var rep = new Reputation { AccountId = 1, ReputationScore = 1.0 };
rep.ApplyBanPenalty(permanent: true, cheating: true); // -4
rep.ApplyBanPenalty(permanent: false, cheating: false); // -0.5
Assert.Equal(0.0, rep.ReputationScore);
}
[Fact]
public void AddCheer_GrowsNoterietyByHalfPerCheer()
{
var rep = new Reputation { AccountId = 1 };
rep.AddCheer(CheerCategory.General);
rep.AddCheer(CheerCategory.Creative);
Assert.Equal(1.0, rep.Noteriety); // 2 x 0.5
Assert.Equal(1, rep.CheerGeneral);
Assert.Equal(1, rep.CheerCreative);
}
[Fact]
public void AddSubscriber_GrowsNoterietyByHalf()
{
var rep = new Reputation { AccountId = 1 };
rep.AddSubscriber();
rep.AddSubscriber();
Assert.Equal(1.0, rep.Noteriety);
}
[Fact]
public void Reports_NeverTouchNoteriety()
{
// Simulates the report flow: ReportCount goes up, Noteriety stays a fame score.
var rep = new Reputation { AccountId = 1 };
for (var i = 0; i < 20; i++) rep.ReportCount += 1;
Assert.Equal(20, rep.ReportCount);
Assert.Equal(0.0, rep.Noteriety);
}
[Fact]
public void ForClient_PassesThroughScoreTrustAndReportCount()
{
var rep = new Reputation { AccountId = 7, ReputationScore = 6.5, ReportCount = 3, Noteriety = 1.0 };
var client = rep.ForClient();
Assert.Equal(7, client.AccountId);
Assert.Equal(6.5, client.ReputationScore);
Assert.Equal("Neutral", client.Trust);
Assert.Equal(3, client.ReportCount);
// Noteriety is normalized to a tiny 0-1 fraction so the client's moderation
// gates (room comments / club creation) never trip on it.
Assert.Equal(Math.Min(0.05, rep.Noteriety / 100.0), client.Noteriety);
Assert.True(client.Noteriety <= 0.05);
}
[Fact]
public void ForClient_NormalizesNoterietyButNeverAboveTheCeiling()
{
var rep = new Reputation { AccountId = 1, Noteriety = 5000 };
var client = rep.ForClient();
Assert.Equal(0.05, client.Noteriety);
}
}