diff --git a/ArenaService.Shared/Dtos/RankingSnapshotEntryResponse.cs b/ArenaService.Shared/Dtos/RankingSnapshotEntryResponse.cs new file mode 100644 index 0000000..fb13066 --- /dev/null +++ b/ArenaService.Shared/Dtos/RankingSnapshotEntryResponse.cs @@ -0,0 +1,15 @@ +using Libplanet.Crypto; + +namespace ArenaService.Shared.Dtos; + +public class RankingSnapshotEntryResponse +{ + public required Address AgentAddress { get; set; } + public required Address AvatarAddress { get; set; } + public required string NameWithHash { get; set; } + public required int Level { get; set; } + public required long Cp { get; set; } + public required int Score { get; set; } +} + + diff --git a/ArenaService.Shared/Repositories/RankingSnapshotRepository.cs b/ArenaService.Shared/Repositories/RankingSnapshotRepository.cs index bcd48a3..b8d360e 100644 --- a/ArenaService.Shared/Repositories/RankingSnapshotRepository.cs +++ b/ArenaService.Shared/Repositories/RankingSnapshotRepository.cs @@ -25,6 +25,13 @@ Task GetRankingSnapshotsCount( int roundId, Func, IQueryable>? includeQuery = null ); + + Task> GetRankingSnapshotEntries( + int seasonId, + int roundId, + int skip = 0, + int take = 1000 + ); } public class RankingSnapshotRepository : IRankingSnapshotRepository @@ -103,4 +110,29 @@ public async Task GetRankingSnapshotsCount( return await query.Where(r => r.SeasonId == seasonId && r.RoundId == roundId).CountAsync(); } + + public async Task> GetRankingSnapshotEntries( + int seasonId, + int roundId, + int skip = 0, + int take = 1000 + ) + { + var query = + from snapshot in _context.RankingSnapshots.AsNoTracking() + join user in _context.Users.AsNoTracking() on snapshot.AvatarAddress equals user.AvatarAddress + where snapshot.SeasonId == seasonId && snapshot.RoundId == roundId + orderby snapshot.Score descending + select new ArenaService.Shared.Dtos.RankingSnapshotEntryResponse + { + AgentAddress = user.AgentAddress, + AvatarAddress = user.AvatarAddress, + NameWithHash = user.NameWithHash, + Level = user.Level, + Cp = user.Cp, + Score = snapshot.Score + }; + + return await query.Skip(skip).Take(take).ToListAsync(); + } } diff --git a/ArenaService.Tests/Controllers/LeaderboardControllerTests.cs b/ArenaService.Tests/Controllers/LeaderboardControllerTests.cs index c2a1c5f..f537946 100644 --- a/ArenaService.Tests/Controllers/LeaderboardControllerTests.cs +++ b/ArenaService.Tests/Controllers/LeaderboardControllerTests.cs @@ -19,6 +19,7 @@ public class LeaderboardControllerTests private readonly Mock _mockSeasonService; private readonly Mock _mockSeasonCacheRepo; private readonly Mock _mockSeasonRepo; + private readonly Mock _mockRankingSnapshotRepo; private readonly LeaderboardController _controller; public LeaderboardControllerTests() @@ -29,6 +30,7 @@ public LeaderboardControllerTests() _mockSeasonService = new Mock(); _mockSeasonCacheRepo = new Mock(); _mockSeasonRepo = new Mock(); + _mockRankingSnapshotRepo = new Mock(); _controller = new LeaderboardController( _mockAllClanRankingRepo.Object, @@ -36,7 +38,8 @@ public LeaderboardControllerTests() _mockLeaderboardRepo.Object, _mockSeasonService.Object, _mockSeasonCacheRepo.Object, - _mockSeasonRepo.Object + _mockSeasonRepo.Object, + _mockRankingSnapshotRepo.Object ); } @@ -175,4 +178,52 @@ public async Task GetCompletedArenaLeaderboard_WithException_ReturnsBadRequest() // Assert var badRequestResult = Assert.IsType(result.Result); } + + [Fact] + public async Task GetRankingSnapshot_RespectsPaginationParameters() + { + // Arrange + int seasonId = 1; + int roundId = 1; + var entries = Enumerable.Range(0, 1500).Select(i => new RankingSnapshotEntryResponse + { + AgentAddress = new Address($"0x{i.ToString("x").PadLeft(40, '0')}"), + AvatarAddress = new Address($"0x{(i + 10000).ToString("x").PadLeft(40, '0')}"), + NameWithHash = $"Player#{i}", + Level = i, + Cp = i * 100, + Score = 2000 - i + }).ToList(); + + _mockRankingSnapshotRepo + .Setup(x => x.GetRankingSnapshotEntries(seasonId, roundId, 0, 1000)) + .ReturnsAsync(entries.Take(1000).ToList()); + _mockRankingSnapshotRepo + .Setup(x => x.GetRankingSnapshotEntries(seasonId, roundId, 1000, 1000)) + .ReturnsAsync(entries.Skip(1000).Take(1000).ToList()); + _mockRankingSnapshotRepo + .Setup(x => x.GetRankingSnapshotEntries(seasonId, roundId, 500, 200)) + .ReturnsAsync(entries.Skip(500).Take(200).ToList()); + + // Act + var firstPageResult = await _controller.GetRankingSnapshot(seasonId, roundId, 0, 1000); + var secondPageResult = await _controller.GetRankingSnapshot(seasonId, roundId, 1000, 1000); + var customPageResult = await _controller.GetRankingSnapshot(seasonId, roundId, 500, 200); + + // Assert + var firstPage = Assert.IsType(firstPageResult.Result); + var secondPage = Assert.IsType(secondPageResult.Result); + var customPage = Assert.IsType(customPageResult.Result); + + var firstPageEntries = Assert.IsType>(firstPage.Value); + var secondPageEntries = Assert.IsType>(secondPage.Value); + var customPageEntries = Assert.IsType>(customPage.Value); + + Assert.Equal(1000, firstPageEntries.Count); + Assert.Equal(500, secondPageEntries.Count); + Assert.Equal(200, customPageEntries.Count); + Assert.Equal(entries[0].AvatarAddress, firstPageEntries.First().AvatarAddress); + Assert.Equal(entries[1000].AvatarAddress, secondPageEntries.First().AvatarAddress); + Assert.Equal(entries[500].AvatarAddress, customPageEntries.First().AvatarAddress); + } } diff --git a/ArenaService/Controllers/LeaderboardController.cs b/ArenaService/Controllers/LeaderboardController.cs index 9debf90..976a604 100644 --- a/ArenaService/Controllers/LeaderboardController.cs +++ b/ArenaService/Controllers/LeaderboardController.cs @@ -17,6 +17,7 @@ public class LeaderboardController : ControllerBase private readonly ISeasonService _seasonService; private readonly ISeasonRepository _seasonRepo; private readonly ISeasonCacheRepository _seasonCacheRepo; + private readonly IRankingSnapshotRepository _rankingSnapshotRepo; public LeaderboardController( IAllClanRankingRepository allClanRankingRepo, @@ -24,7 +25,8 @@ public LeaderboardController( ILeaderboardRepository leaderboardRepo, ISeasonService seasonService, ISeasonCacheRepository seasonCacheRepo, - ISeasonRepository seasonRepo + ISeasonRepository seasonRepo, + IRankingSnapshotRepository rankingSnapshotRepo ) { _allClanRankingRepo = allClanRankingRepo; @@ -33,6 +35,7 @@ ISeasonRepository seasonRepo _seasonService = seasonService; _seasonCacheRepo = seasonCacheRepo; _seasonRepo = seasonRepo; + _rankingSnapshotRepo = rankingSnapshotRepo; } [HttpGet("count")] @@ -44,6 +47,28 @@ public async Task> GetRankingCount(int seasonId, int roundInde return Ok(rankingCount); } + [HttpGet("participants")] + [SwaggerResponse( + StatusCodes.Status200OK, + "Ranking ongoing participants", + typeof(List) + )] + public async Task>> GetRankingSnapshot( + [FromQuery] int seasonId, + [FromQuery] int roundId, + [FromQuery] int skip = 0, + [FromQuery] int take = 1000 + ) + { + // Limit the maximum page size to 1000 + if (take > 1000) + { + take = 1000; + } + var entries = await _rankingSnapshotRepo.GetRankingSnapshotEntries(seasonId, roundId, skip, take); + return Ok(entries); + } + [HttpGet("completed")] [SwaggerResponse( StatusCodes.Status200OK,