mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 15:09:26 -07:00
248 lines
9.5 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|