From 0a071408e5a284e8b3859900dbd1ec337cfc8328 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Thu, 4 Jun 2026 17:05:39 +0200 Subject: [PATCH 1/3] API endpoints to disallow various entities --- .../Descriptions/SharedParamDescriptions.cs | 1 + .../Admin/AdminDisallowanceApiEndpoints.cs | 179 ++++++++++++++++++ .../Moderation/ApiDisallowAssetRequest.cs | 7 + .../Moderation/ApiModerationRequest.cs | 7 + .../Disallowed/ApiDisallowedAssetResponse.cs | 29 +++ .../ApiDisallowedEmailAddressResponse.cs | 27 +++ .../ApiDisallowedEmailDomainResponse.cs | 27 +++ .../ApiDisallowedUsernameResponse.cs | 27 +++ 8 files changed, 304 insertions(+) create mode 100644 Refresh.Interfaces.APIv3/Endpoints/Admin/AdminDisallowanceApiEndpoints.cs create mode 100644 Refresh.Interfaces.APIv3/Endpoints/DataTypes/Request/Moderation/ApiDisallowAssetRequest.cs create mode 100644 Refresh.Interfaces.APIv3/Endpoints/DataTypes/Request/Moderation/ApiModerationRequest.cs create mode 100644 Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Disallowed/ApiDisallowedAssetResponse.cs create mode 100644 Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Disallowed/ApiDisallowedEmailAddressResponse.cs create mode 100644 Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Disallowed/ApiDisallowedEmailDomainResponse.cs create mode 100644 Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Disallowed/ApiDisallowedUsernameResponse.cs diff --git a/Refresh.Interfaces.APIv3/Documentation/Descriptions/SharedParamDescriptions.cs b/Refresh.Interfaces.APIv3/Documentation/Descriptions/SharedParamDescriptions.cs index fd74253b..4ced3cb6 100644 --- a/Refresh.Interfaces.APIv3/Documentation/Descriptions/SharedParamDescriptions.cs +++ b/Refresh.Interfaces.APIv3/Documentation/Descriptions/SharedParamDescriptions.cs @@ -7,4 +7,5 @@ public static class SharedParamDescriptions { public const string UserIdParam = "The UUID or username of the user."; public const string UserIdTypeParam = "The type of ID used to specify the user. Can be 'uuid', 'username' or 'name'."; + public const string DomainToDisallowParam = "The email domain to disallow. If this is a whole address, only the part after the last @ will be used as the domain."; } \ No newline at end of file diff --git a/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminDisallowanceApiEndpoints.cs b/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminDisallowanceApiEndpoints.cs new file mode 100644 index 00000000..6a4abcac --- /dev/null +++ b/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminDisallowanceApiEndpoints.cs @@ -0,0 +1,179 @@ +using AttribDoc.Attributes; +using Bunkum.Core; +using Bunkum.Core.Endpoints; +using Bunkum.Protocols.Http; +using Refresh.Core.Authentication.Permission; +using Refresh.Core.Types.Data; +using Refresh.Database; +using Refresh.Database.Models.Assets; +using Refresh.Database.Models.Users; +using Refresh.Interfaces.APIv3.Documentation.Descriptions; +using Refresh.Interfaces.APIv3.Endpoints.ApiTypes; +using Refresh.Interfaces.APIv3.Endpoints.ApiTypes.Errors; +using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Request.Moderation; +using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Disallowed; +using Refresh.Interfaces.APIv3.Extensions; + +namespace Refresh.Interfaces.APIv3.Endpoints.Admin; + +public class AdminDisallowanceApiEndpoints : EndpointGroup +{ + // TODO: use mod log + // TODO: update disallowance rows if attempted to insert again + + #region Asset hashes + + [ApiV3Endpoint("admin/disallowed/assetHashes"), MinimumRole(GameUserRole.Moderator)] + [DocSummary("Gets a paginated list of disallowed asset hashes, optionally filtered by asset type.")] + [DocError(typeof(ApiValidationError), "The asset type couldn't be parsed")] + public ApiListResponse GetDisallowedAssetHashes(RequestContext context, DataContext dataContext, GameUser user) + { + (int skip, int count) = context.GetPageData(); + + GameAssetType? type = null; + string? typeParam = context.QueryString.Get("type"); + if (typeParam != null) + { + bool parsed = Enum.TryParse(typeParam, true, out GameAssetType typeParsed); + if (!parsed) + { + return new ApiValidationError($"The asset type '{typeParam}' couldn't be parsed. Possible values: " + + string.Join(", ", Enum.GetNames(typeof(GameAssetType)))); + } + + type = typeParsed; + } + + DatabaseList Assets = dataContext.Database.GetDisallowedAssets(type, skip, count); + return DatabaseListExtensions.FromOldList(Assets, dataContext); + } + + [ApiV3Endpoint("admin/disallowed/assetHashes/hash/{hash}", HttpMethods.Post), MinimumRole(GameUserRole.Moderator)] + [DocSummary("Adds an asset hash (with optional asset type) to the list of disallowed hashes.")] + [DocError(typeof(ApiValidationError), "The asset type couldn't be parsed")] + public ApiResponse DisallowAssetHash(RequestContext context, DataContext dataContext, GameUser user, + string hash, ApiDisallowAssetRequest body) + { + GameAssetType type = GameAssetType.Unknown; + if (body.Type != null) + { + bool parsed = Enum.TryParse(body.Type, true, out GameAssetType typeParsed); + if (!parsed) + { + return new ApiValidationError($"The asset type '{body.Type}' couldn't be parsed. Possible values: " + + string.Join(", ", Enum.GetNames(typeof(GameAssetType)))); + } + + type = typeParsed; + } + + (DisallowedAsset disallowed, bool success) = dataContext.Database.DisallowAsset(hash, type, body.Reason ?? ""); + return new(ApiDisallowedAssetResponse.FromOld(disallowed, dataContext)!, success ? Created : OK); + } + + [ApiV3Endpoint("admin/disallowed/assetHashes/hash/{hash}", HttpMethods.Delete), MinimumRole(GameUserRole.Moderator)] + [DocSummary("Removes an asset hash from the list of disallowed hashes.")] + public ApiOkResponse ReallowAssetHash(RequestContext context, DataContext dataContext, GameUser user, + string hash) + { + dataContext.Database.ReallowAsset(hash); + return new ApiOkResponse(); + } + + #endregion + #region Usernames + + [ApiV3Endpoint("admin/disallowed/usernames"), MinimumRole(GameUserRole.Moderator)] + [DocSummary("Gets a paginated list of disallowed usernames.")] + public ApiListResponse GetDisallowedUsernames(RequestContext context, DataContext dataContext, GameUser user) + { + (int skip, int count) = context.GetPageData(); + + DatabaseList Usernames = dataContext.Database.GetDisallowedUsers(skip, count); + return DatabaseListExtensions.FromOldList(Usernames, dataContext); + } + + [ApiV3Endpoint("admin/disallowed/usernames/name/{username}", HttpMethods.Post), MinimumRole(GameUserRole.Moderator)] + [DocSummary("Adds a username to the list of disallowed names.")] + public ApiResponse DisallowUsername(RequestContext context, DataContext dataContext, GameUser user, + string username, ApiModerationRequest body) + { + (DisallowedUser disallowed, bool success) = dataContext.Database.DisallowUser(username, body.Reason ?? ""); + return new(ApiDisallowedUsernameResponse.FromOld(disallowed, dataContext)!, success ? Created : OK); + } + + [ApiV3Endpoint("admin/disallowed/usernames/name/{username}", HttpMethods.Delete), MinimumRole(GameUserRole.Moderator)] + [DocSummary("Removes a username from the list of disallowed names.")] + public ApiOkResponse ReallowUsername(RequestContext context, DataContext dataContext, GameUser user, + string username) + { + dataContext.Database.ReallowUser(username); + return new ApiOkResponse(); + } + + #endregion + #region Email addresses + + [ApiV3Endpoint("admin/disallowed/emailAddresses"), MinimumRole(GameUserRole.Moderator)] + [DocSummary("Gets a paginated list of disallowed email addresses.")] + public ApiListResponse GetDisallowedEmailAddresses(RequestContext context, DataContext dataContext, GameUser user) + { + (int skip, int count) = context.GetPageData(); + + DatabaseList Addresses = dataContext.Database.GetDisallowedEmailAddresses(skip, count); + return DatabaseListExtensions.FromOldList(Addresses, dataContext); + } + + [ApiV3Endpoint("admin/disallowed/emailAddresses/address/{address}", HttpMethods.Post), MinimumRole(GameUserRole.Moderator)] + [DocSummary("Adds an email address to the list of disallowed addresses.")] + public ApiResponse DisallowEmailAddress(RequestContext context, DataContext dataContext, GameUser user, + string address, ApiModerationRequest body) + { + (DisallowedEmailAddress disallowed, bool success) = dataContext.Database.DisallowEmailAddress(address, body.Reason ?? ""); + return new(ApiDisallowedEmailAddressResponse.FromOld(disallowed, dataContext)!, success ? Created : OK); + } + + [ApiV3Endpoint("admin/disallowed/emailAddresses/address/{address}", HttpMethods.Delete), MinimumRole(GameUserRole.Moderator)] + [DocSummary("Removes an email address from the list of disallowed addresses.")] + [DocError(typeof(ApiNotFoundError), ApiNotFoundError.UserMissingErrorWhen)] + [DocError(typeof(ApiValidationError), ApiValidationError.MayNotModifyUserDueToLowRoleErrorWhen)] + public ApiOkResponse ReallowEmailAddress(RequestContext context, DataContext dataContext, GameUser user, + string address) + { + dataContext.Database.ReallowEmailAddress(address); + return new ApiOkResponse(); + } + + #endregion + #region Email domains + + [ApiV3Endpoint("admin/disallowed/emailDomains"), MinimumRole(GameUserRole.Moderator)] + [DocSummary("Gets a paginated list of disallowed email domains.")] + public ApiListResponse GetDisallowedEmailDomains(RequestContext context, DataContext dataContext, GameUser user) + { + (int skip, int count) = context.GetPageData(); + + DatabaseList Domains = dataContext.Database.GetDisallowedEmailDomains(skip, count); + return DatabaseListExtensions.FromOldList(Domains, dataContext); + } + + [ApiV3Endpoint("admin/disallowed/emailDomains/domain/{domain}", HttpMethods.Post), MinimumRole(GameUserRole.Moderator)] + [DocSummary("Adds an email domain to the list of domains.")] + public ApiResponse DisallowEmailDomain(RequestContext context, DataContext dataContext, GameUser user, + [DocSummary(SharedParamDescriptions.DomainToDisallowParam)] string domain, ApiModerationRequest body) + { + (DisallowedEmailDomain disallowed, bool success) = dataContext.Database.DisallowEmailDomain(domain, body.Reason ?? ""); + return new(ApiDisallowedEmailDomainResponse.FromOld(disallowed, dataContext)!, success ? Created : OK); + } + + [ApiV3Endpoint("admin/disallowed/emailDomains/domain/{domain}", HttpMethods.Delete), MinimumRole(GameUserRole.Moderator)] + [DocSummary("Removes an email domain from the list of domains.")] + public ApiOkResponse ReallowEmailDomain(RequestContext context, DataContext dataContext, GameUser user, + [DocSummary(SharedParamDescriptions.DomainToDisallowParam)] string domain) + { + dataContext.Database.ReallowEmailDomain(domain); + return new ApiOkResponse(); + } + + #endregion +} \ No newline at end of file diff --git a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Request/Moderation/ApiDisallowAssetRequest.cs b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Request/Moderation/ApiDisallowAssetRequest.cs new file mode 100644 index 00000000..04e7446d --- /dev/null +++ b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Request/Moderation/ApiDisallowAssetRequest.cs @@ -0,0 +1,7 @@ +namespace Refresh.Interfaces.APIv3.Endpoints.DataTypes.Request.Moderation; + +[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] +public class ApiDisallowAssetRequest : ApiModerationRequest +{ + public string? Type { get; set; } +} \ No newline at end of file diff --git a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Request/Moderation/ApiModerationRequest.cs b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Request/Moderation/ApiModerationRequest.cs new file mode 100644 index 00000000..45682295 --- /dev/null +++ b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Request/Moderation/ApiModerationRequest.cs @@ -0,0 +1,7 @@ +namespace Refresh.Interfaces.APIv3.Endpoints.DataTypes.Request.Moderation; + +[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] +public class ApiModerationRequest +{ + public string? Reason { get; set; } +} \ No newline at end of file diff --git a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Disallowed/ApiDisallowedAssetResponse.cs b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Disallowed/ApiDisallowedAssetResponse.cs new file mode 100644 index 00000000..e2fedf0e --- /dev/null +++ b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Disallowed/ApiDisallowedAssetResponse.cs @@ -0,0 +1,29 @@ +using Refresh.Core.Types.Data; +using Refresh.Database.Models.Assets; + +namespace Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Disallowed; + +[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] +public class ApiDisallowedAssetResponse : IApiResponse, IDataConvertableFrom +{ + public required string AssetHash { get; set; } + public required string AssetType { get; set; } + public required string Reason { get; set; } + public required DateTimeOffset DisallowedAt { get; set; } + + public static ApiDisallowedAssetResponse? FromOld(DisallowedAsset? old, DataContext dataContext) + { + if (old == null) return null; + + return new ApiDisallowedAssetResponse + { + AssetHash = old.AssetHash, + AssetType = old.AssetType.ToString(), + Reason = old.Reason, + DisallowedAt = old.DisallowedAt, + }; + } + + public static IEnumerable FromOldList(IEnumerable oldList, DataContext dataContext) + => oldList.Select(old => FromOld(old, dataContext)).ToList()!; +} \ No newline at end of file diff --git a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Disallowed/ApiDisallowedEmailAddressResponse.cs b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Disallowed/ApiDisallowedEmailAddressResponse.cs new file mode 100644 index 00000000..60dc7d34 --- /dev/null +++ b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Disallowed/ApiDisallowedEmailAddressResponse.cs @@ -0,0 +1,27 @@ +using Refresh.Core.Types.Data; +using Refresh.Database.Models.Users; + +namespace Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Disallowed; + +[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] +public class ApiDisallowedEmailAddressResponse : IApiResponse, IDataConvertableFrom +{ + public required string Address { get; set; } + public required string Reason { get; set; } + public required DateTimeOffset DisallowedAt { get; set; } + + public static ApiDisallowedEmailAddressResponse? FromOld(DisallowedEmailAddress? old, DataContext dataContext) + { + if (old == null) return null; + + return new ApiDisallowedEmailAddressResponse + { + Address = old.Address, + Reason = old.Reason, + DisallowedAt = old.DisallowedAt, + }; + } + + public static IEnumerable FromOldList(IEnumerable oldList, DataContext dataContext) + => oldList.Select(old => FromOld(old, dataContext)).ToList()!; +} \ No newline at end of file diff --git a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Disallowed/ApiDisallowedEmailDomainResponse.cs b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Disallowed/ApiDisallowedEmailDomainResponse.cs new file mode 100644 index 00000000..46c83366 --- /dev/null +++ b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Disallowed/ApiDisallowedEmailDomainResponse.cs @@ -0,0 +1,27 @@ +using Refresh.Core.Types.Data; +using Refresh.Database.Models.Users; + +namespace Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Disallowed; + +[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] +public class ApiDisallowedEmailDomainResponse : IApiResponse, IDataConvertableFrom +{ + public required string Domain { get; set; } + public required string Reason { get; set; } + public required DateTimeOffset DisallowedAt { get; set; } + + public static ApiDisallowedEmailDomainResponse? FromOld(DisallowedEmailDomain? old, DataContext dataContext) + { + if (old == null) return null; + + return new ApiDisallowedEmailDomainResponse + { + Domain = old.Domain, + Reason = old.Reason, + DisallowedAt = old.DisallowedAt, + }; + } + + public static IEnumerable FromOldList(IEnumerable oldList, DataContext dataContext) + => oldList.Select(old => FromOld(old, dataContext)).ToList()!; +} \ No newline at end of file diff --git a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Disallowed/ApiDisallowedUsernameResponse.cs b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Disallowed/ApiDisallowedUsernameResponse.cs new file mode 100644 index 00000000..b7339511 --- /dev/null +++ b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Disallowed/ApiDisallowedUsernameResponse.cs @@ -0,0 +1,27 @@ +using Refresh.Core.Types.Data; +using Refresh.Database.Models.Users; + +namespace Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Disallowed; + +[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] +public class ApiDisallowedUsernameResponse : IApiResponse, IDataConvertableFrom +{ + public required string Username { get; set; } + public required string Reason { get; set; } + public required DateTimeOffset DisallowedAt { get; set; } + + public static ApiDisallowedUsernameResponse? FromOld(DisallowedUser? old, DataContext dataContext) + { + if (old == null) return null; + + return new ApiDisallowedUsernameResponse + { + Username = old.Username, + Reason = old.Reason, + DisallowedAt = old.DisallowedAt, + }; + } + + public static IEnumerable FromOldList(IEnumerable oldList, DataContext dataContext) + => oldList.Select(old => FromOld(old, dataContext)).ToList()!; +} \ No newline at end of file From c86d0573d4058c1824171130293c09d493c2759a Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Thu, 4 Jun 2026 17:06:12 +0200 Subject: [PATCH 2/3] Test entity disallowance endpoints --- .../Tests/ApiV3/DisallowanceApiTests.cs | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 RefreshTests.GameServer/Tests/ApiV3/DisallowanceApiTests.cs diff --git a/RefreshTests.GameServer/Tests/ApiV3/DisallowanceApiTests.cs b/RefreshTests.GameServer/Tests/ApiV3/DisallowanceApiTests.cs new file mode 100644 index 00000000..ebe7ef34 --- /dev/null +++ b/RefreshTests.GameServer/Tests/ApiV3/DisallowanceApiTests.cs @@ -0,0 +1,236 @@ +using Refresh.Database.Models.Users; +using Refresh.Interfaces.APIv3.Endpoints.ApiTypes; +using RefreshTests.GameServer.Extensions; +using Refresh.Database.Models.Authentication; +using Refresh.Database.Models.Assets; +using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Request.Moderation; +using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Disallowed; + +namespace RefreshTests.GameServer.Tests.ApiV3; + +public class DisallowanceApiTests : GameServerTest +{ + [Test] + [TestCase(GameAssetType.Plan)] + [TestCase(null)] + public void DisallowGetAndReallowAssetHash(GameAssetType? type) + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(role: GameUserRole.Moderator); + using HttpClient client = context.GetAuthenticatedClient(TokenType.Api, user); + string hash = "lel"; + string typeStr = (type ?? GameAssetType.Unknown).ToString(); + + // Ensure it's not already there + Assert.That(context.Database.GetDisallowedAssetInfo(hash), Is.Null); + ApiListResponse? disallowedList = client.GetList("/api/v3/admin/disallowed/assetHashes"); + Assert.That(disallowedList?.Data, Is.Not.Null); + Assert.That(disallowedList!.Data, Is.Empty); + + // Create + ApiDisallowAssetRequest request = new() + { + Type = typeStr, + Reason = "making these up surely never gets boring", + }; + + ApiResponse? response = client.PostData($"/api/v3/admin/disallowed/assetHashes/hash/{hash}", request, false); + Assert.That(response?.Data, Is.Not.Null); + Assert.That(response!.Data!.AssetHash, Is.EqualTo(hash)); + Assert.That(response!.Data!.AssetType, Is.EqualTo(request.Type)); + Assert.That(response!.Data!.Reason, Is.EqualTo(request.Reason)); + + context.Database.Refresh(); + + // Try to create again + response = client.PostData($"/api/v3/admin/disallowed/assetHashes/hash/{hash}", request, false); + Assert.That(response?.Data, Is.Not.Null); + Assert.That(response!.Data!.AssetHash, Is.EqualTo(hash)); + Assert.That(response!.Data!.AssetType, Is.EqualTo(request.Type)); + Assert.That(response!.Data!.Reason, Is.EqualTo(request.Reason)); + + context.Database.Refresh(); + + // Ensure it now appears in listings (both unfiltered and filtered) + Assert.That(context.Database.GetDisallowedAssetInfo(hash), Is.Not.Null); + disallowedList = client.GetList($"/api/v3/admin/disallowed/assetHashes"); + Assert.That(disallowedList?.Data, Is.Not.Null); + Assert.That(disallowedList!.Data!.Count(), Is.EqualTo(1)); + Assert.That(disallowedList!.Data![0].AssetHash, Is.EqualTo(hash)); + + disallowedList = client.GetList($"/api/v3/admin/disallowed/assetHashes?type={typeStr}"); + Assert.That(disallowedList?.Data, Is.Not.Null); + Assert.That(disallowedList!.Data!.Count(), Is.EqualTo(1)); + Assert.That(disallowedList!.Data![0].AssetHash, Is.EqualTo(hash)); + + // Remove + client.DeleteData($"/api/v3/admin/disallowed/assetHashes/hash/{hash}", request); + context.Database.Refresh(); + + // Ensure it's no longer there + Assert.That(context.Database.GetDisallowedAssetInfo(hash), Is.Null); + disallowedList = client.GetList($"/api/v3/admin/disallowed/assetHashes"); + Assert.That(disallowedList?.Data, Is.Not.Null); + Assert.That(disallowedList!.Data, Is.Empty); + } + + [Test] + public void DisallowGetAndReallowUsername() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(role: GameUserRole.Moderator); + using HttpClient client = context.GetAuthenticatedClient(TokenType.Api, user); + string name = "your"; + + // Ensure it's not already there + Assert.That(context.Database.IsUserDisallowed(name), Is.False); + ApiListResponse? disallowedList = client.GetList("/api/v3/admin/disallowed/usernames"); + Assert.That(disallowedList?.Data, Is.Not.Null); + Assert.That(disallowedList!.Data, Is.Empty); + + // Create + ApiModerationRequest request = new() + { + Reason = "long", + }; + + ApiResponse? response = client.PostData($"/api/v3/admin/disallowed/usernames/name/{name}", request, false); + Assert.That(response?.Data, Is.Not.Null); + Assert.That(response!.Data!.Username, Is.EqualTo(name)); + Assert.That(response!.Data!.Reason, Is.EqualTo(request.Reason)); + + context.Database.Refresh(); + + // Try to create again + response = client.PostData($"/api/v3/admin/disallowed/usernames/name/{name}", request, false); + Assert.That(response?.Data, Is.Not.Null); + Assert.That(response!.Data!.Username, Is.EqualTo(name)); + Assert.That(response!.Data!.Reason, Is.EqualTo(request.Reason)); + + context.Database.Refresh(); + + // Ensure it now appears in listings + Assert.That(context.Database.IsUserDisallowed(name), Is.True); + disallowedList = client.GetList($"/api/v3/admin/disallowed/usernames"); + Assert.That(disallowedList?.Data, Is.Not.Null); + Assert.That(disallowedList!.Data!.Count(), Is.EqualTo(1)); + Assert.That(disallowedList!.Data![0].Username, Is.EqualTo(name)); + + // Remove + client.DeleteData($"/api/v3/admin/disallowed/usernames/name/{name}", request); + context.Database.Refresh(); + + // Ensure it's no longer there + Assert.That(context.Database.IsUserDisallowed(name), Is.False); + disallowedList = client.GetList($"/api/v3/admin/disallowed/usernames"); + Assert.That(disallowedList?.Data, Is.Not.Null); + Assert.That(disallowedList!.Data, Is.Empty); + } + + [Test] + public void DisallowGetAndReallowEmailAddress() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(role: GameUserRole.Moderator); + using HttpClient client = context.GetAuthenticatedClient(TokenType.Api, user); + string address = "thefunny@brickshitter.real"; + + // Ensure it's not already there + Assert.That(context.Database.IsUserDisallowed(address), Is.False); + ApiListResponse? disallowedList = client.GetList("/api/v3/admin/disallowed/emailAddresses"); + Assert.That(disallowedList?.Data, Is.Not.Null); + Assert.That(disallowedList!.Data, Is.Empty); + + // Create + ApiModerationRequest request = new() + { + Reason = "inapprop", + }; + + ApiResponse? response = client.PostData($"/api/v3/admin/disallowed/emailAddresses/address/{address}", request, false); + Assert.That(response?.Data, Is.Not.Null); + Assert.That(response!.Data!.Address, Is.EqualTo(address)); + Assert.That(response!.Data!.Reason, Is.EqualTo(request.Reason)); + + context.Database.Refresh(); + + // Try to create again + response = client.PostData($"/api/v3/admin/disallowed/emailAddresses/address/{address}", request, false); + Assert.That(response?.Data, Is.Not.Null); + Assert.That(response!.Data!.Address, Is.EqualTo(address)); + Assert.That(response!.Data!.Reason, Is.EqualTo(request.Reason)); + + context.Database.Refresh(); + + // Ensure it now appears in listings + Assert.That(context.Database.IsUserDisallowed(address), Is.True); + disallowedList = client.GetList($"/api/v3/admin/disallowed/emailAddresses"); + Assert.That(disallowedList?.Data, Is.Not.Null); + Assert.That(disallowedList!.Data!.Count(), Is.EqualTo(1)); + Assert.That(disallowedList!.Data![0].Address, Is.EqualTo(address)); + + // Remove + client.DeleteData($"/api/v3/admin/disallowed/emailAddresses/address/{address}", request); + context.Database.Refresh(); + + // Ensure it's no longer there + Assert.That(context.Database.IsUserDisallowed(address), Is.False); + disallowedList = client.GetList($"/api/v3/admin/disallowed/emailAddresses"); + Assert.That(disallowedList?.Data, Is.Not.Null); + Assert.That(disallowedList!.Data, Is.Empty); + } + + [Test] + public void DisallowGetAndReallowEmailDomain() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(role: GameUserRole.Moderator); + using HttpClient client = context.GetAuthenticatedClient(TokenType.Api, user); + string address = "hi@brickshitter.real"; + string domain = "brickshitter.real"; + + // Ensure it's not already there + Assert.That(context.Database.IsEmailDomainDisallowed(address), Is.False); + ApiListResponse? disallowedList = client.GetList("/api/v3/admin/disallowed/emailDomains"); + Assert.That(disallowedList?.Data, Is.Not.Null); + Assert.That(disallowedList!.Data, Is.Empty); + + // Create + ApiModerationRequest request = new() + { + Reason = "inapprop", + }; + + ApiResponse? response = client.PostData($"/api/v3/admin/disallowed/emailDomains/domain/{address}", request, false); + Assert.That(response?.Data, Is.Not.Null); + Assert.That(response!.Data!.Domain, Is.EqualTo(domain)); + Assert.That(response!.Data!.Reason, Is.EqualTo(request.Reason)); + + context.Database.Refresh(); + + // Try to create again + response = client.PostData($"/api/v3/admin/disallowed/emailDomains/domain/{address}", request, false); + Assert.That(response?.Data, Is.Not.Null); + Assert.That(response!.Data!.Domain, Is.EqualTo(domain)); + Assert.That(response!.Data!.Reason, Is.EqualTo(request.Reason)); + + context.Database.Refresh(); + + // Ensure it now appears in listings + Assert.That(context.Database.IsEmailDomainDisallowed(address), Is.True); + disallowedList = client.GetList($"/api/v3/admin/disallowed/emailDomains"); + Assert.That(disallowedList?.Data, Is.Not.Null); + Assert.That(disallowedList!.Data!.Count(), Is.EqualTo(1)); + Assert.That(disallowedList!.Data![0].Domain, Is.EqualTo(domain)); + + // Remove + client.DeleteData($"/api/v3/admin/disallowed/emailDomains/domain/{address}", request); + context.Database.Refresh(); + + // Ensure it's no longer there + Assert.That(context.Database.IsEmailDomainDisallowed(address), Is.False); + disallowedList = client.GetList($"/api/v3/admin/disallowed/emailDomains"); + Assert.That(disallowedList?.Data, Is.Not.Null); + Assert.That(disallowedList!.Data, Is.Empty); + } +} \ No newline at end of file From c2a0f23e92849a4c586a5bbf9b55dbe6432631db Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Thu, 4 Jun 2026 17:24:19 +0200 Subject: [PATCH 3/3] Epic typo --- RefreshTests.GameServer/Tests/ApiV3/DisallowanceApiTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/RefreshTests.GameServer/Tests/ApiV3/DisallowanceApiTests.cs b/RefreshTests.GameServer/Tests/ApiV3/DisallowanceApiTests.cs index ebe7ef34..077dcbb8 100644 --- a/RefreshTests.GameServer/Tests/ApiV3/DisallowanceApiTests.cs +++ b/RefreshTests.GameServer/Tests/ApiV3/DisallowanceApiTests.cs @@ -136,7 +136,7 @@ public void DisallowGetAndReallowEmailAddress() string address = "thefunny@brickshitter.real"; // Ensure it's not already there - Assert.That(context.Database.IsUserDisallowed(address), Is.False); + Assert.That(context.Database.IsEmailAddressDisallowed(address), Is.False); ApiListResponse? disallowedList = client.GetList("/api/v3/admin/disallowed/emailAddresses"); Assert.That(disallowedList?.Data, Is.Not.Null); Assert.That(disallowedList!.Data, Is.Empty); @@ -163,7 +163,7 @@ public void DisallowGetAndReallowEmailAddress() context.Database.Refresh(); // Ensure it now appears in listings - Assert.That(context.Database.IsUserDisallowed(address), Is.True); + Assert.That(context.Database.IsEmailAddressDisallowed(address), Is.True); disallowedList = client.GetList($"/api/v3/admin/disallowed/emailAddresses"); Assert.That(disallowedList?.Data, Is.Not.Null); Assert.That(disallowedList!.Data!.Count(), Is.EqualTo(1)); @@ -174,7 +174,7 @@ public void DisallowGetAndReallowEmailAddress() context.Database.Refresh(); // Ensure it's no longer there - Assert.That(context.Database.IsUserDisallowed(address), Is.False); + Assert.That(context.Database.IsEmailAddressDisallowed(address), Is.False); disallowedList = client.GetList($"/api/v3/admin/disallowed/emailAddresses"); Assert.That(disallowedList?.Data, Is.Not.Null); Assert.That(disallowedList!.Data, Is.Empty);