using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using RadicadeServer.API.Moderation;
using RadicadeServer.DB;
using RadicadeServer.RecNet.Enums.Moderation;
using RadicadeServer.RecNet.Moderation;
using RadicadeServer.Utils;
using Xunit;
namespace RadicadeServer.Tests;
///
/// The support site closes a ticket by telling the game what it decided, so the
/// game endpoint behind that call is the only thing standing between a queue
/// item and a player's appeal state.
///
/// A ban appeal used to have exactly two verdicts, accept and deny. That left a
/// ticket which could be neither accepted nor denied with no way out of the
/// queue, and left staff choosing between lying about a verdict or never
/// clearing the ticket. Reviewing is the third verdict: the ban stands, the
/// appeal stops being pending, and the cooldown starts so the player can appeal
/// again later instead of waiting on a decision that will never come.
///
/// These tests pin the state each verdict leaves behind, because a wrong field
/// here is invisible until a player is locked out of appealing.
///
[Collection(DbFactoryCollection.Name)]
public class BanAppealResolveTests
{
private const long PlayerId = 4242;
private const string AppealCode = "NWVPZX5Q";
private const string SupportKey = "support-test-key";
/* A named in-memory store plus its root, so separate contexts in one test
really do share a database. That is what makes the assertions below check
what was saved rather than what happens to be attached to one context. */
private sealed class TestStore
{
private readonly string _name = Guid.NewGuid().ToString();
private readonly InMemoryDatabaseRoot _root = new();
public RecDBContext Open() => new(
TestDbOptions.Create()
.UseInMemoryDatabase(_name, _root)
.Options);
}
private static ModerationDB Ban(string? appealText) => new()
{
PlayerId = PlayerId,
IsBan = true,
Message = "Cheating",
Category = ReportCategory.CoC_Trolling,
CreatedAt = DateTime.UtcNow.AddDays(-1),
ExpireDuration = DateTime.UtcNow.AddDays(30),
AppealCode = AppealCode,
AppealText = appealText
};
private static async Task SeedAsync(string? appealText)
{
var store = new TestStore();
await using var db = store.Open();
db.Moderation.Add(Ban(appealText));
await db.SaveChangesAsync();
return store;
}
private static async Task> ResolveAsync(TestStore store, string? decision)
{
var previous = DBConfig.AdminApiKey;
DBConfig.AdminApiKey = SupportKey;
try
{
await using var db = store.Open();
var controller = new BanAppealAPI(db);
return await controller.Resolve(AppealCode, SupportKey, decision);
}
finally
{
DBConfig.AdminApiKey = previous;
}
}
private static async Task ReloadAsync(TestStore store)
{
await using var db = store.Open();
return await db.Moderation.AsNoTracking().SingleAsync(x => x.PlayerId == PlayerId);
}
/* AppealResponse ships an anonymous { success, error, value }, so the payload
is read reflectively rather than by adding a type just for tests. */
private static (bool Success, string? Error, string? Value) Read(ActionResult