diff --git a/Refresh.Database/GameDatabaseContext.Assets.cs b/Refresh.Database/GameDatabaseContext.Assets.cs index 7d2ab535..d29da1f2 100644 --- a/Refresh.Database/GameDatabaseContext.Assets.cs +++ b/Refresh.Database/GameDatabaseContext.Assets.cs @@ -111,20 +111,30 @@ public void SetMainlinePhotoHash(GameAsset asset, string hash) => asset.AsMainlinePhotoHash = hash; }); + public bool IsAssetDisallowed(string hash) + { + string hashLower = hash.ToLower(); + return this.DisallowedAssets.Any(u => u.AssetHash == hashLower); + } + public DisallowedAsset? GetDisallowedAssetInfo(string hash) - => this.DisallowedAssets.FirstOrDefault(d => d.AssetHash == hash); + { + string hashLower = hash.ToLower(); + return this.DisallowedAssets.FirstOrDefault(d => d.AssetHash == hashLower); + } /// /// The asset's disallowance info + whether the asset wasn't already disallowed before /// public bool ReallowAsset(string hash) { - DisallowedAsset? existing = this.GetDisallowedAssetInfo(hash); + string hashLower = hash.ToLower(); + DisallowedAsset? existing = this.GetDisallowedAssetInfo(hashLower); if (existing == null) return false; this.DisallowedAssets.Remove(existing); diff --git a/Refresh.Database/GameDatabaseContext.Registration.cs b/Refresh.Database/GameDatabaseContext.Registration.cs index 215b8e23..58e2a531 100644 --- a/Refresh.Database/GameDatabaseContext.Registration.cs +++ b/Refresh.Database/GameDatabaseContext.Registration.cs @@ -219,22 +219,29 @@ public void RemoveEmailVerificationCode(EmailVerificationCode code) } public bool IsUserDisallowed(string username) - => this.DisallowedUsers.Any(u => u.Username == username); + { + string lowercaseUsername = username.ToLower(); + return this.DisallowedUsers.Any(u => u.UsernameLower == lowercaseUsername); + } public DisallowedUser? GetDisallowedUserInfo(string username) - => this.DisallowedUsers.FirstOrDefault(d => d.Username == username); + { + string lowercaseUsername = username.ToLower(); + return this.DisallowedUsers.FirstOrDefault(d => d.UsernameLower == lowercaseUsername); + } public DatabaseList GetDisallowedUsers(int skip, int count) => new(this.DisallowedUsers.OrderByDescending(d => d.DisallowedAt), skip, count); public (DisallowedUser, bool) DisallowUser(string username, string reason) { - DisallowedUser? existing = this.GetDisallowedUserInfo(username); + string lowercaseUsername = username.ToLower(); + DisallowedUser? existing = this.GetDisallowedUserInfo(lowercaseUsername); if (existing != null) return (existing, false); DisallowedUser disallowed = new() { - Username = username, + UsernameLower = lowercaseUsername, Reason = reason, DisallowedAt = this._time.Now, }; @@ -246,7 +253,8 @@ public DatabaseList GetDisallowedUsers(int skip, int count) public bool ReallowUser(string username) { - DisallowedUser? disallowedUser = this.GetDisallowedUserInfo(username); + string lowercaseUsername = username.ToLower(); + DisallowedUser? disallowedUser = this.GetDisallowedUserInfo(lowercaseUsername); if (disallowedUser == null) return false; @@ -257,22 +265,29 @@ public bool ReallowUser(string username) } public bool IsEmailAddressDisallowed(string emailAddress) - => this.DisallowedEmailAddresses.Any(u => u.Address == emailAddress); + { + string emailAddressLower = emailAddress.ToLowerInvariant(); + return this.DisallowedEmailAddresses.Any(u => u.AddressLower == emailAddressLower); + } public DisallowedEmailAddress? GetDisallowedEmailAddressInfo(string emailAddress) - => this.DisallowedEmailAddresses.FirstOrDefault(d => d.Address == emailAddress); + { + string emailAddressLower = emailAddress.ToLowerInvariant(); + return this.DisallowedEmailAddresses.FirstOrDefault(d => d.AddressLower == emailAddressLower); + } public DatabaseList GetDisallowedEmailAddresses(int skip, int count) => new(this.DisallowedEmailAddresses.OrderByDescending(d => d.DisallowedAt), skip, count); public (DisallowedEmailAddress, bool) DisallowEmailAddress(string emailAddress, string reason) { - DisallowedEmailAddress? existing = this.GetDisallowedEmailAddressInfo(emailAddress); + string emailAddressLower = emailAddress.ToLowerInvariant(); + DisallowedEmailAddress? existing = this.GetDisallowedEmailAddressInfo(emailAddressLower); if (existing != null) return (existing, false); DisallowedEmailAddress disallowed = new() { - Address = emailAddress, + AddressLower = emailAddressLower, Reason = reason, DisallowedAt = this._time.Now, }; @@ -284,7 +299,8 @@ public DatabaseList GetDisallowedEmailAddresses(int skip public bool ReallowEmailAddress(string emailAddress) { - DisallowedEmailAddress? disallowed = this.GetDisallowedEmailAddressInfo(emailAddress); + string emailAddressLower = emailAddress.ToLowerInvariant(); + DisallowedEmailAddress? disallowed = this.GetDisallowedEmailAddressInfo(emailAddressLower); if (disallowed == null) return false; @@ -294,19 +310,19 @@ public bool ReallowEmailAddress(string emailAddress) return true; } - private string GetEmailDomainFromAddress(string emailAddress) - => emailAddress.Split('@').Last(); + private string GetLowercaseEmailDomainFromAddress(string emailAddress) + => emailAddress.Split('@').Last().ToLowerInvariant(); public bool IsEmailDomainDisallowed(string emailAddress) { - string emailDomain = this.GetEmailDomainFromAddress(emailAddress); - return this.DisallowedEmailDomains.Any(u => u.Domain == emailDomain); + string emailDomainLower = this.GetLowercaseEmailDomainFromAddress(emailAddress); + return this.DisallowedEmailDomains.Any(u => u.DomainLower == emailDomainLower); } public DisallowedEmailDomain? GetDisallowedEmailDomainInfo(string emailAddress) { - string emailDomain = this.GetEmailDomainFromAddress(emailAddress); - return this.DisallowedEmailDomains.FirstOrDefault(d => d.Domain == emailDomain); + string emailDomainLower = this.GetLowercaseEmailDomainFromAddress(emailAddress); + return this.DisallowedEmailDomains.FirstOrDefault(d => d.DomainLower == emailDomainLower); } public DatabaseList GetDisallowedEmailDomains(int skip, int count) @@ -314,13 +330,13 @@ public DatabaseList GetDisallowedEmailDomains(int skip, i public (DisallowedEmailDomain, bool) DisallowEmailDomain(string emailAddress, string reason) { - string emailDomain = this.GetEmailDomainFromAddress(emailAddress); - DisallowedEmailDomain? existing = this.GetDisallowedEmailDomainInfo(emailDomain); + string emailDomainLower = this.GetLowercaseEmailDomainFromAddress(emailAddress); + DisallowedEmailDomain? existing = this.GetDisallowedEmailDomainInfo(emailDomainLower); if (existing != null) return (existing, false); DisallowedEmailDomain disallowed = new() { - Domain = emailDomain, + DomainLower = emailDomainLower, Reason = reason, DisallowedAt = this._time.Now, }; @@ -332,8 +348,8 @@ public DatabaseList GetDisallowedEmailDomains(int skip, i public bool ReallowEmailDomain(string emailAddress) { - string emailDomain = this.GetEmailDomainFromAddress(emailAddress); - DisallowedEmailDomain? disallowedDomain = this.GetDisallowedEmailDomainInfo(emailDomain); + string emailDomainLower = this.GetLowercaseEmailDomainFromAddress(emailAddress); + DisallowedEmailDomain? disallowedDomain = this.GetDisallowedEmailDomainInfo(emailDomainLower); if (disallowedDomain == null) return false; diff --git a/Refresh.Database/Migrations/20260527172459_DisallowEntitiesCaseInsensitively.cs b/Refresh.Database/Migrations/20260527172459_DisallowEntitiesCaseInsensitively.cs new file mode 100644 index 00000000..ac7b4384 --- /dev/null +++ b/Refresh.Database/Migrations/20260527172459_DisallowEntitiesCaseInsensitively.cs @@ -0,0 +1,85 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Refresh.Database.Migrations +{ + /// + [DbContext(typeof(GameDatabaseContext))] + [Migration("20260527172459_DisallowEntitiesCaseInsensitively")] + public partial class DisallowEntitiesCaseInsensitively : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // Remove case-insensitively duplicate entries before lowercasing primary keys to not cause duplicate keys + // Email Addresses + migrationBuilder.Sql + (""" + DELETE FROM "DisallowedEmailAddresses" + WHERE "Address" NOT IN ( + SELECT min("Address") + FROM "DisallowedEmailAddresses" + GROUP BY lower("Address") + ) + """); + // for some reason, Postgres won't actually execute these separately if we use semicolons, so we have to do separate method calls + migrationBuilder.Sql + (""" + UPDATE "DisallowedEmailAddresses" SET "Address" = lower("Address"); + """); + + // Email Domains + migrationBuilder.Sql + (""" + DELETE FROM "DisallowedEmailDomains" + WHERE "Domain" NOT IN ( + SELECT min("Domain") + FROM "DisallowedEmailDomains" + GROUP BY lower("Domain") + ) + """); + migrationBuilder.Sql + (""" + UPDATE "DisallowedEmailDomains" SET "Domain" = lower("Domain"); + """); + + // Usernames + migrationBuilder.Sql + (""" + DELETE FROM "DisallowedUsers" + WHERE "Username" NOT IN ( + SELECT min("Username") + FROM "DisallowedUsers" + GROUP BY lower("Username") + ) + """); + migrationBuilder.Sql + (""" + UPDATE "DisallowedUsers" SET "Username" = lower("Username"); + """); + + // Assets + migrationBuilder.Sql + (""" + DELETE FROM "DisallowedAssets" + WHERE "AssetHash" NOT IN ( + SELECT min("AssetHash") + FROM "DisallowedAssets" + GROUP BY lower("AssetHash") + ) + """); + migrationBuilder.Sql + (""" + UPDATE "DisallowedAssets" SET "AssetHash" = lower("AssetHash"); + """); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/Refresh.Database/Migrations/20260606101615_RenameDisallowancePKsForClarity.cs b/Refresh.Database/Migrations/20260606101615_RenameDisallowancePKsForClarity.cs new file mode 100644 index 00000000..dbc1b4e6 --- /dev/null +++ b/Refresh.Database/Migrations/20260606101615_RenameDisallowancePKsForClarity.cs @@ -0,0 +1,51 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Refresh.Database.Migrations +{ + /// + [DbContext(typeof(GameDatabaseContext))] + [Migration("20260606101615_RenameDisallowancePKsForClarity")] + public partial class RenameDisallowancePKsForClarity : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Username", + table: "DisallowedUsers", + newName: "UsernameLower"); + + migrationBuilder.RenameColumn( + name: "Domain", + table: "DisallowedEmailDomains", + newName: "DomainLower"); + + migrationBuilder.RenameColumn( + name: "Address", + table: "DisallowedEmailAddresses", + newName: "AddressLower"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "UsernameLower", + table: "DisallowedUsers", + newName: "Username"); + + migrationBuilder.RenameColumn( + name: "DomainLower", + table: "DisallowedEmailDomains", + newName: "Domain"); + + migrationBuilder.RenameColumn( + name: "AddressLower", + table: "DisallowedEmailAddresses", + newName: "Address"); + } + } +} diff --git a/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs b/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs index b04f829c..86774e7d 100644 --- a/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs +++ b/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs @@ -1532,7 +1532,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Refresh.Database.Models.Users.DisallowedEmailAddress", b => { - b.Property("Address") + b.Property("AddressLower") .HasColumnType("text"); b.Property("DisallowedAt") @@ -1541,14 +1541,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Reason") .HasColumnType("text"); - b.HasKey("Address"); + b.HasKey("AddressLower"); b.ToTable("DisallowedEmailAddresses"); }); modelBuilder.Entity("Refresh.Database.Models.Users.DisallowedEmailDomain", b => { - b.Property("Domain") + b.Property("DomainLower") .HasColumnType("text"); b.Property("DisallowedAt") @@ -1557,14 +1557,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Reason") .HasColumnType("text"); - b.HasKey("Domain"); + b.HasKey("DomainLower"); b.ToTable("DisallowedEmailDomains"); }); modelBuilder.Entity("Refresh.Database.Models.Users.DisallowedUser", b => { - b.Property("Username") + b.Property("UsernameLower") .HasColumnType("text"); b.Property("DisallowedAt") @@ -1573,7 +1573,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Reason") .HasColumnType("text"); - b.HasKey("Username"); + b.HasKey("UsernameLower"); b.ToTable("DisallowedUsers"); }); @@ -1602,12 +1602,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Entity") .HasColumnType("smallint"); - b.Property("UploadCount") - .HasColumnType("integer"); - b.Property("ExpiryDate") .HasColumnType("timestamp with time zone"); + b.Property("UploadCount") + .HasColumnType("integer"); + b.HasKey("UserId", "Entity"); b.ToTable("EntityUploadRateLimits"); diff --git a/Refresh.Database/Models/Users/DisallowedEmailAddress.cs b/Refresh.Database/Models/Users/DisallowedEmailAddress.cs index a54c75df..f08153d4 100644 --- a/Refresh.Database/Models/Users/DisallowedEmailAddress.cs +++ b/Refresh.Database/Models/Users/DisallowedEmailAddress.cs @@ -4,8 +4,11 @@ namespace Refresh.Database.Models.Users; public partial class DisallowedEmailAddress { + /// + /// Lower-case email address to allow case-insensitive lookup. + /// [Key] - public string Address { get; set; } + public string AddressLower { get; set; } public string Reason { get; set; } public DateTimeOffset DisallowedAt { get; set; } } \ No newline at end of file diff --git a/Refresh.Database/Models/Users/DisallowedEmailDomain.cs b/Refresh.Database/Models/Users/DisallowedEmailDomain.cs index ae3e297d..1ee0e7d7 100644 --- a/Refresh.Database/Models/Users/DisallowedEmailDomain.cs +++ b/Refresh.Database/Models/Users/DisallowedEmailDomain.cs @@ -4,8 +4,11 @@ namespace Refresh.Database.Models.Users; public partial class DisallowedEmailDomain { + /// + /// Lower-case email domain to allow case-insensitive lookup. + /// [Key] - public string Domain { get; set; } + public string DomainLower { get; set; } public string Reason { get; set; } public DateTimeOffset DisallowedAt { get; set; } } \ No newline at end of file diff --git a/Refresh.Database/Models/Users/DisallowedUser.cs b/Refresh.Database/Models/Users/DisallowedUser.cs index 7dcd2beb..b2f5424e 100644 --- a/Refresh.Database/Models/Users/DisallowedUser.cs +++ b/Refresh.Database/Models/Users/DisallowedUser.cs @@ -4,8 +4,11 @@ namespace Refresh.Database.Models.Users; public partial class DisallowedUser { + /// + /// Lower-case username to allow case-insensitive lookup. + /// [Key] - public string Username { get; set; } + public string UsernameLower { get; set; } public string Reason { get; set; } public DateTimeOffset DisallowedAt { get; set; } } \ No newline at end of file diff --git a/Refresh.Interfaces.APIv3/Endpoints/ResourceApiEndpoints.cs b/Refresh.Interfaces.APIv3/Endpoints/ResourceApiEndpoints.cs index 7f5893db..89ad3214 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/ResourceApiEndpoints.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/ResourceApiEndpoints.cs @@ -200,7 +200,7 @@ IntegrationConfig integration return new ApiValidationError($"You have exceeded your filesize quota."); } - if (database.GetDisallowedAssetInfo(hash) != null) + if (database.IsAssetDisallowed(hash)) { context.Logger.LogWarning(BunkumCategory.UserContent, "User {0} has tried to upload a disallowed asset, rejecting.", user); return ApiModerationError.AssetDisallowedError; diff --git a/Refresh.Interfaces.Game/Endpoints/ResourceEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/ResourceEndpoints.cs index 8b6dc48c..e63759f3 100644 --- a/Refresh.Interfaces.Game/Endpoints/ResourceEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/ResourceEndpoints.cs @@ -62,7 +62,7 @@ public Response UploadAsset(RequestContext context, string hash, string type, by return RequestEntityTooLarge; } - if (database.GetDisallowedAssetInfo(hash) != null) + if (database.IsAssetDisallowed(hash)) { context.Logger.LogWarning(BunkumCategory.UserContent, "User {0} has tried to upload a disallowed asset, rejecting.", user); return Unauthorized; diff --git a/RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs b/RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs index 920df4ef..409ce93d 100644 --- a/RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs +++ b/RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs @@ -46,172 +46,6 @@ public void RegisterAccount() Assert.That(context.Database.GetUserByUsername(username), Is.Not.EqualTo(null)); } - [Test] - public void CannotRegisterAccountWithDisallowedEmailAddress() - { - using TestContext context = this.GetServer(); - - const string email = "guy@lil.com"; - const string disallowReason = "being lil"; - // Not somehow already disallowed - Assert.That(context.Database.IsEmailAddressDisallowed(email), Is.False); - Assert.That(context.Database.GetDisallowedEmailAddressInfo(email), Is.Null); - context.Database.DisallowEmailAddress(email, disallowReason); - - context.Database.Refresh(); - Assert.That(context.Database.IsEmailAddressDisallowed(email), Is.True); - DisallowedEmailAddress? disallowed = context.Database.GetDisallowedEmailAddressInfo(email); - Assert.That(disallowed, Is.Not.Null); - Assert.That(disallowed!.Address, Is.EqualTo(email)); - Assert.That(disallowed!.Reason, Is.EqualTo(disallowReason)); - - ApiResponse? response = context.Http.PostData("/api/v3/register", new ApiRegisterRequest - { - Username = "a_lil_guy", - EmailAddress = email, - PasswordSha512 = "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff", - }, false, true); - Assert.That(response, Is.Not.Null); - Assert.That(response!.Error, Is.Not.Null); - Assert.That(response.Error!.Name, Is.EqualTo("ApiAuthenticationError")); - - context.Database.Refresh(); - Assert.That(context.Database.GetUserByEmailAddress(email), Is.Null); - - // Undo - context.Database.ReallowEmailAddress(email); - context.Database.Refresh(); - Assert.That(context.Database.IsEmailAddressDisallowed(email), Is.False); - Assert.That(context.Database.GetDisallowedEmailAddressInfo(email), Is.Null); - } - - [Test] - [TestCase("guy@moron.com")] // whole address - [TestCase("moron.com")] // just the domain - public void CannotRegisterAccountsWithDisallowedEmailDomain(string addressToBlockWith) - { - using TestContext context = this.GetServer(); - const string disallowReason = "moron email moment"; - const string domain = "moron.com"; - - // Not somehow already disallowed - Assert.That(context.Database.IsEmailDomainDisallowed(addressToBlockWith), Is.False); - Assert.That(context.Database.IsEmailDomainDisallowed(domain), Is.False); - Assert.That(context.Database.GetDisallowedEmailDomainInfo(addressToBlockWith), Is.Null); - context.Database.DisallowEmailDomain(addressToBlockWith, disallowReason); - - context.Database.Refresh(); - Assert.That(context.Database.IsEmailDomainDisallowed(addressToBlockWith), Is.True); - Assert.That(context.Database.IsEmailDomainDisallowed(domain), Is.True); - DisallowedEmailDomain? disallowed = context.Database.GetDisallowedEmailDomainInfo(addressToBlockWith); - Assert.That(disallowed, Is.Not.Null); - Assert.That(disallowed!.Domain, Is.EqualTo(domain)); - Assert.That(disallowed!.Reason, Is.EqualTo(disallowReason)); - - // Attempt 1 (block) - ApiResponse? response = context.Http.PostData("/api/v3/register", new ApiRegisterRequest - { - Username = "a_lil_guy", - EmailAddress = "pisser@moron.com", - PasswordSha512 = "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff", - }, false, true); - Assert.That(response, Is.Not.Null); - Assert.That(response!.Error, Is.Not.Null); - Assert.That(response.Error!.Name, Is.EqualTo("ApiAuthenticationError")); - context.Database.Refresh(); - Assert.That(context.Database.GetUserByEmailAddress("pisser@moron.com"), Is.Null); - - // Attempt 2 (block) - response = context.Http.PostData("/api/v3/register", new ApiRegisterRequest - { - Username = "a_lil_guy", - EmailAddress = "shitter@moron.com", - PasswordSha512 = "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff", - }, false, true); - Assert.That(response, Is.Not.Null); - Assert.That(response!.Error, Is.Not.Null); - Assert.That(response.Error!.Name, Is.EqualTo("ApiAuthenticationError")); - context.Database.Refresh(); - Assert.That(context.Database.GetUserByEmailAddress("shitter@moron.com"), Is.Null); - - // Attempt 3 (block) - response = context.Http.PostData("/api/v3/register", new ApiRegisterRequest - { - Username = "a_lil_guy", - EmailAddress = ".@moron.com", - PasswordSha512 = "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff", - }, false, true); - Assert.That(response, Is.Not.Null); - Assert.That(response!.Error, Is.Not.Null); - Assert.That(response.Error!.Name, Is.EqualTo("ApiAuthenticationError")); - context.Database.Refresh(); - Assert.That(context.Database.GetUserByEmailAddress(".@moron.com"), Is.Null); - - // Attempt 4 (allow) - response = context.Http.PostData("/api/v3/register", new ApiRegisterRequest - { - Username = "a_lil_guy", - EmailAddress = "quacker@hi.com", - PasswordSha512 = "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff", - }); - Assert.That(response, Is.Not.Null); - Assert.That(response!.Error, Is.Null); - Assert.That(response!.Data, Is.Not.Null); - context.Database.Refresh(); - - GameUser? quacker = context.Database.GetUserByEmailAddress("quacker@hi.com"); - Assert.That(quacker, Is.Not.Null); - Assert.That(quacker!.UserId.ToString(), Is.EqualTo(response.Data!.UserId)); - Assert.That(quacker!.Username, Is.EqualTo("a_lil_guy")); - - // Undo - context.Database.ReallowEmailDomain(addressToBlockWith); - context.Database.Refresh(); - Assert.That(context.Database.IsEmailDomainDisallowed(addressToBlockWith), Is.False); - Assert.That(context.Database.IsEmailDomainDisallowed(domain), Is.False); - Assert.That(context.Database.GetDisallowedEmailDomainInfo(addressToBlockWith), Is.Null); - } - - [Test] - public void CannotRegisterAccountWithDisallowedUsername() - { - using TestContext context = this.GetServer(); - const string username = "a_lil_guy"; - const string disallowReason = "writing these is fun lol"; - - // Not somehow already disallowed - Assert.That(context.Database.IsUserDisallowed(username), Is.False); - Assert.That(context.Database.GetDisallowedUserInfo(username), Is.Null); - - context.Database.DisallowUser(username, disallowReason); - context.Database.Refresh(); - - Assert.That(context.Database.IsUserDisallowed(username), Is.True); - DisallowedUser? disallowed = context.Database.GetDisallowedUserInfo(username); - Assert.That(disallowed, Is.Not.Null); - Assert.That(disallowed!.Username, Is.EqualTo(username)); - Assert.That(disallowed!.Reason, Is.EqualTo(disallowReason)); - - ApiResponse? response = context.Http.PostData("/api/v3/register", new ApiRegisterRequest - { - Username = username, - EmailAddress = "guy@lil.com", - PasswordSha512 = "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff", - }, false, true); - Assert.That(response, Is.Not.Null); - Assert.That(response!.Error, Is.Not.EqualTo(null)); - Assert.That(response.Error!.Name, Is.EqualTo("ApiAuthenticationError")); - - context.Database.Refresh(); - Assert.That(context.Database.GetUserByUsername(username), Is.Null); - - // Undo - context.Database.ReallowUser(username); - context.Database.Refresh(); - Assert.That(context.Database.IsUserDisallowed(username), Is.False); - Assert.That(context.Database.GetDisallowedUserInfo(username), Is.Null); - } - [Test] public void CannotRegisterAccountWithPreviouslyTakenUsername() { diff --git a/RefreshTests.GameServer/Tests/Assets/AssetDisallowanceTests.cs b/RefreshTests.GameServer/Tests/Assets/AssetDisallowanceTests.cs index bbeaba9b..d2597e7a 100644 --- a/RefreshTests.GameServer/Tests/Assets/AssetDisallowanceTests.cs +++ b/RefreshTests.GameServer/Tests/Assets/AssetDisallowanceTests.cs @@ -14,35 +14,42 @@ namespace RefreshTests.GameServer.Tests.Assets; public class AssetDisallowanceTests : GameServerTest { [Test] - public void CanDisallowAndReallowAsset() + [TestCase("trash")] + [TestCase("Trash")] + [TestCase("TRASH")] + public void CanDisallowAndReallowAssetCaseInsensitively(string hash) { using TestContext context = this.GetServer(); - string hash = "trash"; + string hashLower = hash.ToLower(); GameAssetType type = GameAssetType.Mesh; // Ensure that the asset isn't already disallowed + Assert.That(context.Database.IsAssetDisallowed(hash), Is.False); Assert.That(context.Database.GetDisallowedAssetInfo(hash), Is.Null); // Disallow (DisallowedAsset disallowed, bool success) = context.Database.DisallowAsset(hash, type, "too ugly"); Assert.That(success, Is.True); - Assert.That(disallowed.AssetHash, Is.EqualTo(hash)); + Assert.That(disallowed.AssetHash, Is.EqualTo(hashLower)); Assert.That(disallowed.AssetType, Is.EqualTo(type)); Assert.That(disallowed.Reason, Is.EqualTo("too ugly")); + context.Database.Refresh(); - // Ensure that the same entity is gotten again, and the DB method doesn't try to insert a new one + // Try to disallow again, and ensure the DB method doesn't try to insert a new one (disallowed, success) = context.Database.DisallowAsset(hash, type, "too ugly"); Assert.That(success, Is.False); - Assert.That(disallowed.AssetHash, Is.EqualTo(hash)); + Assert.That(disallowed.AssetHash, Is.EqualTo(hashLower)); Assert.That(disallowed.AssetType, Is.EqualTo(type)); Assert.That(disallowed.Reason, Is.EqualTo("too ugly")); + context.Database.Refresh(); // ensure that the separately gotten entity is also the same + Assert.That(context.Database.IsAssetDisallowed(hash), Is.True); DisallowedAsset? gottenAgain = context.Database.GetDisallowedAssetInfo(hash); Assert.That(gottenAgain, Is.Not.Null); Assert.That(success, Is.False); - Assert.That(disallowed.AssetHash, Is.EqualTo(hash)); + Assert.That(disallowed.AssetHash, Is.EqualTo(hashLower)); Assert.That(disallowed.AssetType, Is.EqualTo(type)); Assert.That(disallowed.Reason, Is.EqualTo("too ugly")); @@ -52,11 +59,13 @@ public void CanDisallowAndReallowAsset() // Reallow success = context.Database.ReallowAsset(hash); Assert.That(success, Is.True); + Assert.That(context.Database.IsAssetDisallowed(hash), Is.False); Assert.That(context.Database.GetDisallowedAssetInfo(hash), Is.Null); // Reallow again success = context.Database.ReallowAsset(hash); Assert.That(success, Is.False); + Assert.That(context.Database.IsAssetDisallowed(hash), Is.False); Assert.That(context.Database.GetDisallowedAssetInfo(hash), Is.Null); } diff --git a/RefreshTests.GameServer/Tests/Users/UserDisallowanceTests.cs b/RefreshTests.GameServer/Tests/Users/UserDisallowanceTests.cs new file mode 100644 index 00000000..b6da0bb2 --- /dev/null +++ b/RefreshTests.GameServer/Tests/Users/UserDisallowanceTests.cs @@ -0,0 +1,251 @@ +using Refresh.Database.Models.Users; +using Refresh.Interfaces.APIv3.Endpoints.ApiTypes; +using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Request.Authentication; +using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Users; +using RefreshTests.GameServer.Extensions; + +namespace RefreshTests.GameServer.Tests.Users; + +public class UserDisallowanceTests : GameServerTest +{ + [Test] + [TestCase("guy@lil.com")] + [TestCase("Guy@LiL.coM")] + [TestCase("GUY@LIL.COM")] + public void CannotRegisterAccountWithDisallowedEmailAddressCaseInsensitively(string emailAddress) + { + using TestContext context = this.GetServer(); + + const string emailAddressLower = "guy@lil.com"; + const string disallowReason = "being lil"; + // Not somehow already disallowed + Assert.That(context.Database.IsEmailAddressDisallowed(emailAddress), Is.False); + Assert.That(context.Database.GetDisallowedEmailAddressInfo(emailAddress), Is.Null); + + // Disallow + (DisallowedEmailAddress disallowanceReturn, bool success) = context.Database.DisallowEmailAddress(emailAddress, disallowReason); + Assert.That(disallowanceReturn.AddressLower, Is.EqualTo(emailAddressLower)); + Assert.That(disallowanceReturn.Reason, Is.EqualTo(disallowReason)); + Assert.That(success, Is.True); + context.Database.Refresh(); + + // Try to disallow again + (disallowanceReturn, success) = context.Database.DisallowEmailAddress(emailAddress, disallowReason); + Assert.That(disallowanceReturn.AddressLower, Is.EqualTo(emailAddressLower)); + Assert.That(disallowanceReturn.Reason, Is.EqualTo(disallowReason)); + Assert.That(success, Is.False); + context.Database.Refresh(); + + // Ensure it's actually disallowed + Assert.That(context.Database.IsEmailAddressDisallowed(emailAddress), Is.True); + DisallowedEmailAddress? disallowed = context.Database.GetDisallowedEmailAddressInfo(emailAddress); + Assert.That(disallowed, Is.Not.Null); + Assert.That(disallowed!.AddressLower, Is.EqualTo(emailAddressLower)); + Assert.That(disallowed!.Reason, Is.EqualTo(disallowReason)); + + // Try to register + ApiResponse? response = context.Http.PostData("/api/v3/register", new ApiRegisterRequest + { + Username = "a_lil_guy", + EmailAddress = emailAddress, + PasswordSha512 = "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff", + }, false, true); + Assert.That(response, Is.Not.Null); + Assert.That(response!.Error, Is.Not.Null); + Assert.That(response.Error!.Name, Is.EqualTo("ApiAuthenticationError")); + + context.Database.Refresh(); + Assert.That(context.Database.GetUserByEmailAddress(emailAddress), Is.Null); + + // Undo + success = context.Database.ReallowEmailAddress(emailAddress); + Assert.That(success, Is.True); + context.Database.Refresh(); + Assert.That(context.Database.IsEmailAddressDisallowed(emailAddress), Is.False); + Assert.That(context.Database.GetDisallowedEmailAddressInfo(emailAddress), Is.Null); + + // Try to undo again + success = context.Database.ReallowEmailAddress(emailAddress); + Assert.That(success, Is.False); + context.Database.Refresh(); + Assert.That(context.Database.IsEmailAddressDisallowed(emailAddress), Is.False); + Assert.That(context.Database.GetDisallowedEmailAddressInfo(emailAddress), Is.Null); + } + + [Test] + [TestCase("guy@moron.com")] // whole address + [TestCase("moron.com")] // just the domain + [TestCase("MORON.Com")] + [TestCase("GUY@MORoN.cOm")] + public void CannotRegisterAccountsWithDisallowedEmailDomainCaseInsensitively(string addressToBlockWith) + { + using TestContext context = this.GetServer(); + const string disallowReason = "moron email moment"; + const string domain = "moron.com"; + + // Not somehow already disallowed + Assert.That(context.Database.IsEmailDomainDisallowed(addressToBlockWith), Is.False); + Assert.That(context.Database.IsEmailDomainDisallowed(domain), Is.False); + Assert.That(context.Database.GetDisallowedEmailDomainInfo(addressToBlockWith), Is.Null); + + // Disallow + (DisallowedEmailDomain disallowanceReturn, bool success) = context.Database.DisallowEmailDomain(addressToBlockWith, disallowReason); + Assert.That(disallowanceReturn.DomainLower, Is.EqualTo(domain)); + Assert.That(disallowanceReturn.Reason, Is.EqualTo(disallowReason)); + Assert.That(success, Is.True); + context.Database.Refresh(); + + // Try to disallow again + (disallowanceReturn, success) = context.Database.DisallowEmailDomain(addressToBlockWith, disallowReason); + Assert.That(disallowanceReturn.DomainLower, Is.EqualTo(domain)); + Assert.That(disallowanceReturn.Reason, Is.EqualTo(disallowReason)); + Assert.That(success, Is.False); + context.Database.Refresh(); + + // Ensure it's disallowed + Assert.That(context.Database.IsEmailDomainDisallowed(addressToBlockWith), Is.True); + Assert.That(context.Database.IsEmailDomainDisallowed(domain), Is.True); + DisallowedEmailDomain? disallowed = context.Database.GetDisallowedEmailDomainInfo(addressToBlockWith); + Assert.That(disallowed, Is.Not.Null); + Assert.That(disallowed!.DomainLower, Is.EqualTo(domain)); + Assert.That(disallowed!.Reason, Is.EqualTo(disallowReason)); + + // Attempt 1 (block) + ApiResponse? response = context.Http.PostData("/api/v3/register", new ApiRegisterRequest + { + Username = "a_lil_guy", + EmailAddress = "pisser@moron.com", + PasswordSha512 = "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff", + }, false, true); + Assert.That(response, Is.Not.Null); + Assert.That(response!.Error, Is.Not.Null); + Assert.That(response.Error!.Name, Is.EqualTo("ApiAuthenticationError")); + context.Database.Refresh(); + Assert.That(context.Database.GetUserByEmailAddress("pisser@moron.com"), Is.Null); + + // Attempt 2 (block) + response = context.Http.PostData("/api/v3/register", new ApiRegisterRequest + { + Username = "a_lil_guy", + EmailAddress = "shitter@moron.com", + PasswordSha512 = "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff", + }, false, true); + Assert.That(response, Is.Not.Null); + Assert.That(response!.Error, Is.Not.Null); + Assert.That(response.Error!.Name, Is.EqualTo("ApiAuthenticationError")); + context.Database.Refresh(); + Assert.That(context.Database.GetUserByEmailAddress("shitter@moron.com"), Is.Null); + + // Attempt 3 (block) + response = context.Http.PostData("/api/v3/register", new ApiRegisterRequest + { + Username = "a_lil_guy", + EmailAddress = ".@moron.com", + PasswordSha512 = "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff", + }, false, true); + Assert.That(response, Is.Not.Null); + Assert.That(response!.Error, Is.Not.Null); + Assert.That(response.Error!.Name, Is.EqualTo("ApiAuthenticationError")); + context.Database.Refresh(); + Assert.That(context.Database.GetUserByEmailAddress(".@moron.com"), Is.Null); + + // Attempt 4 (allow) + response = context.Http.PostData("/api/v3/register", new ApiRegisterRequest + { + Username = "a_lil_guy", + EmailAddress = "quacker@hi.com", + PasswordSha512 = "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff", + }); + Assert.That(response, Is.Not.Null); + Assert.That(response!.Error, Is.Null); + Assert.That(response!.Data, Is.Not.Null); + context.Database.Refresh(); + + // Ensure Quacker has successfully registered and hasn't been blocked + GameUser? quacker = context.Database.GetUserByEmailAddress("quacker@hi.com"); + Assert.That(quacker, Is.Not.Null); + Assert.That(quacker!.UserId.ToString(), Is.EqualTo(response.Data!.UserId)); + Assert.That(quacker!.Username, Is.EqualTo("a_lil_guy")); + + // Undo + success = context.Database.ReallowEmailDomain(addressToBlockWith); + Assert.That(success, Is.True); + context.Database.Refresh(); + Assert.That(context.Database.IsEmailDomainDisallowed(addressToBlockWith), Is.False); + Assert.That(context.Database.IsEmailDomainDisallowed(domain), Is.False); + Assert.That(context.Database.GetDisallowedEmailDomainInfo(addressToBlockWith), Is.Null); + + // Try to undo again + success = context.Database.ReallowEmailDomain(addressToBlockWith); + Assert.That(success, Is.False); + context.Database.Refresh(); + Assert.That(context.Database.IsEmailDomainDisallowed(addressToBlockWith), Is.False); + Assert.That(context.Database.IsEmailDomainDisallowed(domain), Is.False); + Assert.That(context.Database.GetDisallowedEmailDomainInfo(addressToBlockWith), Is.Null); + } + + [Test] + [TestCase("a_lil_guy")] + [TestCase("a_LiL_guY")] + [TestCase("A_LIL_GUY")] + public void CannotRegisterAccountWithDisallowedUsernameCaseInsensitively(string username) + { + using TestContext context = this.GetServer(); + const string usernameLower = "a_lil_guy"; + const string disallowReason = "writing these is fun lol"; + + // Not somehow already disallowed + Assert.That(context.Database.IsUserDisallowed(username), Is.False); + Assert.That(context.Database.GetDisallowedUserInfo(username), Is.Null); + + // Disallow + (DisallowedUser disallowanceReturn, bool success) = context.Database.DisallowUser(username, disallowReason); + Assert.That(disallowanceReturn.UsernameLower, Is.EqualTo(usernameLower)); + Assert.That(disallowanceReturn.Reason, Is.EqualTo(disallowReason)); + Assert.That(success, Is.True); + context.Database.Refresh(); + + // Try to disallow again + (disallowanceReturn, success) = context.Database.DisallowUser(username, disallowReason); + Assert.That(disallowanceReturn.UsernameLower, Is.EqualTo(usernameLower)); + Assert.That(disallowanceReturn.Reason, Is.EqualTo(disallowReason)); + Assert.That(success, Is.False); + context.Database.Refresh(); + + // Ensure it's disallowed + Assert.That(context.Database.IsUserDisallowed(username), Is.True); + DisallowedUser? disallowed = context.Database.GetDisallowedUserInfo(username); + Assert.That(disallowed, Is.Not.Null); + Assert.That(disallowed!.UsernameLower, Is.EqualTo(usernameLower)); + Assert.That(disallowed!.Reason, Is.EqualTo(disallowReason)); + + // Try to register + ApiResponse? response = context.Http.PostData("/api/v3/register", new ApiRegisterRequest + { + Username = username, + EmailAddress = "guy@lil.com", + PasswordSha512 = "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff", + }, false, true); + Assert.That(response, Is.Not.Null); + Assert.That(response!.Error, Is.Not.EqualTo(null)); + Assert.That(response.Error!.Name, Is.EqualTo("ApiAuthenticationError")); + + // Ensure registration actually failed + context.Database.Refresh(); + Assert.That(context.Database.GetUserByUsername(username), Is.Null); + + // Undo + success = context.Database.ReallowUser(username); + Assert.That(success, Is.True); + context.Database.Refresh(); + Assert.That(context.Database.IsUserDisallowed(username), Is.False); + Assert.That(context.Database.GetDisallowedUserInfo(username), Is.Null); + + // Try to undo again + success = context.Database.ReallowUser(username); + Assert.That(success, Is.False); + context.Database.Refresh(); + Assert.That(context.Database.IsUserDisallowed(username), Is.False); + Assert.That(context.Database.GetDisallowedUserInfo(username), Is.Null); + } +} \ No newline at end of file