diff --git a/Directory.Packages.props b/Directory.Packages.props index 41179c3..2b48da4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,13 +17,13 @@ - - + + - + diff --git a/src/RDMG.Core/Abstractions/Repository/IUserRepository.cs b/src/RDMG.Core/Abstractions/Repository/IUserRepository.cs index 91d9a39..74a27f6 100644 --- a/src/RDMG.Core/Abstractions/Repository/IUserRepository.cs +++ b/src/RDMG.Core/Abstractions/Repository/IUserRepository.cs @@ -4,10 +4,13 @@ namespace RDMG.Core.Abstractions.Repository; public interface IUserRepository { - Task CreateAsync(User user); - Task UpdateAsync(User user); - Task GetByUsernameAsync(string username, bool? deleted = false); - Task GetAsync(int id); - Task> ListAsync(bool? deleted = false); - Task DeleteAsync(int id); + Task CreateAsync(User user, CancellationToken cancellationToken = default); + Task UpdateAsync(User user, CancellationToken cancellationToken = default); + + Task GetByUsernameAsync(string username, bool? deleted = false, + CancellationToken cancellationToken = default); + + Task GetAsync(int id, CancellationToken cancellationToken = default); + Task ListAsync(bool? deleted = false, CancellationToken cancellationToken = default); + Task DeleteAsync(int id, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/RDMG.Core/Abstractions/Services/IDungeonService.cs b/src/RDMG.Core/Abstractions/Services/IDungeonService.cs index 0d89685..245cf61 100644 --- a/src/RDMG.Core/Abstractions/Services/IDungeonService.cs +++ b/src/RDMG.Core/Abstractions/Services/IDungeonService.cs @@ -4,19 +4,28 @@ namespace RDMG.Core.Abstractions.Services; public interface IDungeonService { - Task> GetAllDungeonOptionsAsync(CancellationToken cancellationToken); - Task> GetAllDungeonOptionsForUserAsync(int userId, CancellationToken cancellationToken); + Task GetAllDungeonOptionsAsync(CancellationToken cancellationToken); + Task GetAllDungeonOptionsForUserAsync(int userId, CancellationToken cancellationToken); Task GetDungeonOptionAsync(int id, CancellationToken cancellationToken); - Task GetDungeonOptionByNameAsync(string dungeonName, int userId, CancellationToken cancellationToken); + + Task GetDungeonOptionByNameAsync(string dungeonName, int userId, + CancellationToken cancellationToken); + Task GetDungeonAsync(int id, CancellationToken cancellationToken); - Task CreateOrUpdateDungeonAsync(DungeonOptionModel optionModel, bool addDungeon, int level, CancellationToken cancellationToken); + + Task CreateOrUpdateDungeonAsync(DungeonOptionModel optionModel, bool addDungeon, int level, + CancellationToken cancellationToken); + Task UpdateDungeonAsync(DungeonModel model, CancellationToken cancellationToken); Task CreateDungeonOptionAsync(DungeonOptionModel dungeonOption, CancellationToken cancellationToken); - Task> ListUserDungeonsAsync(int userId, CancellationToken cancellationToken); - Task> ListUserDungeonsByNameAsync(string dungeonName, int userId, CancellationToken cancellationToken); + Task ListUserDungeonsAsync(int userId, CancellationToken cancellationToken); + + Task ListUserDungeonsByNameAsync(string dungeonName, int userId, + CancellationToken cancellationToken); + Task AddDungeonAsync(DungeonModel savedDungeon, CancellationToken cancellationToken); Task DeleteDungeonOptionAsync(int id, CancellationToken cancellationToken); Task DeleteDungeonAsync(int id, CancellationToken cancellationToken); - Task GenerateDungeonAsync(DungeonOptionModel model); + Task GenerateDungeonAsync(DungeonOptionModel model, CancellationToken cancellationToken); Task RenameDungeonAsync(int optionId, int userId, string newName, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/RDMG.Core/Abstractions/Services/IOptionService.cs b/src/RDMG.Core/Abstractions/Services/IOptionService.cs index 40aa578..2b42d29 100644 --- a/src/RDMG.Core/Abstractions/Services/IOptionService.cs +++ b/src/RDMG.Core/Abstractions/Services/IOptionService.cs @@ -5,5 +5,5 @@ namespace RDMG.Core.Abstractions.Services; public interface IOptionService { - Task> ListOptionsAsync(OptionKey? filter = null, CancellationToken cancellationToken = default); + Task ListOptionsAsync(OptionKey? filter = null, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/RDMG.Core/Abstractions/Services/IUserService.cs b/src/RDMG.Core/Abstractions/Services/IUserService.cs index 0d4bab2..b6c2b2d 100644 --- a/src/RDMG.Core/Abstractions/Services/IUserService.cs +++ b/src/RDMG.Core/Abstractions/Services/IUserService.cs @@ -4,11 +4,11 @@ namespace RDMG.Core.Abstractions.Services; public interface IUserService { - Task GetAsync(int id); - Task UpdateAsync(UserModel model); - Task CreateAsync(UserModel model); - Task> ListAsync(bool? deleted = false); - Task DeleteAsync(int id); - Task RestoreAsync(int id); - Task ChangePasswordAsync(ChangePasswordModel model); + Task GetAsync(int id, CancellationToken cancellationToken = default); + Task UpdateAsync(UserModel model, CancellationToken cancellationToken = default); + Task CreateAsync(UserModel model, CancellationToken cancellationToken = default); + Task ListAsync(bool? deleted = false, CancellationToken cancellationToken = default); + Task DeleteAsync(int id, CancellationToken cancellationToken = default); + Task RestoreAsync(int id, CancellationToken cancellationToken = default); + Task ChangePasswordAsync(ChangePasswordModel model, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/RDMG.Core/Services/DungeonService.cs b/src/RDMG.Core/Services/DungeonService.cs index 8d507d1..15cfb5f 100644 --- a/src/RDMG.Core/Services/DungeonService.cs +++ b/src/RDMG.Core/Services/DungeonService.cs @@ -110,7 +110,7 @@ public async Task CreateOrUpdateDungeonAsync(DungeonOptionModel op private async Task UpdateExistingDungeonAsync(DungeonOptionModel optionModel, DungeonOptionModel existingDungeonOption, DungeonModel oldDungeon, CancellationToken cancellationToken) { - var dungeon = await GenerateDungeonAsync(optionModel, existingDungeonOption.Id); + var dungeon = await GenerateDungeonAsync(optionModel, existingDungeonOption.Id, cancellationToken); dungeon.Id = oldDungeon.Id; dungeon.Level = oldDungeon.Level; await UpdateDungeonAsync(dungeon, cancellationToken); @@ -121,7 +121,7 @@ private async Task CreateOptionAndAddDungeonToItAsync(DungeonOptio CancellationToken cancellationToken) { var createdId = await CreateDungeonOptionAsync(optionModel, cancellationToken); - var dungeon = await GenerateDungeonAsync(optionModel, createdId); + var dungeon = await GenerateDungeonAsync(optionModel, createdId, cancellationToken); dungeon.Level = 1; var id = await AddDungeonAsync(dungeon, cancellationToken); dungeon.Id = id; @@ -134,7 +134,7 @@ private async Task AddDungeonToExistingOptionAsync(DungeonOptionMo var existingDungeons = (await ListUserDungeonsByNameAsync(optionModel.DungeonName, optionModel.UserId, cancellationToken)) .ToList(); - var dungeon = await GenerateDungeonAsync(optionModel, optionModel.Id); + var dungeon = await GenerateDungeonAsync(optionModel, optionModel.Id, cancellationToken); dungeon.Level = level; if (existingDungeons.Exists(d => d.Level == level)) { @@ -149,21 +149,23 @@ private async Task AddDungeonToExistingOptionAsync(DungeonOptionMo return dungeon; } - private async Task GenerateDungeonAsync(DungeonOptionModel optionModel, int optionId) + private async Task GenerateDungeonAsync(DungeonOptionModel optionModel, int optionId, + CancellationToken cancellationToken) { - var dungeon = await GenerateDungeonAsync(optionModel); + var dungeon = await GenerateDungeonAsync(optionModel, cancellationToken); dungeon.DungeonOptionId = optionId; return dungeon; } - public async Task GenerateDungeonAsync(DungeonOptionModel model) + public async Task GenerateDungeonAsync(DungeonOptionModel model, CancellationToken cancellationToken) { try { ValidateModel(model); if (model.Corridor) - return await Task.FromResult(_dungeon.Generate(model)); - return await Task.FromResult(_dungeonNcDungeon.Generate(model)); + return await Task.Run(async () => await Task.FromResult(_dungeon.Generate(model)), cancellationToken); + return await Task.Run(async () => await Task.FromResult(_dungeonNcDungeon.Generate(model)), + cancellationToken); } catch (Exception ex) { @@ -172,13 +174,13 @@ public async Task GenerateDungeonAsync(DungeonOptionModel model) } } - public async Task> GetAllDungeonOptionsAsync(CancellationToken cancellationToken) + public async Task GetAllDungeonOptionsAsync(CancellationToken cancellationToken) { try { var options = await _dungeonOptionRepository.GetAllDungeonOptionsAsync(cancellationToken); - return options.Select(_mapper.Map); + return [.. options.Select(_mapper.Map)]; } catch (Exception ex) { @@ -187,14 +189,14 @@ public async Task> GetAllDungeonOptionsAsync(Can } } - public async Task> GetAllDungeonOptionsForUserAsync(int userId, + public async Task GetAllDungeonOptionsForUserAsync(int userId, CancellationToken cancellationToken) { try { var options = await _dungeonOptionRepository.GetAllDungeonOptionsForUserAsync(userId, cancellationToken); - return options.Select(_mapper.Map); + return [.. options.Select(_mapper.Map)]; } catch (Exception ex) { @@ -245,12 +247,12 @@ public async Task DeleteDungeonAsync(int id, CancellationToken cancellatio } } - public async Task> ListUserDungeonsAsync(int userId, CancellationToken cancellationToken) + public async Task ListUserDungeonsAsync(int userId, CancellationToken cancellationToken) { try { var result = await _dungeonRepository.GetAllDungeonsForUserAsync(userId, cancellationToken); - return result.Select(_mapper.Map); + return [.. result.Select(_mapper.Map)]; } catch (Exception ex) { @@ -259,14 +261,14 @@ public async Task> ListUserDungeonsAsync(int userId, C } } - public async Task> ListUserDungeonsByNameAsync(string dungeonName, int userId, + public async Task ListUserDungeonsByNameAsync(string dungeonName, int userId, CancellationToken cancellationToken) { try { var result = await _dungeonRepository.GetAllDungeonByOptionNameForUserAsync(dungeonName, userId, cancellationToken); - return result.Select(_mapper.Map); + return [.. result.Select(_mapper.Map)]; } catch (Exception ex) { diff --git a/src/RDMG.Core/Services/OptionService.cs b/src/RDMG.Core/Services/OptionService.cs index 7483c21..302e578 100644 --- a/src/RDMG.Core/Services/OptionService.cs +++ b/src/RDMG.Core/Services/OptionService.cs @@ -19,13 +19,15 @@ public class OptionService( private readonly IMapper _mapper = mapper; private readonly ILogger _logger = logger; - public async Task> ListOptionsAsync(OptionKey? filter = null, + public async Task ListOptionsAsync(OptionKey? filter = null, CancellationToken cancellationToken = default) { try { - if (_memoryCache.TryGetValue(nameof(ListOptionsAsync), out List? cacheEntry)) - return filter.HasValue ? cacheEntry?.Where(o => o.Key == filter.Value) ?? [] : cacheEntry ?? []; + if (_memoryCache.TryGetValue(nameof(ListOptionsAsync), out OptionModel[]? cacheEntry)) + return filter.HasValue + ? cacheEntry?.Where(o => o.Key == filter.Value).ToArray() ?? [] + : cacheEntry ?? []; var options = await _optionRepository.ListAsync(null, cancellationToken); cacheEntry = [.. options.Select(_mapper.Map)]; @@ -35,7 +37,7 @@ public async Task> ListOptionsAsync(OptionKey? filter = _memoryCache.Set(nameof(ListOptionsAsync), cacheEntry, cacheEntryOptions); - return filter.HasValue ? cacheEntry.Where(o => o.Key == filter.Value) : cacheEntry; + return filter.HasValue ? [.. cacheEntry.Where(o => o.Key == filter.Value)] : cacheEntry; } catch (Exception ex) { diff --git a/src/RDMG.Core/Services/UserService.cs b/src/RDMG.Core/Services/UserService.cs index 0b59268..bf62fe0 100644 --- a/src/RDMG.Core/Services/UserService.cs +++ b/src/RDMG.Core/Services/UserService.cs @@ -6,6 +6,8 @@ using RDMG.Core.Abstractions.Services.Models; using RDMG.Core.Domain; using RDMG.Core.Helpers; +using RDMG.Resources; +using User = RDMG.Core.Domain.User; namespace RDMG.Core.Services; @@ -15,7 +17,7 @@ public class UserService(IMapper mapper, IUserRepository userRepository, ILogger private readonly IMapper _mapper = mapper; private readonly ILogger _logger = logger; - public async Task CreateAsync(UserModel model) + public async Task CreateAsync(UserModel model, CancellationToken cancellationToken = default) { ValidateModel(model); await CheckUserExistAsync(model); @@ -23,7 +25,7 @@ public async Task CreateAsync(UserModel model) { model.Password = PasswordHelper.EncryptPassword(model.Password); var user = _mapper.Map(model); - user = await _userRepository.CreateAsync(user); + user = await _userRepository.CreateAsync(user, cancellationToken); return user.Id; } catch (Exception ex) @@ -38,17 +40,17 @@ private static void ValidateModel(UserModel model) var errors = new List(); ArgumentNullException.ThrowIfNull(model); if (model.Password.Length < 8) - errors.Add(new ServiceException(Resources.Error.PasswordLength)); + errors.Add(new ServiceException(Error.PasswordLength)); if (string.IsNullOrEmpty(model.Username)) - errors.Add(new ServiceException(string.Format(Resources.Error.RequiredValidation, model.Username))); + errors.Add(new ServiceException(string.Format(Error.RequiredValidation, nameof(model.Username)))); if (string.IsNullOrEmpty(model.FirstName)) - errors.Add(new ServiceException(string.Format(Resources.Error.RequiredValidation, model.FirstName))); + errors.Add(new ServiceException(string.Format(Error.RequiredValidation, nameof(model.FirstName)))); if (string.IsNullOrEmpty(model.LastName)) - errors.Add(new ServiceException(string.Format(Resources.Error.RequiredValidation, model.LastName))); + errors.Add(new ServiceException(string.Format(Error.RequiredValidation, nameof(model.LastName)))); if (string.IsNullOrEmpty(model.Email)) - errors.Add(new ServiceException(string.Format(Resources.Error.RequiredValidation, model.Email))); + errors.Add(new ServiceException(string.Format(Error.RequiredValidation, nameof(model.Email)))); if (string.IsNullOrEmpty(model.Role)) - errors.Add(new ServiceException(string.Format(Resources.Error.RequiredValidation, model.Role))); + errors.Add(new ServiceException(string.Format(Error.RequiredValidation, nameof(model.Role)))); if (errors.Count != 0) throw new ServiceAggregateException(errors); } @@ -57,14 +59,14 @@ private async Task CheckUserExistAsync(UserModel model) { var user = await _userRepository.GetByUsernameAsync(model.Username, null); if (user is not null) - throw new ServiceException(string.Format(Resources.Error.UserExist, model.Username)); + throw new ServiceException(string.Format(Error.UserExist, model.Username)); } - public async Task DeleteAsync(int id) + public async Task DeleteAsync(int id, CancellationToken cancellationToken = default) { try { - return await _userRepository.DeleteAsync(id); + return await _userRepository.DeleteAsync(id, cancellationToken); } catch (Exception ex) { @@ -73,12 +75,12 @@ public async Task DeleteAsync(int id) } } - public async Task GetAsync(int id) + public async Task GetAsync(int id, CancellationToken cancellationToken = default) { try { - var user = await _userRepository.GetAsync(id) ?? - throw new ServiceException(Resources.Error.NotFound); + var user = await _userRepository.GetAsync(id, cancellationToken) ?? + throw new ServiceException(Error.NotFound); return _mapper.Map(user); } @@ -89,11 +91,11 @@ public async Task GetAsync(int id) } } - public async Task> ListAsync(bool? deleted = false) + public async Task ListAsync(bool? deleted = false, CancellationToken cancellationToken = default) { try { - var result = await _userRepository.ListAsync(deleted); + var result = await _userRepository.ListAsync(deleted, cancellationToken); return [.. result.Select(_mapper.Map).OrderBy(um => um.Username)]; } @@ -104,18 +106,19 @@ public async Task> ListAsync(bool? deleted = false) } } - public async Task RestoreAsync(int id) + public async Task RestoreAsync(int id, CancellationToken cancellationToken = default) { try { - var user = await _userRepository.GetAsync(id); + var user = await _userRepository.GetAsync(id, cancellationToken); if (user is not null) { user.IsDeleted = false; - await _userRepository.UpdateAsync(user); + await _userRepository.UpdateAsync(user, cancellationToken); + return true; } - return true; + return false; } catch (Exception ex) { @@ -124,19 +127,19 @@ public async Task RestoreAsync(int id) } } - public async Task UpdateAsync(UserModel model) + public async Task UpdateAsync(UserModel model, CancellationToken cancellationToken = default) { ValidateModelForEdit(model); try { - var user = await _userRepository.GetAsync(model.Id); + var user = await _userRepository.GetAsync(model.Id, cancellationToken); if (user is not null) { user.FirstName = model.FirstName; user.LastName = model.LastName; user.Email = model.Email; user.Role = Enum.Parse(model.Role); - await _userRepository.UpdateAsync(user); + await _userRepository.UpdateAsync(user, cancellationToken); } } catch (Exception ex) @@ -151,32 +154,33 @@ private static void ValidateModelForEdit(UserModel model) var errors = new List(); ArgumentNullException.ThrowIfNull(model); if (string.IsNullOrEmpty(model.FirstName)) - errors.Add(new ServiceException(string.Format(Resources.Error.RequiredValidation, model.FirstName))); + errors.Add(new ServiceException(string.Format(Error.RequiredValidation, nameof(model.FirstName)))); if (string.IsNullOrEmpty(model.LastName)) - errors.Add(new ServiceException(string.Format(Resources.Error.RequiredValidation, model.LastName))); + errors.Add(new ServiceException(string.Format(Error.RequiredValidation, nameof(model.LastName)))); if (string.IsNullOrEmpty(model.Email)) - errors.Add(new ServiceException(string.Format(Resources.Error.RequiredValidation, model.Email))); + errors.Add(new ServiceException(string.Format(Error.RequiredValidation, nameof(model.Email)))); if (string.IsNullOrEmpty(model.Role)) - errors.Add(new ServiceException(string.Format(Resources.Error.RequiredValidation, model.Role))); + errors.Add(new ServiceException(string.Format(Error.RequiredValidation, nameof(model.Role)))); if (errors.Count != 0) throw new ServiceAggregateException(errors); } - public async Task ChangePasswordAsync(ChangePasswordModel model) + public async Task ChangePasswordAsync(ChangePasswordModel model, CancellationToken cancellationToken = default) { try { if (model.NewPassword.Length < 8) - throw new ServiceException(Resources.Error.PasswordLength); + throw new ServiceException(Error.PasswordLength); - var user = await _userRepository.GetAsync(model.Id) ?? throw new ServiceException(Resources.Error.NotFound); + var user = await _userRepository.GetAsync(model.Id, cancellationToken) ?? + throw new ServiceException(Error.NotFound); if (!PasswordHelper.CheckPassword(user.Password, model.CurrentPassword)) - throw new ServiceException(Resources.Error.PasswordMissMatch); + throw new ServiceException(Error.PasswordMissMatch); user.Password = PasswordHelper.EncryptPassword(model.NewPassword); - await _userRepository.UpdateAsync(user); + await _userRepository.UpdateAsync(user, cancellationToken); } catch (Exception ex) { diff --git a/src/RDMG.Infrastructure/Data/AppDbContextInitializer.cs b/src/RDMG.Infrastructure/Data/AppDbContextInitializer.cs index 8dd5276..6e6a088 100644 --- a/src/RDMG.Infrastructure/Data/AppDbContextInitializer.cs +++ b/src/RDMG.Infrastructure/Data/AppDbContextInitializer.cs @@ -15,6 +15,10 @@ public class AppDbContextInitializer(IAppDbContext context, IDungeonService dung private readonly IDungeonService _dungeonService = dungeonService; private const string UtDungeonName1 = "Test 1"; public const string UtDungeonName2 = "Test 2"; + public const int TestAdminUserId = 1; + public const int TestUserId = 2; + public const int TestDeletedUserId = 3; + public const int TestNotExistingUserId = 999; public async Task UpdateAsync(CancellationToken cancellationToken) { @@ -34,7 +38,7 @@ public async Task SeedDataAsync(CancellationToken cancellationToken) { await SeedUsersAsync(cancellationToken); await SeedOptionsAsync(cancellationToken); - await SeedDungeonsAsync(1, cancellationToken); + await SeedDungeonsAsync(TestAdminUserId, cancellationToken); } } @@ -42,10 +46,10 @@ public async Task SeedTestBaseAsync(CancellationToken cancellationToken) { if (!_context.Users.Any()) { - await SeedUsersAsync(cancellationToken); + await SeedUsersAsync(cancellationToken, true); await SeedOptionsAsync(cancellationToken); - await SeedDungeonsAsync(1, cancellationToken); - await SeedDungeonsAsync(2, cancellationToken); + await SeedDungeonsAsync(TestAdminUserId, cancellationToken); + await SeedDungeonsAsync(TestUserId, cancellationToken); } } @@ -469,7 +473,7 @@ private async Task SeedDungeonsAsync(int userId, CancellationToken token) UserId = dungeonOption.UserId }; - var sd = await _dungeonService.GenerateDungeonAsync(model); + var sd = await _dungeonService.GenerateDungeonAsync(model, token); sd.Level = 1; await _dungeonService.AddDungeonAsync(sd, token); @@ -516,15 +520,16 @@ private async Task SeedDungeonsAsync(int userId, CancellationToken token) UserId = dungeonOption.UserId }; - sd = await _dungeonService.GenerateDungeonAsync(model); + sd = await _dungeonService.GenerateDungeonAsync(model, token); sd.Level = 1; await _dungeonService.AddDungeonAsync(sd, token); } - private async Task SeedUsersAsync(CancellationToken token) + private async Task SeedUsersAsync(CancellationToken token, bool seedDeletedUtUser = false) { _context.Users.Add(new User { + Id = TestAdminUserId, Username = "TestAdmin", Password = PasswordHelper.EncryptPassword(config.Value.DefaultAdminPassword), FirstName = "Test", @@ -535,6 +540,7 @@ private async Task SeedUsersAsync(CancellationToken token) _context.Users.Add(new User { + Id = TestUserId, Username = "TestUser", Password = PasswordHelper.EncryptPassword(config.Value.DefaultUserPassword), FirstName = "Test", @@ -542,6 +548,22 @@ private async Task SeedUsersAsync(CancellationToken token) Email = "user@user.com", Role = Role.User }); + + if (seedDeletedUtUser) + { + _context.Users.Add(new User + { + Id = TestDeletedUserId, + Username = "UT Deleted User", + Password = PasswordHelper.EncryptPassword(config.Value.DefaultUserPassword), + FirstName = "Test", + LastName = "User", + Email = "user@user.com", + Role = Role.User, + IsDeleted = true + }); + } + await _context.SaveChangesAsync(token); } } \ No newline at end of file diff --git a/src/RDMG.Infrastructure/Repository/UserRepository.cs b/src/RDMG.Infrastructure/Repository/UserRepository.cs index 2e5ad35..c7dd1d2 100644 --- a/src/RDMG.Infrastructure/Repository/UserRepository.cs +++ b/src/RDMG.Infrastructure/Repository/UserRepository.cs @@ -13,37 +13,40 @@ public class UserRepository(IAppDbContext context, IMapper mapper, ILogger _logger = logger; private readonly IMapper _mapper = mapper; - public async Task CreateAsync(User user) + public async Task CreateAsync(User user, CancellationToken cancellationToken = default) { _context.Users.Add(user); - await _context.SaveChangesAsync(CancellationToken.None); + await _context.SaveChangesAsync(cancellationToken); return user; } - public async Task GetAsync(int id) + public async Task GetAsync(int id, CancellationToken cancellationToken = default) { - return await _context.Users.AsNoTracking().FirstOrDefaultAsync(u => u.Id == id); + return await _context.Users.AsNoTracking() + .FirstOrDefaultAsync(u => u.Id == id, cancellationToken: cancellationToken); } - public async Task UpdateAsync(User user) + public async Task UpdateAsync(User user, CancellationToken cancellationToken = default) { - var local = await _context.Users.FirstOrDefaultAsync(u => u.Id == user.Id); + var local = await _context.Users.FirstOrDefaultAsync(u => u.Id == user.Id, + cancellationToken: cancellationToken); if (local is null) return null; _mapper.Map(user, local); - await _context.SaveChangesAsync(CancellationToken.None); + await _context.SaveChangesAsync(cancellationToken); return local; } - public async Task DeleteAsync(int id) + + public async Task DeleteAsync(int id, CancellationToken cancellationToken = default) { - var local = await _context.Users.FirstOrDefaultAsync(u => u.Id == id); + var local = await _context.Users.FirstOrDefaultAsync(u => u.Id == id, cancellationToken: cancellationToken); if (local is not null) { local.IsDeleted = true; - await _context.SaveChangesAsync(CancellationToken.None); + await _context.SaveChangesAsync(cancellationToken); return true; } @@ -51,23 +54,24 @@ public async Task DeleteAsync(int id) return false; } - public async Task GetByUsernameAsync(string username, bool? deleted = false) + public async Task GetByUsernameAsync(string username, bool? deleted = false, + CancellationToken cancellationToken = default) { var query = _context.Users.AsNoTracking(); if (deleted.HasValue) query = query.Where(x => x.IsDeleted == deleted.Value); - return await query.FirstOrDefaultAsync(u => u.Username == username); + return await query.FirstOrDefaultAsync(u => u.Username == username, cancellationToken: cancellationToken); } - public async Task> ListAsync(bool? deleted = false) + public async Task ListAsync(bool? deleted = false, CancellationToken cancellationToken = default) { var query = _context.Users.AsNoTracking(); if (deleted.HasValue) query = query.Where(x => x.IsDeleted == deleted.Value); - return await query.ToListAsync(); + return await query.ToArrayAsync(cancellationToken); } } \ No newline at end of file diff --git a/src/RDMG.Web/RDMG.Web.csproj b/src/RDMG.Web/RDMG.Web.csproj index 3cecb34..37ee94c 100644 --- a/src/RDMG.Web/RDMG.Web.csproj +++ b/src/RDMG.Web/RDMG.Web.csproj @@ -2,7 +2,7 @@ true true - 1.0.4.1 + 1.0.4.2 b2a492d1-90ea-4e42-b3e5-5d962495bef1 diff --git a/tests/RDMG.Core.Tests/DungeonServiceTests/Create.cs b/tests/RDMG.Core.Tests/DungeonServiceTests/Create.cs index 9368723..fc3c137 100644 --- a/tests/RDMG.Core.Tests/DungeonServiceTests/Create.cs +++ b/tests/RDMG.Core.Tests/DungeonServiceTests/Create.cs @@ -45,7 +45,7 @@ public async Task AddDungeonAsync_WithDungeonModel_ReturnsNewEntityId() { var optionsModel = (await _dungeonService.GetAllDungeonOptionsForUserAsync(1, TestContext.Current.CancellationToken)).First(); - var dungeon = await _dungeonService.GenerateDungeonAsync(optionsModel); + var dungeon = await _dungeonService.GenerateDungeonAsync(optionsModel, TestContext.Current.CancellationToken); var result = await _dungeonService.AddDungeonAsync(dungeon, TestContext.Current.CancellationToken); result.ShouldBeGreaterThan(1); diff --git a/tests/RDMG.Core.Tests/DungeonServiceTests/Generate.cs b/tests/RDMG.Core.Tests/DungeonServiceTests/Generate.cs index df25004..7e048bc 100644 --- a/tests/RDMG.Core.Tests/DungeonServiceTests/Generate.cs +++ b/tests/RDMG.Core.Tests/DungeonServiceTests/Generate.cs @@ -30,7 +30,7 @@ public async Task GenerateDungeonAsync_WithValidOptionModel_ReturnsDungeonModel( RoomSize = 20, Corridor = false, UserId = 1 - }); + }, TestContext.Current.CancellationToken); result.DungeonTiles.ShouldNotBeNull(); } diff --git a/tests/RDMG.Core.Tests/TestFixture.cs b/tests/RDMG.Core.Tests/TestFixture.cs index 60b4cdd..77a20a2 100644 --- a/tests/RDMG.Core.Tests/TestFixture.cs +++ b/tests/RDMG.Core.Tests/TestFixture.cs @@ -1,4 +1,6 @@ -using RDMG.Core.Abstractions.Generator; +using Microsoft.Extensions.Options; +using RDMG.Core.Abstractions.Configuration; +using RDMG.Core.Abstractions.Generator; using RDMG.Core.Abstractions.Repository; using RDMG.Core.Abstractions.Services; @@ -10,6 +12,8 @@ public class TestFixture : IDisposable public readonly IOptionService OptionService; public readonly IOptionRepository OptionRepository; public readonly IDungeonNoCorridor DungeonNoCorridor; + public readonly IUserService UserService; + public readonly IOptions Config; private readonly TestEnvironment _env = new(); private bool _disposedValue; @@ -18,6 +22,8 @@ public TestFixture() DungeonService = _env.GetService(); OptionService = _env.GetService(); OptionRepository = _env.GetService(); + UserService = _env.GetService(); + Config = _env.GetService>(); DungeonNoCorridor = _env.GetNcDungeon(); } diff --git a/tests/RDMG.Core.Tests/UserServiceTests/ChangePassword.cs b/tests/RDMG.Core.Tests/UserServiceTests/ChangePassword.cs new file mode 100644 index 0000000..4d2c91d --- /dev/null +++ b/tests/RDMG.Core.Tests/UserServiceTests/ChangePassword.cs @@ -0,0 +1,71 @@ +using Microsoft.Extensions.Options; +using RDMG.Core.Abstractions.Configuration; +using RDMG.Core.Abstractions.Services; +using RDMG.Core.Abstractions.Services.Exceptions; +using RDMG.Core.Abstractions.Services.Models; +using RDMG.Infrastructure.Data; +using RDMG.Resources; +using Shouldly; +using Xunit; + +namespace RDMG.Core.Tests.UserServiceTests; + +public class ChangePassword(TestFixture fixture) : IClassFixture +{ + private readonly IUserService _userService = fixture.UserService; + private readonly IOptions _config = fixture.Config; + private const string Password = "SOMENEWPASSWORD123"; + + [Fact] + public async Task ChangePasswordAsync_WithValidInput_ChangesPassword() + { + var model = new ChangePasswordModel + { + Id = AppDbContextInitializer.TestUserId, + CurrentPassword = _config.Value.DefaultUserPassword, + NewPassword = Password + }; + var oldUserModel = await _userService.GetAsync(model.Id, TestContext.Current.CancellationToken); + var act = async () => { await _userService.ChangePasswordAsync(model, TestContext.Current.CancellationToken); }; + + await act.ShouldNotThrowAsync(); + var newUserModel = await _userService.GetAsync(model.Id, TestContext.Current.CancellationToken); + newUserModel.Password.ShouldNotBe(oldUserModel.Password); + } + + public static TheoryData GetModelsWithErrors() + { + return + [ + (new ChangePasswordModel + { + NewPassword = "length" + }, Error.PasswordLength), + (new ChangePasswordModel + { + NewPassword = Password, + Id = AppDbContextInitializer.TestNotExistingUserId, + }, Error.NotFound), + (new ChangePasswordModel + { + NewPassword = Password, + Id = AppDbContextInitializer.TestUserId, + CurrentPassword = "wrong" + }, Error.PasswordMissMatch) + ]; + } + + [Theory] + [MemberData(nameof(GetModelsWithErrors), MemberType = typeof(ChangePassword))] + public async Task ChangePasswordAsync_WithInValidModel_ThrowsServiceException(ChangePasswordModel model, + string expectedError) + { + var act = async () => + { + await _userService.ChangePasswordAsync(model, cancellationToken: TestContext.Current.CancellationToken); + }; + + var result = await act.ShouldThrowAsync(); + result.Message.ShouldBe(expectedError); + } +} \ No newline at end of file diff --git a/tests/RDMG.Core.Tests/UserServiceTests/Create.cs b/tests/RDMG.Core.Tests/UserServiceTests/Create.cs new file mode 100644 index 0000000..d15e15d --- /dev/null +++ b/tests/RDMG.Core.Tests/UserServiceTests/Create.cs @@ -0,0 +1,58 @@ +using RDMG.Core.Abstractions.Services; +using RDMG.Core.Abstractions.Services.Exceptions; +using RDMG.Core.Abstractions.Services.Models; +using RDMG.Core.Domain; +using RDMG.Infrastructure.Data; +using RDMG.Resources; +using Shouldly; +using Xunit; + +namespace RDMG.Core.Tests.UserServiceTests; + +public class Create(TestFixture fixture) : IClassFixture +{ + private readonly IUserService _userService = fixture.UserService; + + [Fact] + public async Task CreateAsync_WithValidModel_CreatesUser() + { + var model = new UserModel + { + Username = "ddd", + Password = "asdasdada+mnn!", + Email = "dasd@test.com", + FirstName = "John", + LastName = "Doe", + Role = nameof(Role.Admin) + }; + var result = await _userService.CreateAsync(model, cancellationToken: TestContext.Current.CancellationToken); + result.ShouldBeGreaterThan(AppDbContextInitializer.TestDeletedUserId); + } + + [Fact] + public async Task CreateAsync_WithInValidModel_ThrowsServiceAggregateException() + { + var model = new UserModel(); + var expectedErrors = new List + { + string.Format(Error.RequiredValidation, nameof(model.Username)), + string.Format(Error.RequiredValidation, nameof(model.FirstName)), + string.Format(Error.RequiredValidation, nameof(model.LastName)), + string.Format(Error.RequiredValidation, nameof(model.Email)), + string.Format(Error.RequiredValidation, nameof(model.Role)), + Error.PasswordLength + }; + + var act = async () => + { + await _userService.CreateAsync(model, cancellationToken: TestContext.Current.CancellationToken); + }; + + var result = await act.ShouldThrowAsync(); + result.GetInnerExceptions() + .Select(se => se.Message) + .OrderBy(s => s) + .SequenceEqual(expectedErrors.OrderBy(s => s)) + .ShouldBeTrue(); + } +} \ No newline at end of file diff --git a/tests/RDMG.Core.Tests/UserServiceTests/Delete.cs b/tests/RDMG.Core.Tests/UserServiceTests/Delete.cs new file mode 100644 index 0000000..12a21d2 --- /dev/null +++ b/tests/RDMG.Core.Tests/UserServiceTests/Delete.cs @@ -0,0 +1,27 @@ +using RDMG.Core.Abstractions.Services; +using RDMG.Infrastructure.Data; +using Shouldly; +using Xunit; + +namespace RDMG.Core.Tests.UserServiceTests; + +public class Delete(TestFixture fixture) : IClassFixture +{ + private readonly IUserService _userService = fixture.UserService; + + [Fact] + public async Task DeleteAsync_WithValidId_ReturnsTrue() + { + var result = await _userService.DeleteAsync(AppDbContextInitializer.TestAdminUserId, + cancellationToken: TestContext.Current.CancellationToken); + result.ShouldBe(true); + } + + [Fact] + public async Task DeleteAsync_WithInValidId_ReturnsFalse() + { + var result = await _userService.DeleteAsync(AppDbContextInitializer.TestNotExistingUserId, + cancellationToken: TestContext.Current.CancellationToken); + result.ShouldBe(false); + } +} \ No newline at end of file diff --git a/tests/RDMG.Core.Tests/UserServiceTests/Get.cs b/tests/RDMG.Core.Tests/UserServiceTests/Get.cs new file mode 100644 index 0000000..6f77331 --- /dev/null +++ b/tests/RDMG.Core.Tests/UserServiceTests/Get.cs @@ -0,0 +1,22 @@ +using RDMG.Core.Abstractions.Services; +using RDMG.Infrastructure.Data; +using Shouldly; +using Xunit; + +namespace RDMG.Core.Tests.UserServiceTests; + +public class Get(TestFixture fixture) : IClassFixture +{ + private readonly IUserService _userService = fixture.UserService; + + [Theory] + [InlineData(AppDbContextInitializer.TestAdminUserId)] + [InlineData(AppDbContextInitializer.TestUserId)] + [InlineData(AppDbContextInitializer.TestDeletedUserId)] + public async Task GetAsync_WithValidId_ReturnsUser(int id) + { + var result = await _userService.GetAsync(id, TestContext.Current.CancellationToken); + result.ShouldNotBeNull(); + result.Id.ShouldBe(id); + } +} \ No newline at end of file diff --git a/tests/RDMG.Core.Tests/UserServiceTests/List.cs b/tests/RDMG.Core.Tests/UserServiceTests/List.cs new file mode 100644 index 0000000..509b665 --- /dev/null +++ b/tests/RDMG.Core.Tests/UserServiceTests/List.cs @@ -0,0 +1,19 @@ +using RDMG.Core.Abstractions.Services; +using Shouldly; +using Xunit; + +namespace RDMG.Core.Tests.UserServiceTests; + +public class List(TestFixture fixture) : IClassFixture +{ + private readonly IUserService _userService = fixture.UserService; + + [Theory] + [InlineData(false, 2)] + [InlineData(true, 1)] + public async Task ListAsync_WithDeletedFlag_ReturnsAllUsers(bool deleted, int expectedCount) + { + var result = await _userService.ListAsync(deleted, cancellationToken: TestContext.Current.CancellationToken); + result.Length.ShouldBe(expectedCount); + } +} \ No newline at end of file diff --git a/tests/RDMG.Core.Tests/UserServiceTests/Restore.cs b/tests/RDMG.Core.Tests/UserServiceTests/Restore.cs new file mode 100644 index 0000000..3e92c7c --- /dev/null +++ b/tests/RDMG.Core.Tests/UserServiceTests/Restore.cs @@ -0,0 +1,27 @@ +using RDMG.Core.Abstractions.Services; +using RDMG.Infrastructure.Data; +using Shouldly; +using Xunit; + +namespace RDMG.Core.Tests.UserServiceTests; + +public class Restore(TestFixture fixture) : IClassFixture +{ + private readonly IUserService _userService = fixture.UserService; + + [Fact] + public async Task RestoreAsync_WithValidId_ReturnsTrue() + { + var result = await _userService.RestoreAsync(AppDbContextInitializer.TestDeletedUserId, + cancellationToken: TestContext.Current.CancellationToken); + result.ShouldBe(true); + } + + [Fact] + public async Task RestoreAsync_WithInValidId_ReturnsFalse() + { + var result = await _userService.RestoreAsync(AppDbContextInitializer.TestNotExistingUserId, + cancellationToken: TestContext.Current.CancellationToken); + result.ShouldBe(false); + } +} \ No newline at end of file diff --git a/tests/RDMG.Core.Tests/UserServiceTests/Update.cs b/tests/RDMG.Core.Tests/UserServiceTests/Update.cs new file mode 100644 index 0000000..266febe --- /dev/null +++ b/tests/RDMG.Core.Tests/UserServiceTests/Update.cs @@ -0,0 +1,52 @@ +using RDMG.Core.Abstractions.Services; +using RDMG.Core.Abstractions.Services.Exceptions; +using RDMG.Core.Abstractions.Services.Models; +using RDMG.Infrastructure.Data; +using RDMG.Resources; +using Shouldly; +using Xunit; + +namespace RDMG.Core.Tests.UserServiceTests; + +public class Update(TestFixture fixture) : IClassFixture +{ + private readonly IUserService _userService = fixture.UserService; + private const string ModifiedText = "Modified"; + + [Fact] + public async Task UpdateAsync_WithValidModel_UpdatesUser() + { + var adminUser = await _userService.GetAsync(AppDbContextInitializer.TestAdminUserId, + cancellationToken: TestContext.Current.CancellationToken); + adminUser.FirstName = ModifiedText; + await _userService.UpdateAsync(adminUser, cancellationToken: TestContext.Current.CancellationToken); + var result = + await _userService.GetAsync(adminUser.Id, cancellationToken: TestContext.Current.CancellationToken); + result.FirstName.ShouldBe(adminUser.FirstName); + } + + [Fact] + public async Task UpdateAsync_WithInValidModel_ThrowsServiceAggregateException() + { + var model = new UserModel(); + var expectedErrors = new List + { + string.Format(Error.RequiredValidation, nameof(model.FirstName)), + string.Format(Error.RequiredValidation, nameof(model.LastName)), + string.Format(Error.RequiredValidation, nameof(model.Email)), + string.Format(Error.RequiredValidation, nameof(model.Role)) + }; + + var act = async () => + { + await _userService.UpdateAsync(model, cancellationToken: TestContext.Current.CancellationToken); + }; + + var result = await act.ShouldThrowAsync(); + result.GetInnerExceptions() + .Select(se => se.Message) + .OrderBy(s => s) + .SequenceEqual(expectedErrors.OrderBy(s => s)) + .ShouldBeTrue(); + } +} \ No newline at end of file