diff --git a/Refresh.Interfaces.Game/Endpoints/Levels/PublishEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/Levels/PublishEndpoints.cs index 9fbfc276..ab7609ba 100644 --- a/Refresh.Interfaces.Game/Endpoints/Levels/PublishEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/Levels/PublishEndpoints.cs @@ -1,3 +1,4 @@ +using System.Net; using Bunkum.Core; using Bunkum.Core.Endpoints; using Bunkum.Core.RateLimit; @@ -10,6 +11,10 @@ using Refresh.Common.Verification; using Refresh.Core.Authentication.Permission; using Refresh.Core.Configuration; +using Refresh.Core.Helpers; +using Refresh.Core.Importing; +using Refresh.Core.Services; +using Refresh.Core.Types.Assets.Validation; using Refresh.Core.Types.Data; using Refresh.Database; using Refresh.Database.Models; @@ -36,8 +41,8 @@ public class PublishEndpoints : EndpointGroup /// /// The level to verify /// The data context associated with the request - /// Whether validation succeeded - private static bool VerifyLevel(GameLevelRequest body, DataContext dataContext) + /// Whether validation succeeded. OK = success; everything else = failure + private static HttpStatusCode VerifyLevel(GameLevelRequest body, DataContext dataContext, AssetImporter importer, AipiService aipi, bool isActualPublish, bool isInnerLevel = false) { if (body.Title.Length > UgcLimits.TitleLimit) body.Title = body.Title[..UgcLimits.TitleLimit]; @@ -46,24 +51,87 @@ private static bool VerifyLevel(GameLevelRequest body, DataContext dataContext) body.Description = body.Description[..UgcLimits.DescriptionLimit]; if (body.MaxPlayers is > 4 or < 0 || body.MinPlayers is > 4 or < 0) - return false; + { + dataContext.Database.AddPublishFailNotification("Your player number restrictions were invalid.", body.Title, dataContext.User!); + return BadRequest; + } + + if (body.IsAdventure && isInnerLevel) + { + dataContext.Database.AddPublishFailNotification("An adventure may not include inner adventures.", body.Title, dataContext.User!); + return BadRequest; + } - //If the icon hash is a GUID hash, verify that its a valid texture GUID - if (body.IconHash.StartsWith('g') && !dataContext.GuidChecker.IsTextureGuid(dataContext.Game, long.Parse(body.IconHash.AsSpan()[1..]))) - return false; + if (body.IsAdventure && dataContext.Game != TokenGame.LittleBigPlanet3 && dataContext.Game != TokenGame.BetaBuild) + { + dataContext.Database.AddPublishFailNotification("You may only publish adventures in LBP3 or beta builds.", body.Title, dataContext.User!); + return Unauthorized; + } - if (body.IsAdventure && dataContext.Game != TokenGame.LittleBigPlanet3) - return false; + if (!body.IsAdventure && body.Slots != null && body.Slots.Length > 0) + { + dataContext.Database.AddPublishFailNotification("Only adventures may include inner levels.", body.Title, dataContext.User!); + return BadRequest; + } - GameLevel? existingLevel = dataContext.Database.GetLevelByRootResource(body.RootResource); - // If all are true: - // - there is an existing level with this root hash - // - this isn't an update request - // then block the upload - if (existingLevel != null && body.LevelId != existingLevel.LevelId) + // Validate icon + AssetValidationParameters iconParams = new(body.IconHash, dataContext, importer, aipi) + { + MustBeTexture = true, + MustBeInDataStoreIfHash = isActualPublish, // in most cases no assets will be uploaded yet when startPublish is called + AssetContextTypeStr = "icon", + OnNewAssetRefCallback = delegate(string newRef) { body.IconHash = newRef; } + }; + ValidatedAssetResult iconResult = ResourceValidationHelper.ValidateReference(iconParams, dataContext.Logger); + if (iconResult.Status != OK) + { + if (iconResult.ErrorMessage != null) dataContext.Database.AddPublishFailNotification(iconResult.ErrorMessage, body.Title, dataContext.User!); + return iconResult.Status; + } + + // For some stupid reason, inner adventure levels don't include their root hash in the request. + // TODO: validate their root hash once we finally start to read them from the adventure root asset itself, as it is included there. + if (!isInnerLevel) { - dataContext.Database.AddPublishFailNotification("The level you tried to publish has already been uploaded by another user.", body.Title, dataContext.User!); - return false; + // Validate root resource + AssetValidationParameters rootParams = new(body.RootResource, dataContext, importer) + { + MayBeBlank = false, + MayBeGuid = false, + MustBeInDataStoreIfHash = isActualPublish, // in most cases no assets will be uploaded yet when startPublish is called + AssetContextTypeStr = body.IsAdventure ? "adventure asset" : "level asset", + OnNewAssetRefCallback = delegate(string newRef) { body.RootResource = newRef; } + }; + ValidatedAssetResult rootResult = ResourceValidationHelper.ValidateReference(rootParams, dataContext.Logger); + if (rootResult.Status != OK) + { + if (rootResult.ErrorMessage != null) dataContext.Database.AddPublishFailNotification(rootResult.ErrorMessage, body.Title, dataContext.User!); + return rootResult.Status; + } + else if (rootResult.AssetInfo != null && dataContext.Game != TokenGame.LittleBigPlanetPSP) // PSP uses a completely different asset type which we don't validate yet + { + if (body.IsAdventure && rootResult.AssetInfo.AssetType != GameAssetType.AdventureCreateProfile) + { + if (rootResult.ErrorMessage != null) dataContext.Database.AddPublishFailNotification("The adventure asset was badly formatted.", body.Title, dataContext.User!); + return BadRequest; + } + if (!body.IsAdventure && rootResult.AssetInfo.AssetType != GameAssetType.Level) + { + if (rootResult.ErrorMessage != null) dataContext.Database.AddPublishFailNotification("The level asset was badly formatted.", body.Title, dataContext.User!); + return BadRequest; + } + } + + GameLevel? existingLevel = dataContext.Database.GetLevelByRootResource(body.RootResource); + // If all are true: + // - there is an existing level with this root hash + // - this isn't an update request + // then block the upload + if (existingLevel != null && body.LevelId != existingLevel.LevelId) + { + dataContext.Database.AddPublishFailNotification("The level you tried to publish has already been uploaded by another user.", body.Title, dataContext.User!); + return Unauthorized; + } } if (!string.IsNullOrWhiteSpace(body.PublisherLabels)) @@ -76,7 +144,19 @@ private static bool VerifyLevel(GameLevelRequest body, DataContext dataContext) .Take(UgcLimits.MaximumLabels); } - return true; + if (body.Slots != null) + { + foreach (GameLevelRequest innerLevel in body.Slots) + { + HttpStatusCode innerValidationResult = VerifyLevel(innerLevel, dataContext, importer, aipi, isActualPublish, true); + if (innerValidationResult == OK) continue; + + dataContext.Logger.LogInfo(RefreshContext.Publishing, "Failed to verify inner level {0}", innerLevel.LevelId); + return innerValidationResult; + } + } + + return OK; } private static bool IsTimedLevelLimitReached(DataContext dataContext, GameUser user, string levelTitle, EntityUploadRateLimitProperties levelLimit) @@ -106,6 +186,8 @@ public Response StartPublish(RequestContext context, GameLevelRequest body, DataContext dataContext, GameServerConfig config, + AssetImporter importer, + AipiService aipi, GameUser user) { if (dataContext.User!.IsWriteBlocked(config)) @@ -117,22 +199,11 @@ public Response StartPublish(RequestContext context, if (IsTimedLevelLimitReached(dataContext, dataContext.User!, body.Title, user.GetRolePermissionsForUser(config).LevelUploadRateLimit)) return Unauthorized; - //If verifying the request fails, return BadRequest - if (!VerifyLevel(body, dataContext)) + HttpStatusCode validationResult = VerifyLevel(body, dataContext, importer, aipi, false); + if (validationResult != OK) { - context.Logger.LogInfo(RefreshContext.Publishing, "Failed to verify root level"); - return BadRequest; - } - - if (body.Slots != null) - { - foreach (GameLevelRequest innerLevel in body.Slots) - { - if (VerifyLevel(innerLevel, dataContext)) continue; - - context.Logger.LogInfo(RefreshContext.Publishing, "Failed to verify inner level {0}", innerLevel.LevelId); - return BadRequest; - } + context.Logger.LogInfo(RefreshContext.Publishing, "Failed to verify level"); + return validationResult; } List hashes = @@ -163,6 +234,8 @@ public Response PublishLevel(RequestContext context, GameLevelRequest body, DataContext dataContext, GameUser user, + AssetImporter importer, + AipiService aipi, GameServerConfig config) { if (user.IsWriteBlocked(config)) @@ -172,31 +245,11 @@ public Response PublishLevel(RequestContext context, if (IsTimedLevelLimitReached(dataContext, user, body.Title, timedLevelLimit)) return Unauthorized; - //If verifying the request fails, return BadRequest - if (!VerifyLevel(body, dataContext)) return BadRequest; - - string rootResourcePath = context.IsPSP() ? $"psp/{body.RootResource}" : body.RootResource; - - //Check if the root resource is a SHA1 hash - if (!CommonPatterns.Sha1Regex().IsMatch(body.RootResource)) return BadRequest; - //Make sure the root resource exists in the data store - if (!dataContext.DataStore.ExistsInStore(rootResourcePath)) return NotFound; - - GameAsset? asset = dataContext.Cache.GetAssetInfo(body.RootResource, dataContext.Database); - if (asset != null && dataContext.Game != TokenGame.LittleBigPlanetPSP) + HttpStatusCode validationResult = VerifyLevel(body, dataContext, importer, aipi, true); + if (validationResult != OK) { - // ReSharper disable once ConvertIfStatementToSwitchStatement - if (body.IsAdventure && asset.AssetType != GameAssetType.AdventureCreateProfile) - { - dataContext.Database.AddPublishFailNotification("The uploaded adventure data was corrupted.", body.Title, dataContext.User!); - return BadRequest; - } - - if (!body.IsAdventure && asset.AssetType != GameAssetType.Level) - { - dataContext.Database.AddPublishFailNotification("The uploaded level data was corrupted.", body.Title, dataContext.User!); - return BadRequest; - } + context.Logger.LogInfo(RefreshContext.Publishing, "Failed to verify level"); + return validationResult; } if (body.LevelId != 0) // Republish requests contain the id of the old level diff --git a/RefreshTests.GameServer/Tests/Levels/PublishEndpointsTests.cs b/RefreshTests.GameServer/Tests/Levels/PublishEndpointsTests.cs index 51a9d3cd..1f2b58d9 100644 --- a/RefreshTests.GameServer/Tests/Levels/PublishEndpointsTests.cs +++ b/RefreshTests.GameServer/Tests/Levels/PublishEndpointsTests.cs @@ -15,6 +15,8 @@ using System.Security.Cryptography; using System.Text; using System.Net; +using Refresh.Database.Models.Assets; +using Bunkum.Core.Storage; namespace RefreshTests.GameServer.Tests.Levels; @@ -22,6 +24,14 @@ public class PublishEndpointsTests : GameServerTest { private const string TEST_ASSET_HASH = "acddf3f9251c1ddb675ad81ba34ba16135b54aca"; private const string TEST_MISSING_ASSET_HASH = "acddf3f9251c1ddb675ad81ba34ba16135b54acb"; + + private string UploadLevelAsset(IDataStore dataStore) + { + ReadOnlySpan data = "LVLb"u8; + string hash = BitConverter.ToString(SHA1.HashData(data)).Replace("-", "").ToLower(); + dataStore.WriteToStore(hash, data); + return hash; + } [Test] public void PublishLevel() @@ -210,6 +220,76 @@ public void CantPublishLevelWithInvalidRootResource() message = client.PostAsync("/lbp/publish", new StringContent(level.AsXML())).Result; Assert.That(message.StatusCode, Is.EqualTo(BadRequest)); } + + [Test] + public void CantPublishLevelWithDisallowedRootResource() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + + ReadOnlySpan data = "LVLb"u8; + string hash = BitConverter.ToString(SHA1.HashData(data)).Replace("-", "").ToLower(); + context.GetDataStore().WriteToStore(hash, data); + + context.Database.DisallowAsset(hash, GameAssetType.Level, "too many bs obstacles"); + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); + + GameLevelRequest level = new() + { + Title = "Boom Town 2", + RootResource = hash, + }; + + HttpResponseMessage message = client.PostAsync("/lbp/startPublish", new StringContent(level.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(Unauthorized)); + + message = client.PostAsync("/lbp/publish", new StringContent(level.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(Unauthorized)); + } + + [Test] + public void CantPublishLevelWithNoRootResource() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); + + GameLevelRequest level = new() + { + Title = "The best level you've ever played !!!", + RootResource = "0", + }; + + HttpResponseMessage message = client.PostAsync("/lbp/startPublish", new StringContent(level.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(BadRequest)); + + message = client.PostAsync("/lbp/publish", new StringContent(level.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(BadRequest)); + } + + [Test] + public void CantPublishLevelWithMissingRootResource() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); + + GameLevelRequest level = new() + { + Title = "Normal Title!", + IconHash = "g719", + Description = "Normal Description", + Location = new GameLocation(), + RootResource = TEST_MISSING_ASSET_HASH, + }; + + HttpResponseMessage message = client.PostAsync("/lbp/startPublish", new StringContent(level.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(OK)); + + message = client.PostAsync("/lbp/publish", new StringContent(level.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(NotFound)); + } [TestCase(TokenGame.LittleBigPlanet1)] [TestCase(TokenGame.LittleBigPlanet2)] @@ -225,10 +305,8 @@ public void CantPublishLevelWithInvalidIconGuid(TokenGame game) GameLevelRequest level = new() { Title = "Normal Title!", - IconHash = "g0", - Description = "Normal Description", - Location = new GameLocation(), - RootResource = "I AM INVALID!!!", + IconHash = "g1087", + RootResource = this.UploadLevelAsset(context.GetDataStore()), }; HttpResponseMessage message = client.PostAsync("/lbp/startPublish", new StringContent(level.AsXML())).Result; @@ -238,7 +316,8 @@ public void CantPublishLevelWithInvalidIconGuid(TokenGame game) Assert.That(message.StatusCode, Is.EqualTo(BadRequest)); } - public void CanPublishLevelWithInvalidIconGuidPsp() + [Test] + public void CantPublishLevelWithInvalidIconGuidPsp() { using TestContext context = this.GetServer(); GameUser user = context.CreateUser(); @@ -248,41 +327,349 @@ public void CanPublishLevelWithInvalidIconGuidPsp() GameLevelRequest level = new() { Title = "Normal Title!", - IconHash = "g0", - Description = "Normal Description", - Location = new GameLocation(), - RootResource = "I AM INVALID!!!", + IconHash = "g67", // max is g63 + RootResource = this.UploadLevelAsset(context.GetDataStore()), }; HttpResponseMessage message = client.PostAsync("/lbp/startPublish", new StringContent(level.AsXML())).Result; - Assert.That(message.StatusCode, Is.EqualTo(OK)); + Assert.That(message.StatusCode, Is.EqualTo(BadRequest)); message = client.PostAsync("/lbp/publish", new StringContent(level.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(BadRequest)); + } + + [Test] + public void CantPublishLevelWithMissingCustomIcon() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + + ReadOnlySpan data = "TEX "u8; + string hash = BitConverter.ToString(SHA1.HashData(data)).Replace("-", "").ToLower(); + // Don't write to store + + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); + + GameLevelRequest level = new() + { + Title = "67", + IconHash = hash, + RootResource = this.UploadLevelAsset(context.GetDataStore()), + }; + + HttpResponseMessage message = client.PostAsync("/lbp/startPublish", new StringContent(level.AsXML())).Result; Assert.That(message.StatusCode, Is.EqualTo(OK)); + + message = client.PostAsync("/lbp/publish", new StringContent(level.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(NotFound)); } [Test] - public void CantPublishLevelWithMissingRootResource() + public void CantPublishLevelWithDisallowedIcon() { using TestContext context = this.GetServer(); GameUser user = context.CreateUser(); + ReadOnlySpan data = "TEX "u8; + string hash = BitConverter.ToString(SHA1.HashData(data)).Replace("-", "").ToLower(); + context.GetDataStore().WriteToStore(hash, data); + + context.Database.DisallowAsset(hash, GameAssetType.Texture, "too cringe"); using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); GameLevelRequest level = new() { - Title = "Normal Title!", - IconHash = "g719", - Description = "Normal Description", - Location = new GameLocation(), - RootResource = TEST_MISSING_ASSET_HASH, + Title = "67", + IconHash = hash, + RootResource = this.UploadLevelAsset(context.GetDataStore()), + }; + + HttpResponseMessage message = client.PostAsync("/lbp/startPublish", new StringContent(level.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(Unauthorized)); + + message = client.PostAsync("/lbp/publish", new StringContent(level.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(Unauthorized)); + } + + [Test] + [TestCase(TokenGame.LittleBigPlanet1, false)] + [TestCase(TokenGame.LittleBigPlanetPSP, false)] + [TestCase(TokenGame.LittleBigPlanet3, true)] + [TestCase(TokenGame.BetaBuild, true)] + public void TestAdventureUploadsFromVariousGames(TokenGame game, bool success) + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + + ReadOnlySpan data = "ADCb"u8; + string hash = BitConverter.ToString(SHA1.HashData(data)).Replace("-", "").ToLower(); + context.GetDataStore().WriteToStore(hash, data); + + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, game, TokenPlatform.PS3, user); + + GameLevelRequest level = new() + { + Title = "adventure time!", + IsAdventure = true, + Description = "epic joke please laugh", + RootResource = hash, + }; + + HttpResponseMessage message = client.PostAsync("/lbp/startPublish", new StringContent(level.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(success ? OK : Unauthorized)); + + message = client.PostAsync("/lbp/publish", new StringContent(level.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(success ? OK : Unauthorized)); + } + + [Test] + [TestCase("LVLb", false)] + [TestCase("PLNb", false)] + [TestCase("CHKb", false)] + [TestCase("TEX ", false)] + [TestCase("ADCb", true)] + public void TestAdventureRootResourceTypes(string resource, bool success) + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + + ReadOnlySpan data = new(Encoding.UTF8.GetBytes(resource)); + string hash = BitConverter.ToString(SHA1.HashData(data)).Replace("-", "").ToLower(); + context.GetDataStore().WriteToStore(hash, data); + + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet3, TokenPlatform.PS3, user); + + GameLevelRequest level = new() + { + Title = "totally an adventure!", + IsAdventure = true, + RootResource = hash, + }; + + HttpResponseMessage message = client.PostAsync("/lbp/startPublish", new StringContent(level.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(success ? OK : BadRequest)); + + message = client.PostAsync("/lbp/publish", new StringContent(level.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(success ? OK : BadRequest)); + } + + [Test] + [TestCase("LVLb", true)] + [TestCase("PLNb", false)] + [TestCase("CHKb", false)] + [TestCase("TEX ", false)] + [TestCase("ADCb", false)] + public void TestLevelRootResourceTypes(string resource, bool success) + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + + ReadOnlySpan data = new(Encoding.UTF8.GetBytes(resource)); + string hash = BitConverter.ToString(SHA1.HashData(data)).Replace("-", "").ToLower(); + context.GetDataStore().WriteToStore(hash, data); + + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet3, TokenPlatform.PS3, user); + + GameLevelRequest level = new() + { + Title = "totally a level!", + IsAdventure = false, + RootResource = hash, + }; + + HttpResponseMessage message = client.PostAsync("/lbp/startPublish", new StringContent(level.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(success ? OK : BadRequest)); + + message = client.PostAsync("/lbp/publish", new StringContent(level.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(success ? OK : BadRequest)); + } + + [Test] + [TestCase("LVLb")] + [TestCase("PLNb")] + [TestCase("CHKb")] + [TestCase("TEX ")] + [TestCase("wasedrthzjtgerw")] + public void LevelRootResourceTypeIsIgnoredIfPSP(string resource) + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + + ReadOnlySpan data = new(Encoding.UTF8.GetBytes(resource)); + string hash = BitConverter.ToString(SHA1.HashData(data)).Replace("-", "").ToLower(); + context.GetDataStore().WriteToStore($"psp/{hash}", data); + + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanetPSP, TokenPlatform.PSP, user); + client.DefaultRequestHeaders.UserAgent.TryParseAdd("LBPPSP CLIENT"); + + GameLevelRequest level = new() + { + Title = "totally a level!", + IsAdventure = false, + RootResource = hash, }; HttpResponseMessage message = client.PostAsync("/lbp/startPublish", new StringContent(level.AsXML())).Result; Assert.That(message.StatusCode, Is.EqualTo(OK)); message = client.PostAsync("/lbp/publish", new StringContent(level.AsXML())).Result; - Assert.That(message.StatusCode, Is.EqualTo(NotFound)); + Assert.That(message.StatusCode, Is.EqualTo(OK)); + } + + [Test] + public void CantPublishRegularLevelWithInnerLevels() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + + ReadOnlySpan data = "LVLb"u8; + string hash = BitConverter.ToString(SHA1.HashData(data)).Replace("-", "").ToLower(); + context.GetDataStore().WriteToStore(hash, data); + + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet3, TokenPlatform.PS3, user); + + GameLevelRequest level = new() + { + Title = "totally an adventure!", + IsAdventure = false, + RootResource = hash, + Slots = + [ + new() + { + Title = "Hi lol", + RootResource = "", + } + ] + }; + + HttpResponseMessage message = client.PostAsync("/lbp/startPublish", new StringContent(level.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(BadRequest)); + + message = client.PostAsync("/lbp/publish", new StringContent(level.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(BadRequest)); + } + + [Test] + public void CanPublishAdventureWithInnerLevels() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + + ReadOnlySpan data = "ADCb"u8; + string hash = BitConverter.ToString(SHA1.HashData(data)).Replace("-", "").ToLower(); + context.GetDataStore().WriteToStore(hash, data); + + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet3, TokenPlatform.PS3, user); + + GameLevelRequest level = new() + { + Title = "totally an adventure!", + IsAdventure = true, + RootResource = hash, + Slots = + [ + new() + { + Title = "Hi lol", + RootResource = "", + } + ] + }; + + HttpResponseMessage message = client.PostAsync("/lbp/startPublish", new StringContent(level.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(OK)); + + message = client.PostAsync("/lbp/publish", new StringContent(level.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(OK)); + } + + [Test] + public void CantPublishAdventureWithRecursiveInnerLevels() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + + ReadOnlySpan data = "ADCb"u8; + string hash = BitConverter.ToString(SHA1.HashData(data)).Replace("-", "").ToLower(); + context.GetDataStore().WriteToStore(hash, data); + + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet3, TokenPlatform.PS3, user); + + GameLevelRequest level = new() + { + Title = "Deep adventure", + IsAdventure = true, + RootResource = hash, + Slots = + [ + new() + { + Title = "Hi lol", + IsAdventure = false, + RootResource = "", + Slots = + [ + new() + { + Title = "Super secret", + IsAdventure = false, + RootResource = "", + Slots = + [ + new() + { + Title = "This is getting ridiculous...", + IsAdventure = false, + RootResource = "", + } + ] + } + ] + } + ] + }; + + HttpResponseMessage message = client.PostAsync("/lbp/startPublish", new StringContent(level.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(BadRequest)); + + message = client.PostAsync("/lbp/publish", new StringContent(level.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(BadRequest)); + } + + [Test] + public void CantPublishAdventureWithInnerAdventures() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + + ReadOnlySpan data = "ADCb"u8; + string hash = BitConverter.ToString(SHA1.HashData(data)).Replace("-", "").ToLower(); + context.GetDataStore().WriteToStore(hash, data); + + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet3, TokenPlatform.PS3, user); + + GameLevelRequest level = new() + { + Title = "Very adventurous", + IsAdventure = true, + RootResource = hash, + Slots = + [ + new() + { + Title = "Hi lol", + IsAdventure = true, + RootResource = "", + } + ] + }; + + HttpResponseMessage message = client.PostAsync("/lbp/startPublish", new StringContent(level.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(BadRequest)); + + message = client.PostAsync("/lbp/publish", new StringContent(level.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(BadRequest)); } [Test] @@ -448,7 +835,7 @@ public void CantPublishSameRootLevelHashTwice() //As user 2, try to publish a level with the same root hash message = client2.PostAsync("/lbp/publish", new StringContent(level.AsXML())).Result; - Assert.That(message.StatusCode, Is.EqualTo(BadRequest)); + Assert.That(message.StatusCode, Is.EqualTo(Unauthorized)); } [Test]