using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; using Vanguard.Server.Configuration; using Vanguard.Server.Matches; using Vanguard.Server.Meta; using Vanguard.Server.Storage; namespace Vanguard.Server.Tests; public class MetaProfileServiceTests { private static readonly DateTimeOffset Start = new(2026, 10, 21, 12, 0, 0, TimeSpan.Zero); [Fact] public async Task Forfeit_And_Leave_Record_History_Without_Gold_Or_Xp() { var factory = new TestFactory(); await CreateUsersAsync(factory); var service = NewService(factory); await service.RecordAsync(new MatchResult( "m-concede", GameMode.ClassicRanked, "p1", "p2", 0, MatchFinishReason.Concede)); await service.RecordAsync(new MatchResult( "m-leave", GameMode.ClassicRanked, "p1", "p2", 0, MatchFinishReason.Leave)); await using var db = factory.CreateDbContext(); var rows = await db.MatchHistory.OrderBy(row => row.MatchId).ToListAsync(); Assert.Equal(4, rows.Count); Assert.All(rows, row => { Assert.Equal(0, row.Gold); Assert.Equal(0, row.Xp); }); Assert.Contains(rows, row => row.FinishReason == "concede"); Assert.Contains(rows, row => row.FinishReason == "leave"); } [Fact] public async Task Normal_Finish_Gives_Gold_Xp_And_Is_Idempotent() { var factory = new TestFactory(); await CreateUsersAsync(factory); var service = NewService(factory); var result = new MatchResult( "m-normal", GameMode.ClassicRanked, "p1", "p2", 0, MatchFinishReason.Normal); await service.RecordAsync(result); await service.RecordAsync(result); await using var db = factory.CreateDbContext(); var rows = await db.MatchHistory.OrderBy(row => row.Result).ToListAsync(); Assert.Equal(2, rows.Count); var win = Assert.Single(rows, row => row.Result == "win"); var loss = Assert.Single(rows, row => row.Result == "loss"); Assert.Equal(30, win.Gold); Assert.Equal(150, win.Xp); Assert.Equal(15, loss.Gold); Assert.Equal(75, loss.Xp); var winner = await db.Users.SingleAsync(user => user.NormalizedUserName == "P1"); var loser = await db.Users.SingleAsync(user => user.NormalizedUserName == "P2"); Assert.Equal(150, winner.Xp); Assert.Equal(75, loser.Xp); Assert.True(winner.RankedMmr > 1000); Assert.True(loser.RankedMmr < 1000); } private static MetaProfileService NewService(TestFactory factory) => new( factory, new MetaOptions(), new FakeTimeProvider(Start), NullLogger.Instance); private static async Task CreateUsersAsync(TestFactory factory) { await using var db = factory.CreateDbContext(); db.Users.AddRange( new UserEntity { Id = Guid.NewGuid(), UserName = "p1", NormalizedUserName = "P1", PasswordHash = "x", CreatedAt = Start, RankedMmr = 1000, CasualMmr = 1000, }, new UserEntity { Id = Guid.NewGuid(), UserName = "p2", NormalizedUserName = "P2", PasswordHash = "x", CreatedAt = Start, RankedMmr = 1000, CasualMmr = 1000, }); await db.SaveChangesAsync(); } private sealed class TestFactory : IDbContextFactory { private readonly DbContextOptions _options; public TestFactory() { _options = new DbContextOptionsBuilder() .UseInMemoryDatabase("meta-profile-" + Guid.NewGuid().ToString("N")) .Options; } public MatchDbContext CreateDbContext() => new(_options); } }