diff --git a/Refresh.Database/Models/Levels/Tag.cs b/Refresh.Database/Models/Levels/Tag.cs index 49059aaf4..0d1ef58c8 100644 --- a/Refresh.Database/Models/Levels/Tag.cs +++ b/Refresh.Database/Models/Levels/Tag.cs @@ -82,6 +82,9 @@ public enum Tag : byte Electric = 75, Frosty, Insane, + // This isn't in TagsMap on purpose, because this is only used as the default value by FromLbpString(), + // because GetValueOrDefault() will never return null in this case. "Invalid" tag uploads should be rejected. + Invalid, } public static class TagExtensions @@ -193,5 +196,5 @@ static TagExtensions() public static string? ToLbpString(this Tag tag) => StringMap.GetValueOrDefault(tag); - public static Tag? FromLbpString(string str) => TagsMap.GetValueOrDefault(str); + public static Tag FromLbpString(string str) => TagsMap.GetValueOrDefault(str, Tag.Invalid); } \ No newline at end of file diff --git a/Refresh.Interfaces.Game/Endpoints/Levels/LeaderboardEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/Levels/LeaderboardEndpoints.cs index 1b4e972f0..80e1a68cf 100644 --- a/Refresh.Interfaces.Game/Endpoints/Levels/LeaderboardEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/Levels/LeaderboardEndpoints.cs @@ -1,3 +1,4 @@ +using System.Xml.Serialization; using Bunkum.Core; using Bunkum.Core.Endpoints; using Bunkum.Core.Responses; @@ -26,7 +27,21 @@ public class LeaderboardEndpoints : EndpointGroup [GameEndpoint("play/{slotType}/{id}", ContentType.Xml, HttpMethods.Post)] [MinimumRole(GameUserRole.Restricted)] [EndpointRateLimit(EndpointBucketId.GamePlayLevel)] - public Response PlayLevel(RequestContext context, GameUser user, GameDatabaseContext database, string slotType, int id, DataContext dataContext) + public Response PlayLevel(RequestContext context, GameUser user, GameDatabaseContext database, string slotType, int id) + { + Response realResponse = this.PlayLevelInternal(context, user, database, slotType, id); + + // PSP always wants OK, else it will cancel login during downloaded level play data uploads. + if (context.IsPSP()) + { + context.Logger.LogTrace(BunkumCategory.Game, $"Actual response status is {realResponse.StatusCode}, but the client is LBP PSP, so we will return OK instead."); + return OK; + } + + return realResponse; + } + + private Response PlayLevelInternal(RequestContext context, GameUser user, GameDatabaseContext database, string slotType, int id) { GameLevel? level = database.GetLevelByIdAndType(slotType, id); if (level == null) return NotFound; @@ -94,6 +109,39 @@ public Response GetUserScores(RequestContext context, GameUser user, GameDatabas [EndpointRateLimit(EndpointBucketId.GameUploadLevelScore)] [RequireEmailVerified] public Response SubmitScore(RequestContext context, GameUser user, GameServerConfig config, + GameDatabaseContext database, string slotType, int id, string body, Token token, + DataContext dataContext) + { + // Parsing manually because we don't want unparseable requests from PSP to cause MainMiddleware to return BadRequest to it, + // else it will explode or something. + SerializedScore? score = null; + try + { + XmlSerializer serializer = new(typeof(SerializedScore)); + object? obj = serializer.Deserialize(new StringReader(body)) ?? throw new Exception(); + score = (SerializedScore)obj; + } + catch (Exception e) + { + context.Logger.LogError(BunkumCategory.UserContent, $"Failed to parse score upload: {e}\n\nXML: {body}"); + } + + Response realResponse = score == null + ? BadRequest + : this.SubmitScoreInternal(context, user, config, database, slotType, id, score, token, dataContext); + + // PSP always wants OK, else it will cancel login during downloaded level play data uploads. + // Maybe it also expects an actual score list, so return an empty one aswell. We will see whether we need to do more later. + if (context.IsPSP()) + { + context.Logger.LogTrace(BunkumCategory.Game, $"Actual response status is {realResponse.StatusCode}, but the client is LBP PSP, so we will return OK instead."); + return new Response(new SerializedScoreLeaderboardList(), ContentType.Xml); + } + + return realResponse; + } + + private Response SubmitScoreInternal(RequestContext context, GameUser user, GameServerConfig config, GameDatabaseContext database, string slotType, int id, SerializedScore body, Token token, DataContext dataContext) { diff --git a/Refresh.Interfaces.Game/Endpoints/RelationEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/RelationEndpoints.cs index 8a1b78fa5..02156c12d 100644 --- a/Refresh.Interfaces.Game/Endpoints/RelationEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/RelationEndpoints.cs @@ -140,8 +140,24 @@ public Response ClearQueue(RequestContext context, GameDatabaseContext database, [GameEndpoint("tag/{slotType}/{id}", HttpMethods.Post)] [RequireEmailVerified] + [AllowEmptyBody] [EndpointRateLimit(EndpointBucketId.TagLevel)] - public Response SubmitTagsForLevel(RequestContext context, GameDatabaseContext database, GameUser user, DataContext dataContext, + public Response SubmitTagForLevel(RequestContext context, GameDatabaseContext database, GameUser user, + string slotType, int id, string? body, GameServerConfig config) + { + Response realResponse = this.SubmitTagForLevelInternal(context, database, user, slotType, id, body ?? "", config); + + // PSP always wants OK, else it will cancel login during downloaded level play data uploads. + if (context.IsPSP()) + { + context.Logger.LogTrace(BunkumCategory.Game, $"Actual response status is {realResponse.StatusCode}, but the client is LBP PSP, so we will return OK instead."); + return OK; + } + + return realResponse; + } + + private Response SubmitTagForLevelInternal(RequestContext context, GameDatabaseContext database, GameUser user, string slotType, int id, string body, GameServerConfig config) { if (user.IsWriteBlocked(config)) @@ -156,13 +172,14 @@ public Response SubmitTagsForLevel(RequestContext context, GameDatabaseContext d if (!body.StartsWith("t=")) return BadRequest; - Tag? tag = TagExtensions.FromLbpString(body[2..]); + Tag tag = TagExtensions.FromLbpString(body[2..]); - // If it was an invalid tag, return BadRequest - if (tag == null) + // If it was an invalid tag, return BadRequest. + // Cannot be null due to weird behaviour with GetValueOrDefault(), so do it like this instead. + if (tag == Tag.Invalid) return BadRequest; - database.AddTagRelation(user, level, tag.Value); + database.AddTagRelation(user, level, tag); return OK; } diff --git a/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs index 01180d52e..0823a414c 100644 --- a/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs @@ -1,4 +1,6 @@ using System.Drawing; +using System.Text; +using System.Xml.Serialization; using Bunkum.Core; using Bunkum.Core.Endpoints; using Bunkum.Core.Responses; @@ -26,7 +28,38 @@ public class ReportingEndpoints : EndpointGroup [GameEndpoint("grief", HttpMethods.Post, ContentType.Xml)] [RequireEmailVerified] [EndpointRateLimit(EndpointBucketId.GameUploadGriefReport)] - public Response UploadReport(RequestContext context, GameDatabaseContext database, GameReport body, GameUser user, + public Response UploadReport(RequestContext context, GameDatabaseContext database, string body, GameUser user, + IDateTimeProvider time, Token token, GameServerConfig config) + { + // Parsing manually because we don't want unparseable requests from PSP to cause MainMiddleware to return BadRequest to it, + // else it will explode or something. + GameReport? report = null; + try + { + XmlSerializer serializer = new(typeof(GameReport)); + object? obj = serializer.Deserialize(new StringReader(body)) ?? throw new Exception(); + report = (GameReport)obj; + } + catch (Exception e) + { + context.Logger.LogError(BunkumCategory.UserContent, $"Failed to parse grief report: {e}\n\nXML: {body}"); + } + + Response realResponse = report == null + ? BadRequest + : this.UploadReportInternal(context, database, report, user, time, token, config); + + // PSP always wants OK, else it will softlock the save for some stupid reason (according to the comment below atleast). + if (context.IsPSP()) + { + context.Logger.LogTrace(BunkumCategory.Game, $"Actual response status is {realResponse.StatusCode}, but the client is LBP PSP, so we will return OK instead."); + return OK; + } + + return realResponse; + } + + private Response UploadReportInternal(RequestContext context, GameDatabaseContext database, GameReport body, GameUser user, IDateTimeProvider time, Token token, GameServerConfig config) { if (user.IsWriteBlocked(config)) @@ -105,7 +138,7 @@ public Response UploadReport(RequestContext context, GameDatabaseContext databas GameUser? playerUser = database.GetUserByUsername(player.Username); // what if user doesnt exist return new ReportPlayerRelation { - User = playerUser, + User = playerUser, IsReporter = player.Reporter, IsInGameNow = player.IngameNow, PlayerNumber = player.PlayerNumber, diff --git a/Refresh.Interfaces.Game/Endpoints/ReviewEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/ReviewEndpoints.cs index de425fe58..ffd51ebe2 100644 --- a/Refresh.Interfaces.Game/Endpoints/ReviewEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/ReviewEndpoints.cs @@ -15,6 +15,7 @@ using Refresh.Database.Models.Users; using Refresh.Interfaces.Game.Types.Reviews; using Refresh.Core.RateLimits.EndpointRateLimiting; +using Refresh.Common; namespace Refresh.Interfaces.Game.Endpoints; @@ -53,7 +54,22 @@ public Response SubmitLevelDpadRating(RequestContext context, GameDatabaseContex [RequireEmailVerified] [EndpointRateLimit(EndpointBucketId.RateLevel)] public Response SubmitLevelStarRating(RequestContext context, GameDatabaseContext database, GameUser user, string slotType, int id, - DataContext dataContext, GameServerConfig config) + GameServerConfig config) + { + Response realResponse = this.SubmitLevelStarRatingInternal(context, database, user, slotType, id, config); + + // PSP always wants OK, else it will cancel login during downloaded level play data uploads. + if (context.IsPSP()) + { + context.Logger.LogTrace(BunkumCategory.Game, $"Actual response status is {realResponse.StatusCode}, but the client is LBP PSP, so we will return OK instead."); + return OK; + } + + return realResponse; + } + + private Response SubmitLevelStarRatingInternal(RequestContext context, GameDatabaseContext database, GameUser user, string slotType, int id, + GameServerConfig config) { if (user.IsWriteBlocked(config)) return Unauthorized; diff --git a/RefreshTests.GameServer/Tests/Levels/ScoreLeaderboardTests.cs b/RefreshTests.GameServer/Tests/Levels/ScoreLeaderboardTests.cs index 888d83969..cebd41ddc 100644 --- a/RefreshTests.GameServer/Tests/Levels/ScoreLeaderboardTests.cs +++ b/RefreshTests.GameServer/Tests/Levels/ScoreLeaderboardTests.cs @@ -690,10 +690,12 @@ public void DoesntPlayLevelWithInvalidCount() client.DefaultRequestHeaders.UserAgent.TryParseAdd("LBPPSP CLIENT"); HttpResponseMessage message = client.PostAsync($"/lbp/play/user/{level.LevelId}?count=gtgnyegth", new ReadOnlyMemoryContent(Array.Empty())).Result; - Assert.That(message.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(message.StatusCode, Is.EqualTo(OK)); + Assert.That(context.Database.GetTotalPlaysForLevelByUser(level, user), Is.Zero); // invalid play count wasn't saved despite OK response HttpResponseMessage message2 = client.PostAsync($"/lbp/play/user/{level.LevelId}?count=-5", new ReadOnlyMemoryContent(Array.Empty())).Result; - Assert.That(message2.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(message2.StatusCode, Is.EqualTo(OK)); + Assert.That(context.Database.GetTotalPlaysForLevelByUser(level, user), Is.Zero); // invalid play count wasn't saved despite OK response } [Test] diff --git a/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs b/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs new file mode 100644 index 000000000..cf92e404e --- /dev/null +++ b/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs @@ -0,0 +1,384 @@ +using System.Reflection; +using Refresh.Common.Helpers; +using Refresh.Database.Models.Authentication; +using Refresh.Database.Models.Levels; +using Refresh.Database.Models.Reports; +using Refresh.Database.Models.Users; +using Refresh.Interfaces.Game.Types.Report; +using Refresh.Interfaces.Game.Types.UserData.Leaderboard; +using RefreshTests.GameServer.Extensions; + +namespace RefreshTests.GameServer.Tests.Psp; + +public class LbpPspExplosionPreventionTests : GameServerTest +{ + private const string TEST_ASSET_HASH = "0ec63b140374ba704a58fa0c743cb357683313dd"; + private static readonly byte[] TestAsset = ResourceHelper.ReadResource("RefreshTests.GameServer.Resources.1x1.png", Assembly.GetExecutingAssembly()); + + [Test] + public void GetOkOnPlayErrorsIfPsp() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + GameLevel level = context.CreateLevel(context.CreateUser()); + + HttpClient nonPsp = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet2, TokenPlatform.PS3, user); + HttpClient psp = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanetPSP, TokenPlatform.PSP, user); + psp.DefaultRequestHeaders.UserAgent.TryParseAdd("LBPPSP CLIENT"); + context.Database.Refresh(); + + // Ensure both get OK if everything is OK + Assert.That(nonPsp.PostAsync($"/lbp/play/user/{level.LevelId}", null).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(psp.PostAsync($"/lbp/play/user/{level.LevelId}", null).Result.StatusCode, Is.EqualTo(OK)); + + // Non-existent level + Assert.That(nonPsp.PostAsync($"/lbp/play/user/22432", null).Result.StatusCode, Is.EqualTo(NotFound)); + Assert.That(psp.PostAsync($"/lbp/play/user/22432", null).Result.StatusCode, Is.EqualTo(OK)); + } + + [Test] + public void GetOkOnLevelHeartErrorsIfPsp() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + GameLevel level = context.CreateLevel(context.CreateUser()); + + HttpClient nonPsp = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet2, TokenPlatform.PS3, user); + HttpClient psp = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanetPSP, TokenPlatform.PSP, user); + psp.DefaultRequestHeaders.UserAgent.TryParseAdd("LBPPSP CLIENT"); + + // Ensure both get OK if everything is OK + Assert.That(nonPsp.PostAsync($"/lbp/favourite/slot/user/{level.LevelId}", null).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(psp.PostAsync($"/lbp/favourite/slot/user/{level.LevelId}", null).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(nonPsp.PostAsync($"/lbp/unfavourite/slot/user/{level.LevelId}", null).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(psp.PostAsync($"/lbp/unfavourite/slot/user/{level.LevelId}", null).Result.StatusCode, Is.EqualTo(OK)); + + // Non-existent level + Assert.That(nonPsp.PostAsync($"/lbp/favourite/slot/user/22432", null).Result.StatusCode, Is.EqualTo(NotFound)); + Assert.That(psp.PostAsync($"/lbp/favourite/slot/user/22432", null).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(nonPsp.PostAsync($"/lbp/unfavourite/slot/user/22432", null).Result.StatusCode, Is.EqualTo(NotFound)); + Assert.That(psp.PostAsync($"/lbp/unfavourite/slot/user/22432", null).Result.StatusCode, Is.EqualTo(OK)); + + // Blocked by read-only mode + context.Server.Value.GameServerConfig.NewUserPermissions.ReadOnlyMode = true; + context.Server.Value.GameServerConfig.NormalUserPermissions.ReadOnlyMode = true; + context.Server.Value.GameServerConfig.TrustedUserPermissions.ReadOnlyMode = true; + + Assert.That(nonPsp.PostAsync($"/lbp/favourite/slot/user/{level.LevelId}", null).Result.StatusCode, Is.EqualTo(Unauthorized)); + Assert.That(psp.PostAsync($"/lbp/favourite/slot/user/{level.LevelId}", null).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(nonPsp.PostAsync($"/lbp/unfavourite/slot/user/{level.LevelId}", null).Result.StatusCode, Is.EqualTo(Unauthorized)); + Assert.That(psp.PostAsync($"/lbp/unfavourite/slot/user/{level.LevelId}", null).Result.StatusCode, Is.EqualTo(OK)); + } + + [Test] + public void GetOkOnUserHeartErrorsIfPsp() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + GameUser userToHeart = context.CreateUser(); + + HttpClient nonPsp = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet2, TokenPlatform.PS3, user); + HttpClient psp = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanetPSP, TokenPlatform.PSP, user); + psp.DefaultRequestHeaders.UserAgent.TryParseAdd("LBPPSP CLIENT"); + + // Ensure both get OK if everything is OK + Assert.That(nonPsp.PostAsync($"/lbp/favourite/user/{userToHeart.Username}", null).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(psp.PostAsync($"/lbp/favourite/user/{userToHeart.Username}", null).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(nonPsp.PostAsync($"/lbp/unfavourite/user/{userToHeart.Username}", null).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(psp.PostAsync($"/lbp/unfavourite/user/{userToHeart.Username}", null).Result.StatusCode, Is.EqualTo(OK)); + + // Non-existent user + Assert.That(nonPsp.PostAsync($"/lbp/favourite/user/lel420", null).Result.StatusCode, Is.EqualTo(NotFound)); + Assert.That(psp.PostAsync($"/lbp/favourite/user/lel420", null).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(nonPsp.PostAsync($"/lbp/unfavourite/user/lel420", null).Result.StatusCode, Is.EqualTo(NotFound)); + Assert.That(psp.PostAsync($"/lbp/unfavourite/user/lel420", null).Result.StatusCode, Is.EqualTo(OK)); + + // Blocked by read-only mode + context.Server.Value.GameServerConfig.NewUserPermissions.ReadOnlyMode = true; + context.Server.Value.GameServerConfig.NormalUserPermissions.ReadOnlyMode = true; + context.Server.Value.GameServerConfig.TrustedUserPermissions.ReadOnlyMode = true; + + Assert.That(nonPsp.PostAsync($"/lbp/favourite/user/{userToHeart.Username}", null).Result.StatusCode, Is.EqualTo(Unauthorized)); + Assert.That(psp.PostAsync($"/lbp/favourite/user/{userToHeart.Username}", null).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(nonPsp.PostAsync($"/lbp/unfavourite/user/{userToHeart.Username}", null).Result.StatusCode, Is.EqualTo(Unauthorized)); + Assert.That(psp.PostAsync($"/lbp/unfavourite/user/{userToHeart.Username}", null).Result.StatusCode, Is.EqualTo(OK)); + } + + [Test] + public void GetOkOnLevelRateErrorsIfPsp() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + GameLevel level = context.CreateLevel(context.CreateUser()); + + HttpClient nonPsp = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet1, TokenPlatform.PS3, user); + HttpClient psp = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanetPSP, TokenPlatform.PSP, user); + psp.DefaultRequestHeaders.UserAgent.TryParseAdd("LBPPSP CLIENT"); + + // Didn't play the level yet + Assert.That(nonPsp.PostAsync($"/lbp/rate/user/{level.LevelId}?rating=1", null).Result.StatusCode, Is.EqualTo(Unauthorized)); + Assert.That(psp.PostAsync($"/lbp/rate/user/{level.LevelId}?rating=1", null).Result.StatusCode, Is.EqualTo(OK)); + + // Ensure both get OK if the level is played, and everything else is OK + HttpResponseMessage message = nonPsp.PostAsync($"/lbp/play/user/{level.LevelId}", null).Result; + Assert.That(message.StatusCode, Is.EqualTo(OK)); + + Assert.That(nonPsp.PostAsync($"/lbp/rate/user/{level.LevelId}?rating=1", null).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(psp.PostAsync($"/lbp/rate/user/{level.LevelId}?rating=1", null).Result.StatusCode, Is.EqualTo(OK)); + + // Non-existent level + Assert.That(nonPsp.PostAsync($"/lbp/rate/user/22432?rating=1", null).Result.StatusCode, Is.EqualTo(NotFound)); + Assert.That(psp.PostAsync($"/lbp/rate/user/22432?rating=1", null).Result.StatusCode, Is.EqualTo(OK)); + + // Invalid rating + Assert.That(nonPsp.PostAsync($"/lbp/rate/user/{level.LevelId}?rating=67", null).Result.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(psp.PostAsync($"/lbp/rate/user/{level.LevelId}?rating=67", null).Result.StatusCode, Is.EqualTo(OK)); + + // Missing rating + Assert.That(nonPsp.PostAsync($"/lbp/rate/user/{level.LevelId}", null).Result.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(psp.PostAsync($"/lbp/rate/user/{level.LevelId}", null).Result.StatusCode, Is.EqualTo(OK)); + + // Blocked by read-only mode + context.Server.Value.GameServerConfig.NewUserPermissions.ReadOnlyMode = true; + context.Server.Value.GameServerConfig.NormalUserPermissions.ReadOnlyMode = true; + context.Server.Value.GameServerConfig.TrustedUserPermissions.ReadOnlyMode = true; + + Assert.That(nonPsp.PostAsync($"/lbp/rate/user/{level.LevelId}?rating=1", null).Result.StatusCode, Is.EqualTo(Unauthorized)); + Assert.That(psp.PostAsync($"/lbp/rate/user/{level.LevelId}?rating=1", null).Result.StatusCode, Is.EqualTo(OK)); + } + + [Test] + public void GetOkOnLevelTagErrorsIfPsp() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + GameLevel level = context.CreateLevel(context.CreateUser()); + + HttpClient nonPsp = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet1, TokenPlatform.PS3, user); + HttpClient psp = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanetPSP, TokenPlatform.PSP, user); + psp.DefaultRequestHeaders.UserAgent.TryParseAdd("LBPPSP CLIENT"); + + // Ensure both get OK if everything is OK + Assert.That(nonPsp.PostAsync($"/lbp/tag/user/{level.LevelId}", new StringContent("t=TAG_Weird")).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(psp.PostAsync($"/lbp/tag/user/{level.LevelId}", new StringContent("t=TAG_Weird")).Result.StatusCode, Is.EqualTo(OK)); + + // Non-existent level + Assert.That(nonPsp.PostAsync($"/lbp/tag/user/22432", new StringContent("t=TAG_Weird")).Result.StatusCode, Is.EqualTo(NotFound)); + Assert.That(psp.PostAsync($"/lbp/tag/user/22432", new StringContent("t=TAG_Weird")).Result.StatusCode, Is.EqualTo(OK)); + + // Invalid tag + Assert.That(nonPsp.PostAsync($"/lbp/tag/user/{level.LevelId}", new StringContent("t=TAG_ILOVEPSP")).Result.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(psp.PostAsync($"/lbp/tag/user/{level.LevelId}", new StringContent("t=TAG_ILOVEPSP")).Result.StatusCode, Is.EqualTo(OK)); + + // Missing tag + Assert.That(nonPsp.PostAsync($"/lbp/tag/user/{level.LevelId}", new StringContent("t=")).Result.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(psp.PostAsync($"/lbp/tag/user/{level.LevelId}", new StringContent("t=")).Result.StatusCode, Is.EqualTo(OK)); + + // Missing body + Assert.That(nonPsp.PostAsync($"/lbp/tag/user/{level.LevelId}", null).Result.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(psp.PostAsync($"/lbp/tag/user/{level.LevelId}", null).Result.StatusCode, Is.EqualTo(OK)); + + // Blocked by read-only mode + context.Server.Value.GameServerConfig.NewUserPermissions.ReadOnlyMode = true; + context.Server.Value.GameServerConfig.NormalUserPermissions.ReadOnlyMode = true; + context.Server.Value.GameServerConfig.TrustedUserPermissions.ReadOnlyMode = true; + + Assert.That(nonPsp.PostAsync($"/lbp/tag/user/{level.LevelId}?t=TAG_Weird", null).Result.StatusCode, Is.EqualTo(Unauthorized)); + Assert.That(psp.PostAsync($"/lbp/tag/user/{level.LevelId}?t=TAG_Weird", null).Result.StatusCode, Is.EqualTo(OK)); + } + + [Test] + public void GetOkOnScoreUploadErrorsIfPsp() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + GameLevel level = context.CreateLevel(context.CreateUser()); + + HttpClient nonPsp = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet1, TokenPlatform.PS3, user); + HttpClient psp = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanetPSP, TokenPlatform.PSP, user); + psp.DefaultRequestHeaders.UserAgent.TryParseAdd("LBPPSP CLIENT"); + + SerializedScore score = new() + { + Host = true, + ScoreType = 1, + Score = 5, + PlayerUsernames = [user.Username], + }; + + // Didn't play the level yet (PSP score is still saved) + Assert.That(nonPsp.PostAsync($"/lbp/scoreboard/user/{level.LevelId}", new StringContent(score.AsXML())).Result.StatusCode, Is.EqualTo(Unauthorized)); + Assert.That(psp.PostAsync($"/lbp/scoreboard/user/{level.LevelId}", new StringContent(score.AsXML())).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(context.Database.GetTotalCompletionsForLevelByUser(level, user), Is.EqualTo(1)); + + // Ensure both get OK if the level is played, and everything else is OK + HttpResponseMessage message = nonPsp.PostAsync($"/lbp/play/user/{level.LevelId}", null).Result; + Assert.That(message.StatusCode, Is.EqualTo(OK)); + + Assert.That(nonPsp.PostAsync($"/lbp/scoreboard/user/{level.LevelId}", new StringContent(score.AsXML())).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(psp.PostAsync($"/lbp/scoreboard/user/{level.LevelId}", new StringContent(score.AsXML())).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(context.Database.GetTotalCompletionsForLevelByUser(level, user), Is.EqualTo(3)); + + // Non-existent level + Assert.That(nonPsp.PostAsync($"/lbp/scoreboard/user/67676", new StringContent(score.AsXML())).Result.StatusCode, Is.EqualTo(NotFound)); + Assert.That(psp.PostAsync($"/lbp/scoreboard/user/67676", new StringContent(score.AsXML())).Result.StatusCode, Is.EqualTo(OK)); + + // Invalid XML in response + Assert.That(nonPsp.PostAsync($"/lbp/scoreboard/user/{level.LevelId}", new StringContent("nut")).Result.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(psp.PostAsync($"/lbp/scoreboard/user/{level.LevelId}", new StringContent("nut")).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(context.Database.GetTotalCompletionsForLevelByUser(level, user), Is.EqualTo(3)); + + // Score is negative (PSP score is still saved) + score.Score = -420; + Assert.That(nonPsp.PostAsync($"/lbp/scoreboard/user/{level.LevelId}", new StringContent(score.AsXML())).Result.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(psp.PostAsync($"/lbp/scoreboard/user/{level.LevelId}", new StringContent(score.AsXML())).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(context.Database.GetTotalCompletionsForLevelByUser(level, user), Is.EqualTo(4)); + + // Score is too high (PSP score is still saved) + score.Score = int.MaxValue; + Assert.That(nonPsp.PostAsync($"/lbp/scoreboard/user/{level.LevelId}", new StringContent(score.AsXML())).Result.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(psp.PostAsync($"/lbp/scoreboard/user/{level.LevelId}", new StringContent(score.AsXML())).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(context.Database.GetTotalCompletionsForLevelByUser(level, user), Is.EqualTo(5)); + + // Missing username in participant list (PSP score is still saved) + score.Score = 5; + score.PlayerUsernames = []; + Assert.That(nonPsp.PostAsync($"/lbp/scoreboard/user/{level.LevelId}", new StringContent(score.AsXML())).Result.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(psp.PostAsync($"/lbp/scoreboard/user/{level.LevelId}", new StringContent(score.AsXML())).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(context.Database.GetTotalCompletionsForLevelByUser(level, user), Is.EqualTo(6)); + + // Invalid score type (PSP score is still saved) + score.ScoreType = 90; + score.PlayerUsernames = []; + Assert.That(nonPsp.PostAsync($"/lbp/scoreboard/user/{level.LevelId}", new StringContent(score.AsXML())).Result.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(psp.PostAsync($"/lbp/scoreboard/user/{level.LevelId}", new StringContent(score.AsXML())).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(context.Database.GetTotalCompletionsForLevelByUser(level, user), Is.EqualTo(7)); + + // Blocked by read-only mode + context.Server.Value.GameServerConfig.NewUserPermissions.ReadOnlyMode = true; + context.Server.Value.GameServerConfig.NormalUserPermissions.ReadOnlyMode = true; + context.Server.Value.GameServerConfig.TrustedUserPermissions.ReadOnlyMode = true; + + Assert.That(nonPsp.PostAsync($"/lbp/scoreboard/user/{level.LevelId}", new StringContent(score.AsXML())).Result.StatusCode, Is.EqualTo(Unauthorized)); + Assert.That(psp.PostAsync($"/lbp/scoreboard/user/{level.LevelId}", new StringContent(score.AsXML())).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(context.Database.GetTotalCompletionsForLevelByUser(level, user), Is.EqualTo(7)); + } + + [Test] + public void GetOkOnGriefReportErrorsIfPsp() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + GameLevel level = context.CreateLevel(context.CreateUser()); + + HttpClient nonPsp = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet1, TokenPlatform.PS3, user); + HttpClient psp = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanetPSP, TokenPlatform.PSP, user); + psp.DefaultRequestHeaders.UserAgent.TryParseAdd("LBPPSP CLIENT"); + + //Upload our """photo""" + HttpResponseMessage message = nonPsp.PostAsync($"/lbp/upload/{TEST_ASSET_HASH}", new ReadOnlyMemoryContent(TestAsset)).Result; + Assert.That(message.StatusCode, Is.EqualTo(OK)); + + GameReport report = new() + { + InfoBubble = [], + Type = GriefReportType.TermsOfService, + Marqee = new Marqee + { + Rect = new Rect + { + Top = 10, + Left = 10, + Bottom = 20, + Right = 20, + }, + }, + LevelOwner = level.Publisher!.Username, + InitialStateHash = null, + LevelType = "user", + LevelId = level.LevelId, + Description = "This is a description, sent by LBP3", + GriefStateHash = null, + JpegHash = TEST_ASSET_HASH, + Players = + [ + new() + { + Username = user.Username, + Rectangle = new Rect + { + Top = 10, + Left = 10, + Bottom = 20, + Right = 20, + }, + Reporter = true, + IngameNow = true, + PlayerNumber = 0, + Text = "Some text", + ScreenRect = new ScreenRect + { + Rect = new Rect + { + Top = 10, + Left = 10, + Bottom = 20, + Right = 20, + }, + }, + }, + ], + ScreenElements = new ScreenElements(), + }; + + // Ensure both get OK if everything is OK + Assert.That(nonPsp.PostAsync("/lbp/grief", new StringContent(report.AsXML())).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(psp.PostAsync("/lbp/grief", new StringContent(report.AsXML())).Result.StatusCode, Is.EqualTo(OK)); + + // Insert too many players + for (int i = 0; i < 7; i++) + { + report.Players = report.Players.Append(new() + { + Username = user.Username, + Rectangle = new Rect + { + Top = 10, + Left = 10, + Bottom = 20, + Right = 20, + }, + Reporter = true, + IngameNow = true, + PlayerNumber = 0, + Text = "Some text", + ScreenRect = new ScreenRect + { + Rect = new Rect + { + Top = 10, + Left = 10, + Bottom = 20, + Right = 20, + }, + }, + } + ).ToArray(); + } + + Assert.That(nonPsp.PostAsync("/lbp/grief", new StringContent(report.AsXML())).Result.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(psp.PostAsync("/lbp/grief", new StringContent(report.AsXML())).Result.StatusCode, Is.EqualTo(OK)); + + // Invalid XML in request + Assert.That(nonPsp.PostAsync("/lbp/grief", new StringContent("these")).Result.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(psp.PostAsync("/lbp/grief", new StringContent("these")).Result.StatusCode, Is.EqualTo(OK)); + + // Blocked by read-only mode + context.Server.Value.GameServerConfig.NewUserPermissions.ReadOnlyMode = true; + context.Server.Value.GameServerConfig.NormalUserPermissions.ReadOnlyMode = true; + context.Server.Value.GameServerConfig.TrustedUserPermissions.ReadOnlyMode = true; + + Assert.That(nonPsp.PostAsync("/lbp/grief", new StringContent(report.AsXML())).Result.StatusCode, Is.EqualTo(Unauthorized)); + Assert.That(psp.PostAsync("/lbp/grief", new StringContent(report.AsXML())).Result.StatusCode, Is.EqualTo(OK)); + } +} \ No newline at end of file