mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-26 15:39:01 -07:00
232 lines
7.9 KiB
C#
232 lines
7.9 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Storage;
|
|
using RadicadeServer.DB;
|
|
using RadicadeServer.Leaderboard;
|
|
using RadicadeServer.RecNet.Auth;
|
|
using RadicadeServer.RecNet.Leaderboards;
|
|
using Xunit;
|
|
|
|
namespace RadicadeServer.Tests;
|
|
|
|
/// <summary>
|
|
/// The room scoreboard / watch leaderboard path: the client posts a stat with
|
|
/// CheckAndSetStat (SetStatRequestDTO: StatChannel + RoomId + StatValue, with identity
|
|
/// coming from the auth token rather than the body), then reads it back with GetRanks,
|
|
/// GetNearbyScores or GetPlayerRank. These tests drive the store end to end, because the
|
|
/// conformance suite only ever exercised an empty board.
|
|
/// </summary>
|
|
public class LeaderboardTests
|
|
{
|
|
private const long RoomA = 25;
|
|
private const long RoomB = 32;
|
|
|
|
private sealed class TestDbContextFactory : IDbContextFactory<RecDBContext>
|
|
{
|
|
private readonly DbContextOptions<RecDBContext> options;
|
|
|
|
public TestDbContextFactory()
|
|
{
|
|
var root = new InMemoryDatabaseRoot();
|
|
options = TestDbOptions.Create()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString(), root)
|
|
.Options;
|
|
}
|
|
|
|
public RecDBContext CreateDbContext() => new(options);
|
|
}
|
|
|
|
private static async Task SeedProfilesAsync(IDbContextFactory<RecDBContext> factory, params long[] ids)
|
|
{
|
|
await using var db = factory.CreateDbContext();
|
|
foreach (var id in ids)
|
|
db.Profiles.Add(new Profile
|
|
{
|
|
accountId = id,
|
|
profileImage = "default.png",
|
|
username = $"user{id}",
|
|
displayName = $"User {id}",
|
|
});
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
private static string Stat(long roomId, int channel, int value) =>
|
|
$"{{\"StatChannel\":{channel},\"RoomId\":{roomId},\"StatValue\":{value}}}";
|
|
|
|
[Fact]
|
|
public async Task SubmitScore_RanksPlayersAndKeepsRoomsSeparate()
|
|
{
|
|
var factory = new TestDbContextFactory();
|
|
await SeedProfilesAsync(factory, 2, 3);
|
|
|
|
await LeaderboardStore.SubmitScoreAsync(factory, Stat(RoomA, 1, 10), 2, 0);
|
|
await LeaderboardStore.SubmitScoreAsync(factory, Stat(RoomA, 1, 40), 3, 0);
|
|
// A different room's board on the same channel number must not leak into RoomA.
|
|
await LeaderboardStore.SubmitScoreAsync(factory, Stat(RoomB, 1, 999), 2, 0);
|
|
|
|
var board = await LeaderboardStore.GetRanksAsync(factory, new GetRanksRequest
|
|
{
|
|
RoomId = RoomA,
|
|
StatChannel = 1,
|
|
RankStart = 1,
|
|
RankEnd = 10,
|
|
});
|
|
|
|
Assert.Equal(2, board.Total);
|
|
Assert.Equal(2, board.Rows.Count);
|
|
Assert.Equal(3, board.Rows[0].PlayerId);
|
|
Assert.Equal(40, board.Rows[0].Score);
|
|
Assert.Equal(1, board.Rows[0].Rank);
|
|
Assert.Equal(2, board.Rows[1].PlayerId);
|
|
Assert.Equal(2, board.Rows[1].Rank);
|
|
Assert.Equal("User 3", board.Rows[0].PlayerName);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SubmitScore_KeepsEachPlayersBestRatherThanTheirLast()
|
|
{
|
|
var factory = new TestDbContextFactory();
|
|
await SeedProfilesAsync(factory, 2);
|
|
|
|
await LeaderboardStore.SubmitScoreAsync(factory, Stat(RoomA, 1, 50), 2, 0);
|
|
await LeaderboardStore.SubmitScoreAsync(factory, Stat(RoomA, 1, 10), 2, 0);
|
|
|
|
var board = await LeaderboardStore.GetRanksAsync(factory, new GetRanksRequest
|
|
{
|
|
RoomId = RoomA,
|
|
StatChannel = 1,
|
|
RankStart = 1,
|
|
RankEnd = 10,
|
|
});
|
|
|
|
Assert.Equal(1, board.Total);
|
|
Assert.Equal(50, board.Rows[0].Score);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SubmitScore_AscendingBoardRanksTheLowerTimeFirst()
|
|
{
|
|
var factory = new TestDbContextFactory();
|
|
await SeedProfilesAsync(factory, 2, 3);
|
|
|
|
// A race board: lower is better, and the client marks it with SortAscending.
|
|
await LeaderboardStore.SubmitScoreAsync(factory, "{\"StatChannel\":2,\"RoomId\":25,\"Time\":9000}", 2, 0);
|
|
await LeaderboardStore.SubmitScoreAsync(factory, "{\"StatChannel\":2,\"RoomId\":25,\"Time\":4000}", 3, 0);
|
|
|
|
var board = await LeaderboardStore.GetRanksAsync(factory, new GetRanksRequest
|
|
{
|
|
RoomId = RoomA,
|
|
StatChannel = 2,
|
|
SortAscending = true,
|
|
RankStart = 1,
|
|
RankEnd = 10,
|
|
});
|
|
|
|
Assert.Equal(2, board.Total);
|
|
Assert.Equal(3, board.Rows[0].PlayerId);
|
|
Assert.Equal(1, board.Rows[0].Rank);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SubmitScore_FallsBackToTheAuthenticatedPlayerWhenTheBodyHasNoPlayerId()
|
|
{
|
|
// The client's SetStatRequestDTO has no player field at all; identity is the token.
|
|
var factory = new TestDbContextFactory();
|
|
await SeedProfilesAsync(factory, 2);
|
|
|
|
var result = await LeaderboardStore.SubmitScoreAsync(factory, Stat(RoomA, 3, 7), 2, 0);
|
|
Assert.NotNull(result);
|
|
|
|
await using var db = factory.CreateDbContext();
|
|
var stored = await db.LeaderboardScores.SingleAsync();
|
|
Assert.Equal(2, stored.PlayerId);
|
|
Assert.Equal(7, stored.Score);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetNearbyScores_CentresTheWindowOnTheAskingPlayer()
|
|
{
|
|
var factory = new TestDbContextFactory();
|
|
await SeedProfilesAsync(factory, 2, 3, 4, 5, 6);
|
|
|
|
for (var i = 0; i < 5; i++)
|
|
await LeaderboardStore.SubmitScoreAsync(factory, Stat(RoomA, 1, 10 * (i + 1)), 2 + i, 0);
|
|
|
|
// Player 4 scored 30, so they sit third of five.
|
|
var nearby = await LeaderboardStore.GetNearbyScoresAsync(factory, new GetNearbyScoresRequest
|
|
{
|
|
RoomId = RoomA,
|
|
StatChannel = 1,
|
|
PlayerId = 4,
|
|
WindowSize = 3,
|
|
});
|
|
|
|
Assert.Equal(5, nearby.Total);
|
|
Assert.Equal(3, nearby.MyRank);
|
|
Assert.Contains(nearby.Rows, r => r.PlayerId == 4 && r.Rank == 3);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EmptyBoard_StillNamesTheAskingPlayer()
|
|
{
|
|
// The scoreboard renders the row the server hands back, so the synthetic row for
|
|
// a player who has not scored yet must still carry their name rather than an
|
|
// empty string. Names were loaded only for players already on the board, which on
|
|
// an empty board is nobody.
|
|
var factory = new TestDbContextFactory();
|
|
await SeedProfilesAsync(factory, 2);
|
|
|
|
var ranks = await LeaderboardStore.GetRanksAsync(factory, new GetRanksRequest
|
|
{
|
|
RoomId = RoomA,
|
|
StatChannel = 1,
|
|
PlayerId = 2,
|
|
RankStart = 1,
|
|
RankEnd = 10,
|
|
});
|
|
Assert.Equal("User 2", ranks.Rows[0].PlayerName);
|
|
|
|
var nearby = await LeaderboardStore.GetNearbyScoresAsync(factory, new GetNearbyScoresRequest
|
|
{
|
|
RoomId = RoomA,
|
|
StatChannel = 1,
|
|
PlayerId = 2,
|
|
WindowSize = 10,
|
|
});
|
|
Assert.Equal("User 2", nearby.Rows[0].PlayerName);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetPlayerRank_NamesTheAskingPlayerWhenTheyHaveNotScored()
|
|
{
|
|
var factory = new TestDbContextFactory();
|
|
await SeedProfilesAsync(factory, 2);
|
|
|
|
var rank = await LeaderboardStore.GetPlayerRankAsync(factory, new GetRankRequest
|
|
{
|
|
RoomId = RoomA,
|
|
StatChannel = 1,
|
|
PlayerId = 2,
|
|
});
|
|
|
|
var name = rank.GetType().GetProperty("PlayerName")?.GetValue(rank) as string;
|
|
Assert.Equal("User 2", name);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetPlayerRank_ReportsZeroForAPlayerWhoHasNotScored()
|
|
{
|
|
var factory = new TestDbContextFactory();
|
|
await SeedProfilesAsync(factory, 2, 3);
|
|
await LeaderboardStore.SubmitScoreAsync(factory, Stat(RoomA, 1, 10), 3, 0);
|
|
|
|
var rank = await LeaderboardStore.GetPlayerRankAsync(factory, new GetRankRequest
|
|
{
|
|
RoomId = RoomA,
|
|
StatChannel = 1,
|
|
PlayerId = 2,
|
|
});
|
|
|
|
Assert.NotNull(rank);
|
|
}
|
|
}
|