Skip to content

Commit 3a7ea22

Browse files
committed
Actually test upload rate-limits for levels/photos/playlists
1 parent 4460cb7 commit 3a7ea22

3 files changed

Lines changed: 300 additions & 92 deletions

File tree

RefreshTests.GameServer/Tests/Levels/PublishEndpointsTests.cs

Lines changed: 50 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using Refresh.Interfaces.Game.Types.Levels;
1515
using System.Security.Cryptography;
1616
using System.Text;
17+
using System.Net;
1718

1819
namespace RefreshTests.GameServer.Tests.Levels;
1920

@@ -496,113 +497,91 @@ public void CantUnpublishSomeoneElsesLevel()
496497
}
497498

498499
[Test]
499-
[TestCase(2, 2)]
500-
public void CantPublishAfterExceedingTimedLevelLimit(int levelQuota, int uploadAttemptsAfterExceeding)
500+
[TestCase(4, 4, 1)]
501+
public void LevelUploadsGetRateLimitedTemporarily(int levelQuota, int uploadAttemptsAfterExceeding, int timeSpanHours)
501502
{
502503
using TestContext context = this.GetServer();
503504
GameUser user = context.CreateUser("thepublisher");
504505

505506
// Prepare config
506507
GameServerConfig config = context.Server.Value.GameServerConfig;
507-
EntityUploadRateLimitProperties timedLevelLimit = new()
508+
EntityUploadRateLimitProperties uploadConfig = new()
508509
{
509510
Enabled = true,
510511
UploadQuota = levelQuota,
511-
TimeSpanHours = 1,
512+
TimeSpanHours = timeSpanHours,
512513
};
513-
config.NormalUserPermissions.LevelUploadRateLimit = timedLevelLimit;
514+
config.NormalUserPermissions.LevelUploadRateLimit = uploadConfig;
514515

515516
using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user);
517+
int publishAttempts = 0;
516518

517-
// Upload level asset
518-
HttpResponseMessage assetUploadMessage = client.PostAsync($"/lbp/upload/{TEST_ASSET_HASH}", new ReadOnlyMemoryContent("LVLb"u8.ToArray())).Result;
519-
Assert.That(assetUploadMessage.StatusCode, Is.EqualTo(OK));
519+
// Not blocked yet
520+
Assert.That(context.Database.GetUploadRateLimit(user, GameDatabaseEntity.Level), Is.Null);
521+
Assert.That(context.Database.GetRemainingTimeIfUploadRateLimitReached(user, GameDatabaseEntity.Level, uploadConfig.UploadQuota), Is.Null);
520522

521-
// Fill up quota
522-
SpamSuccessfulUploads(timedLevelLimit.UploadQuota, client, 0);
523-
524-
// Try to upload more levels after exceeding quota
525-
for (int i = 0; i < uploadAttemptsAfterExceeding; i++)
526-
{
527-
GameLevelRequest level = PrepareLevelPublishRequest(client, i + timedLevelLimit.UploadQuota);
528-
529-
HttpResponseMessage message = client.PostAsync("/lbp/startPublish", new StringContent(level.AsXML())).Result;
530-
Assert.That(message.StatusCode, Is.EqualTo(Unauthorized));
531-
message = client.PostAsync("/lbp/publish", new StringContent(level.AsXML())).Result;
532-
Assert.That(message.StatusCode, Is.EqualTo(Unauthorized));
533-
}
534-
535-
// Check amount of levels
536-
DatabaseList<GameLevel> levelsByUser = context.Database.GetLevelsByUser(user, 1000, 0, new(TokenGame.LittleBigPlanet3), user);
537-
Assert.That(levelsByUser.TotalItems, Is.EqualTo(timedLevelLimit.UploadQuota));
538-
539-
// Ensure there were error notifications sent for each blocked request to both /startPublish and /publish
540-
DatabaseList<GameNotification> newNotifications = context.Database.GetNotificationsByUser(user, 1000, 0);
541-
Assert.That(newNotifications.TotalItems, Is.EqualTo(uploadAttemptsAfterExceeding * 2));
542-
}
523+
// Fill up half of quota
524+
publishAttempts += SpamPublishUniqueLevels(uploadConfig.UploadQuota / 2, client, publishAttempts);
525+
context.Database.Refresh();
543526

544-
[Test]
545-
[TestCase(2, 2)]
546-
public void ResetTimedLevelLimitAfterExpiry(int levelQuota, int uploadAttemptsAfterExceeding)
547-
{
548-
using TestContext context = this.GetServer();
549-
GameUser user = context.CreateUser("thepublisher");
527+
// There is rate-limit data in DB, but the rate-limit hasn't been triggered yet
528+
EntityUploadRateLimit? uploadLimit = context.Database.GetUploadRateLimit(user, GameDatabaseEntity.Level);
529+
Assert.That(uploadLimit, Is.Not.Null);
530+
Assert.That(uploadLimit!.UploadCount, Is.EqualTo(uploadConfig.UploadQuota / 2));
531+
Assert.That(context.Database.GetRemainingTimeIfUploadRateLimitReached(user, GameDatabaseEntity.Level, uploadConfig.UploadQuota), Is.Null);
532+
533+
// Try to upload more levels to reach quota
534+
publishAttempts += SpamPublishUniqueLevels(uploadConfig.UploadQuota / 2, client, publishAttempts);
535+
context.Database.Refresh();
550536

551-
// Prepare config
552-
GameServerConfig config = context.Server.Value.GameServerConfig;
553-
EntityUploadRateLimitProperties timedLevelLimit = new()
554-
{
555-
Enabled = true,
556-
UploadQuota = levelQuota,
557-
// Having this be 0 causes the server to always set the expiry date to now, making the expiry date be not null but always expired,
558-
// causing both /startPublish and /publish to always reset the limit after setting it in a previous /publish request, and allowing publish requests
559-
TimeSpanHours = 0,
560-
};
561-
config.NormalUserPermissions.LevelUploadRateLimit = timedLevelLimit;
537+
// Ensure there were no notifications sent
538+
Assert.That(context.Database.GetNotificationCountByUser(user), Is.Zero);
562539

563-
using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user);
540+
// There is rate-limit data in DB, and the rate-limit has been triggered
541+
uploadLimit = context.Database.GetUploadRateLimit(user, GameDatabaseEntity.Level);
542+
Assert.That(uploadLimit, Is.Not.Null);
543+
Assert.That(uploadLimit!.UploadCount, Is.EqualTo(uploadConfig.UploadQuota));
544+
Assert.That(context.Database.GetRemainingTimeIfUploadRateLimitReached(user, GameDatabaseEntity.Level, uploadConfig.UploadQuota), Is.Not.Null);
564545

565-
// Upload level asset
566-
HttpResponseMessage message = client.PostAsync($"/lbp/upload/{TEST_ASSET_HASH}", new ReadOnlyMemoryContent("LVLb"u8.ToArray())).Result;
567-
Assert.That(message.StatusCode, Is.EqualTo(OK));
546+
// levels are blocked
547+
publishAttempts += SpamPublishUniqueLevels(uploadAttemptsAfterExceeding, client, publishAttempts, Unauthorized);
548+
context.Database.Refresh();
568549

569-
// Fill up quota
570-
SpamSuccessfulUploads(timedLevelLimit.UploadQuota, client, 0);
571-
572-
// Try to upload more levels after exceeding quota
573-
SpamSuccessfulUploads(uploadAttemptsAfterExceeding, client, timedLevelLimit.UploadQuota);
550+
// Check amount of levels, and ensure there were notifications sent for every failed level
551+
Assert.That(context.Database.GetTotalLevelsByUser(user), Is.EqualTo(uploadConfig.UploadQuota));
552+
Assert.That(context.Database.GetNotificationCountByUser(user), Is.EqualTo(uploadAttemptsAfterExceeding * 2));
574553

575-
// Check amount of levels
576-
DatabaseList<GameLevel> levelsByUser = context.Database.GetLevelsByUser(user, 1000, 0, new(TokenGame.LittleBigPlanet3), user);
577-
Assert.That(levelsByUser.TotalItems, Is.EqualTo(timedLevelLimit.UploadQuota + uploadAttemptsAfterExceeding));
554+
// Expire limit naturally by trying to publish again later
555+
context.Time.TimestampMilliseconds = 1000 * 60 * 60 * timeSpanHours + 10;
556+
publishAttempts += SpamPublishUniqueLevels(uploadAttemptsAfterExceeding, client, publishAttempts);
557+
context.Database.Refresh();
578558

579-
// Ensure there were no notifications sent
580-
DatabaseList<GameNotification> newNotifications = context.Database.GetNotificationsByUser(user, 1000, 0);
581-
Assert.That(newNotifications.TotalItems, Is.EqualTo(0));
559+
// there are more levels now, and no new notifications
560+
Assert.That(context.Database.GetTotalLevelsByUser(user), Is.EqualTo(uploadConfig.UploadQuota * 2));
561+
Assert.That(context.Database.GetNotificationCountByUser(user), Is.EqualTo(uploadAttemptsAfterExceeding * 2));
582562
}
583563

584-
private void SpamSuccessfulUploads(int uploads, HttpClient client, int startIndex)
564+
private int SpamPublishUniqueLevels(int uploads, HttpClient client, int startIndex, HttpStatusCode expectedStatus = OK)
585565
{
586566
for (int i = 0; i < uploads; i++)
587567
{
588568
// Upload level
589-
GameLevelRequest level = PrepareLevelPublishRequest(client, startIndex + i);
569+
GameLevelRequest level = PrepareUniqueLevelPublishRequest(client, startIndex + i);
590570

591571
HttpResponseMessage message = client.PostAsync("/lbp/startPublish", new StringContent(level.AsXML())).Result;
592-
Assert.That(message.StatusCode, Is.EqualTo(OK));
572+
Assert.That(message.StatusCode, Is.EqualTo(expectedStatus));
593573
message = client.PostAsync("/lbp/publish", new StringContent(level.AsXML())).Result;
594-
Assert.That(message.StatusCode, Is.EqualTo(OK));
574+
Assert.That(message.StatusCode, Is.EqualTo(expectedStatus));
595575
}
576+
577+
return uploads;
596578
}
597579

598-
private GameLevelRequest PrepareLevelPublishRequest(HttpClient client, int uniqueValue)
580+
private GameLevelRequest PrepareUniqueLevelPublishRequest(HttpClient client, int uniqueValue)
599581
{
600582
// upload root asset
601583
ReadOnlySpan<byte> rootResource = new(Encoding.ASCII.GetBytes($"LVLb {uniqueValue}"));
602-
603-
string rootHash = BitConverter.ToString(SHA1.HashData(rootResource))
604-
.Replace("-", "")
605-
.ToLower();
584+
string rootHash = BitConverter.ToString(SHA1.HashData(rootResource)).Replace("-", "").ToLower();
606585

607586
HttpResponseMessage message = client.PostAsync($"/lbp/upload/{rootHash}", new ReadOnlyMemoryContent(rootResource.ToArray())).Result;
608587
Assert.That(message.StatusCode, Is.EqualTo(OK));

RefreshTests.GameServer/Tests/Photos/PhotoEndpointsTests.cs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
using Refresh.Interfaces.Game.Types.Lists;
99
using Refresh.Database.Helpers;
1010
using System.Security.Cryptography;
11+
using Refresh.Core.Configuration;
12+
using Refresh.Database.Models;
13+
using System.Text;
14+
using System.Net;
1115

1216
namespace RefreshTests.GameServer.Tests.Photos;
1317

@@ -508,4 +512,115 @@ public void CannotUploadPhotoIfDuplicatePlan()
508512
Assert.That(message.StatusCode, Is.EqualTo(BadRequest));
509513
Assert.That(context.Database.GetNotificationCountByUser(user), Is.EqualTo(1));
510514
}
515+
516+
[Test]
517+
[TestCase(4, 4, 1)]
518+
public void PhotoUploadsGetRateLimitedTemporarily(int photoQuota, int uploadAttemptsAfterExceeding, int timeSpanHours)
519+
{
520+
using TestContext context = this.GetServer();
521+
GameUser user = context.CreateUser("thepublisher");
522+
GameLevel level = context.CreateLevel(user);
523+
524+
// Prepare config
525+
GameServerConfig config = context.Server.Value.GameServerConfig;
526+
EntityUploadRateLimitProperties uploadConfig = new()
527+
{
528+
Enabled = true,
529+
UploadQuota = photoQuota,
530+
TimeSpanHours = timeSpanHours,
531+
};
532+
config.NormalUserPermissions.PhotoUploadRateLimit = uploadConfig;
533+
534+
using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user);
535+
int publishAttempts = 0;
536+
537+
// Not blocked yet
538+
Assert.That(context.Database.GetUploadRateLimit(user, GameDatabaseEntity.Photo), Is.Null);
539+
Assert.That(context.Database.GetRemainingTimeIfUploadRateLimitReached(user, GameDatabaseEntity.Photo, uploadConfig.UploadQuota), Is.Null);
540+
541+
// Fill up half of quota
542+
publishAttempts += SpamUploadUniquePhotos(uploadConfig.UploadQuota / 2, client, publishAttempts, level);
543+
context.Database.Refresh();
544+
545+
// There is rate-limit data in DB, but the rate-limit hasn't been triggered yet
546+
EntityUploadRateLimit? uploadLimit = context.Database.GetUploadRateLimit(user, GameDatabaseEntity.Photo);
547+
Assert.That(uploadLimit, Is.Not.Null);
548+
Assert.That(uploadLimit!.UploadCount, Is.EqualTo(uploadConfig.UploadQuota / 2));
549+
Assert.That(context.Database.GetRemainingTimeIfUploadRateLimitReached(user, GameDatabaseEntity.Photo, uploadConfig.UploadQuota), Is.Null);
550+
551+
// Try to upload more photos to reach quota
552+
publishAttempts += SpamUploadUniquePhotos(uploadConfig.UploadQuota / 2, client, publishAttempts, level);
553+
context.Database.Refresh();
554+
555+
// Ensure there were no notifications sent
556+
Assert.That(context.Database.GetNotificationCountByUser(user), Is.Zero);
557+
558+
// There is rate-limit data in DB, and the rate-limit has been triggered
559+
uploadLimit = context.Database.GetUploadRateLimit(user, GameDatabaseEntity.Photo);
560+
Assert.That(uploadLimit, Is.Not.Null);
561+
Assert.That(uploadLimit!.UploadCount, Is.EqualTo(uploadConfig.UploadQuota));
562+
Assert.That(context.Database.GetRemainingTimeIfUploadRateLimitReached(user, GameDatabaseEntity.Photo, uploadConfig.UploadQuota), Is.Not.Null);
563+
564+
// photos are blocked
565+
publishAttempts += SpamUploadUniquePhotos(uploadAttemptsAfterExceeding, client, publishAttempts, level, Unauthorized);
566+
context.Database.Refresh();
567+
568+
// Check amount of photos, and ensure there were notifications sent for every failed photo
569+
Assert.That(context.Database.GetTotalPhotosByUser(user), Is.EqualTo(uploadConfig.UploadQuota));
570+
Assert.That(context.Database.GetNotificationCountByUser(user), Is.EqualTo(uploadAttemptsAfterExceeding));
571+
572+
// Expire limit naturally by trying to publish again later
573+
context.Time.TimestampMilliseconds = 1000 * 60 * 60 * timeSpanHours + 10;
574+
publishAttempts += SpamUploadUniquePhotos(uploadAttemptsAfterExceeding, client, publishAttempts, level);
575+
context.Database.Refresh();
576+
577+
// there are more levels now, and no new notifications
578+
Assert.That(context.Database.GetTotalPhotosByUser(user), Is.EqualTo(uploadConfig.UploadQuota * 2));
579+
Assert.That(context.Database.GetNotificationCountByUser(user), Is.EqualTo(uploadAttemptsAfterExceeding));
580+
}
581+
582+
private int SpamUploadUniquePhotos(int uploads, HttpClient client, int startIndex, GameLevel level, HttpStatusCode expectedStatus = OK)
583+
{
584+
for (int i = 0; i < uploads; i++)
585+
{
586+
// Upload level
587+
SerializedPhoto photo = PrepareUniquePhotoUploadRequest(client, level, startIndex + i);
588+
589+
HttpResponseMessage message = client.PostAsync($"/lbp/uploadPhoto", new StringContent(photo.AsXML())).Result;
590+
Assert.That(message.StatusCode, Is.EqualTo(expectedStatus));
591+
}
592+
593+
return uploads;
594+
}
595+
596+
private SerializedPhoto PrepareUniquePhotoUploadRequest(HttpClient client, GameLevel level, int uniqueValue)
597+
{
598+
// upload """photo"""
599+
ReadOnlySpan<byte> imageResource = new(Encoding.ASCII.GetBytes($"TEX {uniqueValue}"));
600+
string imageHash = BitConverter.ToString(SHA1.HashData(imageResource)).Replace("-", "").ToLower();
601+
HttpResponseMessage message = client.PostAsync($"/lbp/upload/{imageHash}", new ReadOnlyMemoryContent(imageResource.ToArray())).Result;
602+
Assert.That(message.StatusCode, Is.EqualTo(OK));
603+
604+
// upload """plan"""
605+
ReadOnlySpan<byte> planResource = new(Encoding.ASCII.GetBytes($"PLNb {uniqueValue}"));
606+
string planHash = BitConverter.ToString(SHA1.HashData(planResource)).Replace("-", "").ToLower();
607+
message = client.PostAsync($"/lbp/upload/{planHash}", new ReadOnlyMemoryContent(planResource.ToArray())).Result;
608+
Assert.That(message.StatusCode, Is.EqualTo(OK));
609+
610+
// prepare request body
611+
return new()
612+
{
613+
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
614+
SmallHash = imageHash,
615+
MediumHash = imageHash,
616+
LargeHash = imageHash,
617+
PlanHash = planHash,
618+
Level = new SerializedPhotoLevel
619+
{
620+
LevelId = level.LevelId,
621+
Title = level.Title,
622+
Type = "user",
623+
},
624+
};
625+
}
511626
}

0 commit comments

Comments
 (0)