From f743b276963c0568e0967b5deec5955b8d7f4815 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Wed, 10 Jun 2026 10:46:00 -0400 Subject: [PATCH] feat(ranking): stub /ranking/* (6 endpoints) Rankings menu opens. Period picker shows deterministic monthly schedule. Every leaderboard returns { period, ranking: [] }. Endpoints: - /ranking/get_viewable_ranking_period_list - /ranking/master_point_{rotation,unlimited}_info - /ranking/rank_match_class_win_{rotation,unlimited}_info - /ranking/two_pick_win_info Co-Authored-By: Claude Opus 4.7 --- .../Controllers/RankingController.cs | 86 +++++++++++ .../Dtos/Ranking/ClassWinInfoRequestDto.cs | 15 ++ .../Dtos/Ranking/MasterPointInfoRequestDto.cs | 12 ++ .../Dtos/Ranking/MasterPointPeriodEntryDto.cs | 11 ++ .../Dtos/Ranking/MonthlyRankingResponseDto.cs | 16 ++ .../Models/Dtos/Ranking/PeriodEntryDto.cs | 26 ++++ .../Dtos/Ranking/PeriodListResponseDto.cs | 33 ++++ .../Models/Dtos/Ranking/RankingEntryDto.cs | 44 ++++++ .../Dtos/Ranking/TwoPickPeriodEntryDto.cs | 14 ++ .../Dtos/Ranking/TwoPickWinInfoRequestDto.cs | 12 ++ .../Controllers/RankingControllerTests.cs | 146 ++++++++++++++++++ 11 files changed, 415 insertions(+) create mode 100644 SVSim.EmulatedEntrypoint/Controllers/RankingController.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/ClassWinInfoRequestDto.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/MasterPointInfoRequestDto.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/MasterPointPeriodEntryDto.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/MonthlyRankingResponseDto.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/PeriodEntryDto.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/PeriodListResponseDto.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/RankingEntryDto.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/TwoPickPeriodEntryDto.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/TwoPickWinInfoRequestDto.cs create mode 100644 SVSim.UnitTests/Controllers/RankingControllerTests.cs diff --git a/SVSim.EmulatedEntrypoint/Controllers/RankingController.cs b/SVSim.EmulatedEntrypoint/Controllers/RankingController.cs new file mode 100644 index 0000000..5009793 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Controllers/RankingController.cs @@ -0,0 +1,86 @@ +using Microsoft.AspNetCore.Mvc; +using SVSim.EmulatedEntrypoint.Models.Dtos.Ranking; +using SVSim.EmulatedEntrypoint.Models.Dtos.Requests; +using SVSim.EmulatedEntrypoint.Services; + +namespace SVSim.EmulatedEntrypoint.Controllers; + +/// +/// /ranking/* — Rankings menu. Stub: the period picker renders a real +/// deterministic monthly schedule, but every leaderboard returns an empty +/// `ranking: []`. See docs/superpowers/specs/2026-06-10-ranking-stubs-design.md. +/// +[Route("ranking")] +public sealed class RankingController : SVSimController +{ + [HttpPost("get_viewable_ranking_period_list")] + public IActionResult GetViewablePeriodList([FromBody] BaseRequest req) + { + if (!TryGetViewerId(out _)) return Unauthorized(); + var now = DateTime.UtcNow; + return Ok(new PeriodListResponseDto + { + RankMatch = ToBase(RankingPeriodSchedule.GenerateFor(RankingPeriodSchedule.Family.RankMatch, now)), + MasterPoint = ToMasterPoint(RankingPeriodSchedule.GenerateFor(RankingPeriodSchedule.Family.MasterPoint, now)), + TwoPick = ToTwoPick(RankingPeriodSchedule.GenerateFor(RankingPeriodSchedule.Family.TwoPick, now)), + Sealed = ToBase(RankingPeriodSchedule.GenerateFor(RankingPeriodSchedule.Family.Sealed, now)), + // Crossover arrays stay empty — captured prod returned [] for both. + }); + } + + [HttpPost("master_point_rotation_info")] + public IActionResult MasterPointRotation([FromBody] MasterPointInfoRequestDto req) + => RankingFor(RankingPeriodSchedule.Family.MasterPoint, req.PeriodId); + + [HttpPost("master_point_unlimited_info")] + public IActionResult MasterPointUnlimited([FromBody] MasterPointInfoRequestDto req) + => RankingFor(RankingPeriodSchedule.Family.MasterPoint, req.PeriodId); + + [HttpPost("rank_match_class_win_rotation_info")] + public IActionResult RankMatchClassWinRotation([FromBody] ClassWinInfoRequestDto req) + => RankingFor(RankingPeriodSchedule.Family.RankMatch, req.PeriodId); + + [HttpPost("rank_match_class_win_unlimited_info")] + public IActionResult RankMatchClassWinUnlimited([FromBody] ClassWinInfoRequestDto req) + => RankingFor(RankingPeriodSchedule.Family.RankMatch, req.PeriodId); + + [HttpPost("two_pick_win_info")] + public IActionResult TwoPickWin([FromBody] TwoPickWinInfoRequestDto req) + => RankingFor(RankingPeriodSchedule.Family.TwoPick, req.PeriodId); + + private IActionResult RankingFor(RankingPeriodSchedule.Family family, int periodId) + { + if (!TryGetViewerId(out _)) return Unauthorized(); + var entry = RankingPeriodSchedule.TryFindById(family, periodId, DateTime.UtcNow); + var periodDto = entry is null + ? new PeriodEntryDto { Id = periodId.ToString() } + : new PeriodEntryDto + { + Id = entry.Id, + PeriodNum = entry.PeriodNum, + BeginTime = entry.BeginTime, + EndTime = entry.EndTime, + }; + return Ok(new MonthlyRankingResponseDto { Period = periodDto, Ranking = new() }); + } + + private static List ToBase(IReadOnlyList src) + => src.Select(e => new PeriodEntryDto + { + Id = e.Id, PeriodNum = e.PeriodNum, BeginTime = e.BeginTime, EndTime = e.EndTime, + }).ToList(); + + private static List ToMasterPoint(IReadOnlyList src) + => src.Select(e => new MasterPointPeriodEntryDto + { + Id = e.Id, PeriodNum = e.PeriodNum, BeginTime = e.BeginTime, EndTime = e.EndTime, + NecessaryScore = "0", + }).ToList(); + + private static List ToTwoPick(IReadOnlyList src) + => src.Select(e => new TwoPickPeriodEntryDto + { + Id = e.Id, PeriodNum = e.PeriodNum, BeginTime = e.BeginTime, EndTime = e.EndTime, + Type = "2", Over460 = "1", + }).ToList(); +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/ClassWinInfoRequestDto.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/ClassWinInfoRequestDto.cs new file mode 100644 index 0000000..392fa58 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/ClassWinInfoRequestDto.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; +using MessagePack; +using SVSim.EmulatedEntrypoint.Models.Dtos.Requests; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Ranking; + +[MessagePackObject] +public sealed class ClassWinInfoRequestDto : BaseRequest +{ + [JsonPropertyName("period_id"), Key("period_id")] + public int PeriodId { get; set; } + + [JsonPropertyName("class_id"), Key("class_id")] + public int ClassId { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/MasterPointInfoRequestDto.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/MasterPointInfoRequestDto.cs new file mode 100644 index 0000000..e408d97 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/MasterPointInfoRequestDto.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; +using MessagePack; +using SVSim.EmulatedEntrypoint.Models.Dtos.Requests; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Ranking; + +[MessagePackObject] +public sealed class MasterPointInfoRequestDto : BaseRequest +{ + [JsonPropertyName("period_id"), Key("period_id")] + public int PeriodId { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/MasterPointPeriodEntryDto.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/MasterPointPeriodEntryDto.cs new file mode 100644 index 0000000..db9f328 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/MasterPointPeriodEntryDto.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; +using MessagePack; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Ranking; + +[MessagePackObject] +public sealed class MasterPointPeriodEntryDto : PeriodEntryDto +{ + [JsonPropertyName("necessary_score"), Key("necessary_score")] + public string NecessaryScore { get; set; } = "0"; +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/MonthlyRankingResponseDto.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/MonthlyRankingResponseDto.cs new file mode 100644 index 0000000..1c4c6e3 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/MonthlyRankingResponseDto.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; +using MessagePack; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Ranking; + +[MessagePackObject] +public sealed class MonthlyRankingResponseDto +{ + [JsonPropertyName("period"), Key("period")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public PeriodEntryDto Period { get; set; } = new(); + + [JsonPropertyName("ranking"), Key("ranking")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public List Ranking { get; set; } = new(); +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/PeriodEntryDto.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/PeriodEntryDto.cs new file mode 100644 index 0000000..56ca976 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/PeriodEntryDto.cs @@ -0,0 +1,26 @@ +using System.Text.Json.Serialization; +using MessagePack; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Ranking; + +/// +/// Base period-entry shape used by /ranking/get_viewable_ranking_period_list's +/// rank_match[] and crossover_* arrays. Master-point and two-pick variants +/// add fields — see and +/// . +/// +[MessagePackObject] +public class PeriodEntryDto +{ + [JsonPropertyName("id"), Key("id")] + public string Id { get; set; } = "0"; + + [JsonPropertyName("period_num"), Key("period_num")] + public string PeriodNum { get; set; } = "0"; + + [JsonPropertyName("begin_time"), Key("begin_time")] + public string BeginTime { get; set; } = ""; + + [JsonPropertyName("end_time"), Key("end_time")] + public string EndTime { get; set; } = ""; +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/PeriodListResponseDto.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/PeriodListResponseDto.cs new file mode 100644 index 0000000..bc921c6 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/PeriodListResponseDto.cs @@ -0,0 +1,33 @@ +using System.Text.Json.Serialization; +using MessagePack; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Ranking; + +[MessagePackObject] +public sealed class PeriodListResponseDto +{ + // All required per spec; emit empty list, never null. + [JsonPropertyName("rank_match"), Key("rank_match")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public List RankMatch { get; set; } = new(); + + [JsonPropertyName("master_point"), Key("master_point")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public List MasterPoint { get; set; } = new(); + + [JsonPropertyName("two_pick"), Key("two_pick")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public List TwoPick { get; set; } = new(); + + [JsonPropertyName("sealed"), Key("sealed")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public List Sealed { get; set; } = new(); + + [JsonPropertyName("crossover_rank_match"), Key("crossover_rank_match")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public List CrossoverRankMatch { get; set; } = new(); + + [JsonPropertyName("crossover_master_point"), Key("crossover_master_point")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public List CrossoverMasterPoint { get; set; } = new(); +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/RankingEntryDto.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/RankingEntryDto.cs new file mode 100644 index 0000000..fc7e29e --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/RankingEntryDto.cs @@ -0,0 +1,44 @@ +using System.Text.Json.Serialization; +using MessagePack; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Ranking; + +/// +/// One row in a /ranking/* leaderboard's `ranking[]` array. Stub server never +/// emits these; the type exists so the DTO compiles and so wire-shape tests can +/// parse captured prod frames into it. Wire-type quirks (per capture frame 65): +/// viewer_id/score/ranking_rank are STRINGS; rank/emblem_id/degree_id are NUMBERS. +/// +[MessagePackObject] +public sealed class RankingEntryDto +{ + [JsonPropertyName("viewer_id"), Key("viewer_id")] + public string ViewerId { get; set; } = "0"; + + [JsonPropertyName("score"), Key("score")] + public string Score { get; set; } = "0"; + + [JsonPropertyName("ranking_rank"), Key("ranking_rank")] + public string RankingRank { get; set; } = "0"; + + [JsonPropertyName("name"), Key("name")] + public string Name { get; set; } = ""; + + [JsonPropertyName("country_code"), Key("country_code")] + public string CountryCode { get; set; } = ""; + + [JsonPropertyName("rank"), Key("rank")] + public int Rank { get; set; } + + [JsonPropertyName("emblem_id"), Key("emblem_id")] + public long EmblemId { get; set; } + + [JsonPropertyName("degree_id"), Key("degree_id")] + public long DegreeId { get; set; } + + [JsonPropertyName("last_play_time"), Key("last_play_time")] + public string LastPlayTime { get; set; } = ""; + + [JsonPropertyName("guild_name"), Key("guild_name")] + public string GuildName { get; set; } = ""; +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/TwoPickPeriodEntryDto.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/TwoPickPeriodEntryDto.cs new file mode 100644 index 0000000..5dbfbec --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/TwoPickPeriodEntryDto.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; +using MessagePack; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Ranking; + +[MessagePackObject] +public sealed class TwoPickPeriodEntryDto : PeriodEntryDto +{ + [JsonPropertyName("type"), Key("type")] + public string Type { get; set; } = "2"; + + [JsonPropertyName("over_460"), Key("over_460")] + public string Over460 { get; set; } = "1"; +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/TwoPickWinInfoRequestDto.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/TwoPickWinInfoRequestDto.cs new file mode 100644 index 0000000..deae94b --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Ranking/TwoPickWinInfoRequestDto.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; +using MessagePack; +using SVSim.EmulatedEntrypoint.Models.Dtos.Requests; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Ranking; + +[MessagePackObject] +public sealed class TwoPickWinInfoRequestDto : BaseRequest +{ + [JsonPropertyName("period_id"), Key("period_id")] + public int PeriodId { get; set; } +} diff --git a/SVSim.UnitTests/Controllers/RankingControllerTests.cs b/SVSim.UnitTests/Controllers/RankingControllerTests.cs new file mode 100644 index 0000000..72d596f --- /dev/null +++ b/SVSim.UnitTests/Controllers/RankingControllerTests.cs @@ -0,0 +1,146 @@ +using System.Net; +using System.Net.Http.Json; +using System.Text; +using System.Text.Json; +using SVSim.UnitTests.Infrastructure; + +namespace SVSim.UnitTests.Controllers; + +public class RankingControllerTests +{ + private static StringContent JsonBody(string json) => new(json, Encoding.UTF8, "application/json"); + + private const string EmptyBody = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":""}"""; + + [Test] + public async Task GetViewablePeriodList_returns_six_family_arrays() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(steamId: 76_561_198_000_030_001UL); + using var client = factory.CreateAuthenticatedClient(viewerId); + + var resp = await client.PostAsync("/ranking/get_viewable_ranking_period_list", JsonBody(EmptyBody)); + Assert.That(resp.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + + var raw = await resp.Content.ReadAsStringAsync(); + using var doc = JsonDocument.Parse(raw); + var data = doc.RootElement; + + foreach (var key in new[] { "rank_match", "master_point", "two_pick", "sealed", + "crossover_rank_match", "crossover_master_point" }) + { + Assert.That(data.TryGetProperty(key, out _), Is.True, $"missing key {key}"); + } + Assert.That(data.GetProperty("rank_match").GetArrayLength(), Is.GreaterThan(0)); + Assert.That(data.GetProperty("crossover_rank_match").GetArrayLength(), Is.EqualTo(0)); + Assert.That(data.GetProperty("crossover_master_point").GetArrayLength(), Is.EqualTo(0)); + + // Master-point entries carry the extra "necessary_score" field per capture. + var mp0 = data.GetProperty("master_point")[0]; + Assert.That(mp0.GetProperty("necessary_score").GetString(), Is.EqualTo("0")); + + // Two-pick entries carry "type" and "over_460". + var tp0 = data.GetProperty("two_pick")[0]; + Assert.That(tp0.GetProperty("type").GetString(), Is.EqualTo("2")); + Assert.That(tp0.GetProperty("over_460").GetString(), Is.EqualTo("1")); + } + + [Test] + public async Task MasterPointRotationInfo_returns_empty_ranking_with_period_echoed() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(steamId: 76_561_198_000_030_001UL); + using var client = factory.CreateAuthenticatedClient(viewerId); + + var resp = await client.PostAsync("/ranking/master_point_rotation_info", + JsonBody("""{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","period_id":1}""")); + Assert.That(resp.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + + var raw = await resp.Content.ReadAsStringAsync(); + using var doc = JsonDocument.Parse(raw); + Assert.That(doc.RootElement.GetProperty("ranking").GetArrayLength(), Is.EqualTo(0)); + Assert.That(doc.RootElement.GetProperty("period").GetProperty("id").GetString(), Is.EqualTo("1")); + } + + [Test] + public async Task MasterPointUnlimitedInfo_returns_empty_ranking() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(steamId: 76_561_198_000_030_001UL); + using var client = factory.CreateAuthenticatedClient(viewerId); + + var resp = await client.PostAsync("/ranking/master_point_unlimited_info", + JsonBody("""{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","period_id":1}""")); + Assert.That(resp.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + var raw = await resp.Content.ReadAsStringAsync(); + using var doc = JsonDocument.Parse(raw); + Assert.That(doc.RootElement.GetProperty("ranking").GetArrayLength(), Is.EqualTo(0)); + } + + [Test] + public async Task RankMatchClassWinRotationInfo_accepts_class_id_and_returns_empty() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(steamId: 76_561_198_000_030_001UL); + using var client = factory.CreateAuthenticatedClient(viewerId); + + var resp = await client.PostAsync("/ranking/rank_match_class_win_rotation_info", + JsonBody("""{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","period_id":1,"class_id":3}""")); + Assert.That(resp.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + var raw = await resp.Content.ReadAsStringAsync(); + using var doc = JsonDocument.Parse(raw); + Assert.That(doc.RootElement.GetProperty("ranking").GetArrayLength(), Is.EqualTo(0)); + } + + [Test] + public async Task RankMatchClassWinUnlimitedInfo_returns_empty() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(steamId: 76_561_198_000_030_001UL); + using var client = factory.CreateAuthenticatedClient(viewerId); + + var resp = await client.PostAsync("/ranking/rank_match_class_win_unlimited_info", + JsonBody("""{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","period_id":1,"class_id":1}""")); + Assert.That(resp.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + } + + [Test] + public async Task TwoPickWinInfo_returns_empty_ranking() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(steamId: 76_561_198_000_030_001UL); + using var client = factory.CreateAuthenticatedClient(viewerId); + + var resp = await client.PostAsync("/ranking/two_pick_win_info", + JsonBody("""{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","period_id":1}""")); + Assert.That(resp.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + var raw = await resp.Content.ReadAsStringAsync(); + using var doc = JsonDocument.Parse(raw); + Assert.That(doc.RootElement.GetProperty("ranking").GetArrayLength(), Is.EqualTo(0)); + } + + [Test] + public async Task MasterPointRotationInfo_unknown_period_returns_empty_period() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(steamId: 76_561_198_000_030_001UL); + using var client = factory.CreateAuthenticatedClient(viewerId); + + var resp = await client.PostAsync("/ranking/master_point_rotation_info", + JsonBody("""{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","period_id":99999}""")); + Assert.That(resp.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + var raw = await resp.Content.ReadAsStringAsync(); + using var doc = JsonDocument.Parse(raw); + Assert.That(doc.RootElement.GetProperty("ranking").GetArrayLength(), Is.EqualTo(0)); + } + + [Test] + public async Task Unauthenticated_get_viewable_period_list_returns_401() + { + using var factory = new SVSimTestFactory(); + var client = factory.CreateClient(); + + var resp = await client.PostAsync("/ranking/get_viewable_ranking_period_list", JsonBody(EmptyBody)); + Assert.That(resp.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized)); + } +}