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

270 lines
10 KiB
C#

using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RadicadeServer.API.Moderation;
using RadicadeServer.DB;
using RadicadeServer.RecNet.Accounts;
using RadicadeServer.RecNet.Auth;
using RadicadeServer.RecNet.Enums.Moderation;
using RadicadeServer.RecNet.Moderation;
using RadicadeServer.Utils;
using Xunit;
namespace RadicadeServer.Tests;
[Collection(DbFactoryCollection.Name)]
public class ModerationBlockDetailsTests
{
private static RecDBContext NewDb() => new(
TestDbOptions.Create()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options);
private static PlayerReportingAPI NewApi(RecDBContext db, long? accountId)
{
var identity = new ClaimsIdentity("TestAuth");
if (accountId.HasValue)
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accountId.Value.ToString()));
return new PlayerReportingAPI(
db)
{
ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) }
}
};
}
private static void AssertReportSucceeded(ActionResult<OkResponse> result)
{
var ok = Assert.IsType<OkObjectResult>(result.Result);
Assert.True(Assert.IsType<OkResponse>(ok.Value).Success);
}
private static async Task AddReporterAsync(RecDBContext db, long accountId, string? roles = null)
{
db.Profiles.Add(new Profile
{
accountId = accountId,
username = $"reporter{accountId}",
displayName = $"Reporter {accountId}",
profileImage = DBConfig.DefaultProfileImage,
role = roles
});
await db.SaveChangesAsync();
}
[Theory]
[InlineData(0, false)]
[InlineData(5, false)]
[InlineData(6, true)]
[InlineData(7, true)]
public void AutomaticTimeoutRequiresMoreThanFiveReportsFromAPlayer(int reportCount, bool expected)
{
Assert.Equal(expected, PlayerReportingAPI.ShouldApplyAutomaticTimeout(reportCount, reporterIsModerator: false));
}
[Fact]
public async Task TheSixthPublicReportCreatesAnAutomaticTimeout()
{
using var db = NewDb();
await AddReporterAsync(db, 6);
db.Reputations.Add(new Reputation { AccountId = 42, ReportCount = 5 });
await db.SaveChangesAsync();
AssertReportSucceeded(await NewApi(db, 6).CreateReport(new CreateReportRequest
{
PlayerIdReported = 42,
ReportCategory = ReportCategory.CoC_Trolling,
Details = "report details",
RoomId = 0
}));
var moderation = await db.Moderation.SingleAsync();
Assert.Equal("Automatic Timeout", moderation.BanMethod);
Assert.Equal("You have received a timeout due to a high number of reports for Trolling.", moderation.Message);
Assert.True(moderation.CreatedAt <= DateTime.UtcNow);
Assert.Equal(6, (await db.Reputations.SingleAsync(x => x.AccountId == 42)).ReportCount);
}
[Fact]
public async Task SelfAndUnauthenticatedReportsDoNotCountTowardAutomaticTimeouts()
{
using var db = NewDb();
db.Reputations.Add(new Reputation { AccountId = 42, ReportCount = 5 });
await db.SaveChangesAsync();
AssertReportSucceeded(await NewApi(db, 42).CreateReport(new CreateReportRequest
{
PlayerIdReported = 42,
ReportCategory = ReportCategory.CoC_Trolling
}));
Assert.Empty(await db.Moderation.ToListAsync());
Assert.Equal(5, (await db.Reputations.SingleAsync(x => x.AccountId == 42)).ReportCount);
AssertReportSucceeded(await NewApi(db, null).CreateReport(new CreateReportRequest
{
PlayerIdReported = 42,
ReportCategory = ReportCategory.CoC_Trolling
}));
Assert.Empty(await db.Moderation.ToListAsync());
Assert.Equal(5, (await db.Reputations.SingleAsync(x => x.AccountId == 42)).ReportCount);
}
[Fact]
public async Task ModeratorReportCreatesDirectTimeoutWithoutAlsoUsingPublicReportEscalation()
{
using var db = NewDb();
await AddReporterAsync(db, 6, "moderator");
db.Reputations.Add(new Reputation { AccountId = 42, ReportCount = 0 });
await db.SaveChangesAsync();
AssertReportSucceeded(await NewApi(db, 6).CreateReport(new CreateReportRequest
{
PlayerIdReported = 42,
ReportCategory = ReportCategory.CoC_Trolling,
Details = "Moderator observed trolling"
}));
var moderation = await db.Moderation.SingleAsync();
Assert.Equal("Moderator Report", moderation.BanMethod);
Assert.Equal("Moderator observed trolling", moderation.Message);
Assert.Equal(0, (await db.Reputations.SingleAsync(x => x.AccountId == 42)).ReportCount);
}
[Fact]
public async Task InvalidReportCategoryIsRejectedBeforeItCounts()
{
using var db = NewDb();
await AddReporterAsync(db, 6);
var result = await NewApi(db, 6).CreateReport(new CreateReportRequest
{
PlayerIdReported = 42,
ReportCategory = (ReportCategory)9999
});
Assert.IsType<BadRequestObjectResult>(result.Result);
Assert.Empty(await db.Reputations.ToListAsync());
}
[Theory]
[InlineData(ReportCategory.CoC_Trolling, "Trolling")]
[InlineData(ReportCategory.CoC_Sexual, "Sexual behavior")]
public void AutomaticTimeoutReasonUsesTheReportCategory(ReportCategory category, string expected)
{
Assert.Equal(expected, PlayerReportingAPI.GetAutomaticTimeoutReason(category));
}
[Fact]
public async Task VoiceModerationBlockDetailsCarryStartTimeButNotModeratorOnlyNote()
{
using var db = NewDb();
var startedAt = DateTime.UtcNow;
db.Moderation.Add(new ModerationDB
{
PlayerId = 42,
IsBan = true,
Message = "Sexually explicit behavior or photos",
ModeratorNote = "Sexual harassment in the Rec Center",
BanMethod = "Voice Moderation",
Category = ReportCategory.CoC_Sexual,
CreatedAt = startedAt,
ExpireDuration = startedAt.AddDays(1),
AppealCode = "ABC12345"
});
await db.SaveChangesAsync();
var api = NewApi(db, 42);
api.ControllerContext.HttpContext.Request.Headers["X-Forwarded-For"] = "192.0.2.42";
var result = Assert.IsType<OkObjectResult>(await api.GetBlockDetails());
var details = Assert.IsType<BlockDetails>(result.Value);
Assert.Equal(startedAt, details.TimeoutStartedAt);
Assert.Equal("VoiceModeration", details.BanType);
Assert.Equal("Sexually explicit behavior or photos", details.Message);
}
[Theory]
[InlineData("Automatic Timeout", "You have received a timeout due to a high number of reports for Trolling", "You have received a timeout due to a high number of reports for Trolling.")]
[InlineData("Moderator Report", "You were reported by a moderator", "You have received a timeout. You were reported by a moderator.")]
[InlineData("Moderator Report", "You were reported by a moderator.", "You have received a timeout. a moderator report.")]
[InlineData("Timeout", "Trolling", "You have received a timeout due to a high number of reports for Trolling.")]
[InlineData("Automatic Timeout", "You have received a timeout due to a high number of reports for Trolling.", "You have received a timeout due to a high number of reports for Trolling.")]
public async Task PersistedTimeoutBanHasClientFacingTimeoutCopy(string banMethod, string storedReason, string expectedMessage)
{
using var db = NewDb();
var startedAt = DateTime.UtcNow;
db.Moderation.Add(new ModerationDB
{
PlayerId = 42,
IsBan = true,
Message = storedReason,
BanMethod = banMethod,
Category = ReportCategory.CoC_Trolling,
CreatedAt = startedAt,
ExpireDuration = startedAt.AddHours(1),
AppealCode = "ABC12345"
});
await db.SaveChangesAsync();
var api = NewApi(db, 42);
api.ControllerContext.HttpContext.Request.Headers["X-Forwarded-For"] = "192.0.2.42";
var result = Assert.IsType<OkObjectResult>(await api.GetBlockDetails());
var details = Assert.IsType<BlockDetails>(result.Value);
Assert.Equal("Timeout", details.BanType);
Assert.Equal(startedAt, details.TimeoutStartedAt);
Assert.Equal(expectedMessage, details.DisplayReason);
Assert.Equal(expectedMessage, details.Message);
}
[Fact]
public async Task HardwareBanShowsTheOriginalRelatedAccountOnItsOwnLine()
{
using var db = NewDb();
var startedAt = DateTime.UtcNow;
db.Players.Add(new PlayerData { AccountId = 42, DeviceId = "shared-device" });
db.Profiles.Add(new Profile
{
accountId = 12,
username = "HealthyMoose640",
displayName = "HealthyMoose640",
profileImage = DBConfig.DefaultProfileImage
});
db.HardwareBans.Add(new HardwareBan
{
DeviceId = "shared-device",
BannedAccountId = 12,
Reason = "Moderator. Account ban",
CreatedAt = startedAt,
ExpiresAt = startedAt.AddDays(1)
});
await db.SaveChangesAsync();
var api = NewApi(db, 42);
api.ControllerContext.HttpContext.Request.Headers["X-Forwarded-For"] = "192.0.2.42";
var result = Assert.IsType<OkObjectResult>(await api.GetBlockDetails());
var details = Assert.IsType<BlockDetails>(result.Value);
Assert.Contains("\nRelated account: @HealthyMoose640", details.Message);
Assert.Equal(startedAt, details.TimeoutStartedAt);
}
/* Every ban the client is shown has to carry the moment it started, because
that is the "Banned since" line. Kicks must not: they are not bans and the
screen has nothing to date. */
[Fact]
public void BanBlockDetailsCarryBannedSinceWhileKicksDoNot()
{
var startedAt = DateTime.UtcNow.AddDays(-3);
var ban = PlayerUtils.BuildBlockDetails(true, ReportCategory.Misc, "reason", startedAt.AddDays(5), startedAtUtc: startedAt);
Assert.Equal(startedAt, ban.TimeoutStartedAt);
var kick = PlayerUtils.BuildBlockDetails(true, ReportCategory.Misc, "kicked", DateTime.UtcNow.AddMinutes(15));
Assert.Null(kick.TimeoutStartedAt);
}
}