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

248 lines
9.5 KiB
C#

using System.Security.Claims;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RadicadeServer.API.Moderation;
using RadicadeServer.API.Notifications;
using RadicadeServer.DB;
using RadicadeServer.RecNet.Auth;
using RadicadeServer.RecNet.Enums.Messages;
using RadicadeServer.RecNet.Enums.Moderation;
using RadicadeServer.RecNet.Moderation;
using RadicadeServer.RecNet.Responses;
using Xunit;
namespace RadicadeServer.Tests;
/// <summary>
/// Cheering another player (`api/PlayerCheer/v1/create`) and the cheer badges a profile reads
/// back out of `api/playerReputation/v1/{id}`.
///
/// Two things were wrong with the loop. A cheer only ever moved a column: the target was never
/// sent a PlayerCheer message, so nothing appeared on their client at all. And the reputation
/// payload hardcoded IsCheerful = true while flooring every category count at 1, so the client
/// drew all five cheer badges as already earned - on accounts that had never been cheered.
///
/// These tests pin the ledger end to end: the cheer is stored, the target is told, the count is
/// reported as it really is, and the badge appears at Reputation.CheerBadgeThreshold cheers.
/// </summary>
[Collection(DbFactoryCollection.Name)]
public class PlayerCheerTests
{
private const long CheererId = 41;
private const long TargetId = 42;
private static readonly JsonSerializerOptions ServerJson = new()
{
PropertyNamingPolicy = null
};
private sealed class TestDbContextFactory : IDbContextFactory<RecDBContext>
{
private readonly DbContextOptions<RecDBContext> options = TestDbOptions.Create()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
public RecDBContext CreateDbContext() => new(options);
}
private static async Task<(RecDBContext Db, IDbContextFactory<RecDBContext> Factory, IDbContextFactory<RecDBContext> Previous)>
CreateContextAsync()
{
var factory = new TestDbContextFactory();
var previous = Notify.DbFactory;
Notify.DbFactory = factory;
var db = factory.CreateDbContext();
db.Profiles.AddRange(ProfileFor(CheererId), ProfileFor(TargetId));
db.Players.AddRange(new PlayerData { AccountId = CheererId }, new PlayerData { AccountId = TargetId });
await db.SaveChangesAsync();
return (db, factory, previous);
}
private static Profile ProfileFor(long id) => new()
{
accountId = id,
username = $"player{id}",
displayName = $"Player {id}",
profileImage = string.Empty,
createdAt = DateTime.UtcNow
};
private static void SetUser(Controller controller, long userId)
{
var identity = new ClaimsIdentity("TestAuth");
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId.ToString()));
controller.ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) }
};
}
private static void SetFormBody(Controller controller, string body)
{
var request = controller.HttpContext.Request;
request.ContentType = "application/x-www-form-urlencoded";
request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body));
request.ContentLength = request.Body.Length;
}
private static async Task<ActionResult<Success>> CheerAsync(ReputationAPI api, long to, int category, long from = CheererId)
{
SetUser(api, from);
SetFormBody(api, $"PlayerIdTo={to}&CheerCategory={category}");
return await api.CheerPlayer();
}
private static JsonElement ReputationJson(RecDBContext db, long accountId)
{
var result = Assert.IsType<OkObjectResult>(new ReputationAPI(db).GetReputation(accountId));
using var doc = JsonDocument.Parse(JsonSerializer.Serialize(result.Value, ServerJson));
return doc.RootElement.Clone();
}
[Fact]
public async Task Cheer_ReachesTheTargetAsAPlayerCheerMessage()
{
var (db, factory, previous) = await CreateContextAsync();
await using var disposeDb = db;
try
{
var result = await CheerAsync(new ReputationAPI(db), TargetId, (int)CheerCategory.Helpful);
Assert.IsType<OkObjectResult>(result.Result);
await using var verify = factory.CreateDbContext();
var message = Assert.Single(verify.Messages);
Assert.Equal(TargetId, message.ToPlayerId);
Assert.Equal(CheererId, message.FromPlayerId);
Assert.Equal(MessageType.PlayerCheer, message.Type);
Assert.Equal(((int)CheerCategory.Helpful).ToString(), message.Data);
}
finally
{
Notify.DbFactory = previous;
}
}
[Fact]
public async Task Cheer_CountsOnlyTheCategoryThatWasCheered()
{
var (db, _, previous) = await CreateContextAsync();
await using var disposeDb = db;
try
{
var api = new ReputationAPI(db);
Assert.IsType<OkObjectResult>((await CheerAsync(api, TargetId, (int)CheerCategory.Helpful)).Result);
Assert.IsType<OkObjectResult>((await CheerAsync(api, TargetId, (int)CheerCategory.Helpful)).Result);
var json = ReputationJson(db, TargetId);
// Reported as stored: a count of 2 stays 2, and the categories nobody cheered
// stay at 0 instead of being floored up to a badge-looking 1.
Assert.Equal(2, json.GetProperty("CheerHelpful").GetInt32());
Assert.Equal(0, json.GetProperty("CheerGeneral").GetInt32());
Assert.Equal(0, json.GetProperty("CheerCreative").GetInt32());
Assert.Equal(0, json.GetProperty("CheerGreatHost").GetInt32());
Assert.Equal(0, json.GetProperty("CheerSportsman").GetInt32());
}
finally
{
Notify.DbFactory = previous;
}
}
[Fact]
public async Task Cheer_BadgeIsEarnedAtTheFifthCheer()
{
var (db, _, previous) = await CreateContextAsync();
await using var disposeDb = db;
try
{
var api = new ReputationAPI(db);
for (var i = 0; i < Reputation.CheerBadgeThreshold - 1; i++)
Assert.IsType<OkObjectResult>((await CheerAsync(api, TargetId, (int)CheerCategory.General)).Result);
var before = await db.Reputations.AsNoTracking().FirstAsync(x => x.AccountId == TargetId);
Assert.False(before.CheerBadgeEarned);
Assert.IsType<OkObjectResult>((await CheerAsync(api, TargetId, (int)CheerCategory.General)).Result);
var after = await db.Reputations.AsNoTracking().FirstAsync(x => x.AccountId == TargetId);
Assert.True(after.CheerBadgeEarned);
Assert.Equal(Reputation.CheerBadgeThreshold, after.CheerGeneral);
// The badge is what the stored flag tracks; the wire gate the client reads to
// decide whether the target may hand cheers out stays open either way.
Assert.True(ReputationJson(db, TargetId).GetProperty("IsCheerful").GetBoolean());
}
finally
{
Notify.DbFactory = previous;
}
}
[Fact]
public void Reputation_ReportsTheGateOpenAndTheCountersExactlyAsStored()
{
// IsCheerful on the wire is the "may hand out cheers" gate, which the reference
// server answers true for every account - it is not the earned badge. What has to
// stay honest is the counters: a stale row must not carry a badge the player never
// earned, and nobody has cheered this account so every counter reads 0.
var options = TestDbOptions.Create()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
using var db = new RecDBContext(options);
db.Reputations.Add(new Reputation { AccountId = TargetId, IsCheerful = true, CheerGeneral = 1, CheerCreative = 1 });
db.SaveChanges();
var json = ReputationJson(db, TargetId);
Assert.True(json.GetProperty("IsCheerful").GetBoolean());
Assert.Equal(1, json.GetProperty("CheerGeneral").GetInt32());
Assert.Equal(0, json.GetProperty("CheerHelpful").GetInt32());
}
[Fact]
public async Task Cheer_RejectsYourselfAndUnknownCategories()
{
var (db, _, previous) = await CreateContextAsync();
await using var disposeDb = db;
try
{
var api = new ReputationAPI(db);
Assert.IsType<BadRequestObjectResult>(
(await CheerAsync(api, CheererId, (int)CheerCategory.General)).Result);
Assert.IsType<BadRequestObjectResult>(
(await CheerAsync(api, TargetId, 999)).Result);
Assert.Empty(db.Messages);
Assert.Empty(db.Reputations);
}
finally
{
Notify.DbFactory = previous;
}
}
[Fact]
public async Task Cheer_CreditStaysADailyAllowanceAndNotABadge()
{
var (db, _, previous) = await CreateContextAsync();
await using var disposeDb = db;
try
{
// CheerCredit is how many cheers the player may hand out. An account with no credit
// recorded keeps the client's 20-cheer default, which is why it is still floored -
// unlike the five badge counts, which are reported exactly as stored.
Assert.Equal(20, ReputationJson(db, CheererId).GetProperty("CheerCredit").GetInt32());
}
finally
{
Notify.DbFactory = previous;
}
}
}