diff --git a/Refresh.Database/GameDatabaseContext.Users.cs b/Refresh.Database/GameDatabaseContext.Users.cs index 7c607964..e483c1d9 100644 --- a/Refresh.Database/GameDatabaseContext.Users.cs +++ b/Refresh.Database/GameDatabaseContext.Users.cs @@ -85,6 +85,20 @@ public partial class GameDatabaseContext // Users return this.GameUsersIncluded.FirstOrDefault(u => u.UserId == objectId); } + public GameUser? GetUserByIdAndType(string idType, string id) + { + switch (idType.ToLower()) + { + case "username": + case "name": + return this.GetUserByUsername(id, false); + case "uuid": + return this.GetUserByUuid(id); + default: + return null; + } + } + public DatabaseList GetUsers(int count, int skip) => new(this.GameUsersIncluded.OrderByDescending(u => u.JoinDate), skip, count); @@ -232,6 +246,27 @@ public void UpdateUserData(GameUser user, IApiEditUserRequest data) }); } + public void UpdateUserData(GameUser user, IApiAdminEditUserRequest data) + { + if (data.IconHash != null) + user.IconHash = data.IconHash; + + if (data.VitaIconHash != null) + user.VitaIconHash = data.VitaIconHash; + + if (data.BetaIconHash != null) + user.BetaIconHash = data.BetaIconHash; + + if (data.Description != null) + user.Description = data.Description; + + if (data.Role != null) + user.Role = data.Role.Value; + + this.GameUsers.Update(user); + this.SaveChanges(); + } + public void UpdatePlanetModdedStatus(GameUser user) { user.AreLbp2PlanetsModded = this.GetPlanetModdedStatus(user.Lbp2PlanetsHash); diff --git a/Refresh.Database/Query/IApiAdminEditUserRequest.cs b/Refresh.Database/Query/IApiAdminEditUserRequest.cs new file mode 100644 index 00000000..fa952a5b --- /dev/null +++ b/Refresh.Database/Query/IApiAdminEditUserRequest.cs @@ -0,0 +1,12 @@ +using Refresh.Database.Models.Users; + +namespace Refresh.Database.Query; + +public interface IApiAdminEditUserRequest +{ + string? IconHash { get; set; } + string? VitaIconHash { get; set; } + string? BetaIconHash { get; set; } + string? Description { get; set; } + GameUserRole? Role { get; set; } +} \ No newline at end of file diff --git a/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminUserApiEndpoints.cs b/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminUserApiEndpoints.cs index 0bfb0b2a..7a7db93a 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminUserApiEndpoints.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminUserApiEndpoints.cs @@ -3,10 +3,12 @@ using Bunkum.Core.Endpoints; using Bunkum.Core.Storage; using Bunkum.Protocols.Http; +using Refresh.Common.Constants; using Refresh.Common.Verification; using Refresh.Core.Authentication.Permission; using Refresh.Core.Types.Data; using Refresh.Database; +using Refresh.Database.Models.Moderation; using Refresh.Database.Models.Users; using Refresh.Interfaces.APIv3.Documentation.Attributes; using Refresh.Interfaces.APIv3.Endpoints.ApiTypes; @@ -159,6 +161,85 @@ public ApiOkResponse ResetUserPlanetsByUsername(RequestContext context, GameData database.ResetUserPlanets(user); return new ApiOkResponse(); } + + [ApiV3Endpoint("admin/users/{idType}/{id}", HttpMethods.Patch), MinimumRole(GameUserRole.Moderator)] + [DocSummary("Updates the specified user's profile with the given data")] + [DocError(typeof(ApiNotFoundError), ApiNotFoundError.UserMissingErrorWhen)] + [DocError(typeof(ApiValidationError), ApiValidationError.MayNotOverwriteRoleErrorWhen)] + [DocError(typeof(ApiValidationError), ApiValidationError.RoleMissingErrorWhen)] + [DocError(typeof(ApiValidationError), ApiValidationError.WrongRoleUpdateMethodErrorWhen)] + [DocError(typeof(ApiNotFoundError), ApiNotFoundError.IconMissingErrorWhen)] + [DocError(typeof(ApiValidationError), ApiValidationError.InvalidUsernameErrorWhen)] + public ApiResponse UpdateUser(RequestContext context, GameDatabaseContext database, + GameUser user, ApiAdminUpdateUserRequest body, DataContext dataContext, + [DocSummary("The type of identifier used to look up the user. Can be either 'uuid' or 'username'.")] string idType, + [DocSummary("The UUID or username of the user, depending on the specified ID type.")] string id) + { + GameUser? targetUser = database.GetUserByIdAndType(idType, id); + + if (targetUser == null) + return ApiNotFoundError.UserMissingError; + + // Only admins may edit anyone's role. + // TODO: Maybe moderators should also be able to set roles, but only for users below them, and to roles below them? + if (body.Role != null) + { + if (user.Role < GameUserRole.Admin) + return ApiValidationError.MayNotOverwriteRoleError; + + if (!Enum.IsDefined(typeof(GameUserRole), body.Role)) + return ApiValidationError.RoleMissingError; + + // All roles below regular user are special and must be given using different endpoints because they require extra information. + // Incase the implementation of #286 requires a guest role, that one will very likely be below User aswell, and it should also not + // be assignable with this endpoint (when should a user ever be demoted to a temporary guest?) + if (body.Role < GameUserRole.User) + return ApiValidationError.WrongRoleUpdateMethodError; + } + + if (body.IconHash != null) + { + if (body.IconHash.IsBlankHash()) + body.IconHash = "0"; + else if (database.GetAssetFromHash(body.IconHash) == null) + return ApiNotFoundError.IconMissingError; + } + + if (body.VitaIconHash != null) + { + if (body.VitaIconHash.IsBlankHash()) + body.VitaIconHash = "0"; + else if (database.GetAssetFromHash(body.VitaIconHash) == null) + return ApiNotFoundError.IconMissingError; + } + + if (body.BetaIconHash != null) + { + if (body.BetaIconHash.IsBlankHash()) + body.BetaIconHash = "0"; + else if (database.GetAssetFromHash(body.BetaIconHash) == null) + return ApiNotFoundError.IconMissingError; + } + + if (body.Username != null) + { + if (!database.IsUsernameValid(body.Username)) + return new ApiValidationError(ApiValidationError.InvalidUsernameErrorWhen + + " Are you sure you used a PSN/RPCN username?"); + + database.RenameUser(targetUser, body.Username); + } + + // Trim description + if (body.Description != null && body.Description.Length > UgcLimits.DescriptionLimit) + body.Description = body.Description[..UgcLimits.DescriptionLimit]; + + database.UpdateUserData(targetUser, body); + // TODO: In ApiV4, moderation actions should also provide reasons + database.CreateModerationAction(targetUser, ModerationActionType.UserModification, user, ""); + + return ApiExtendedGameUserResponse.FromOld(targetUser, dataContext); + } [ApiV3Endpoint("admin/users/uuid/{uuid}", HttpMethods.Delete), MinimumRole(GameUserRole.Moderator)] [DocSummary("Deletes a user user by their UUID.")] diff --git a/Refresh.Interfaces.APIv3/Endpoints/ApiTypes/Errors/ApiNotFoundError.cs b/Refresh.Interfaces.APIv3/Endpoints/ApiTypes/Errors/ApiNotFoundError.cs index b8c6b437..ba3deb97 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/ApiTypes/Errors/ApiNotFoundError.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/ApiTypes/Errors/ApiNotFoundError.cs @@ -24,6 +24,9 @@ public class ApiNotFoundError : ApiError public const string PhotoMissingErrorWhen = "The photo could not be found"; public static readonly ApiNotFoundError PhotoMissingError = new(PhotoMissingErrorWhen); + + public const string IconMissingErrorWhen = "The icon could not be found"; + public static readonly ApiNotFoundError IconMissingError = new(IconMissingErrorWhen); public const string ContestMissingErrorWhen = "The contest could not be found"; public static readonly ApiNotFoundError ContestMissingError = new(ContestMissingErrorWhen); diff --git a/Refresh.Interfaces.APIv3/Endpoints/ApiTypes/Errors/ApiValidationError.cs b/Refresh.Interfaces.APIv3/Endpoints/ApiTypes/Errors/ApiValidationError.cs index e7371b47..6c57a5f1 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/ApiTypes/Errors/ApiValidationError.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/ApiTypes/Errors/ApiValidationError.cs @@ -62,14 +62,24 @@ public class ApiValidationError : ApiError public const string EmailDoesNotActuallyExistErrorWhen = "The email address given does not exist. Are you sure you typed it in correctly?"; public static readonly ApiValidationError EmailDoesNotActuallyExistError = new(EmailDoesNotActuallyExistErrorWhen); - public const string BadUserLookupIdTypeWhen = "The ID type used to specify the user is not supported"; - public static readonly ApiValidationError BadUserLookupIdType = new(BadUserLookupIdTypeWhen); + public const string MayNotOverwriteRoleErrorWhen = "You may not overwrite user roles because you are not an admin"; + public static readonly ApiValidationError MayNotOverwriteRoleError = new(MayNotOverwriteRoleErrorWhen); + + public const string WrongRoleUpdateMethodErrorWhen = "The specified role cannot be assigned to the user using this endpoint."; + public static readonly ApiValidationError WrongRoleUpdateMethodError = new(WrongRoleUpdateMethodErrorWhen); + + public const string RoleMissingErrorWhen = "The specified role does not exist."; + public static readonly ApiValidationError RoleMissingError = new(RoleMissingErrorWhen); public const string ContestOrganizerIdParseErrorWhen = "The organizer's user ID could not be parsed by the server"; public static readonly ApiValidationError ContestOrganizerIdParseError = new(ContestOrganizerIdParseErrorWhen); public const string ContestDataMissingErrorWhen = "The contest must at least have a title, aswell as a start and end date specified"; public static readonly ApiValidationError ContestDataMissingError = new(ContestDataMissingErrorWhen); + + public const string InvalidUsernameErrorWhen = "The username must be valid. The requirements are 3 to 16 alphanumeric characters, plus hyphens and underscores."; + public static readonly ApiValidationError InvalidUsernameError = new(InvalidUsernameErrorWhen); + // TODO: Split off error messages which are actually 401 or anything else that isn't 400 public ApiValidationError(string message) : base(message) {} } diff --git a/Refresh.Interfaces.APIv3/Endpoints/AuthenticationApiEndpoints.cs b/Refresh.Interfaces.APIv3/Endpoints/AuthenticationApiEndpoints.cs index e90122c0..1b9fa586 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/AuthenticationApiEndpoints.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/AuthenticationApiEndpoints.cs @@ -288,6 +288,7 @@ public ApiOkResponse DenyVerificationRequest(RequestContext context, GameDatabas [ApiV3Endpoint("register", HttpMethods.Post), Authentication(false)] [DocSummary("Registers a new user.")] + [DocError(typeof(ApiValidationError), ApiValidationError.InvalidUsernameErrorWhen)] [DocRequestBody(typeof(ApiRegisterRequest))] #if !DEBUG [RateLimitSettings(3600, 10, 3600 / 2, "register")] @@ -315,10 +316,8 @@ public ApiResponse Register(RequestContext context, return new ApiAuthenticationError("You aren't allowed to play on this instance."); if (!database.IsUsernameValid(body.Username)) - return new ApiValidationError( - "The username must be valid. " + - "The requirements are 3 to 16 alphanumeric characters, plus hyphens and underscores. " + - "Are you sure you used your PSN/RPCN username?"); + return new ApiValidationError(ApiValidationError.InvalidUsernameErrorWhen + + " Are you sure you used your PSN/RPCN username?"); if (database.IsUsernameQueued(body.Username) || database.IsEmailQueued(body.EmailAddress)) return UserInQueueError(); diff --git a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Request/ApiAdminUpdateUserRequest.cs b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Request/ApiAdminUpdateUserRequest.cs new file mode 100644 index 00000000..7e51d4aa --- /dev/null +++ b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Request/ApiAdminUpdateUserRequest.cs @@ -0,0 +1,15 @@ +using Refresh.Database.Models.Users; +using Refresh.Database.Query; + +namespace Refresh.Interfaces.APIv3.Endpoints.DataTypes.Request; + +[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] +public class ApiAdminUpdateUserRequest : IApiAdminEditUserRequest +{ + public string? Username { get; set; } + public string? IconHash { get; set; } + public string? VitaIconHash { get; set; } + public string? BetaIconHash { get; set; } + public string? Description { get; set; } + public GameUserRole? Role { get; set; } +} \ No newline at end of file diff --git a/Refresh.Interfaces.APIv3/Endpoints/ReviewApiEndpoints.cs b/Refresh.Interfaces.APIv3/Endpoints/ReviewApiEndpoints.cs index 8deafdab..3c9ed4ad 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/ReviewApiEndpoints.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/ReviewApiEndpoints.cs @@ -40,26 +40,13 @@ public ApiListResponse GetReviewsForLevel(RequestContext [ApiV3Endpoint("users/{userIdType}/{userId}/reviews"), Authentication(false)] [DocUsesPageData, DocSummary("Gets a list of the reviews posted by a user.")] - [DocError(typeof(ApiValidationError), ApiValidationError.BadUserLookupIdTypeWhen)] [DocError(typeof(ApiNotFoundError), ApiNotFoundError.UserMissingErrorWhen)] public ApiListResponse GetReviewsByUser(RequestContext context, GameDatabaseContext database, IDataStore dataStore, DataContext dataContext, [DocSummary("The type of identifier used to look up the user. Can be either 'uuid' or 'username'.")] string userIdType, [DocSummary("The UUID or username of the user, depending on the specified ID type.")] string userId) { - GameUser? user; - switch (userIdType) - { - case "uuid": - user = database.GetUserByUuid(userId); - break; - case "username": - user = database.GetUserByUsername(userId); - break; - default: - return ApiValidationError.BadUserLookupIdType; - }; - + GameUser? user = database.GetUserByIdAndType(userIdType, userId); if (user == null) return ApiNotFoundError.UserMissingError; (int skip, int count) = context.GetPageData(); diff --git a/RefreshTests.GameServer/GameServer/TestRefreshGameServer.cs b/RefreshTests.GameServer/GameServer/TestRefreshGameServer.cs index 824e93ab..c87e6524 100644 --- a/RefreshTests.GameServer/GameServer/TestRefreshGameServer.cs +++ b/RefreshTests.GameServer/GameServer/TestRefreshGameServer.cs @@ -73,6 +73,7 @@ protected override void SetupServices() this.Server.AddService(); this.Server.AddService(); this.Server.AddService(); + this.Server.AddService(); // Must always be last, see comment in RefreshGameServer this.Server.AddService(); diff --git a/RefreshTests.GameServer/TestContext.cs b/RefreshTests.GameServer/TestContext.cs index 28f10c49..4e15858e 100644 --- a/RefreshTests.GameServer/TestContext.cs +++ b/RefreshTests.GameServer/TestContext.cs @@ -101,12 +101,13 @@ public HttpClient GetAuthenticatedClient(TokenType type, TokenGame game, TokenPl return client; } - public GameUser CreateUser(string? username = null, GameUserRole role = GameUserRole.User) + public GameUser CreateUser(string? username = null, GameUserRole role = GameUserRole.User, bool verifyEmail = true) { username ??= this.UserIncrement.ToString(); GameUser user = this.Database.CreateUser(username, $"{username}@{username}.local"); if (role != GameUserRole.User) this.Database.SetUserRole(user, role); + if (verifyEmail) this.Database.VerifyUserEmail(user); return user; } diff --git a/RefreshTests.GameServer/Tests/ApiV3/ModerationApiTests.cs b/RefreshTests.GameServer/Tests/ApiV3/ModerationApiTests.cs new file mode 100644 index 00000000..5243a8f9 --- /dev/null +++ b/RefreshTests.GameServer/Tests/ApiV3/ModerationApiTests.cs @@ -0,0 +1,218 @@ +using MongoDB.Bson; +using Refresh.Database.Models.Authentication; +using Refresh.Database.Models.Moderation; +using Refresh.Database.Models.Users; +using Refresh.Interfaces.APIv3.Endpoints.ApiTypes; +using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Request; +using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Users; +using RefreshTests.GameServer.Extensions; + +namespace RefreshTests.GameServer.Tests.ApiV3; + +public class ModerationApiTests : GameServerTest +{ + [Test] + [TestCase(GameUserRole.Restricted)] + [TestCase(GameUserRole.User)] + [TestCase(GameUserRole.Curator)] + [TestCase(GameUserRole.Moderator)] + [TestCase(GameUserRole.Admin)] + public void MayEditOtherUsersProfile(GameUserRole actorRole) + { + using TestContext context = this.GetServer(); + GameUser actor = context.CreateUser(null, actorRole); + GameUser target = context.CreateUser(null, GameUserRole.User); + HttpClient client = context.GetAuthenticatedClient(TokenType.Api, actor); + + ApiAdminUpdateUserRequest request = new() + { + Description = "lol" + }; + ApiResponse? response = client.PatchData($"/api/v3/admin/users/uuid/{target.UserId}", request, false, false); + + context.Database.Refresh(); + + if (actorRole < GameUserRole.Moderator) + { + // In this case response altogether is null because RoleService is the one to return Unauthorized, and it doesn't include any response body. + // But we only care about Data being null in order to be able to tell that the request has failed. + Assert.That(response?.Data, Is.Null); + + GameUser? targetUpdated = context.Database.GetUserByObjectId(target.UserId); + Assert.That(targetUpdated, Is.Not.Null); + Assert.That(targetUpdated!.Description, Is.Empty); + Assert.That(context.Database.GetModerationActionsForObject(target.UserId.ToString(), ModerationObjectType.User, 0, 1).TotalItems, Is.Zero); + } + else + { + Assert.That(response?.Data, Is.Not.Null); + Assert.That(response!.Data!.UserId.ToString(), Is.EqualTo(target.UserId.ToString())); + + GameUser? targetUpdated = context.Database.GetUserByObjectId(target.UserId); + Assert.That(targetUpdated, Is.Not.Null); + Assert.That(targetUpdated!.Description, Is.EqualTo("lol")); + Assert.That(context.Database.GetModerationActionsForObject(target.UserId.ToString(), ModerationObjectType.User, 0, 1).TotalItems, Is.EqualTo(1)); + } + } + + [Test] + [TestCase(GameUserRole.Restricted)] + [TestCase(GameUserRole.User)] + [TestCase(GameUserRole.Curator)] + [TestCase(GameUserRole.Moderator)] + [TestCase(GameUserRole.Admin)] + public void MayEditOtherUsersRole(GameUserRole actorRole) + { + using TestContext context = this.GetServer(); + GameUser actor = context.CreateUser(null, actorRole); + GameUser target = context.CreateUser(null, GameUserRole.User); + HttpClient client = context.GetAuthenticatedClient(TokenType.Api, actor); + + ApiAdminUpdateUserRequest request = new() + { + Role = GameUserRole.Trusted + }; + ApiResponse? response = client.PatchData($"/api/v3/admin/users/uuid/{target.UserId}", request, false, false); + + context.Database.Refresh(); + + if (actorRole < GameUserRole.Admin) + { + // Error is either Unauthorized with no body if it was blocked by RoleService, or 400 with a body if it's blocked by the method + // (happens if the user is a moderator). Understand both cases as a failure. + Assert.That(response?.Data, Is.Null); + + GameUser? targetUpdated = context.Database.GetUserByObjectId(target.UserId); + Assert.That(targetUpdated, Is.Not.Null); + Assert.That(targetUpdated!.Role, Is.EqualTo(GameUserRole.User)); + Assert.That(context.Database.GetModerationActionsForObject(target.UserId.ToString(), ModerationObjectType.User, 0, 1).TotalItems, Is.Zero); + } + else + { + Assert.That(response?.Data, Is.Not.Null); + Assert.That(response!.Data!.UserId.ToString(), Is.EqualTo(target.UserId.ToString())); + + GameUser? targetUpdated = context.Database.GetUserByObjectId(target.UserId); + Assert.That(targetUpdated, Is.Not.Null); + Assert.That(targetUpdated!.Role, Is.EqualTo(GameUserRole.Trusted)); + Assert.That(context.Database.GetModerationActionsForObject(target.UserId.ToString(), ModerationObjectType.User, 0, 1).TotalItems, Is.EqualTo(1)); + } + } + + [Test] + [TestCase(GameUserRole.Restricted)] + [TestCase(GameUserRole.User)] + [TestCase(GameUserRole.Curator)] + [TestCase(GameUserRole.Moderator)] + [TestCase(GameUserRole.Admin)] + public void MayRenameOtherUser(GameUserRole actorRole) + { + string initialUsername = "hiii"; + string newUsername = "lolol"; + + using TestContext context = this.GetServer(); + GameUser actor = context.CreateUser(role: actorRole); + HttpClient client = context.GetAuthenticatedClient(TokenType.Api, actor); + + GameUser target = context.CreateUser(initialUsername, GameUserRole.User); + + ApiAdminUpdateUserRequest request = new() + { + Username = newUsername + }; + ApiResponse? response = client.PatchData($"/api/v3/admin/users/uuid/{target.UserId}", request, false, false); + + context.Database.Refresh(); + + if (actorRole < GameUserRole.Moderator) + { + Assert.That(response?.Data, Is.Null); + + GameUser? targetUpdated = context.Database.GetUserByObjectId(target.UserId); + Assert.That(targetUpdated, Is.Not.Null); + Assert.That(targetUpdated!.Username, Is.EqualTo(initialUsername)); + Assert.That(context.Database.GetNotificationCountByUser(target), Is.Zero); + Assert.That(context.Database.GetModerationActionsForObject(target.UserId.ToString(), ModerationObjectType.User, 0, 1).TotalItems, Is.Zero); + } + else + { + Assert.That(response?.Data, Is.Not.Null); + Assert.That(response!.Data!.UserId.ToString(), Is.EqualTo(target.UserId.ToString())); + + GameUser? targetUpdated = context.Database.GetUserByObjectId(target.UserId); + Assert.That(targetUpdated, Is.Not.Null); + Assert.That(targetUpdated!.Username, Is.EqualTo(newUsername)); + + Assert.That(context.Database.GetNotificationCountByUser(target), Is.EqualTo(1)); + Assert.That(context.Database.GetModerationActionsForObject(target.UserId.ToString(), ModerationObjectType.User, 0, 1).TotalItems, Is.EqualTo(1)); + } + } + + [Test] + [TestCase("")] + [TestCase("0")] + public void IconGetsReset(string newIcon) + { + using TestContext context = this.GetServer(); + GameUser actor = context.CreateUser(null, GameUserRole.Moderator); + GameUser target = context.CreateUser(null, GameUserRole.User); + HttpClient client = context.GetAuthenticatedClient(TokenType.Api, actor); + + // Prepare + string fakeIconHash = "asdfcgvhbjnkmlö"; + context.Database.UpdateUserData(target, new ApiAdminUpdateUserRequest() + { + IconHash = fakeIconHash + }); + + Assert.That(context.Database.GetUserByObjectId(target.UserId)?.IconHash, Is.EqualTo(fakeIconHash)); + + // Now try resetting + ApiUpdateUserRequest request = new() + { + IconHash = newIcon + }; + ApiResponse? response = client.PatchData($"/api/v3/admin/users/uuid/{target.UserId}", request); + Assert.That(response, Is.Not.Null); + Assert.That(response!.Data!.IconHash, Is.EqualTo("0")); + + context.Database.Refresh(); + + GameUser? userUpdated = context.Database.GetUserByObjectId(target.UserId); + Assert.That(userUpdated, Is.Not.Null); + Assert.That(userUpdated!.IconHash, Is.EqualTo("0")); + } + + [Test] + public void CannotEditUnknownUser() + { + using TestContext context = this.GetServer(); + GameUser actor = context.CreateUser(null, GameUserRole.Moderator); + HttpClient client = context.GetAuthenticatedClient(TokenType.Api, actor); + + ApiAdminUpdateUserRequest request = new() + { + Description = "lol" + }; + ApiResponse? response = client.PatchData($"/api/v3/admin/users/uuid/{ObjectId.GenerateNewId()}", request, false, true); + Assert.That(response?.Error, Is.Not.Null); + Assert.That(response!.Error!.StatusCode, Is.EqualTo(NotFound)); + } + + [Test] + public void CannotEditUserIfIdTypeIsUnknown() + { + using TestContext context = this.GetServer(); + GameUser actor = context.CreateUser(null, GameUserRole.Moderator); + GameUser target = context.CreateUser(null, GameUserRole.User); + HttpClient client = context.GetAuthenticatedClient(TokenType.Api, actor); + + ApiAdminUpdateUserRequest request = new() + { + Description = "lol" + }; + ApiResponse? response = client.PatchData($"/api/v3/admin/users/mmmmmmm/{target.UserId}", request, false, true); + Assert.That(response?.Error, Is.Not.Null); + Assert.That(response!.Error!.StatusCode, Is.EqualTo(NotFound)); + } +} \ No newline at end of file