From 1dd6f2122014222ea4125ca204e2eed13bbddd15 Mon Sep 17 00:00:00 2001 From: Beyley Thomas Date: Tue, 17 Oct 2023 20:25:14 -0700 Subject: [PATCH 1/6] Send BadRequest when `count` is set by a non-PSP in play/user/{id} --- .../Game/Levels/LeaderboardEndpoints.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Refresh.GameServer/Endpoints/Game/Levels/LeaderboardEndpoints.cs b/Refresh.GameServer/Endpoints/Game/Levels/LeaderboardEndpoints.cs index e429fce9..5f8987a2 100644 --- a/Refresh.GameServer/Endpoints/Game/Levels/LeaderboardEndpoints.cs +++ b/Refresh.GameServer/Endpoints/Game/Levels/LeaderboardEndpoints.cs @@ -31,14 +31,23 @@ public Response PlayLevel(RequestContext context, GameUser user, GameDatabaseCon int count = 1; //If we are on PSP and it has sent a `count` parameter... - if (context.IsPSP() && context.QueryString.Get("count") != null) + if (context.QueryString.Get("count") != null) { + //Count parameters are invalid on non-PSP clients + if (!context.IsPSP()) return BadRequest; + //Parse the count if (!int.TryParse(context.QueryString["count"], out count)) { //If it fails, send a bad request back to the client return BadRequest; } + + //Sanitize against invalid values + if (count < 1) + { + return BadRequest; + } } database.PlayLevel(level, user, count); @@ -119,12 +128,12 @@ public Response SubmitScore(RequestContext context, GameUser user, GameDatabaseC [GameEndpoint("topscores/user/{id}/{type}", ContentType.Xml)] [MinimumRole(GameUserRole.Restricted)] [RateLimitSettings(RequestTimeoutDuration, MaxRequestAmount, RequestBlockDuration, BucketName)] - public SerializedScoreList? GetTopScoresForLevel(RequestContext context, GameDatabaseContext database, int id, int type) + public Response GetTopScoresForLevel(RequestContext context, GameDatabaseContext database, int id, int type) { GameLevel? level = database.GetLevelById(id); - if (level == null) return null; + if (level == null) return NotFound; (int skip, int count) = context.GetPageData(); - return SerializedScoreList.FromSubmittedEnumerable(database.GetTopScoresForLevel(level, count, skip, (byte)type).Items); + return new Response(SerializedScoreList.FromSubmittedEnumerable(database.GetTopScoresForLevel(level, count, skip, (byte)type).Items), ContentType.Xml); } } \ No newline at end of file From dc44caa9e38139285a03487b2deac2b6ea00bbfa Mon Sep 17 00:00:00 2001 From: Beyley Thomas Date: Tue, 17 Oct 2023 20:48:57 -0700 Subject: [PATCH 2/6] Actually set StoryId for developer levels This prevents duplicate story levels from being created every time theres a request to any developer level related endpoint. --- Refresh.GameServer/Database/GameDatabaseContext.Levels.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Refresh.GameServer/Database/GameDatabaseContext.Levels.cs b/Refresh.GameServer/Database/GameDatabaseContext.Levels.cs index d4ab9379..c11bdb16 100644 --- a/Refresh.GameServer/Database/GameDatabaseContext.Levels.cs +++ b/Refresh.GameServer/Database/GameDatabaseContext.Levels.cs @@ -42,6 +42,7 @@ public GameLevel GetStoryLevelById(int id) Publisher = null, Location = GameLocation.Zero, Source = GameLevelSource.Story, + StoryId = id, }; //Add the new story level to the database From ddc5cf029a2407efdf0a1a0c3f7b033257d55b01 Mon Sep 17 00:00:00 2001 From: Beyley Thomas Date: Tue, 17 Oct 2023 20:49:17 -0700 Subject: [PATCH 3/6] Validate story level IDs for developer leaderboard GET --- .../Endpoints/Game/Levels/LeaderboardEndpoints.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Refresh.GameServer/Endpoints/Game/Levels/LeaderboardEndpoints.cs b/Refresh.GameServer/Endpoints/Game/Levels/LeaderboardEndpoints.cs index 5f8987a2..73632203 100644 --- a/Refresh.GameServer/Endpoints/Game/Levels/LeaderboardEndpoints.cs +++ b/Refresh.GameServer/Endpoints/Game/Levels/LeaderboardEndpoints.cs @@ -58,6 +58,12 @@ public Response PlayLevel(RequestContext context, GameUser user, GameDatabaseCon [RateLimitSettings(RequestTimeoutDuration, MaxRequestAmount, RequestBlockDuration, BucketName)] public Response GetDeveloperScores(RequestContext context, GameUser user, GameDatabaseContext database, int id, Token token) { + //No story levels have an ID < 0 + if (id < 0) + { + return BadRequest; + } + GameLevel level = database.GetStoryLevelById(id); MultiLeaderboard multiLeaderboard = new(database, level, token.TokenGame); From 9d1826f900379ff544530fa4dc6f2d8618c0e0b4 Mon Sep 17 00:00:00 2001 From: Beyley Thomas Date: Tue, 17 Oct 2023 20:50:00 -0700 Subject: [PATCH 4/6] Add tests to reach 100% test coverage in LeaderboardEndpoints --- .../HttpContentExtensions.cs | 14 + RefreshTests.GameServer/ObjectExtensions.cs | 16 + .../Tests/Levels/ScoreLeaderboardTests.cs | 275 +++++++++++++++++- 3 files changed, 297 insertions(+), 8 deletions(-) create mode 100644 RefreshTests.GameServer/HttpContentExtensions.cs create mode 100644 RefreshTests.GameServer/ObjectExtensions.cs diff --git a/RefreshTests.GameServer/HttpContentExtensions.cs b/RefreshTests.GameServer/HttpContentExtensions.cs new file mode 100644 index 00000000..bc8632f0 --- /dev/null +++ b/RefreshTests.GameServer/HttpContentExtensions.cs @@ -0,0 +1,14 @@ +using System.Xml; +using System.Xml.Serialization; + +namespace RefreshTests.GameServer; + +public static class HttpContentExtensions +{ + public async static Task ReadAsXML(this HttpContent content) + { + XmlSerializer serializer = new(typeof(T)); + + return (T)serializer.Deserialize(new XmlTextReader(await content.ReadAsStreamAsync()))!; + } +} \ No newline at end of file diff --git a/RefreshTests.GameServer/ObjectExtensions.cs b/RefreshTests.GameServer/ObjectExtensions.cs new file mode 100644 index 00000000..17146453 --- /dev/null +++ b/RefreshTests.GameServer/ObjectExtensions.cs @@ -0,0 +1,16 @@ +using System.Xml.Serialization; + +namespace RefreshTests.GameServer; + +public static class ObjectExtensions +{ + public static string AsXML(this object obj) + { + XmlSerializer serializer = new(obj.GetType()); + + TextWriter writer = new StringWriter(); + serializer.Serialize(writer, obj); + + return writer.ToString()!; + } +} \ No newline at end of file diff --git a/RefreshTests.GameServer/Tests/Levels/ScoreLeaderboardTests.cs b/RefreshTests.GameServer/Tests/Levels/ScoreLeaderboardTests.cs index c7536c6a..203f4830 100644 --- a/RefreshTests.GameServer/Tests/Levels/ScoreLeaderboardTests.cs +++ b/RefreshTests.GameServer/Tests/Levels/ScoreLeaderboardTests.cs @@ -1,6 +1,7 @@ using MongoDB.Bson; using Refresh.GameServer.Authentication; using Refresh.GameServer.Types.Levels; +using Refresh.GameServer.Types.Lists; using Refresh.GameServer.Types.UserData; using Refresh.GameServer.Types.UserData.Leaderboard; @@ -17,20 +18,201 @@ public async Task SubmitsScore() using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); - string scorePayload = $@" -true -1 -{user.Username} -0 -"; + SerializedScore score = new() + { + Host = true, + ScoreType = 1, + Score = 5, + }; + + HttpResponseMessage message = await client.PostAsync($"/lbp/scoreboard/user/{level.LevelId}", new StringContent(score.AsXML())); + Assert.That(message.StatusCode, Is.EqualTo(OK)); + + message = await client.GetAsync($"/lbp/topscores/user/{level.LevelId}/1"); + Assert.That(message.StatusCode, Is.EqualTo(OK)); + + SerializedScoreList scores = await message.Content.ReadAsXML(); + Assert.That(scores.Scores, Has.Count.EqualTo(1)); + Assert.That(scores.Scores[0].Player, Is.EqualTo(user.Username)); + Assert.That(scores.Scores[0].Score, Is.EqualTo(5)); - HttpResponseMessage message = await client.PostAsync($"/lbp/scoreboard/user/{level.LevelId}", new StringContent(scorePayload)); + message = await client.GetAsync($"/lbp/scoreboard/user/{level.LevelId}"); Assert.That(message.StatusCode, Is.EqualTo(OK)); + + SerializedMultiLeaderboardResponse scoresMulti = await message.Content.ReadAsXML(); + SerializedPlayerLeaderboardResponse singleplayerScores = scoresMulti.Scoreboards.First(s => s.PlayerCount == 1); + Assert.That(singleplayerScores.Scores, Has.Count.EqualTo(1)); + Assert.That(singleplayerScores.Scores[0].Player, Is.EqualTo(user.Username)); + Assert.That(singleplayerScores.Scores[0].Score, Is.EqualTo(5)); + } + + [Test] + public async Task SubmitsDeveloperScore() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); + + SerializedScore score = new() + { + Host = true, + ScoreType = 1, + Score = 5, + }; + + HttpResponseMessage message = await client.PostAsync($"/lbp/scoreboard/developer/1", new StringContent(score.AsXML())); + Assert.That(message.StatusCode, Is.EqualTo(OK)); + + context.Database.Refresh(); + + // message = await client.GetAsync($"/lbp/topscores/developer/{level.LevelId}/1"); + // Assert.That(message.StatusCode, Is.EqualTo(OK)); + // + // SerializedScoreList scores = await message.Content.ReadAsXML(); + // Assert.That(scores.Scores, Has.Count.EqualTo(1)); + // Assert.That(scores.Scores[0].Player, Is.EqualTo(user.Username)); + // Assert.That(scores.Scores[0].Score, Is.EqualTo(5)); + + message = await client.GetAsync($"/lbp/scoreboard/developer/1"); + Assert.That(message.StatusCode, Is.EqualTo(OK)); + + SerializedMultiLeaderboardResponse scoresMulti = await message.Content.ReadAsXML(); + SerializedPlayerLeaderboardResponse singleplayerScores = scoresMulti.Scoreboards.First(s => s.PlayerCount == 1); + Assert.That(singleplayerScores.Scores, Has.Count.EqualTo(1)); + Assert.That(singleplayerScores.Scores[0].Player, Is.EqualTo(user.Username)); + Assert.That(singleplayerScores.Scores[0].Score, Is.EqualTo(5)); + } + + [Test] + public async Task DosentGetLeaderboardForInvalidLevel() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); + + HttpResponseMessage message2 = await client.GetAsync($"/lbp/topscores/user/{int.MaxValue}/1"); + Assert.That(message2.StatusCode, Is.EqualTo(NotFound)); + } + + [Test] + public async Task DosentGetMultiLeaderboardForInvalidLevel() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); + + HttpResponseMessage message = await client.GetAsync($"/lbp/scoreboard/user/{int.MaxValue}"); + Assert.That(message.StatusCode, Is.EqualTo(NotFound)); + } + + [Test] + public async Task DoesntSubmitInvalidScore() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + GameLevel level = context.CreateLevel(user); + + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); + + SerializedScore score = new() + { + Host = true, + ScoreType = 1, + Score = -1, + }; + + HttpResponseMessage message = await client.PostAsync($"/lbp/scoreboard/user/{level.LevelId}", new StringContent(score.AsXML())); + Assert.That(message.StatusCode, Is.EqualTo(BadRequest)); + + context.Database.Refresh(); + + List scores = context.Database.GetTopScoresForLevel(level, 1, 0, 1).Items.ToList(); + Assert.That(scores, Has.Count.EqualTo(0)); + } + + [Test] + public async Task DoesntSubmitDeveloperInvalidScore() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + GameLevel level = context.CreateLevel(user); + + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); + + SerializedScore score = new() + { + Host = true, + ScoreType = 1, + Score = -1, + }; + + HttpResponseMessage message = await client.PostAsync($"/lbp/scoreboard/developer/{level.LevelId}", new StringContent(score.AsXML())); + Assert.That(message.StatusCode, Is.EqualTo(BadRequest)); context.Database.Refresh(); List scores = context.Database.GetTopScoresForLevel(level, 1, 0, 1).Items.ToList(); - Assert.That(scores, Has.Count.EqualTo(1)); + Assert.That(scores, Has.Count.EqualTo(0)); + } + + [Test] + public async Task DoesntSubmitsScoreToInvalidLevel() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); + + SerializedScore score = new() + { + Host = true, + ScoreType = 1, + Score = 0, + }; + + HttpResponseMessage message = await client.PostAsync($"/lbp/scoreboard/user/{int.MaxValue}", new StringContent(score.AsXML())); + Assert.That(message.StatusCode, Is.EqualTo(NotFound)); + } + + [Test] + public async Task DoesntSubmitDeveloperScoreToInvalidLevel() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); + + SerializedScore score = new() + { + Host = true, + ScoreType = 1, + Score = 0, + }; + + HttpResponseMessage message = await client.PostAsync($"/lbp/scoreboard/developer/-1", new StringContent(score.AsXML())); + Assert.That(message.StatusCode, Is.EqualTo(BadRequest)); + } + + [Test] + public async Task DoesntGetDeveloperScoresForInvalidLevel() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); + + SerializedScore score = new() + { + Host = true, + ScoreType = 1, + Score = 0, + }; + + HttpResponseMessage message = await client.GetAsync($"/lbp/scoreboard/developer/-1"); + Assert.That(message.StatusCode, Is.EqualTo(BadRequest)); } /// The number of scores to try to fetch from the database @@ -103,4 +285,81 @@ public void FailsWithInvalidNumber() Assert.That(() => context.Database.GetRankedScoresAroundScore(score, 2), Throws.ArgumentException); } + + [Test] + public async Task PlayLevel() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + GameLevel level = context.CreateLevel(user); + + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); + + HttpResponseMessage message = await client.PostAsync($"/lbp/play/user/{level.LevelId}", new ReadOnlyMemoryContent(Array.Empty())); + Assert.That(message.StatusCode, Is.EqualTo(OK)); + + context.Database.Refresh(); + + Assert.That(level.AllPlays.Count(), Is.EqualTo(1)); + } + + [Test] + public async Task DoesntPlayInvalidLevel() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); + + HttpResponseMessage message = await client.PostAsync($"/lbp/play/user/{int.MaxValue}", new ReadOnlyMemoryContent(Array.Empty())); + Assert.That(message.StatusCode, Is.EqualTo(NotFound)); + } + + [Test] + public async Task PlayLevelWithCount() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + GameLevel level = context.CreateLevel(user); + + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); + client.DefaultRequestHeaders.UserAgent.TryParseAdd("LBPPSP CLIENT"); + + HttpResponseMessage message = await client.PostAsync($"/lbp/play/user/{level.LevelId}?count=2", new ReadOnlyMemoryContent(Array.Empty())); + Assert.That(message.StatusCode, Is.EqualTo(OK)); + + context.Database.Refresh(); + + Assert.That(level.AllPlays.AsEnumerable().Sum(p => p.Count), Is.EqualTo(2)); + } + + [Test] + public async Task DoesntPlayLevelWithInvalidCount() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + GameLevel level = context.CreateLevel(user); + + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); + client.DefaultRequestHeaders.UserAgent.TryParseAdd("LBPPSP CLIENT"); + + HttpResponseMessage message = await client.PostAsync($"/lbp/play/user/{level.LevelId}?count=gtgnyegth", new ReadOnlyMemoryContent(Array.Empty())); + Assert.That(message.StatusCode, Is.EqualTo(BadRequest)); + + HttpResponseMessage message2 = await client.PostAsync($"/lbp/play/user/{level.LevelId}?count=-5", new ReadOnlyMemoryContent(Array.Empty())); + Assert.That(message2.StatusCode, Is.EqualTo(BadRequest)); + } + + [Test] + public async Task DoesntPlayLevelWithCountOnMainline() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + GameLevel level = context.CreateLevel(user); + + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); + + HttpResponseMessage message = await client.PostAsync($"/lbp/play/user/{level.LevelId}?count=3", new ReadOnlyMemoryContent(Array.Empty())); + Assert.That(message.StatusCode, Is.EqualTo(BadRequest)); + } } \ No newline at end of file From dc4f313715f03d42a39fa616aafa9adeb112c7dd Mon Sep 17 00:00:00 2001 From: jvyden Date: Wed, 18 Oct 2023 00:53:59 -0400 Subject: [PATCH 5/6] Ignore TokenRefreshingWorks for now --- .../Tests/Authentication/AuthenticationIntegrationTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/RefreshTests.GameServer/Tests/Authentication/AuthenticationIntegrationTests.cs b/RefreshTests.GameServer/Tests/Authentication/AuthenticationIntegrationTests.cs index 9efaf96e..0dc788d9 100644 --- a/RefreshTests.GameServer/Tests/Authentication/AuthenticationIntegrationTests.cs +++ b/RefreshTests.GameServer/Tests/Authentication/AuthenticationIntegrationTests.cs @@ -56,6 +56,7 @@ public async Task ApiAuthenticationWorks() } [Test] + [Ignore("Borked")] public async Task TokenRefreshingWorks() { using TestContext context = this.GetServer(); From 0c239cf9683f49c4846ca68b8d3f0f5bda0a5531 Mon Sep 17 00:00:00 2001 From: jvyden Date: Wed, 18 Oct 2023 16:17:40 -0400 Subject: [PATCH 6/6] Don't skip token refreshing test --- .../Tests/Authentication/AuthenticationIntegrationTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/RefreshTests.GameServer/Tests/Authentication/AuthenticationIntegrationTests.cs b/RefreshTests.GameServer/Tests/Authentication/AuthenticationIntegrationTests.cs index 0dc788d9..9efaf96e 100644 --- a/RefreshTests.GameServer/Tests/Authentication/AuthenticationIntegrationTests.cs +++ b/RefreshTests.GameServer/Tests/Authentication/AuthenticationIntegrationTests.cs @@ -56,7 +56,6 @@ public async Task ApiAuthenticationWorks() } [Test] - [Ignore("Borked")] public async Task TokenRefreshingWorks() { using TestContext context = this.GetServer();