From b043c5392df6d72a84990b6a5650caeb7f726537 Mon Sep 17 00:00:00 2001 From: Toaster Date: Sat, 10 Oct 2026 18:35:26 +0200 Subject: [PATCH 01/16] Add service + attribute to override status with OK for LBP PSP if wanted --- .../OverrideStatusIfPspAttribute.cs | 10 +++++++ .../Services/OverrideStatusService.cs | 26 +++++++++++++++++++ Refresh.GameServer/RefreshGameServer.cs | 1 + 3 files changed, 37 insertions(+) create mode 100644 Refresh.Core/Attributes/OverrideStatusIfPspAttribute.cs create mode 100644 Refresh.Core/Services/OverrideStatusService.cs diff --git a/Refresh.Core/Attributes/OverrideStatusIfPspAttribute.cs b/Refresh.Core/Attributes/OverrideStatusIfPspAttribute.cs new file mode 100644 index 00000000..6d93f56e --- /dev/null +++ b/Refresh.Core/Attributes/OverrideStatusIfPspAttribute.cs @@ -0,0 +1,10 @@ +namespace Refresh.Core.RateLimits.EndpointRateLimiting; + +[AttributeUsage(AttributeTargets.Method)] +public class OverrideStatusIfPspAttribute : Attribute +{ + public OverrideStatusIfPspAttribute() + { + + } +} \ No newline at end of file diff --git a/Refresh.Core/Services/OverrideStatusService.cs b/Refresh.Core/Services/OverrideStatusService.cs new file mode 100644 index 00000000..358eea42 --- /dev/null +++ b/Refresh.Core/Services/OverrideStatusService.cs @@ -0,0 +1,26 @@ +using System.Reflection; +using Bunkum.Core.Database; +using Bunkum.Core.Responses; +using Bunkum.Core.Services; +using Bunkum.Listener.Request; +using NotEnoughLogs; +using Refresh.Core.RateLimits.EndpointRateLimiting; + +namespace Refresh.Core.Services; + +public class OverrideStatusService : Service +{ + internal OverrideStatusService(Logger logger) : base(logger) + {} + + public override void AfterRequestHandled(ListenerContext context, Response? response, MethodInfo method, Lazy database) + { + // If the client is LBP PSP, and it's an endpoint which may not return non-OK responses to PSP, override with OK, + // else LBP PSP will explode or something. + if (context.ResponseCode != OK && context.RequestHeaders.Get("User-Agent") == "LBPPSP CLIENT" + && method.GetCustomAttributes().Any()) + { + context.ResponseCode = OK; + } + } +} \ No newline at end of file diff --git a/Refresh.GameServer/RefreshGameServer.cs b/Refresh.GameServer/RefreshGameServer.cs index 56ff2727..a428a0aa 100644 --- a/Refresh.GameServer/RefreshGameServer.cs +++ b/Refresh.GameServer/RefreshGameServer.cs @@ -169,6 +169,7 @@ protected override void SetupServices() this.Server.AddService(); this.Server.AddService(); this.Server.AddService(); + this.Server.AddService(); if(this._configStore.Integration!.AipiEnabled) this.Server.AddService(); From b53cb4b8ca19d7af7b54bd0ebd3cfdd90123065a Mon Sep 17 00:00:00 2001 From: Toaster Date: Sat, 10 Oct 2026 18:37:37 +0200 Subject: [PATCH 02/16] Fix namespaces --- Refresh.Core/Attributes/OverrideStatusIfPspAttribute.cs | 2 +- Refresh.Core/Services/OverrideStatusService.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Refresh.Core/Attributes/OverrideStatusIfPspAttribute.cs b/Refresh.Core/Attributes/OverrideStatusIfPspAttribute.cs index 6d93f56e..30e50745 100644 --- a/Refresh.Core/Attributes/OverrideStatusIfPspAttribute.cs +++ b/Refresh.Core/Attributes/OverrideStatusIfPspAttribute.cs @@ -1,4 +1,4 @@ -namespace Refresh.Core.RateLimits.EndpointRateLimiting; +namespace Refresh.Core.Attributes; [AttributeUsage(AttributeTargets.Method)] public class OverrideStatusIfPspAttribute : Attribute diff --git a/Refresh.Core/Services/OverrideStatusService.cs b/Refresh.Core/Services/OverrideStatusService.cs index 358eea42..0251aa33 100644 --- a/Refresh.Core/Services/OverrideStatusService.cs +++ b/Refresh.Core/Services/OverrideStatusService.cs @@ -4,7 +4,7 @@ using Bunkum.Core.Services; using Bunkum.Listener.Request; using NotEnoughLogs; -using Refresh.Core.RateLimits.EndpointRateLimiting; +using Refresh.Core.Attributes; namespace Refresh.Core.Services; From 540bd058e7ea4955b8f2061d7478efd80714bfc5 Mon Sep 17 00:00:00 2001 From: Toaster Date: Sat, 10 Oct 2026 18:43:04 +0200 Subject: [PATCH 03/16] Add OverrideStatusIfPspAttribute to various endpoints --- .../Endpoints/Levels/LeaderboardEndpoints.cs | 4 ++++ Refresh.Interfaces.Game/Endpoints/RelationEndpoints.cs | 6 ++++++ Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs | 2 ++ Refresh.Interfaces.Game/Endpoints/ReviewEndpoints.cs | 2 ++ 4 files changed, 14 insertions(+) diff --git a/Refresh.Interfaces.Game/Endpoints/Levels/LeaderboardEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/Levels/LeaderboardEndpoints.cs index 1b4e972f..6bc3313e 100644 --- a/Refresh.Interfaces.Game/Endpoints/Levels/LeaderboardEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/Levels/LeaderboardEndpoints.cs @@ -4,6 +4,7 @@ using Bunkum.Listener.Protocol; using Bunkum.Protocols.Http; using Refresh.Common.Time; +using Refresh.Core.Attributes; using Refresh.Core.Authentication.Permission; using Refresh.Core.Configuration; using Refresh.Core.RateLimits.EndpointRateLimiting; @@ -25,6 +26,7 @@ public class LeaderboardEndpoints : EndpointGroup // LBP1 doesn't send any requests to this endpoint if a user enters an online user level. [GameEndpoint("play/{slotType}/{id}", ContentType.Xml, HttpMethods.Post)] [MinimumRole(GameUserRole.Restricted)] + [OverrideStatusIfPsp] [EndpointRateLimit(EndpointBucketId.GamePlayLevel)] public Response PlayLevel(RequestContext context, GameUser user, GameDatabaseContext database, string slotType, int id, DataContext dataContext) { @@ -58,6 +60,7 @@ public Response PlayLevel(RequestContext context, GameUser user, GameDatabaseCon [GameEndpoint("scoreboard/{slotType}/{id}", HttpMethods.Get, ContentType.Xml)] [MinimumRole(GameUserRole.Restricted)] + [OverrideStatusIfPsp] [EndpointRateLimit(EndpointBucketId.GameGetListOfLevelScores)] public Response GetUserScores(RequestContext context, GameUser user, GameDatabaseContext database, string slotType, int id, Token token, DataContext dataContext) @@ -92,6 +95,7 @@ public Response GetUserScores(RequestContext context, GameUser user, GameDatabas [GameEndpoint("scoreboard/{slotType}/{id}", ContentType.Xml, HttpMethods.Post)] [EndpointRateLimit(EndpointBucketId.GameUploadLevelScore)] + [OverrideStatusIfPsp] // Let's see whether this is enough, or if we should actually return a serialized score list [RequireEmailVerified] public Response SubmitScore(RequestContext context, GameUser user, GameServerConfig config, GameDatabaseContext database, string slotType, int id, SerializedScore body, Token token, diff --git a/Refresh.Interfaces.Game/Endpoints/RelationEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/RelationEndpoints.cs index 8a1b78fa..4c927a71 100644 --- a/Refresh.Interfaces.Game/Endpoints/RelationEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/RelationEndpoints.cs @@ -3,6 +3,7 @@ using Bunkum.Core.Responses; using Bunkum.Listener.Protocol; using Bunkum.Protocols.Http; +using Refresh.Core.Attributes; using Refresh.Core.Authentication.Permission; using Refresh.Core.Configuration; using Refresh.Core.RateLimits.EndpointRateLimiting; @@ -19,6 +20,7 @@ public class RelationEndpoints : EndpointGroup { [GameEndpoint("favourite/slot/{slotType}/{id}", HttpMethods.Post)] [RequireEmailVerified] + [OverrideStatusIfPsp] [EndpointRateLimit(EndpointBucketId.HeartLevel)] public Response FavouriteLevel(RequestContext context, GameDatabaseContext database, GameUser user, string slotType, int id, GameServerConfig config, DataContext dataContext) @@ -37,6 +39,7 @@ public Response FavouriteLevel(RequestContext context, GameDatabaseContext datab [GameEndpoint("unfavourite/slot/{slotType}/{id}", HttpMethods.Post)] [RequireEmailVerified] + [OverrideStatusIfPsp] [EndpointRateLimit(EndpointBucketId.HeartLevel)] public Response UnfavouriteLevel(RequestContext context, GameDatabaseContext database, GameUser user, string slotType, int id, GameServerConfig config, DataContext dataContext) @@ -55,6 +58,7 @@ public Response UnfavouriteLevel(RequestContext context, GameDatabaseContext dat [GameEndpoint("favourite/user/{username}", HttpMethods.Post)] [RequireEmailVerified] + [OverrideStatusIfPsp] [EndpointRateLimit(EndpointBucketId.HeartUser)] public Response FavouriteUser(RequestContext context, GameDatabaseContext database, GameUser user, string username, GameServerConfig config, DataContext dataContext) @@ -73,6 +77,7 @@ public Response FavouriteUser(RequestContext context, GameDatabaseContext databa [GameEndpoint("unfavourite/user/{username}", HttpMethods.Post)] [RequireEmailVerified] + [OverrideStatusIfPsp] [EndpointRateLimit(EndpointBucketId.HeartUser)] public Response UnfavouriteUser(RequestContext context, GameDatabaseContext database, GameUser user, string username, GameServerConfig config, DataContext dataContext) @@ -140,6 +145,7 @@ public Response ClearQueue(RequestContext context, GameDatabaseContext database, [GameEndpoint("tag/{slotType}/{id}", HttpMethods.Post)] [RequireEmailVerified] + [OverrideStatusIfPsp] [EndpointRateLimit(EndpointBucketId.TagLevel)] public Response SubmitTagsForLevel(RequestContext context, GameDatabaseContext database, GameUser user, DataContext dataContext, string slotType, int id, string body, GameServerConfig config) diff --git a/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs index 01180d52..588749e5 100644 --- a/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs @@ -6,6 +6,7 @@ using Bunkum.Protocols.Http; using Refresh.Common.Constants; using Refresh.Common.Time; +using Refresh.Core.Attributes; using Refresh.Core.Authentication.Permission; using Refresh.Core.Configuration; using Refresh.Core.RateLimits.EndpointRateLimiting; @@ -25,6 +26,7 @@ public class ReportingEndpoints : EndpointGroup // TODO: LBP1 beta builds upload all related assets after sending the report itself to this endpoint, handle that case [GameEndpoint("grief", HttpMethods.Post, ContentType.Xml)] [RequireEmailVerified] + [OverrideStatusIfPsp] [EndpointRateLimit(EndpointBucketId.GameUploadGriefReport)] public Response UploadReport(RequestContext context, GameDatabaseContext database, GameReport body, GameUser user, IDateTimeProvider time, Token token, GameServerConfig config) diff --git a/Refresh.Interfaces.Game/Endpoints/ReviewEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/ReviewEndpoints.cs index de425fe5..539863d2 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.Core.Attributes; namespace Refresh.Interfaces.Game.Endpoints; @@ -51,6 +52,7 @@ public Response SubmitLevelDpadRating(RequestContext context, GameDatabaseContex [GameEndpoint("rate/{slotType}/{id}", ContentType.Xml, HttpMethods.Post)] [AllowEmptyBody] [RequireEmailVerified] + [OverrideStatusIfPsp] [EndpointRateLimit(EndpointBucketId.RateLevel)] public Response SubmitLevelStarRating(RequestContext context, GameDatabaseContext database, GameUser user, string slotType, int id, DataContext dataContext, GameServerConfig config) From 5c282d65129476629a22708dd4dc7e90c83652e6 Mon Sep 17 00:00:00 2001 From: Toaster Date: Sat, 10 Oct 2026 18:45:08 +0200 Subject: [PATCH 04/16] Print log if the service is about to overwrite response code for psp --- Refresh.Core/Services/OverrideStatusService.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Refresh.Core/Services/OverrideStatusService.cs b/Refresh.Core/Services/OverrideStatusService.cs index 0251aa33..8a1c07ae 100644 --- a/Refresh.Core/Services/OverrideStatusService.cs +++ b/Refresh.Core/Services/OverrideStatusService.cs @@ -1,4 +1,5 @@ using System.Reflection; +using Bunkum.Core; using Bunkum.Core.Database; using Bunkum.Core.Responses; using Bunkum.Core.Services; @@ -20,6 +21,7 @@ public override void AfterRequestHandled(ListenerContext context, Response? resp if (context.ResponseCode != OK && context.RequestHeaders.Get("User-Agent") == "LBPPSP CLIENT" && method.GetCustomAttributes().Any()) { + this.Logger.LogInfo(BunkumCategory.Game, $"Overriding original response '{context.ResponseCode}' with OK because the client is LBP PSP."); context.ResponseCode = OK; } } From 999a98ae19f8822d540d0789519405a2d4873fe7 Mon Sep 17 00:00:00 2001 From: Toaster Date: Sat, 10 Oct 2026 19:39:47 +0200 Subject: [PATCH 05/16] Add tests for PSP status override --- .../GameServer/TestRefreshGameServer.cs | 1 + .../Tests/Psp/LbpPspStatusOverrideTests.cs | 318 ++++++++++++++++++ 2 files changed, 319 insertions(+) create mode 100644 RefreshTests.GameServer/Tests/Psp/LbpPspStatusOverrideTests.cs diff --git a/RefreshTests.GameServer/GameServer/TestRefreshGameServer.cs b/RefreshTests.GameServer/GameServer/TestRefreshGameServer.cs index 690a2ee6..605ae67d 100644 --- a/RefreshTests.GameServer/GameServer/TestRefreshGameServer.cs +++ b/RefreshTests.GameServer/GameServer/TestRefreshGameServer.cs @@ -75,6 +75,7 @@ protected override void SetupServices() this.Server.AddService(); this.Server.AddService(); this.Server.AddService(); + this.Server.AddService(); // Must always be last, see comment in RefreshGameServer this.Server.AddService(); diff --git a/RefreshTests.GameServer/Tests/Psp/LbpPspStatusOverrideTests.cs b/RefreshTests.GameServer/Tests/Psp/LbpPspStatusOverrideTests.cs new file mode 100644 index 00000000..c448f1b5 --- /dev/null +++ b/RefreshTests.GameServer/Tests/Psp/LbpPspStatusOverrideTests.cs @@ -0,0 +1,318 @@ +using System.Reflection; +using Refresh.Common.Helpers; +using Refresh.Core.Configuration; +using Refresh.Core.RateLimits.EndpointRateLimiting; +using Refresh.Core.Services; +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 RefreshTests.GameServer.Extensions; + +namespace RefreshTests.GameServer.Tests.Psp; + +public class LbpPspStatusOverrideTests : 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)); + + // Blocked by rate-limit + EndpointRateLimitConfig rateLimitConfig = new(); + rateLimitConfig.Buckets.Add(EndpointBucketId.GamePlayLevel.ToString(), new(100, 1, 200)); + TestEndpointRateLimiter rateLimiter = new(context.Time, context.Server.Value.Logger, new()); + context.Server.Value.Server.AddService(new GameRateLimitService(context.Server.Value.Logger, rateLimiter, context.Server.Value.GetService())); + + // Rate-limited on second one + Assert.That(nonPsp.PostAsync($"/lbp/play/user/{level.LevelId}", null).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(nonPsp.PostAsync($"/lbp/play/user/{level.LevelId}", null).Result.StatusCode, Is.EqualTo(TooManyRequests)); + // still OK (user should already get rate-limited on first one, but they still receive OK) + Assert.That(psp.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)); + } + + [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"); + + // Ensure both get OK if everything is OK + Assert.That(nonPsp.PostAsync($"/lbp/rate/slot/user/{level.LevelId}?rating=1", null).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(psp.PostAsync($"/lbp/rate/slot/user/{level.LevelId}?rating=1", null).Result.StatusCode, Is.EqualTo(OK)); + + // Non-existent level + Assert.That(nonPsp.PostAsync($"/lbp/rate/slot/user/22432?rating=1", null).Result.StatusCode, Is.EqualTo(NotFound)); + Assert.That(psp.PostAsync($"/lbp/rate/slot/user/22432?rating=1", null).Result.StatusCode, Is.EqualTo(OK)); + + // Invalid rating + Assert.That(nonPsp.PostAsync($"/lbp/rate/slot/user/22432?rating=67", null).Result.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(psp.PostAsync($"/lbp/rate/slot/user/22432?rating=67", null).Result.StatusCode, Is.EqualTo(OK)); + + // Missing rating + Assert.That(nonPsp.PostAsync($"/lbp/rate/slot/user/22432", null).Result.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(psp.PostAsync($"/lbp/rate/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/rate/slot/user/{level.LevelId}?rating=1", null).Result.StatusCode, Is.EqualTo(Unauthorized)); + Assert.That(psp.PostAsync($"/lbp/rate/slot/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/slot/user/{level.LevelId}?t=TAG_Weird", null).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(psp.PostAsync($"/lbp/tag/slot/user/{level.LevelId}?t=TAG_Weird", null).Result.StatusCode, Is.EqualTo(OK)); + + // Non-existent level + Assert.That(nonPsp.PostAsync($"/lbp/tag/slot/user/22432?t=TAG_Weird", null).Result.StatusCode, Is.EqualTo(NotFound)); + Assert.That(psp.PostAsync($"/lbp/tag/slot/user/22432?t=TAG_Weird", null).Result.StatusCode, Is.EqualTo(OK)); + + // Invalid tag + Assert.That(nonPsp.PostAsync($"/lbp/tag/slot/user/22432?t=TAG_ILOVEPSP", null).Result.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(psp.PostAsync($"/lbp/tag/slot/user/22432?t=TAG_ILOVEPSP", null).Result.StatusCode, Is.EqualTo(OK)); + + // Missing tag + Assert.That(nonPsp.PostAsync($"/lbp/tag/slot/user/22432", null).Result.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(psp.PostAsync($"/lbp/tag/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/tag/slot/user/{level.LevelId}?t=TAG_Weird", null).Result.StatusCode, Is.EqualTo(Unauthorized)); + Assert.That(psp.PostAsync($"/lbp/tag/slot/user/{level.LevelId}?t=TAG_Weird", null).Result.StatusCode, Is.EqualTo(OK)); + } + + [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)); + + // 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)); + + // Blocked by rate-limit + EndpointRateLimitConfig rateLimitConfig = new(); + rateLimitConfig.Buckets.Add(EndpointBucketId.GameUploadGriefReport.ToString(), new(100, 1, 200)); + TestEndpointRateLimiter rateLimiter = new(context.Time, context.Server.Value.Logger, new()); + context.Server.Value.Server.AddService(new GameRateLimitService(context.Server.Value.Logger, rateLimiter, context.Server.Value.GetService())); + + Assert.That(nonPsp.PostAsync("/lbp/grief", new StringContent(report.AsXML())).Result.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(nonPsp.PostAsync("/lbp/grief", new StringContent(report.AsXML())).Result.StatusCode, Is.EqualTo(TooManyRequests)); + Assert.That(psp.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)); + } +} \ No newline at end of file From 22c579c3065ac72d5ca9557c74767435059afebe Mon Sep 17 00:00:00 2001 From: Toaster Date: Mon, 12 Oct 2026 14:47:35 +0200 Subject: [PATCH 06/16] Service.AfterRequestHandled() is bugged, so catch bad status manually if PSP --- .../OverrideStatusIfPspAttribute.cs | 10 ---- .../Services/OverrideStatusService.cs | 28 ---------- Refresh.GameServer/RefreshGameServer.cs | 1 - .../Endpoints/Levels/LeaderboardEndpoints.cs | 54 +++++++++++++++++-- .../Endpoints/RelationEndpoints.cs | 17 +++--- .../Endpoints/ReportingEndpoints.cs | 39 ++++++++++++-- .../Endpoints/ReviewEndpoints.cs | 20 +++++-- .../GameServer/TestRefreshGameServer.cs | 1 - ...s.cs => LbpPspExplosionPreventionTests.cs} | 2 +- 9 files changed, 112 insertions(+), 60 deletions(-) delete mode 100644 Refresh.Core/Attributes/OverrideStatusIfPspAttribute.cs delete mode 100644 Refresh.Core/Services/OverrideStatusService.cs rename RefreshTests.GameServer/Tests/Psp/{LbpPspStatusOverrideTests.cs => LbpPspExplosionPreventionTests.cs} (99%) diff --git a/Refresh.Core/Attributes/OverrideStatusIfPspAttribute.cs b/Refresh.Core/Attributes/OverrideStatusIfPspAttribute.cs deleted file mode 100644 index 30e50745..00000000 --- a/Refresh.Core/Attributes/OverrideStatusIfPspAttribute.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Refresh.Core.Attributes; - -[AttributeUsage(AttributeTargets.Method)] -public class OverrideStatusIfPspAttribute : Attribute -{ - public OverrideStatusIfPspAttribute() - { - - } -} \ No newline at end of file diff --git a/Refresh.Core/Services/OverrideStatusService.cs b/Refresh.Core/Services/OverrideStatusService.cs deleted file mode 100644 index 8a1c07ae..00000000 --- a/Refresh.Core/Services/OverrideStatusService.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Reflection; -using Bunkum.Core; -using Bunkum.Core.Database; -using Bunkum.Core.Responses; -using Bunkum.Core.Services; -using Bunkum.Listener.Request; -using NotEnoughLogs; -using Refresh.Core.Attributes; - -namespace Refresh.Core.Services; - -public class OverrideStatusService : Service -{ - internal OverrideStatusService(Logger logger) : base(logger) - {} - - public override void AfterRequestHandled(ListenerContext context, Response? response, MethodInfo method, Lazy database) - { - // If the client is LBP PSP, and it's an endpoint which may not return non-OK responses to PSP, override with OK, - // else LBP PSP will explode or something. - if (context.ResponseCode != OK && context.RequestHeaders.Get("User-Agent") == "LBPPSP CLIENT" - && method.GetCustomAttributes().Any()) - { - this.Logger.LogInfo(BunkumCategory.Game, $"Overriding original response '{context.ResponseCode}' with OK because the client is LBP PSP."); - context.ResponseCode = OK; - } - } -} \ No newline at end of file diff --git a/Refresh.GameServer/RefreshGameServer.cs b/Refresh.GameServer/RefreshGameServer.cs index a428a0aa..56ff2727 100644 --- a/Refresh.GameServer/RefreshGameServer.cs +++ b/Refresh.GameServer/RefreshGameServer.cs @@ -169,7 +169,6 @@ protected override void SetupServices() this.Server.AddService(); this.Server.AddService(); this.Server.AddService(); - this.Server.AddService(); if(this._configStore.Integration!.AipiEnabled) this.Server.AddService(); diff --git a/Refresh.Interfaces.Game/Endpoints/Levels/LeaderboardEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/Levels/LeaderboardEndpoints.cs index 6bc3313e..80e1a68c 100644 --- a/Refresh.Interfaces.Game/Endpoints/Levels/LeaderboardEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/Levels/LeaderboardEndpoints.cs @@ -1,10 +1,10 @@ +using System.Xml.Serialization; using Bunkum.Core; using Bunkum.Core.Endpoints; using Bunkum.Core.Responses; using Bunkum.Listener.Protocol; using Bunkum.Protocols.Http; using Refresh.Common.Time; -using Refresh.Core.Attributes; using Refresh.Core.Authentication.Permission; using Refresh.Core.Configuration; using Refresh.Core.RateLimits.EndpointRateLimiting; @@ -26,9 +26,22 @@ public class LeaderboardEndpoints : EndpointGroup // LBP1 doesn't send any requests to this endpoint if a user enters an online user level. [GameEndpoint("play/{slotType}/{id}", ContentType.Xml, HttpMethods.Post)] [MinimumRole(GameUserRole.Restricted)] - [OverrideStatusIfPsp] [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; @@ -60,7 +73,6 @@ public Response PlayLevel(RequestContext context, GameUser user, GameDatabaseCon [GameEndpoint("scoreboard/{slotType}/{id}", HttpMethods.Get, ContentType.Xml)] [MinimumRole(GameUserRole.Restricted)] - [OverrideStatusIfPsp] [EndpointRateLimit(EndpointBucketId.GameGetListOfLevelScores)] public Response GetUserScores(RequestContext context, GameUser user, GameDatabaseContext database, string slotType, int id, Token token, DataContext dataContext) @@ -95,9 +107,41 @@ public Response GetUserScores(RequestContext context, GameUser user, GameDatabas [GameEndpoint("scoreboard/{slotType}/{id}", ContentType.Xml, HttpMethods.Post)] [EndpointRateLimit(EndpointBucketId.GameUploadLevelScore)] - [OverrideStatusIfPsp] // Let's see whether this is enough, or if we should actually return a serialized score list [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 4c927a71..9ae70fb1 100644 --- a/Refresh.Interfaces.Game/Endpoints/RelationEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/RelationEndpoints.cs @@ -3,7 +3,6 @@ using Bunkum.Core.Responses; using Bunkum.Listener.Protocol; using Bunkum.Protocols.Http; -using Refresh.Core.Attributes; using Refresh.Core.Authentication.Permission; using Refresh.Core.Configuration; using Refresh.Core.RateLimits.EndpointRateLimiting; @@ -20,7 +19,6 @@ public class RelationEndpoints : EndpointGroup { [GameEndpoint("favourite/slot/{slotType}/{id}", HttpMethods.Post)] [RequireEmailVerified] - [OverrideStatusIfPsp] [EndpointRateLimit(EndpointBucketId.HeartLevel)] public Response FavouriteLevel(RequestContext context, GameDatabaseContext database, GameUser user, string slotType, int id, GameServerConfig config, DataContext dataContext) @@ -39,7 +37,6 @@ public Response FavouriteLevel(RequestContext context, GameDatabaseContext datab [GameEndpoint("unfavourite/slot/{slotType}/{id}", HttpMethods.Post)] [RequireEmailVerified] - [OverrideStatusIfPsp] [EndpointRateLimit(EndpointBucketId.HeartLevel)] public Response UnfavouriteLevel(RequestContext context, GameDatabaseContext database, GameUser user, string slotType, int id, GameServerConfig config, DataContext dataContext) @@ -58,7 +55,6 @@ public Response UnfavouriteLevel(RequestContext context, GameDatabaseContext dat [GameEndpoint("favourite/user/{username}", HttpMethods.Post)] [RequireEmailVerified] - [OverrideStatusIfPsp] [EndpointRateLimit(EndpointBucketId.HeartUser)] public Response FavouriteUser(RequestContext context, GameDatabaseContext database, GameUser user, string username, GameServerConfig config, DataContext dataContext) @@ -77,7 +73,6 @@ public Response FavouriteUser(RequestContext context, GameDatabaseContext databa [GameEndpoint("unfavourite/user/{username}", HttpMethods.Post)] [RequireEmailVerified] - [OverrideStatusIfPsp] [EndpointRateLimit(EndpointBucketId.HeartUser)] public Response UnfavouriteUser(RequestContext context, GameDatabaseContext database, GameUser user, string username, GameServerConfig config, DataContext dataContext) @@ -145,9 +140,17 @@ public Response ClearQueue(RequestContext context, GameDatabaseContext database, [GameEndpoint("tag/{slotType}/{id}", HttpMethods.Post)] [RequireEmailVerified] - [OverrideStatusIfPsp] [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. + return context.IsPSP() ? OK : realResponse; + } + + private Response SubmitTagForLevelInternal(RequestContext context, GameDatabaseContext database, GameUser user, string slotType, int id, string body, GameServerConfig config) { if (user.IsWriteBlocked(config)) diff --git a/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs index 588749e5..0823a414 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; @@ -6,7 +8,6 @@ using Bunkum.Protocols.Http; using Refresh.Common.Constants; using Refresh.Common.Time; -using Refresh.Core.Attributes; using Refresh.Core.Authentication.Permission; using Refresh.Core.Configuration; using Refresh.Core.RateLimits.EndpointRateLimiting; @@ -26,9 +27,39 @@ public class ReportingEndpoints : EndpointGroup // TODO: LBP1 beta builds upload all related assets after sending the report itself to this endpoint, handle that case [GameEndpoint("grief", HttpMethods.Post, ContentType.Xml)] [RequireEmailVerified] - [OverrideStatusIfPsp] [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)) @@ -107,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 539863d2..ffd51ebe 100644 --- a/Refresh.Interfaces.Game/Endpoints/ReviewEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/ReviewEndpoints.cs @@ -15,7 +15,7 @@ using Refresh.Database.Models.Users; using Refresh.Interfaces.Game.Types.Reviews; using Refresh.Core.RateLimits.EndpointRateLimiting; -using Refresh.Core.Attributes; +using Refresh.Common; namespace Refresh.Interfaces.Game.Endpoints; @@ -52,10 +52,24 @@ public Response SubmitLevelDpadRating(RequestContext context, GameDatabaseContex [GameEndpoint("rate/{slotType}/{id}", ContentType.Xml, HttpMethods.Post)] [AllowEmptyBody] [RequireEmailVerified] - [OverrideStatusIfPsp] [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/GameServer/TestRefreshGameServer.cs b/RefreshTests.GameServer/GameServer/TestRefreshGameServer.cs index 605ae67d..690a2ee6 100644 --- a/RefreshTests.GameServer/GameServer/TestRefreshGameServer.cs +++ b/RefreshTests.GameServer/GameServer/TestRefreshGameServer.cs @@ -75,7 +75,6 @@ protected override void SetupServices() this.Server.AddService(); this.Server.AddService(); this.Server.AddService(); - this.Server.AddService(); // Must always be last, see comment in RefreshGameServer this.Server.AddService(); diff --git a/RefreshTests.GameServer/Tests/Psp/LbpPspStatusOverrideTests.cs b/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs similarity index 99% rename from RefreshTests.GameServer/Tests/Psp/LbpPspStatusOverrideTests.cs rename to RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs index c448f1b5..ac94338e 100644 --- a/RefreshTests.GameServer/Tests/Psp/LbpPspStatusOverrideTests.cs +++ b/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs @@ -12,7 +12,7 @@ namespace RefreshTests.GameServer.Tests.Psp; -public class LbpPspStatusOverrideTests : GameServerTest +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()); From ae465f7f07f90aeda4d4091fb9abf7771ed1bc50 Mon Sep 17 00:00:00 2001 From: Toaster Date: Mon, 12 Oct 2026 15:06:27 +0200 Subject: [PATCH 07/16] PSP explosion prevention: Remove rate-limit tests, test score uploads, test invalid XML, fix level tag tests --- .../Psp/LbpPspExplosionPreventionTests.cs | 125 +++++++++++++----- 1 file changed, 92 insertions(+), 33 deletions(-) diff --git a/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs b/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs index ac94338e..279d3c21 100644 --- a/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs +++ b/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs @@ -1,13 +1,11 @@ using System.Reflection; using Refresh.Common.Helpers; -using Refresh.Core.Configuration; -using Refresh.Core.RateLimits.EndpointRateLimiting; -using Refresh.Core.Services; 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; @@ -36,19 +34,6 @@ public void GetOkOnPlayErrorsIfPsp() // 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)); - - // Blocked by rate-limit - EndpointRateLimitConfig rateLimitConfig = new(); - rateLimitConfig.Buckets.Add(EndpointBucketId.GamePlayLevel.ToString(), new(100, 1, 200)); - TestEndpointRateLimiter rateLimiter = new(context.Time, context.Server.Value.Logger, new()); - context.Server.Value.Server.AddService(new GameRateLimitService(context.Server.Value.Logger, rateLimiter, context.Server.Value.GetService())); - - // Rate-limited on second one - Assert.That(nonPsp.PostAsync($"/lbp/play/user/{level.LevelId}", null).Result.StatusCode, Is.EqualTo(OK)); - Assert.That(nonPsp.PostAsync($"/lbp/play/user/{level.LevelId}", null).Result.StatusCode, Is.EqualTo(TooManyRequests)); - // still OK (user should already get rate-limited on first one, but they still receive OK) - Assert.That(psp.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)); } [Test] @@ -167,18 +152,22 @@ public void GetOkOnLevelTagErrorsIfPsp() psp.DefaultRequestHeaders.UserAgent.TryParseAdd("LBPPSP CLIENT"); // Ensure both get OK if everything is OK - Assert.That(nonPsp.PostAsync($"/lbp/tag/slot/user/{level.LevelId}?t=TAG_Weird", null).Result.StatusCode, Is.EqualTo(OK)); - Assert.That(psp.PostAsync($"/lbp/tag/slot/user/{level.LevelId}?t=TAG_Weird", null).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(nonPsp.PostAsync($"/lbp/tag/slot/user/{level.LevelId}", new StringContent("t=TAG_Weird")).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(psp.PostAsync($"/lbp/tag/slot/user/{level.LevelId}", new StringContent("t=TAG_Weird")).Result.StatusCode, Is.EqualTo(OK)); // Non-existent level - Assert.That(nonPsp.PostAsync($"/lbp/tag/slot/user/22432?t=TAG_Weird", null).Result.StatusCode, Is.EqualTo(NotFound)); - Assert.That(psp.PostAsync($"/lbp/tag/slot/user/22432?t=TAG_Weird", null).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(nonPsp.PostAsync($"/lbp/tag/slot/user/22432", new StringContent("t=TAG_Weird")).Result.StatusCode, Is.EqualTo(NotFound)); + Assert.That(psp.PostAsync($"/lbp/tag/slot/user/22432", new StringContent("t=TAG_Weird")).Result.StatusCode, Is.EqualTo(OK)); // Invalid tag - Assert.That(nonPsp.PostAsync($"/lbp/tag/slot/user/22432?t=TAG_ILOVEPSP", null).Result.StatusCode, Is.EqualTo(BadRequest)); - Assert.That(psp.PostAsync($"/lbp/tag/slot/user/22432?t=TAG_ILOVEPSP", null).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(nonPsp.PostAsync($"/lbp/tag/slot/user/22432", new StringContent("t=TAG_ILOVEPSP")).Result.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(psp.PostAsync($"/lbp/tag/slot/user/22432", new StringContent("t=TAG_ILOVEPSP")).Result.StatusCode, Is.EqualTo(OK)); // Missing tag + Assert.That(nonPsp.PostAsync($"/lbp/tag/slot/user/22432", new StringContent("t=")).Result.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(psp.PostAsync($"/lbp/tag/slot/user/22432", new StringContent("t=")).Result.StatusCode, Is.EqualTo(OK)); + + // Missing body Assert.That(nonPsp.PostAsync($"/lbp/tag/slot/user/22432", null).Result.StatusCode, Is.EqualTo(BadRequest)); Assert.That(psp.PostAsync($"/lbp/tag/slot/user/22432", null).Result.StatusCode, Is.EqualTo(OK)); @@ -191,6 +180,83 @@ public void GetOkOnLevelTagErrorsIfPsp() Assert.That(psp.PostAsync($"/lbp/tag/slot/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 + 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.Zero); + + // Ensure both get OK if the level is played, and everything else is OK + HttpResponseMessage message = nonPsp.PostAsync($"/lbp/play/user/{level.LevelId}", new StringContent(score.AsXML())).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(2)); + + // 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(2)); + + // 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(3)); + + // 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(4)); + + // 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(5)); + + // 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(6)); + + // 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/slot/user/{level.LevelId}?t=TAG_Weird", null).Result.StatusCode, Is.EqualTo(Unauthorized)); + Assert.That(psp.PostAsync($"/lbp/tag/slot/user/{level.LevelId}?t=TAG_Weird", null).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(context.Database.GetTotalCompletionsForLevelByUser(level, user), Is.EqualTo(6)); + } + [Test] public void GetOkOnGriefReportErrorsIfPsp() { @@ -296,6 +362,10 @@ public void GetOkOnGriefReportErrorsIfPsp() 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; @@ -303,16 +373,5 @@ public void GetOkOnGriefReportErrorsIfPsp() 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)); - - // Blocked by rate-limit - EndpointRateLimitConfig rateLimitConfig = new(); - rateLimitConfig.Buckets.Add(EndpointBucketId.GameUploadGriefReport.ToString(), new(100, 1, 200)); - TestEndpointRateLimiter rateLimiter = new(context.Time, context.Server.Value.Logger, new()); - context.Server.Value.Server.AddService(new GameRateLimitService(context.Server.Value.Logger, rateLimiter, context.Server.Value.GetService())); - - Assert.That(nonPsp.PostAsync("/lbp/grief", new StringContent(report.AsXML())).Result.StatusCode, Is.EqualTo(BadRequest)); - Assert.That(nonPsp.PostAsync("/lbp/grief", new StringContent(report.AsXML())).Result.StatusCode, Is.EqualTo(TooManyRequests)); - Assert.That(psp.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)); } } \ No newline at end of file From 5e29b1bfb3832899f5275ac879b5de51388b8c6b Mon Sep 17 00:00:00 2001 From: Toaster Date: Mon, 12 Oct 2026 15:17:56 +0200 Subject: [PATCH 08/16] I can't count --- .../Psp/LbpPspExplosionPreventionTests.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs b/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs index 279d3c21..b1b77c9a 100644 --- a/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs +++ b/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs @@ -199,10 +199,10 @@ public void GetOkOnScoreUploadErrorsIfPsp() PlayerUsernames = [user.Username], }; - // Didn't play the level yet + // 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.Zero); + 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}", new StringContent(score.AsXML())).Result; @@ -210,7 +210,7 @@ public void GetOkOnScoreUploadErrorsIfPsp() 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(2)); + 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)); @@ -219,33 +219,33 @@ public void GetOkOnScoreUploadErrorsIfPsp() // 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(2)); + 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(3)); + 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(4)); + 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(5)); + 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(6)); + Assert.That(context.Database.GetTotalCompletionsForLevelByUser(level, user), Is.EqualTo(7)); // Blocked by read-only mode context.Server.Value.GameServerConfig.NewUserPermissions.ReadOnlyMode = true; @@ -254,7 +254,7 @@ public void GetOkOnScoreUploadErrorsIfPsp() Assert.That(nonPsp.PostAsync($"/lbp/tag/slot/user/{level.LevelId}?t=TAG_Weird", null).Result.StatusCode, Is.EqualTo(Unauthorized)); Assert.That(psp.PostAsync($"/lbp/tag/slot/user/{level.LevelId}?t=TAG_Weird", null).Result.StatusCode, Is.EqualTo(OK)); - Assert.That(context.Database.GetTotalCompletionsForLevelByUser(level, user), Is.EqualTo(6)); + Assert.That(context.Database.GetTotalCompletionsForLevelByUser(level, user), Is.EqualTo(7)); } [Test] From 6a55f99a024d077346cf977404ee910da9e3fe31 Mon Sep 17 00:00:00 2001 From: Toaster Date: Mon, 12 Oct 2026 15:22:19 +0200 Subject: [PATCH 09/16] Forgot to paste the log here --- Refresh.Interfaces.Game/Endpoints/RelationEndpoints.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Refresh.Interfaces.Game/Endpoints/RelationEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/RelationEndpoints.cs index 9ae70fb1..2cefcaa6 100644 --- a/Refresh.Interfaces.Game/Endpoints/RelationEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/RelationEndpoints.cs @@ -147,7 +147,13 @@ public Response SubmitTagForLevel(RequestContext context, GameDatabaseContext da 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. - return context.IsPSP() ? OK : realResponse; + 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, From 47cdb2b771a1f263158aa98d31cd11cfe8be515e Mon Sep 17 00:00:00 2001 From: Toaster Date: Mon, 12 Oct 2026 15:39:15 +0200 Subject: [PATCH 10/16] Fix DoesntPlayLevelWithInvalidCount() --- .../Tests/Levels/ScoreLeaderboardTests.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/RefreshTests.GameServer/Tests/Levels/ScoreLeaderboardTests.cs b/RefreshTests.GameServer/Tests/Levels/ScoreLeaderboardTests.cs index 888d8396..cebd41dd 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] From 9ecb787d7d5118b2a55f88128c8aa2b356f7255d Mon Sep 17 00:00:00 2001 From: Toaster Date: Mon, 12 Oct 2026 15:40:33 +0200 Subject: [PATCH 11/16] Fix level ID not getting passed sometimes --- .../Psp/LbpPspExplosionPreventionTests.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs b/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs index b1b77c9a..337ae624 100644 --- a/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs +++ b/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs @@ -124,12 +124,12 @@ public void GetOkOnLevelRateErrorsIfPsp() Assert.That(psp.PostAsync($"/lbp/rate/slot/user/22432?rating=1", null).Result.StatusCode, Is.EqualTo(OK)); // Invalid rating - Assert.That(nonPsp.PostAsync($"/lbp/rate/slot/user/22432?rating=67", null).Result.StatusCode, Is.EqualTo(BadRequest)); - Assert.That(psp.PostAsync($"/lbp/rate/slot/user/22432?rating=67", null).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(nonPsp.PostAsync($"/lbp/rate/slot/user/{level.LevelId}?rating=67", null).Result.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(psp.PostAsync($"/lbp/rate/slot/user/{level.LevelId}?rating=67", null).Result.StatusCode, Is.EqualTo(OK)); // Missing rating - Assert.That(nonPsp.PostAsync($"/lbp/rate/slot/user/22432", null).Result.StatusCode, Is.EqualTo(BadRequest)); - Assert.That(psp.PostAsync($"/lbp/rate/slot/user/22432", null).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(nonPsp.PostAsync($"/lbp/rate/slot/user/{level.LevelId}", null).Result.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(psp.PostAsync($"/lbp/rate/slot/user/{level.LevelId}", null).Result.StatusCode, Is.EqualTo(OK)); // Blocked by read-only mode context.Server.Value.GameServerConfig.NewUserPermissions.ReadOnlyMode = true; @@ -160,16 +160,16 @@ public void GetOkOnLevelTagErrorsIfPsp() Assert.That(psp.PostAsync($"/lbp/tag/slot/user/22432", new StringContent("t=TAG_Weird")).Result.StatusCode, Is.EqualTo(OK)); // Invalid tag - Assert.That(nonPsp.PostAsync($"/lbp/tag/slot/user/22432", new StringContent("t=TAG_ILOVEPSP")).Result.StatusCode, Is.EqualTo(BadRequest)); - Assert.That(psp.PostAsync($"/lbp/tag/slot/user/22432", new StringContent("t=TAG_ILOVEPSP")).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(nonPsp.PostAsync($"/lbp/tag/slot/user/{level.LevelId}", new StringContent("t=TAG_ILOVEPSP")).Result.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(psp.PostAsync($"/lbp/tag/slot/user/{level.LevelId}", new StringContent("t=TAG_ILOVEPSP")).Result.StatusCode, Is.EqualTo(OK)); // Missing tag - Assert.That(nonPsp.PostAsync($"/lbp/tag/slot/user/22432", new StringContent("t=")).Result.StatusCode, Is.EqualTo(BadRequest)); - Assert.That(psp.PostAsync($"/lbp/tag/slot/user/22432", new StringContent("t=")).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(nonPsp.PostAsync($"/lbp/tag/slot/user/{level.LevelId}", new StringContent("t=")).Result.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(psp.PostAsync($"/lbp/tag/slot/user/{level.LevelId}", new StringContent("t=")).Result.StatusCode, Is.EqualTo(OK)); // Missing body - Assert.That(nonPsp.PostAsync($"/lbp/tag/slot/user/22432", null).Result.StatusCode, Is.EqualTo(BadRequest)); - Assert.That(psp.PostAsync($"/lbp/tag/slot/user/22432", null).Result.StatusCode, Is.EqualTo(OK)); + Assert.That(nonPsp.PostAsync($"/lbp/tag/slot/user/{level.LevelId}", null).Result.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(psp.PostAsync($"/lbp/tag/slot/user/{level.LevelId}", null).Result.StatusCode, Is.EqualTo(OK)); // Blocked by read-only mode context.Server.Value.GameServerConfig.NewUserPermissions.ReadOnlyMode = true; From 08b5407f48e0252d6e326d5106594ded5d0667a5 Mon Sep 17 00:00:00 2001 From: Toaster Date: Mon, 12 Oct 2026 15:45:57 +0200 Subject: [PATCH 12/16] Pasted wrong endpoint route --- .../Tests/Psp/LbpPspExplosionPreventionTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs b/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs index 337ae624..2cbccdd4 100644 --- a/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs +++ b/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs @@ -252,8 +252,8 @@ public void GetOkOnScoreUploadErrorsIfPsp() context.Server.Value.GameServerConfig.NormalUserPermissions.ReadOnlyMode = true; context.Server.Value.GameServerConfig.TrustedUserPermissions.ReadOnlyMode = true; - Assert.That(nonPsp.PostAsync($"/lbp/tag/slot/user/{level.LevelId}?t=TAG_Weird", null).Result.StatusCode, Is.EqualTo(Unauthorized)); - Assert.That(psp.PostAsync($"/lbp/tag/slot/user/{level.LevelId}?t=TAG_Weird", null).Result.StatusCode, Is.EqualTo(OK)); + 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)); } From 54a72557dcaacb57444553e7d0c16a8d043e35ab Mon Sep 17 00:00:00 2001 From: Toaster Date: Mon, 12 Oct 2026 15:46:27 +0200 Subject: [PATCH 13/16] no "slot" after "tag" --- .../Psp/LbpPspExplosionPreventionTests.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs b/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs index 2cbccdd4..c5618484 100644 --- a/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs +++ b/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs @@ -152,32 +152,32 @@ public void GetOkOnLevelTagErrorsIfPsp() psp.DefaultRequestHeaders.UserAgent.TryParseAdd("LBPPSP CLIENT"); // Ensure both get OK if everything is OK - Assert.That(nonPsp.PostAsync($"/lbp/tag/slot/user/{level.LevelId}", new StringContent("t=TAG_Weird")).Result.StatusCode, Is.EqualTo(OK)); - Assert.That(psp.PostAsync($"/lbp/tag/slot/user/{level.LevelId}", new StringContent("t=TAG_Weird")).Result.StatusCode, Is.EqualTo(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/slot/user/22432", new StringContent("t=TAG_Weird")).Result.StatusCode, Is.EqualTo(NotFound)); - Assert.That(psp.PostAsync($"/lbp/tag/slot/user/22432", new StringContent("t=TAG_Weird")).Result.StatusCode, Is.EqualTo(OK)); + 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/slot/user/{level.LevelId}", new StringContent("t=TAG_ILOVEPSP")).Result.StatusCode, Is.EqualTo(BadRequest)); - Assert.That(psp.PostAsync($"/lbp/tag/slot/user/{level.LevelId}", new StringContent("t=TAG_ILOVEPSP")).Result.StatusCode, Is.EqualTo(OK)); + 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/slot/user/{level.LevelId}", new StringContent("t=")).Result.StatusCode, Is.EqualTo(BadRequest)); - Assert.That(psp.PostAsync($"/lbp/tag/slot/user/{level.LevelId}", new StringContent("t=")).Result.StatusCode, Is.EqualTo(OK)); + 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/slot/user/{level.LevelId}", null).Result.StatusCode, Is.EqualTo(BadRequest)); - Assert.That(psp.PostAsync($"/lbp/tag/slot/user/{level.LevelId}", null).Result.StatusCode, Is.EqualTo(OK)); + 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/slot/user/{level.LevelId}?t=TAG_Weird", null).Result.StatusCode, Is.EqualTo(Unauthorized)); - Assert.That(psp.PostAsync($"/lbp/tag/slot/user/{level.LevelId}?t=TAG_Weird", null).Result.StatusCode, Is.EqualTo(OK)); + 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] From 002682cb9cf4756bddc6cbe868d807b390d44a43 Mon Sep 17 00:00:00 2001 From: Toaster Date: Mon, 12 Oct 2026 15:48:08 +0200 Subject: [PATCH 14/16] Same goes for "rate" --- .../Psp/LbpPspExplosionPreventionTests.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs b/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs index c5618484..9c87c62c 100644 --- a/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs +++ b/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs @@ -116,28 +116,28 @@ public void GetOkOnLevelRateErrorsIfPsp() psp.DefaultRequestHeaders.UserAgent.TryParseAdd("LBPPSP CLIENT"); // Ensure both get OK if everything is OK - Assert.That(nonPsp.PostAsync($"/lbp/rate/slot/user/{level.LevelId}?rating=1", null).Result.StatusCode, Is.EqualTo(OK)); - Assert.That(psp.PostAsync($"/lbp/rate/slot/user/{level.LevelId}?rating=1", null).Result.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/slot/user/22432?rating=1", null).Result.StatusCode, Is.EqualTo(NotFound)); - Assert.That(psp.PostAsync($"/lbp/rate/slot/user/22432?rating=1", null).Result.StatusCode, Is.EqualTo(OK)); + 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/slot/user/{level.LevelId}?rating=67", null).Result.StatusCode, Is.EqualTo(BadRequest)); - Assert.That(psp.PostAsync($"/lbp/rate/slot/user/{level.LevelId}?rating=67", null).Result.StatusCode, Is.EqualTo(OK)); + 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/slot/user/{level.LevelId}", null).Result.StatusCode, Is.EqualTo(BadRequest)); - Assert.That(psp.PostAsync($"/lbp/rate/slot/user/{level.LevelId}", null).Result.StatusCode, Is.EqualTo(OK)); + 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/slot/user/{level.LevelId}?rating=1", null).Result.StatusCode, Is.EqualTo(Unauthorized)); - Assert.That(psp.PostAsync($"/lbp/rate/slot/user/{level.LevelId}?rating=1", null).Result.StatusCode, Is.EqualTo(OK)); + 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] From 11a0c4e40f9e4e37a2119e5858c643263c00d24f Mon Sep 17 00:00:00 2001 From: Toaster Date: Mon, 12 Oct 2026 15:51:34 +0200 Subject: [PATCH 15/16] Fix GetOkOnLevelRateErrorsIfPsp() --- .../Tests/Psp/LbpPspExplosionPreventionTests.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs b/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs index 9c87c62c..cf92e404 100644 --- a/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs +++ b/RefreshTests.GameServer/Tests/Psp/LbpPspExplosionPreventionTests.cs @@ -114,8 +114,15 @@ public void GetOkOnLevelRateErrorsIfPsp() 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)); - // Ensure both get OK if everything is 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)); @@ -205,7 +212,7 @@ public void GetOkOnScoreUploadErrorsIfPsp() 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}", new StringContent(score.AsXML())).Result; + 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)); From cab1291e19675c4ba85e6b26ea75e586cbf99ffa Mon Sep 17 00:00:00 2001 From: Toaster Date: Mon, 12 Oct 2026 16:01:45 +0200 Subject: [PATCH 16/16] Work around weird GetValueOrDefault() behaviour, don't immediately reject empty bodies --- Refresh.Database/Models/Levels/Tag.cs | 5 ++++- .../Endpoints/RelationEndpoints.cs | 14 ++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/Refresh.Database/Models/Levels/Tag.cs b/Refresh.Database/Models/Levels/Tag.cs index 49059aaf..0d1ef58c 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/RelationEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/RelationEndpoints.cs index 2cefcaa6..02156c12 100644 --- a/Refresh.Interfaces.Game/Endpoints/RelationEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/RelationEndpoints.cs @@ -140,11 +140,12 @@ public Response ClearQueue(RequestContext context, GameDatabaseContext database, [GameEndpoint("tag/{slotType}/{id}", HttpMethods.Post)] [RequireEmailVerified] + [AllowEmptyBody] [EndpointRateLimit(EndpointBucketId.TagLevel)] public Response SubmitTagForLevel(RequestContext context, GameDatabaseContext database, GameUser user, - string slotType, int id, string body, GameServerConfig config) + string slotType, int id, string? body, GameServerConfig config) { - Response realResponse = this.SubmitTagForLevelInternal(context, database, user, slotType, id, body, 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()) @@ -171,13 +172,14 @@ private Response SubmitTagForLevelInternal(RequestContext context, GameDatabaseC 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; }