Merge pull request #7 from Shchoholiev/feature/SA-14-guest-authorization

SA-14 Authorization enhancement
This commit is contained in:
Serhii Shchoholiev 2023-10-16 18:02:00 -04:00 committed by GitHub
commit db2715e17f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1652 additions and 1256 deletions

View File

@ -14,6 +14,12 @@
// "protocol": "https"
// }
// }
// Container is not working on M1 Mac
// "runArgs": [
// "--platform=linux/amd64"
// ],
"customizations": {
"vscode": {
"extensions": [
@ -22,7 +28,8 @@
"patcx.vscode-nuget-gallery",
"mhutchie.git-graph",
"fernandoescolar.vscode-solution-explorer",
"formulahendry.dotnet-test-explorer"
"formulahendry.dotnet-test-explorer",
"GitHub.copilot"
]
}
}

View File

@ -1,3 +1,6 @@
{
"files.exclude": {
"**/bin": true
},
"editor.formatOnType": true
}

View File

@ -7,14 +7,14 @@ namespace ShoppingAssistantApi.Api.Mutations;
public class AccessMutation
{
public Task<TokensModel> LoginAsync(AccessUserModel login, CancellationToken cancellationToken,
[Service] IUserManager userManager)
=> userManager.LoginAsync(login, cancellationToken);
[Service] IUserManager userManager)
=> userManager.LoginAsync(login, cancellationToken);
public Task<TokensModel> AccessGuestAsync(AccessGuestModel guest, CancellationToken cancellationToken,
[Service] IUserManager userManager)
=> userManager.AccessGuestAsync(guest, cancellationToken);
[Service] IUserManager userManager)
=> userManager.AccessGuestAsync(guest, cancellationToken);
public Task<TokensModel> RefreshUserTokenAsync(TokensModel model, CancellationToken cancellationToken,
[Service] ITokensService tokensService)
=> tokensService.RefreshUserAsync(model, cancellationToken);
public Task<TokensModel> RefreshAccessTokenAsync(TokensModel model, CancellationToken cancellationToken,
[Service] IUserManager userManager)
=> userManager.RefreshAccessTokenAsync(model, cancellationToken);
}

View File

@ -1,22 +1,14 @@
using ShoppingAssistantApi.Application.IServices.Identity;
using HotChocolate.Authorization;
using ShoppingAssistantApi.Application.IServices;
using ShoppingAssistantApi.Application.Models.CreateDtos;
using ShoppingAssistantApi.Application.Models.Dtos;
using ShoppingAssistantApi.Application.Models.Identity;
namespace ShoppingAssistantApi.Api.Mutations;
[ExtendObjectType(OperationTypeNames.Mutation)]
public class RolesMutation
{
public Task<TokensModel> AddToRoleAsync(string roleName, string id, CancellationToken cancellationToken,
[Service] IUserManager userManager)
=> userManager.AddToRoleAsync(roleName, id, cancellationToken);
public Task<TokensModel> RemoveFromRoleAsync(string roleName, string id, CancellationToken cancellationToken,
[Service] IUserManager userManager)
=> userManager.RemoveFromRoleAsync(roleName, id, cancellationToken);
[Authorize]
public Task<RoleDto> AddRole(RoleCreateDto roleDto, CancellationToken cancellationToken,
[Service] IRolesService rolesService)
=> rolesService.AddRoleAsync(roleDto, cancellationToken);

View File

@ -10,11 +10,21 @@ public class UsersMutation
{
[Authorize]
public Task<UpdateUserModel> UpdateUserAsync(UserDto userDto, CancellationToken cancellationToken,
[Service] IUserManager userManager)
=> userManager.UpdateAsync(userDto, cancellationToken);
[Service] IUserManager userManager)
=> userManager.UpdateAsync(userDto, cancellationToken);
[Authorize]
public Task<UpdateUserModel> UpdateUserByAdminAsync(string id, UserDto userDto, CancellationToken cancellationToken,
[Service] IUserManager userManager)
=> userManager.UpdateUserByAdminAsync(id, userDto, cancellationToken);
public Task<UserDto> UpdateUserByAdminAsync(string id, UserDto userDto, CancellationToken cancellationToken,
[Service] IUserManager userManager)
=> userManager.UpdateUserByAdminAsync(id, userDto, cancellationToken);
[Authorize]
public Task<UserDto> AddToRoleAsync(string roleName, string userId, CancellationToken cancellationToken,
[Service] IUserManager userManager)
=> userManager.AddToRoleAsync(roleName, userId, cancellationToken);
[Authorize]
public Task<UserDto> RemoveFromRoleAsync(string roleName, string userId, CancellationToken cancellationToken,
[Service] IUserManager userManager)
=> userManager.RemoveFromRoleAsync(roleName, userId, cancellationToken);
}

View File

@ -1,4 +1,5 @@
using ShoppingAssistantApi.Domain.Common;
using MongoDB.Bson;
using ShoppingAssistantApi.Domain.Common;
using System.Linq.Expressions;
namespace ShoppingAssistantApi.Application.IRepositories;
@ -7,6 +8,10 @@ public interface IBaseRepository<TEntity> where TEntity : EntityBase
{
Task<TEntity> AddAsync(TEntity entity, CancellationToken cancellationToken);
Task<TEntity> GetOneAsync(ObjectId id, CancellationToken cancellationToken);
Task<TEntity> GetOneAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken);
Task<List<TEntity>> GetPageAsync(int pageNumber, int pageSize, CancellationToken cancellationToken);
Task<List<TEntity>> GetPageAsync(int pageNumber, int pageSize, Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken);

View File

@ -0,0 +1,8 @@
using ShoppingAssistantApi.Domain.Entities;
namespace ShoppingAssistantApi.Application.IRepositories;
public interface IRefreshTokensRepository : IBaseRepository<RefreshToken>
{
}

View File

@ -1,5 +1,4 @@
using ShoppingAssistantApi.Application.Models.Identity;
using System.Security.Claims;
using System.Security.Claims;
namespace ShoppingAssistantApi.Application.IServices.Identity;
@ -9,5 +8,5 @@ public interface ITokensService
string GenerateRefreshToken();
Task<TokensModel> RefreshUserAsync(TokensModel tokensModel, CancellationToken cancellationToken);
ClaimsPrincipal GetPrincipalFromExpiredToken(string token);
}

View File

@ -10,11 +10,13 @@ public interface IUserManager
Task<TokensModel> LoginAsync(AccessUserModel login, CancellationToken cancellationToken);
Task<TokensModel> AddToRoleAsync(string roleName, string id, CancellationToken cancellationToken);
Task<UserDto> AddToRoleAsync(string roleName, string userId, CancellationToken cancellationToken);
Task<TokensModel> RemoveFromRoleAsync(string roleName, string id, CancellationToken cancellationToken);
Task<UserDto> RemoveFromRoleAsync(string roleName, string userId, CancellationToken cancellationToken);
Task<UpdateUserModel> UpdateAsync(UserDto userDto, CancellationToken cancellationToken);
Task<UpdateUserModel> UpdateUserByAdminAsync(string id, UserDto userDto, CancellationToken cancellationToken);
Task<UserDto> UpdateUserByAdminAsync(string id, UserDto userDto, CancellationToken cancellationToken);
Task<TokensModel> RefreshAccessTokenAsync(TokensModel tokensModel, CancellationToken cancellationToken);
}

View File

@ -5,9 +5,9 @@ namespace ShoppingAssistantApi.Domain.Entities;
public class Message : EntityBase
{
public required string Text { get; set; }
public string Text { get; set; }
public required string Role { get; set; }
public string Role { get; set; }
public required ObjectId WishlistId { get; set; }
public ObjectId WishlistId { get; set; }
}

View File

@ -6,17 +6,17 @@ namespace ShoppingAssistantApi.Domain.Entities;
public class Product : EntityBase
{
public required string Url { get; set; }
public string Url { get; set; }
public required string Name { get; set; }
public string Name { get; set; }
public required string Description { get; set; }
public string Description { get; set; }
public required double Rating { get; set; }
public double Rating { get; set; }
public required string[] ImagesUrls { get; set; }
public string[] ImagesUrls { get; set; }
public required bool WasOpened { get; set; }
public bool WasOpened { get; set; }
public required ObjectId WishlistId { get; set; }
public ObjectId WishlistId { get; set; }
}

View File

@ -0,0 +1,10 @@
using ShoppingAssistantApi.Domain.Common;
namespace ShoppingAssistantApi.Domain.Entities;
public class RefreshToken : EntityBase
{
public string Token { get; set; }
public DateTime ExpiryDateUTC { get; set; }
}

View File

@ -13,8 +13,4 @@ public class User : EntityBase
public string? Email { get; set; }
public string? PasswordHash { get; set; }
public string RefreshToken { get; set; }
public DateTime RefreshTokenExpiryDate { get; set; }
}

View File

@ -4,9 +4,9 @@ namespace ShoppingAssistantApi.Domain.Entities;
public class Wishlist : EntityBase
{
public required string Name { get; set; }
public string Name { get; set; }
public required string Type { get; set; }
public string Type { get; set; }
public ICollection<Message>? Messages { get; set; }
}

View File

@ -5,10 +5,7 @@ using System.Text;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using MongoDB.Bson;
using ShoppingAssistantApi.Application.IRepositories;
using ShoppingAssistantApi.Application.IServices.Identity;
using ShoppingAssistantApi.Application.Models.Identity;
namespace ShoppingAssistantApi.Infrastructure.Services.Identity;
@ -16,50 +13,16 @@ public class TokensService : ITokensService
{
private readonly IConfiguration _configuration;
private readonly IUsersRepository _usersRepository;
private readonly ILogger _logger;
public TokensService(IConfiguration configuration, IUsersRepository usersRepository,
ILogger<TokensService> logger)
public TokensService(
IConfiguration configuration,
ILogger<TokensService> logger)
{
this._configuration = configuration;
this._usersRepository = usersRepository;
this._logger = logger;
}
public async Task<TokensModel> RefreshUserAsync(TokensModel tokensModel, CancellationToken cancellationToken)
{
var principal = this.GetPrincipalFromExpiredToken(tokensModel.AccessToken);
var userId = principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
if (!ObjectId.TryParse(userId, out var objectId))
{
throw new InvalidDataException("Provided id is invalid.");
}
var user = await this._usersRepository.GetUserAsync(objectId, cancellationToken);
if (user == null || user?.RefreshToken != tokensModel.RefreshToken
|| user?.RefreshTokenExpiryDate <= DateTime.UtcNow)
{
throw new SecurityTokenExpiredException();
}
var newAccessToken = this.GenerateAccessToken(principal.Claims);
var newRefreshToken = this.GenerateRefreshToken();
user.RefreshToken = newRefreshToken;
user.RefreshTokenExpiryDate = DateTime.UtcNow.AddDays(30);
await this._usersRepository.UpdateUserAsync(user, cancellationToken);
this._logger.LogInformation($"Refreshed user tokens.");
return new TokensModel
{
AccessToken = newAccessToken,
RefreshToken = newRefreshToken
};
}
public string GenerateAccessToken(IEnumerable<Claim> claims)
{
var tokenOptions = GetTokenOptions(claims);
@ -73,18 +36,16 @@ public class TokensService : ITokensService
public string GenerateRefreshToken()
{
var randomNumber = new byte[32];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(randomNumber);
var refreshToken = Convert.ToBase64String(randomNumber);
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomNumber);
var refreshToken = Convert.ToBase64String(randomNumber);
this._logger.LogInformation($"Generated new refresh token.");
this._logger.LogInformation($"Generated new refresh token.");
return refreshToken;
}
return refreshToken;
}
private ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
public ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
{
var tokenValidationParameters = new TokenValidationParameters
{
@ -96,11 +57,10 @@ public class TokensService : ITokensService
ValidateLifetime = false
};
var tokenHandler = new JwtSecurityTokenHandler();
SecurityToken securityToken;
var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out securityToken);
var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out var securityToken);
var jwtSecurityToken = securityToken as JwtSecurityToken;
if (jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256,
StringComparison.InvariantCultureIgnoreCase))
if (jwtSecurityToken == null
|| !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
throw new SecurityTokenException("Invalid token");
this._logger.LogInformation($"Returned data from expired access token.");
@ -117,7 +77,7 @@ public class TokensService : ITokensService
var tokenOptions = new JwtSecurityToken(
issuer: _configuration.GetValue<string>("JsonWebTokenKeys:ValidIssuer"),
audience: _configuration.GetValue<string>("JsonWebTokenKeys:ValidAudience"),
expires: DateTime.UtcNow.AddMinutes(5),
expires: DateTime.UtcNow.AddMinutes(15),
claims: claims,
signingCredentials: signinCredentials
);

View File

@ -1,7 +1,6 @@
using AutoMapper;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Win32;
using MongoDB.Bson;
using ShoppingAssistantApi.Application.Exceptions;
using ShoppingAssistantApi.Application.GlobalInstances;
@ -14,23 +13,32 @@ using ShoppingAssistantApi.Domain.Entities;
using System.Security.Claims;
using System.Text.RegularExpressions;
namespace ShoppingAssistantApi.Infrastructure.Services.Identity;
public class UserManager : IUserManager
public class UserManager : ServiceBase, IUserManager
{
private readonly IUsersRepository _usersRepository;
private readonly ILogger _logger;
private readonly IPasswordHasher _passwordHasher;
private readonly ITokensService _tokensService;
private readonly IRolesRepository _rolesRepository;
private readonly IRefreshTokensRepository _refreshTokensRepository;
private readonly IMapper _mapper;
private readonly IRolesRepository _rolesRepository;
private readonly ILogger _logger;
public UserManager(IUsersRepository usersRepository, ILogger<UserManager> logger, IPasswordHasher passwordHasher, ITokensService tokensService, IMapper mapper, IRolesRepository rolesRepository)
public UserManager(
IUsersRepository usersRepository,
IPasswordHasher passwordHasher,
ITokensService tokensService,
IRolesRepository rolesRepository,
IRefreshTokensRepository refreshTokensRepository,
IMapper mapper,
ILogger<UserManager> logger)
{
this._usersRepository = usersRepository;
this._logger = logger;
@ -38,15 +46,16 @@ public class UserManager : IUserManager
this._tokensService = tokensService;
this._mapper = mapper;
this._rolesRepository = rolesRepository;
this._refreshTokensRepository = refreshTokensRepository;
}
public async Task<TokensModel> LoginAsync(AccessUserModel login, CancellationToken cancellationToken)
{
var user = login.Email != null
? await this._usersRepository.GetUserAsync(x => x.Email == login.Email, cancellationToken)
: await this._usersRepository.GetUserAsync(x => x.Phone == login.Phone, cancellationToken);
_logger.LogInformation($"Logging in user with email: {login.Email} and phone: {login.Phone}.");
var user = string.IsNullOrEmpty(login.Phone)
? await this._usersRepository.GetUserAsync(u => u.Email == login.Email, cancellationToken)
: await this._usersRepository.GetUserAsync(u => u.Phone == login.Phone, cancellationToken);
if (user == null)
{
throw new EntityNotFoundException<User>();
@ -57,197 +66,216 @@ public class UserManager : IUserManager
throw new InvalidDataException("Invalid password!");
}
user.RefreshToken = this.GetRefreshToken();
user.RefreshTokenExpiryDate = DateTime.UtcNow.AddDays(30);
await this._usersRepository.UpdateUserAsync(user, cancellationToken);
var tokens = this.GetUserTokens(user);
var refreshToken = await AddRefreshToken(user.Id, cancellationToken);
this._logger.LogInformation($"Logged in user with email: {login.Email}.");
var tokens = this.GetUserTokens(user, refreshToken);
this._logger.LogInformation($"Logged in user with email: {login.Email} and phone: {login.Phone}.");
return tokens;
}
public async Task<TokensModel> AccessGuestAsync(AccessGuestModel guest, CancellationToken cancellationToken)
{
_logger.LogInformation($"Logging in / Registering guest with guest id: {guest.GuestId}.");
var user = await this._usersRepository.GetUserAsync(x => x.GuestId == guest.GuestId, cancellationToken);
if (user != null)
if (user == null)
{
user.RefreshToken = this.GetRefreshToken();
await this._usersRepository.UpdateUserAsync(user, cancellationToken);
var userTokens = this.GetUserTokens(user);
var role = await this._rolesRepository.GetRoleAsync(r => r.Name == "Guest", cancellationToken);
user = new User
{
GuestId = guest.GuestId,
Roles = new List<Role> { role },
CreatedDateUtc = DateTime.UtcNow,
CreatedById = ObjectId.Empty // Default value for all new users
};
this._logger.LogInformation($"Logged in guest with guest id: {guest.GuestId}.");
await this._usersRepository.AddAsync(user, cancellationToken);
return userTokens;
this._logger.LogInformation($"Created guest with guest id: {guest.GuestId}.");
}
var role = await this._rolesRepository.GetRoleAsync(r => r.Name == "Guest", cancellationToken);
var refreshToken = await AddRefreshToken(user.Id, cancellationToken);
var tokens = this.GetUserTokens(user, refreshToken);
var newUser = new User
{
GuestId = guest.GuestId,
Roles = new List<Role> { role },
RefreshToken = this.GetRefreshToken(),
RefreshTokenExpiryDate = DateTime.Now.AddDays(30),
CreatedDateUtc = DateTime.UtcNow,
LastModifiedDateUtc = DateTime.UtcNow
};
await this._usersRepository.AddAsync(newUser, cancellationToken);
var tokens = this.GetUserTokens(newUser);
this._logger.LogInformation($"Created guest with guest id: {guest.GuestId}.");
this._logger.LogInformation($"Logged in guest with guest id: {guest.GuestId}.");
return tokens;
}
public async Task<TokensModel> AddToRoleAsync(string roleName, string id, CancellationToken cancellationToken)
public async Task<TokensModel> RefreshAccessTokenAsync(TokensModel tokensModel, CancellationToken cancellationToken)
{
_logger.LogInformation($"Refreshing access token.");
var principal = _tokensService.GetPrincipalFromExpiredToken(tokensModel.AccessToken);
var userId = ParseObjectId(principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value);
var refreshTokenModel = await this._refreshTokensRepository
.GetOneAsync(r =>
r.Token == tokensModel.RefreshToken
&& r.CreatedById == userId
&& r.IsDeleted == false, cancellationToken);
if (refreshTokenModel == null || refreshTokenModel.ExpiryDateUTC < DateTime.UtcNow)
{
throw new SecurityTokenExpiredException();
}
var refreshToken = refreshTokenModel.Token;
// Update Refresh token if it expires in less than 7 days to keep user constantly logged in if he uses the app
if (refreshTokenModel.ExpiryDateUTC.AddDays(-7) < DateTime.UtcNow)
{
await _refreshTokensRepository.DeleteAsync(refreshTokenModel, cancellationToken);
var newRefreshToken = await AddRefreshToken(userId, cancellationToken);
refreshToken = newRefreshToken.Token;
}
var tokens = new TokensModel
{
AccessToken = _tokensService.GenerateAccessToken(principal.Claims),
RefreshToken = refreshToken
};
this._logger.LogInformation($"Refreshed access token.");
return tokens;
}
public async Task<UserDto> AddToRoleAsync(string roleName, string userId, CancellationToken cancellationToken)
{
_logger.LogInformation($"Adding Role: {roleName} to User with Id: {userId}.");
var role = await this._rolesRepository.GetRoleAsync(r => r.Name == roleName, cancellationToken);
if (role == null)
{
throw new EntityNotFoundException<Role>();
}
if (!ObjectId.TryParse(id, out var objectId))
{
throw new InvalidDataException("Provided id is invalid.");
}
var user = await this._usersRepository.GetUserAsync(objectId, cancellationToken);
var userObjectId = ParseObjectId(userId);
var user = await this._usersRepository.GetUserAsync(userObjectId, cancellationToken);
if (user == null)
{
throw new EntityNotFoundException<User>();
}
user.Roles.Add(role);
await this._usersRepository.UpdateUserAsync(user, cancellationToken);
var tokens = this.GetUserTokens(user);
var updatedUser = await this._usersRepository.UpdateUserAsync(user, cancellationToken);
var userDto = this._mapper.Map<UserDto>(updatedUser);
this._logger.LogInformation($"Added role {roleName} to user with id: {id}.");
this._logger.LogInformation($"Added Role: {roleName} to User with Id: {userId}.");
return tokens;
return userDto;
}
public async Task<TokensModel> RemoveFromRoleAsync(string roleName, string id, CancellationToken cancellationToken)
public async Task<UserDto> RemoveFromRoleAsync(string roleName, string userId, CancellationToken cancellationToken)
{
_logger.LogInformation($"Removing Role: {roleName} from User with Id: {userId}.");
var role = await this._rolesRepository.GetRoleAsync(r => r.Name == roleName, cancellationToken);
if (role == null)
{
throw new EntityNotFoundException<Role>();
}
if (!ObjectId.TryParse(id, out var objectId))
{
throw new InvalidDataException("Provided id is invalid.");
}
var user = await this._usersRepository.GetUserAsync(objectId, cancellationToken);
var userObjectId = ParseObjectId(userId);
var user = await this._usersRepository.GetUserAsync(userObjectId, cancellationToken);
if (user == null)
{
throw new EntityNotFoundException<User>();
}
var deletedRole = user.Roles.Find(x => x.Name == role.Name);
user.Roles.Remove(deletedRole);
await this._usersRepository.UpdateUserAsync(user, cancellationToken);
var tokens = this.GetUserTokens(user);
this._logger.LogInformation($"Added role {roleName} to user with id: {id}.");
var updatedUser = await this._usersRepository.UpdateUserAsync(user, cancellationToken);
var userDto = this._mapper.Map<UserDto>(updatedUser);
return tokens;
this._logger.LogInformation($"Removed Role: {roleName} from User with Id: {userId}.");
return userDto;
}
public async Task<UpdateUserModel> UpdateAsync(UserDto userDto, CancellationToken cancellationToken)
{
if (userDto.Email != null) ValidateEmail(userDto.Email);
if (userDto.Phone != null) ValidateNumber(userDto.Phone);
if (userDto.Roles.Any(x => x.Name == "Guest") && !userDto.Roles.Any(x => x.Name == "User"))
{
if (userDto.Password != null && (userDto.Email != null || userDto.Phone != null))
{
var roleEntity = await this._rolesRepository.GetRoleAsync(x => x.Name == "User", cancellationToken);
var roleDto = this._mapper.Map<RoleDto>(roleEntity);
userDto.Roles.Add(roleDto);
}
}
_logger.LogInformation($"Updating user with id: {GlobalUser.Id}.");
var user = await this._usersRepository.GetUserAsync(x => x.Id == GlobalUser.Id, cancellationToken);
if (user == null)
{
throw new EntityNotFoundException<User>();
}
if (userDto.Roles.Any(x => x.Name == "User") && userDto.Email != null)
{
if (await this._usersRepository.GetUserAsync(x => x.Email == userDto.Email, cancellationToken) != null)
{
throw new EntityAlreadyExistsException<User>("email", userDto.Email);
}
}
if (userDto.Roles.Any(x => x.Name == "User") && userDto.Phone != null)
{
if (await this._usersRepository.GetUserAsync(x => x.Phone == userDto.Phone, cancellationToken) != null)
{
throw new EntityAlreadyExistsException<User>("phone", userDto.Phone);
}
}
await ValidateUserAsync(userDto, user, cancellationToken);
this._mapper.Map(userDto, user);
if (!userDto.Password.IsNullOrEmpty())
if (!string.IsNullOrEmpty(userDto.Password))
{
user.PasswordHash = this._passwordHasher.Hash(userDto.Password);
}
user.RefreshToken = this.GetRefreshToken();
await this._usersRepository.UpdateUserAsync(user, cancellationToken);
await CheckAndUpgradeToUserAsync(user, cancellationToken);
var tokens = this.GetUserTokens(user);
var updatedUser = await this._usersRepository.UpdateUserAsync(user, cancellationToken);
this._logger.LogInformation($"Update user with id: {GlobalUser.Id.ToString()}.");
var refreshToken = await AddRefreshToken(user.Id, cancellationToken);
var tokens = this.GetUserTokens(user, refreshToken);
return new UpdateUserModel() { Tokens = tokens, User = this._mapper.Map<UserDto>(user) };
var updatedUserDto = this._mapper.Map<UserDto>(updatedUser);
this._logger.LogInformation($"Update user with id: {GlobalUser.Id}.");
return new UpdateUserModel()
{
Tokens = tokens,
User = updatedUserDto
};
}
public async Task<UpdateUserModel> UpdateUserByAdminAsync(string id, UserDto userDto, CancellationToken cancellationToken)
public async Task<UserDto> UpdateUserByAdminAsync(string id, UserDto userDto, CancellationToken cancellationToken)
{
if (!ObjectId.TryParse(id, out var objectId))
{
throw new InvalidDataException("Provided id is invalid.");
}
var user = await this._usersRepository.GetUserAsync(objectId, cancellationToken);
_logger.LogInformation($"Admin updating User with Id: {id}.");
var userObjectId = ParseObjectId(id);
var user = await this._usersRepository.GetUserAsync(userObjectId, cancellationToken);
if (user == null)
{
throw new EntityNotFoundException<User>();
}
await ValidateUserAsync(userDto, user, cancellationToken);
this._mapper.Map(userDto, user);
var updatedUser = await this._usersRepository.UpdateUserAsync(user, cancellationToken);
user.RefreshToken = this.GetRefreshToken();
await this._usersRepository.UpdateUserAsync(user, cancellationToken);
var updatedUserDto = this._mapper.Map<UserDto>(updatedUser);
this._logger.LogInformation($"Admin updated User with Id: {id}.");
var tokens = this.GetUserTokens(user);
this._logger.LogInformation($"Update user with id: {id}.");
return new UpdateUserModel() { Tokens = tokens, User = this._mapper.Map<UserDto>(user) };
return updatedUserDto;
}
private string GetRefreshToken()
private async Task<RefreshToken> AddRefreshToken(ObjectId userId, CancellationToken cancellationToken)
{
var refreshToken = this._tokensService.GenerateRefreshToken();
_logger.LogInformation($"Adding new refresh token for user with Id : {userId}.");
this._logger.LogInformation($"Returned new refresh token.");
var refreshToken = new RefreshToken
{
Token = _tokensService.GenerateRefreshToken(),
ExpiryDateUTC = DateTime.UtcNow.AddDays(30),
CreatedById = userId,
CreatedDateUtc = DateTime.UtcNow
};
await this._refreshTokensRepository.AddAsync(refreshToken, cancellationToken);
this._logger.LogInformation($"Added new refresh token.");
return refreshToken;
}
private TokensModel GetUserTokens(User user)
private TokensModel GetUserTokens(User user, RefreshToken refreshToken)
{
var claims = this.GetClaims(user);
var accessToken = this._tokensService.GenerateAccessToken(claims);
@ -257,7 +285,7 @@ public class UserManager : IUserManager
return new TokensModel
{
AccessToken = accessToken,
RefreshToken = user.RefreshToken,
RefreshToken = refreshToken.Token,
};
}
@ -265,21 +293,56 @@ public class UserManager : IUserManager
{
var claims = new List<Claim>()
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Email, user.Email ?? string.Empty),
new Claim(ClaimTypes.MobilePhone, user.Phone ?? string.Empty),
new (ClaimTypes.NameIdentifier, user.Id.ToString()),
new (ClaimTypes.Email, user.Email ?? string.Empty),
new (ClaimTypes.MobilePhone, user.Phone ?? string.Empty),
};
foreach (var role in user.Roles)
{
claims.Add(new Claim(ClaimTypes.Role, role.Name));
claims.Add(new (ClaimTypes.Role, role.Name));
}
this._logger.LogInformation($"Returned claims for user with id: {user.Id.ToString()}.");
this._logger.LogInformation($"Returned claims for User with Id: {user.Id}.");
return claims;
}
private async Task CheckAndUpgradeToUserAsync(User user, CancellationToken cancellationToken)
{
if (user.Roles.Any(x => x.Name == "Guest") && !user.Roles.Any(x => x.Name == "User"))
{
if (!string.IsNullOrEmpty(user.PasswordHash) && (!string.IsNullOrEmpty(user.Email) || !string.IsNullOrEmpty(user.Phone)))
{
var role = await this._rolesRepository.GetRoleAsync(x => x.Name == "User", cancellationToken);
user.Roles.Add(role);
}
}
}
private async Task ValidateUserAsync(UserDto userDto, User user, CancellationToken cancellationToken)
{
if (!string.IsNullOrEmpty(userDto.Email))
{
ValidateEmail(userDto.Email);
if (userDto.Email != user.Email
&& await this._usersRepository.ExistsAsync(x => x.Email == userDto.Email, cancellationToken))
{
throw new EntityAlreadyExistsException<User>("email", userDto.Email);
}
}
if (!string.IsNullOrEmpty(userDto.Phone))
{
ValidatePhone(userDto.Phone);
if (userDto.Phone != user.Phone
&& await this._usersRepository.ExistsAsync(x => x.Phone == userDto.Phone, cancellationToken))
{
throw new EntityAlreadyExistsException<User>("phone", userDto.Phone);
}
}
}
private void ValidateEmail(string email)
{
string regex = @"^[^@\s]+@[^@\s]+\.[^@\s]+$";
@ -290,7 +353,7 @@ public class UserManager : IUserManager
}
}
private void ValidateNumber(string phone)
private void ValidatePhone(string phone)
{
string regex = @"^\+[0-9]{1,15}$";

View File

@ -33,14 +33,16 @@ public class RolesService : IRolesService
entity.CreatedDateUtc = DateTime.UtcNow;
entity.LastModifiedDateUtc = DateTime.UtcNow;
await this._repository.AddAsync(entity, cancellationToken);
return this._mapper.Map<RoleDto>(entity);
}
public async Task<PagedList<RoleDto>> GetRolesPageAsync(int pageNumber, int pageSize, CancellationToken cancellationToken)
{
var entities = await this._repository.GetPageAsync(pageNumber, pageSize, cancellationToken);
var dtos = this._mapper.Map<List<RoleDto>>(entities);
var count = await this._repository.GetTotalCountAsync();
var dtos = this._mapper.Map<List<RoleDto>>(entities);
return new PagedList<RoleDto>(dtos, pageNumber, pageSize, count);
}
}

View File

@ -0,0 +1,15 @@
namespace ShoppingAssistantApi.Infrastructure.Services;
using MongoDB.Bson;
public abstract class ServiceBase
{
public ObjectId ParseObjectId(string? id)
{
if (ObjectId.TryParse(id, out ObjectId objectId))
{
return objectId;
}
throw new InvalidDataException("Provided id cannot be parsed to a MongoDb ObjectId.");
}
}

View File

@ -16,4 +16,6 @@ public class MongoDbContext
}
public IMongoDatabase Db => this._db;
public MongoClient Client => this._client;
}

View File

@ -13,6 +13,7 @@ public static class RepositoriesExtention
services.AddScoped<IRolesRepository, RolesRepository>();
services.AddScoped<IUsersRepository, UsersRepository>();
services.AddScoped<IRefreshTokensRepository, RefreshTokensRepository>();
services.AddScoped<IWishlistsRepository, WishlistsRepository>();
services.AddScoped<IMessagesRepository, MessagesRepository>();
services.AddScoped<IProductsRepository, ProductsRepository>();

View File

@ -1,11 +1,13 @@
using MongoDB.Driver;
using MongoDB.Bson;
using MongoDB.Driver;
using ShoppingAssistantApi.Application.IRepositories;
using ShoppingAssistantApi.Domain.Common;
using ShoppingAssistantApi.Persistance.Database;
using System.Linq.Expressions;
namespace ShoppingAssistantApi.Persistance.Repositories;
public abstract class BaseRepository<TEntity> where TEntity : EntityBase
public abstract class BaseRepository<TEntity> : IBaseRepository<TEntity> where TEntity : EntityBase
{
protected MongoDbContext _db;
@ -17,6 +19,16 @@ public abstract class BaseRepository<TEntity> where TEntity : EntityBase
this._collection = _db.Db.GetCollection<TEntity>(collectionName);
}
public async Task<TEntity> GetOneAsync(ObjectId id, CancellationToken cancellationToken)
{
return await this._collection.Find(x => x.Id == id).FirstOrDefaultAsync(cancellationToken);
}
public async Task<TEntity> GetOneAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken)
{
return await this._collection.Find(predicate).FirstOrDefaultAsync(cancellationToken);
}
public async Task<TEntity> AddAsync(TEntity entity, CancellationToken cancellationToken)
{
await this._collection.InsertOneAsync(entity, new InsertOneOptions(), cancellationToken);

View File

@ -1,9 +0,0 @@
using ShoppingAssistantApi.Domain.Entities;
using ShoppingAssistantApi.Persistance.Database;
namespace ShoppingAssistantApi.Persistance.Repositories;
public class ProductsRepository : BaseRepository<Product>
{
public ProductsRepository(MongoDbContext db) : base(db, "Products") { }
}

View File

@ -0,0 +1,10 @@
using ShoppingAssistantApi.Application.IRepositories;
using ShoppingAssistantApi.Domain.Entities;
using ShoppingAssistantApi.Persistance.Database;
namespace ShoppingAssistantApi.Persistance.Repositories;
public class RefreshTokensRepository : BaseRepository<RefreshToken>, IRefreshTokensRepository
{
public RefreshTokensRepository(MongoDbContext db) : base(db, "RefreshTokens") { }
}

View File

@ -27,8 +27,6 @@ public class UsersRepository : BaseRepository<User>, IUsersRepository
var updateDefinition = Builders<User>.Update
.Set(u => u.Email, user.Email)
.Set(u => u.Phone, user.Phone)
.Set(u => u.RefreshToken, user.RefreshToken)
.Set(u => u.RefreshTokenExpiryDate, user.RefreshTokenExpiryDate)
.Set(u => u.GuestId, user.GuestId)
.Set(u => u.Roles, user.Roles)
.Set(u => u.PasswordHash, user.PasswordHash)
@ -42,7 +40,5 @@ public class UsersRepository : BaseRepository<User>, IUsersRepository
return await this._collection.FindOneAndUpdateAsync(
Builders<User>.Filter.Eq(u => u.Id, user.Id), updateDefinition, options, cancellationToken);
}
}

View File

@ -10,7 +10,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GraphQL.Client" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.9" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="Mongo2Go" Version="3.1.3" />

View File

@ -1,65 +0,0 @@
using System.Text;
using Newtonsoft.Json;
using ShoppingAssistantApi.Application.Models.Identity;
namespace ShoppingAssistantApi.Tests.TestExtentions;
public static class AccessExtention
{
public static async Task<TokensModel> Login(string email, string password, HttpClient httpClient)
{
var mutation = new
{
query = "mutation Login($login: AccessUserModelInput!) { login(login: $login) { accessToken refreshToken }}",
variables = new
{
login = new
{
email = email,
password = password
}
}
};
var jsonPayload = JsonConvert.SerializeObject(mutation);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
using var response = await httpClient.PostAsync("graphql", content);
var responseString = await response.Content.ReadAsStringAsync();
var document = JsonConvert.DeserializeObject<dynamic>(responseString);
return new TokensModel
{
AccessToken = (string)document.data.login.accessToken,
RefreshToken = (string)document.data.login.refreshToken
};
}
public static async Task<TokensModel> CreateGuest(string guestId, HttpClient httpClient)
{
var mutation = new
{
query = "mutation AccessGuest($guest: AccessGuestModelInput!) { accessGuest(guest: $guest) { accessToken, refreshToken } }",
variables = new
{
guest = new
{
guestId
}
}
};
var jsonPayload = JsonConvert.SerializeObject(mutation);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
using var response = await httpClient.PostAsync("graphql", content);
var responseString = await response.Content.ReadAsStringAsync();
var document = JsonConvert.DeserializeObject<dynamic>(responseString);
return new TokensModel
{
AccessToken = (string)document.data.accessGuest.accessToken,
RefreshToken = (string)document.data.accessGuest.refreshToken
};
}
}

View File

@ -0,0 +1,255 @@
using Microsoft.Extensions.Logging;
using MongoDB.Bson;
using MongoDB.Driver;
using ShoppingAssistantApi.Domain.Entities;
using ShoppingAssistantApi.Domain.Enums;
using ShoppingAssistantApi.Infrastructure.Services.Identity;
using ShoppingAssistantApi.Persistance.Database;
namespace ShoppingAssistantApi.Tests.TestExtentions;
public class DbInitializer
{
private readonly MongoDbContext _dbContext;
public DbInitializer(MongoDbContext dbContext)
{
_dbContext = dbContext;
}
public void InitializeDb()
{
_dbContext.Client.DropDatabase(_dbContext.Db.DatabaseNamespace.DatabaseName);
InitializeUsersAsync().Wait();
InitializeWishlistsAsync().Wait();
InitializeMessagesAsync().Wait();
InitializeProductsAsync().Wait();
}
public async Task InitializeUsersAsync()
{
#region Roles
var rolesCollection = _dbContext.Db.GetCollection<Role>("Roles");
var questRole = new Role
{
Name = "Guest"
};
await rolesCollection.InsertOneAsync(questRole);
var userRole = new Role
{
Name = "User"
};
await rolesCollection.InsertOneAsync(userRole);
var adminRole = new Role
{
Name = "Admin"
};
await rolesCollection.InsertOneAsync(adminRole);
#endregion
#region Users
var passwordHasher = new PasswordHasher(new Logger<PasswordHasher>(new LoggerFactory()));
var usersCollection = _dbContext.Db.GetCollection<User>("Users");
var testUser = new User
{
Id = ObjectId.Parse("652c3b89ae02a3135d6409fc"),
Email = "test@gmail.com",
Phone = "+380953326869",
Roles = new List<Role> { questRole, userRole },
PasswordHash = passwordHasher.Hash("Yuiop12345"),
CreatedById = ObjectId.Empty,
CreatedDateUtc = DateTime.UtcNow
};
await usersCollection.InsertOneAsync(testUser);
var adminUser = new User
{
Id = ObjectId.Parse("652c3b89ae02a3135d6408fc"),
Email = "admin@gmail.com",
Phone = "+12345678901",
Roles = new List<Role> { questRole, userRole, adminRole },
PasswordHash = passwordHasher.Hash("Yuiop12345"),
CreatedById = ObjectId.Empty,
CreatedDateUtc = DateTime.UtcNow
};
await usersCollection.InsertOneAsync(adminUser);
var wishlistsUser = new User
{
Id = ObjectId.Parse("652c3b89ae02a3135d6418fc"),
Email = "wishlists@gmail.com",
Phone = "+12234567890",
Roles = new List<Role> { questRole },
PasswordHash = passwordHasher.Hash("Yuiop12345"),
CreatedById = ObjectId.Empty,
CreatedDateUtc = DateTime.UtcNow
};
await usersCollection.InsertOneAsync(wishlistsUser);
#endregion
}
public async Task InitializeWishlistsAsync()
{
var wishlistsCollection = _dbContext.Db.GetCollection<Wishlist>("Wishlists");
var usersCollection = _dbContext.Db.GetCollection<User>("Users");
var user1 = await (await usersCollection.FindAsync(x => x.Email!.Equals("wishlists@gmail.com"))).FirstAsync();
var user2 = await (await usersCollection.FindAsync(x => x.Email!.Equals("test@gmail.com"))).FirstAsync();
var wishlistId1 = ObjectId.Parse("ab79cde6f69abcd3efab65cd");
var wishlistId2 = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab");
var wishlists = new Wishlist[]
{
new Wishlist
{
Id = wishlistId1,
Name = "Gaming PC",
Type = WishlistTypes.Product.ToString(),
CreatedById = user1.Id,
CreatedDateUtc = DateTime.UtcNow
},
new Wishlist
{
Id = wishlistId2,
Name = "Generic Wishlist Name",
Type = WishlistTypes.Product.ToString(),
CreatedById = user2.Id,
CreatedDateUtc = DateTime.UtcNow
}
};
await wishlistsCollection.InsertManyAsync(wishlists);
}
public async Task InitializeMessagesAsync()
{
var messagesCollection = _dbContext.Db.GetCollection<Message>("Messages");
var usersCollection = _dbContext.Db.GetCollection<User>("Users");
var user1 = await (await usersCollection.FindAsync(x => x.Email!.Equals("wishlists@gmail.com"))).FirstAsync();
var user2 = await (await usersCollection.FindAsync(x => x.Email!.Equals("test@gmail.com"))).FirstAsync();
var wishlistId1 = ObjectId.Parse("ab79cde6f69abcd3efab65cd");
var wishlistId2 = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab");
var messages = new Message[]
{
new Message
{
Text = "Message 1",
Role = MessageRoles.User.ToString(),
WishlistId = wishlistId1,
CreatedById = user1.Id,
CreatedDateUtc = DateTime.UtcNow
},
new Message
{
Text = "Message 2",
Role = MessageRoles.Application.ToString(),
WishlistId = wishlistId1,
CreatedDateUtc = DateTime.UtcNow.AddSeconds(5)
},
new Message
{
Text = "Message 3",
Role = MessageRoles.User.ToString(),
WishlistId = wishlistId1,
CreatedById = user1.Id,
CreatedDateUtc = DateTime.UtcNow.AddSeconds(20)
},
new Message
{
Text = "Message 4",
Role = MessageRoles.Application.ToString(),
WishlistId = wishlistId1,
CreatedDateUtc = DateTime.UtcNow.AddSeconds(25)
},
new Message
{
Text = "Message 5",
Role = MessageRoles.User.ToString(),
WishlistId = wishlistId1,
CreatedById = user1.Id,
CreatedDateUtc = DateTime.UtcNow.AddSeconds(45)
},
new Message
{
Text = "Message 6",
Role = MessageRoles.Application.ToString(),
WishlistId = wishlistId1,
CreatedDateUtc = DateTime.UtcNow.AddSeconds(50)
},
new Message
{
Text = "Prompt",
Role = MessageRoles.User.ToString(),
WishlistId = wishlistId2,
CreatedById = user2.Id,
CreatedDateUtc = DateTime.UtcNow
}
};
await messagesCollection.InsertManyAsync(messages);
}
public async Task InitializeProductsAsync()
{
var productsCollection = _dbContext.Db.GetCollection<Product>("Products");
var usersCollection = _dbContext.Db.GetCollection<User>("Users");
var user1 = await (await usersCollection.FindAsync(x => x.Email!.Equals("wishlists@gmail.com"))).FirstAsync();
var user2 = await (await usersCollection.FindAsync(x => x.Email!.Equals("test@gmail.com"))).FirstAsync();
var wishlistId1 = ObjectId.Parse("ab79cde6f69abcd3efab65cd");
var wishlistId2 = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab");
var products = new Product[]
{
new Product
{
Name = "AMD Ryzen 5 5600G 6-Core 12-Thread Unlocked Desktop Processor with Radeon Graphics",
Description = "Features best-in-class graphics performance in a desktop processor for smooth 1080p gaming, no graphics card required",
Rating = 4.8,
Url = "https://a.co/d/5ceuIrq",
ImagesUrls = new string[]
{
"https://m.media-amazon.com/images/I/51f2hkWjTlL._AC_SL1200_.jpg",
"https://m.media-amazon.com/images/I/51iji7Gel-L._AC_SL1200_.jpg"
},
WasOpened = false,
WishlistId = wishlistId1,
CreatedById = user1.Id,
CreatedDateUtc = DateTime.UtcNow
},
new Product
{
Name = "Samsung 970 EVO Plus SSD 2TB NVMe M.2 Internal Solid State Hard Drive, V-NAND Technology, Storage and Memory Expansion for Gaming, Graphics w/ Heat Control, Max Speed, MZ-V7S2T0B/AM ",
Description = "7 Year Limited Warranty: The 970 EVO Plus provides up to 1200 TBW (Terabytes Written) with 5-years of protection for exceptional endurance powered by the latest V-NAND technology and Samsung's reputation for quality ",
Rating = 4.8,
Url = "https://a.co/d/gxnuqs1",
ImagesUrls = new string[]
{
"https://m.media-amazon.com/images/I/51Brl+iYtvL._AC_SL1001_.jpg",
"https://m.media-amazon.com/images/I/51GOfLlVwoL._AC_SL1001_.jpg"
},
WasOpened = false,
WishlistId = wishlistId1,
CreatedById = user1.Id,
CreatedDateUtc = DateTime.UtcNow
},
};
await productsCollection.InsertManyAsync(products);
}
}

View File

@ -1,48 +1,64 @@
using Microsoft.AspNetCore.Hosting;
using System.Runtime.InteropServices;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Mongo2Go;
using ShoppingAssistantApi.Persistance.PersistanceExtentions;
using ShoppingAssistantApi.Persistance.Database;
namespace ShoppingAssistantApi.Tests.TestExtentions;
public class TestingFactory<TEntryPoint> : WebApplicationFactory<Program> where TEntryPoint : Program
{
private readonly MongoDbRunner _runner = MongoDbRunner.Start();
private MongoDbRunner? _runner;
private bool _isDataInitialaized = false;
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
// Mongo2Go is not supported on ARM64 so we need to use a real MongoDB instance
Console.WriteLine($"[ARCH]: {RuntimeInformation.ProcessArchitecture}");
var connectionString = string.Empty;
if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{
connectionString = "mongodb+srv://api:pUe2dLT8llwEgwzq@cluster0.3q6mxmw.mongodb.net/?retryWrites=true&w=majority";
}
else
{
_runner = MongoDbRunner.Start();
connectionString = _runner.ConnectionString;
}
builder.ConfigureAppConfiguration((context, config) =>
{
var dbConfig = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string>()
{
{ "ConnectionStrings:MongoDb", _runner.ConnectionString }
})
.Build();
var dbConfig = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>()
{
{ "ConnectionStrings:MongoDb", connectionString }
})
.Build();
config.AddConfiguration(dbConfig);
});
}
public async Task InitialaizeData()
public void InitialaizeDatabase()
{
if (!_isDataInitialaized)
{
_isDataInitialaized = true;
using var scope = Services.CreateScope();
var initialaizer = new DbInitialaizer(scope.ServiceProvider);
using var cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = cancellationTokenSource.Token;
await initialaizer.InitialaizeDb(cancellationToken);
}
if (_isDataInitialaized) return;
using var scope = Services.CreateScope();
var mongodbContext = scope.ServiceProvider.GetRequiredService<MongoDbContext>();
var initialaizer = new DbInitializer(mongodbContext);
initialaizer.InitializeDb();
_isDataInitialaized = true;
}
protected override void Dispose(bool disposing)
{
_runner.Dispose();
_runner?.Dispose();
base.Dispose(disposing);
}
}

View File

@ -1,50 +0,0 @@
using System.Net.Http.Headers;
using System.Text;
using Newtonsoft.Json;
using ShoppingAssistantApi.Application.Models.Dtos;
namespace ShoppingAssistantApi.Tests.TestExtentions;
public static class UserExtention
{
public static async Task<UserDto> GetCurrentUser(HttpClient httpClient)
{
var query = new
{
query = "query CurrentUser { currentUser { id, guestId, phone, email, refreshToken, refreshTokenExpiryDate, roles { id, name }}}",
variables = new { }
};
var jsonPayload = JsonConvert.SerializeObject(query);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
using var response = await httpClient.PostAsync("graphql", content);
var responseString = await response.Content.ReadAsStringAsync();
var document = JsonConvert.DeserializeObject<dynamic>(responseString);
return JsonConvert.DeserializeObject<UserDto>(document.data.currentUser.ToString());
}
public static async Task<List<UserDto>> GetUsers(int amount, HttpClient httpClient)
{
var accessToken = await AccessExtention.Login("mykhailo.bilodid@nure.ua", "Yuiop12345", httpClient);
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.AccessToken);
var query = new
{
query = "query UsersPage($pageNumber: Int!, $pageSize: Int!) { usersPage(pageNumber: $pageNumber, pageSize: $pageSize) { items { id, email, phone }}}",
variables = new
{
pageNumber = 1,
pageSize = amount
}
};
var jsonPayload = JsonConvert.SerializeObject(query);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
using var response = await httpClient.PostAsync("graphql", content);
var responseString = await response.Content.ReadAsStringAsync();
var document = JsonConvert.DeserializeObject<dynamic>(responseString);
return JsonConvert.DeserializeObject<List<UserDto>>(document.data.usersPage.items.ToString());
}
}

View File

@ -1,26 +1,21 @@
using System.Net;
using System.Text;
using Xunit;
using Newtonsoft.Json.Linq;
using ShoppingAssistantApi.Application.Models.Identity;
using ShoppingAssistantApi.Tests.TestExtentions;
using Newtonsoft.Json;
using Xunit;
namespace ShoppingAssistantApi.Tests.Tests;
[Collection("Tests")]
public class AccessTests : IClassFixture<TestingFactory<Program>>
// TODO: make errors test more descrptive
public class AccessTests : TestsBase
{
private readonly HttpClient _httpClient;
public AccessTests(TestingFactory<Program> factory)
{
_httpClient = factory.CreateClient();
factory.InitialaizeData().GetAwaiter().GetResult();
}
: base(factory)
{ }
[Fact]
public async Task AccessGuestAsync_ValidGuid_ReturnsTokensModel()
{
// Arrange
var mutation = new
{
query = "mutation AccessGuest($guest: AccessGuestModelInput!) { accessGuest(guest: $guest) { accessToken, refreshToken } }",
@ -33,27 +28,20 @@ public class AccessTests : IClassFixture<TestingFactory<Program>>
}
};
var jsonPayload = JsonConvert.SerializeObject(mutation);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
// Act
var jsonObject = await SendGraphQlRequestAsync(mutation);
var tokens = (TokensModel?) jsonObject?.data?.accessGuest?.ToObject<TokensModel>();
using var response = await _httpClient.PostAsync("graphql", content);
response.EnsureSuccessStatusCode();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var responseString = await response.Content.ReadAsStringAsync();
var document = JsonConvert.DeserializeObject<dynamic>(responseString);
var accessToken = (string)document.data.accessGuest.accessToken;
var refreshToken = (string)document.data.accessGuest.refreshToken;
Assert.NotNull(accessToken);
Assert.NotNull(refreshToken);
}
// Assert
Assert.NotNull(tokens);
Assert.NotNull(tokens.AccessToken);
Assert.NotNull(tokens.RefreshToken);
}
[Theory]
[InlineData("")]
[InlineData(null)]
[InlineData("invalid-guid-format")]
public async Task AccessGuestAsync_InvalidGuid_ReturnsInternalServerError(string guestId)
public async Task AccessGuestAsync_InvalidGuid_ReturnsErrors(string guestId)
{
var mutation = new
{
@ -67,19 +55,19 @@ public class AccessTests : IClassFixture<TestingFactory<Program>>
}
};
var jsonPayload = JsonConvert.SerializeObject(mutation);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
var jsonObject = await SendGraphQlRequestAsync(mutation);
var errors = (JArray?) jsonObject?.errors;
using var response = await _httpClient.PostAsync("graphql", content);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
Assert.NotNull(errors);
Assert.True(errors.Count > 0);
}
[Theory]
[InlineData("invalid-email-format", null, "Yuiop12345")]
[InlineData(null, "invalid-phone", "Yuiop12345")]
[InlineData("test@gmail.com", null, "random-password")]
[InlineData(null, null, "Yuiop12345")]
[InlineData(null, null, "")]
[InlineData("mihail.beloded.work@gmail.com", null, "")]
public async Task LoginAsync_InvalidCredentials_ReturnsInternalServerError(string email, string phone, string password)
public async Task LoginAsync_InvalidCredentials_ReturnsErrors(string email, string phone, string password)
{
var mutation = new
{
@ -95,17 +83,17 @@ public class AccessTests : IClassFixture<TestingFactory<Program>>
}
};
var jsonPayload = JsonConvert.SerializeObject(mutation);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
var jsonObject = await SendGraphQlRequestAsync(mutation);
var errors = (JArray?) jsonObject?.errors;
using var response = await _httpClient.PostAsync("graphql", content);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
Assert.NotNull(errors);
Assert.True(errors.Count > 0);
}
[Theory]
[InlineData("mykhailo.bilodid@nure.ua", "+380953326869", "Yuiop12345")]
[InlineData(null, "+380953326888", "Yuiop12345")]
[InlineData("mykhailo.bilodid@nure.ua", null, "Yuiop12345")]
[InlineData("test@gmail.com", "+380953326869", "Yuiop12345")]
[InlineData(null, "+380953326869", "Yuiop12345")]
[InlineData("test@gmail.com", null, "Yuiop12345")]
public async Task LoginAsync_ValidCredentials_ReturnsTokensModel(string email, string phone, string password)
{
var mutation = new
@ -121,83 +109,85 @@ public class AccessTests : IClassFixture<TestingFactory<Program>>
}
}
};
var jsonObject = await SendGraphQlRequestAsync(mutation);
var tokens = (TokensModel?) jsonObject?.data?.login?.ToObject<TokensModel>();
var jsonPayload = JsonConvert.SerializeObject(mutation);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
using var response = await _httpClient.PostAsync("graphql", content);
response.EnsureSuccessStatusCode();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var responseString = await response.Content.ReadAsStringAsync();
var document = JsonConvert.DeserializeObject<dynamic>(responseString);
var accessToken = (string)document.data.login.accessToken;
var refreshToken = (string)document.data.login.refreshToken;
Assert.NotNull(accessToken);
Assert.NotNull(refreshToken);
Assert.NotNull(tokens);
Assert.NotNull(tokens.AccessToken);
Assert.NotNull(tokens.RefreshToken);
}
[Fact]
public async Task RefreshUserTokenAsync_ValidTokensModel_ReturnsTokensModel()
{
var tokensModel = await AccessExtention.CreateGuest(new Guid().ToString(), _httpClient);
var accessToken = tokensModel.AccessToken;
var refreshToken = tokensModel.RefreshToken;
var tokensModel = await CreateGuestAsync();
var mutation = new
{
query = "mutation RefreshToken($model: TokensModelInput!) { refreshUserToken(model: $model) { accessToken refreshToken }}",
query = "mutation RefreshToken($model: TokensModelInput!) { refreshAccessToken(model: $model) { accessToken refreshToken }}",
variables = new
{
model = new
{
accessToken,
refreshToken
accessToken = tokensModel.AccessToken,
refreshToken = tokensModel.RefreshToken
}
}
};
var jsonObject = await SendGraphQlRequestAsync(mutation);
var tokens = (TokensModel?) jsonObject?.data?.refreshAccessToken?.ToObject<TokensModel>();
var jsonPayload = JsonConvert.SerializeObject(mutation);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
using var response = await _httpClient.PostAsync("graphql", content);
response.EnsureSuccessStatusCode();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var responseString = await response.Content.ReadAsStringAsync();
var document = JsonConvert.DeserializeObject<dynamic>(responseString);
var accessTokenResult = (string)document.data.refreshUserToken.accessToken;
var refreshTokenResult = (string)document.data.refreshUserToken.refreshToken;
Assert.NotNull(accessTokenResult);
Assert.NotNull(refreshTokenResult);
Assert.NotNull(tokens);
Assert.NotNull(tokens.AccessToken);
Assert.NotNull(tokens.RefreshToken);
}
[Theory]
[InlineData(null, null)]
[InlineData("invalid-access-token", "invalid-refresh-token")]
public async Task RefreshUserTokenAsync_InvalidTokensModel_ReturnsInternalServerError(string refreshToken, string accessToken)
[Fact]
public async Task RefreshAccessTokenAsync_NonExistingRefreshToken_ReturnsErrors()
{
var mutation = new
{
query = "mutation RefreshToken($model: TokensModelInput!) { refreshUserToken(model: $model) { accessToken refreshToken }}",
query = "mutation RefreshToken($model: TokensModelInput!) { refreshAccessToken(model: $model) { accessToken refreshToken }}",
variables = new
{
model = new
{
accessToken,
refreshToken
accessToken = "random-access-token",
refreshToken = "random-refresh-token"
}
}
};
var jsonPayload = JsonConvert.SerializeObject(mutation);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
var jsonObject = await SendGraphQlRequestAsync(mutation);
var errors = (JArray?) jsonObject?.errors;
using var response = await _httpClient.PostAsync("graphql", content);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
Assert.NotNull(errors);
Assert.True(errors.Count > 0);
}
private async Task<TokensModel?> CreateGuestAsync()
{
var mutation = new
{
query = @"
mutation AccessGuest($guest: AccessGuestModelInput!) {
accessGuest(guest: $guest) {
accessToken, refreshToken
}
}",
variables = new
{
guest = new
{
guestId = Guid.NewGuid()
}
}
};
var jsonObject = await SendGraphQlRequestAsync(mutation);
var tokens = (TokensModel?) jsonObject?.data?.accessGuest?.ToObject<TokensModel>();
return tokens;
}
}

View File

@ -1,138 +1,42 @@
using System.Net;
using System.Text;
using Xunit;
using ShoppingAssistantApi.Tests.TestExtentions;
using System.Net.Http.Headers;
using Newtonsoft.Json;
using GreenDonut;
using ShoppingAssistantApi.Tests.TestExtentions;
using Newtonsoft.Json.Linq;
using ShoppingAssistantApi.Application.Paging;
using ShoppingAssistantApi.Application.Models.Dtos;
namespace ShoppingAssistantApi.Tests.Tests;
[Collection("Tests")]
public class RolesTests : IClassFixture<TestingFactory<Program>>
// TODO: make errors test more descrptive
public class RolesTests : TestsBase
{
private readonly HttpClient _httpClient;
public RolesTests(TestingFactory<Program> factory)
: base(factory)
{ }
[Fact]
public async Task AddRole_ValidName_ReturnsCreatedRole()
{
_httpClient = factory.CreateClient();
factory.InitialaizeData().GetAwaiter().GetResult();
await LoginAsync("admin@gmail.com", "Yuiop12345");
var mutation = new
{
query = "mutation AddRole ($dto: RoleCreateDtoInput!){ addRole (roleDto: $dto) { id, name }} ",
variables = new
{
dto = new
{
name = "NewRole"
}
}
};
var jsonObject = await SendGraphQlRequestAsync(mutation);
var role = jsonObject?.data?.addRole?.ToObject<RoleDto>();
Assert.NotNull(role);
Assert.Equal("NewRole", role.Name);
}
[Fact]
public async Task AddToRoleAsync_ValidRoleName_ReturnsTokensModel()
{
var usersPage = await UserExtention.GetUsers(10, _httpClient);
var mutation = new
{
query = "mutation AddToRole($roleName: String!, $id: String!) { addToRole(roleName: $roleName, id: $id) { accessToken, refreshToken }}",
variables = new
{
roleName = "Admin",
id = usersPage[0].Id,
}
};
var jsonPayload = JsonConvert.SerializeObject(mutation);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
using var response = await _httpClient.PostAsync("graphql", content);
response.EnsureSuccessStatusCode();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var responseString = await response.Content.ReadAsStringAsync();
var document = JsonConvert.DeserializeObject<dynamic>(responseString);
var accessToken = (string)document.data.addToRole.accessToken;
var refreshToken = (string)document.data.addToRole.refreshToken;
Assert.NotNull(accessToken);
Assert.NotNull(refreshToken);
}
[Theory]
[InlineData("")]
[InlineData("InvalidRole")]
public async Task AddToRoleAsync_InvalidRoleName_ReturnsInternalServerError(string roleName)
{
var usersPage = await UserExtention.GetUsers(10, _httpClient);
var mutation = new
{
query = "mutation AddToRole($roleName: String!, $id: String!) { addToRole(roleName: $roleName, id: $id) { accessToken, refreshToken }}",
variables = new
{
roleName,
id = usersPage[0].Id,
}
};
var jsonPayload = JsonConvert.SerializeObject(mutation);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
using var response = await _httpClient.PostAsync("graphql", content);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
}
[Fact]
public async Task RemoveFromRoleAsync_ValidRoleName_ReturnsTokensModel()
{
var usersPage = await UserExtention.GetUsers(10, _httpClient);
var mutation = new
{
query = "mutation RemoveFromRole($roleName: String!, $id: String!) { removeFromRole(roleName: $roleName, id: $id) { accessToken, refreshToken }}",
variables = new
{
roleName = "Admin",
id = usersPage[0].Id,
}
};
var jsonPayload = JsonConvert.SerializeObject(mutation);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
using var response = await _httpClient.PostAsync("graphql", content);
response.EnsureSuccessStatusCode();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var responseString = await response.Content.ReadAsStringAsync();
var document = JsonConvert.DeserializeObject<dynamic>(responseString);
var accessToken = (string)document.data.removeFromRole.accessToken;
var refreshToken = (string)document.data.removeFromRole.refreshToken;
Assert.NotNull(accessToken);
Assert.NotNull(refreshToken);
}
[Theory]
[InlineData("")]
[InlineData("InvalidRole")]
public async Task RemoveFromRoleAsync_InvalidRoleName_ReturnsInternalServerError(string roleName)
{
var usersPage = await UserExtention.GetUsers(10, _httpClient);
var mutation = new
{
query = "mutation RemoveFromRole($roleName: String!, $id: String!) { removeFromRole(roleName: $roleName, id: $id) { accessToken, refreshToken }}",
variables = new
{
roleName,
id = usersPage[0].Id,
}
};
var jsonPayload = JsonConvert.SerializeObject(mutation);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
using var response = await _httpClient.PostAsync("graphql", content);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
}
[Theory]
[InlineData("User")]
[InlineData(null)]
public async Task AddRole_InvalidRoleName_ReturnsInternalServerError(string roleName)
public async Task AddRole_Unauthorized_ReturnsErrors()
{
var mutation = new
{
@ -141,23 +45,45 @@ public class RolesTests : IClassFixture<TestingFactory<Program>>
{
dto = new
{
name = roleName
name = "NewRole"
}
}
};
var jsonObject = await SendGraphQlRequestAsync(mutation);
var errors = (JArray?) jsonObject?.errors;
Assert.NotNull(errors);
Assert.True(errors.Count > 0);
}
[Fact]
public async Task AddRole_ExistingRoleName_ReturnsErrors()
{
await LoginAsync("admin@gmail.com", "Yuiop12345");
var mutation = new
{
query = "mutation AddRole ($dto: RoleCreateDtoInput!){ addRole (roleDto: $dto) { id, name }} ",
variables = new
{
dto = new
{
name = "User"
}
}
};
var jsonPayload = JsonConvert.SerializeObject(mutation);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
var jsonObject = await SendGraphQlRequestAsync(mutation);
var errors = (JArray?) jsonObject?.errors;
using var response = await _httpClient.PostAsync("graphql", content);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
Assert.NotNull(errors);
Assert.True(errors.Count > 0);
}
[Fact]
public async Task GetRolesPageAsync_ValidPageNumberAndSize_ReturnsRolesPagedList()
{
var tokensModel = await AccessExtention.Login("mykhailo.bilodid@nure.ua", "Yuiop12345", _httpClient);
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokensModel.AccessToken);
await LoginAsync("admin@gmail.com", "Yuiop12345");
var query = new
{
query = "query RolesPage($pageNumber: Int!, $pageSize: Int!) { rolesPage(pageNumber: $pageNumber, pageSize: $pageSize) { items { id, name } }}",
@ -167,18 +93,11 @@ public class RolesTests : IClassFixture<TestingFactory<Program>>
pageSize = 3
}
};
var jsonObject = await SendGraphQlRequestAsync(query);
var pagedList = (PagedList<RoleDto>?) jsonObject?.data?.rolesPage?.ToObject<PagedList<RoleDto>>();
var jsonPayload = JsonConvert.SerializeObject(query);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
using var response = await _httpClient.PostAsync("graphql", content);
response.EnsureSuccessStatusCode();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var responseString = await response.Content.ReadAsStringAsync();
var document = JsonConvert.DeserializeObject<dynamic>(responseString);
var items = document.data.rolesPage.items;
Assert.NotEmpty(items);
Assert.NotNull(pagedList);
Assert.NotEmpty(pagedList.Items);
}
}

View File

@ -0,0 +1,66 @@
using System.Net.Http.Headers;
using System.Text;
using Newtonsoft.Json;
using ShoppingAssistantApi.Application.Models.Dtos;
using ShoppingAssistantApi.Tests.TestExtentions;
namespace ShoppingAssistantApi.Tests.Tests;
public class TestsBase: IClassFixture<TestingFactory<Program>>
{
private protected HttpClient _httpClient;
public TestsBase(TestingFactory<Program> factory)
{
_httpClient = factory.CreateClient();
factory.InitialaizeDatabase();
}
public async Task LoginAsync(string email, string password)
{
var mutation = new
{
query = "mutation Login($login: AccessUserModelInput!) { login(login: $login) { accessToken refreshToken }}",
variables = new
{
login = new
{
email = email,
password = password
}
}
};
var jsonObject = await SendGraphQlRequestAsync(mutation);
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", (string?) jsonObject?.data?.login?.accessToken);
}
public async Task<dynamic?> SendGraphQlRequestAsync(object request)
{
var jsonPayload = JsonConvert.SerializeObject(request);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
using var response = await _httpClient.PostAsync("graphql", content);
var responseString = await response.Content.ReadAsStringAsync();
Console.WriteLine(responseString);
var jsonObject = JsonConvert.DeserializeObject<dynamic>(responseString);
return jsonObject;
}
public async Task<UserDto> GetCurrentUserAsync()
{
var query = new
{
query = "query CurrentUser { currentUser { id, guestId, phone, email, roles { id, name }}}",
variables = new { }
};
var jsonObject = await SendGraphQlRequestAsync(query);
var user = (UserDto?) jsonObject?.data?.currentUser?.ToObject<UserDto>();
return user;
}
}

View File

@ -1,222 +1,172 @@
using ShoppingAssistantApi.Tests.TestExtentions;
using System.Net.Http.Headers;
using System.Net;
using System.Text;
using Xunit;
using Newtonsoft.Json;
using ShoppingAssistantApi.Application.Models.Dtos;
using ShoppingAssistantApi.Application.Models.Identity;
using Newtonsoft.Json.Linq;
using ShoppingAssistantApi.Application.Paging;
namespace ShoppingAssistantApi.Tests.Tests;
[Collection("Tests")]
public class UsersTests : IClassFixture<TestingFactory<Program>>
// TODO: make errors test more descrptive
public class UsersTests : TestsBase
{
private readonly HttpClient _httpClient;
public UsersTests(TestingFactory<Program> factory)
{
_httpClient = factory.CreateClient();
factory.InitialaizeData().GetAwaiter().GetResult();
}
: base(factory)
{ }
[Fact]
public async Task UpdateUserAsync_ValidUserModel_ReturnsUpdateUserModel()
{
var tokensModel = await AccessExtention.CreateGuest(Guid.NewGuid().ToString(), _httpClient);
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokensModel.AccessToken);
var user = await UserExtention.GetCurrentUser(_httpClient);
var roles = new object[1];
foreach(var role in user.Roles)
{
roles[0] = new
{
id = role.Id,
name = role.Name
};
}
await LoginAsync("test@gmail.com", "Yuiop12345");
var user = await GetCurrentUserAsync();
var mutation = new
{
query = "mutation UpdateUser($userDto: UserDtoInput!) { updateUser(userDto: $userDto) { tokens { accessToken, refreshToken }, user { email } }}",
query = @"
mutation UpdateUser($userDto: UserDtoInput!) {
updateUser(userDto: $userDto) {
tokens { accessToken, refreshToken },
user { email, phone }
}
}",
variables = new
{
userDto = new
{
id = user.Id,
guestId = user.GuestId,
roles = roles,
email = "testing@gmail.com",
password = "Yuiop12345",
refreshTokenExpiryDate = user.RefreshTokenExpiryDate
roles = user.Roles.Select(r => new { id = r.Id, name = r.Name }),
email = user.Email,
phone = "+12345678902",
}
}
};
var jsonPayload = JsonConvert.SerializeObject(mutation);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
var jsonObject = await SendGraphQlRequestAsync(mutation);
var tokens = (TokensModel?) jsonObject?.data?.updateUser?.tokens?.ToObject<TokensModel>();
var updatedUser = (UserDto?) jsonObject?.data?.updateUser?.user?.ToObject<UserDto>();
using var response = await _httpClient.PostAsync("graphql", content);
response.EnsureSuccessStatusCode();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var responseString = await response.Content.ReadAsStringAsync();
var document = JsonConvert.DeserializeObject<dynamic>(responseString);
var accessTokenResult = (string)document.data.updateUser.tokens.accessToken;
var refreshTokenResult = (string)document.data.updateUser.tokens.refreshToken;
var userResult = JsonConvert.DeserializeObject<UserDto>(document.data.updateUser.user.ToString());
Assert.NotNull(accessTokenResult);
Assert.NotNull(refreshTokenResult);
Assert.NotNull(userResult.Email);
Assert.NotNull(tokens);
Assert.NotNull(tokens.AccessToken);
Assert.NotNull(tokens.RefreshToken);
Assert.NotNull(updatedUser);
Assert.NotNull(updatedUser.Email);
Assert.Equal(user.Email, updatedUser.Email);
Assert.Equal("+12345678902", updatedUser.Phone);
}
[Fact]
public async Task UpdateUserByAdminAsync_ValidUserModel_ReturnsUpdateUserModel()
{
var tokensModel = await AccessExtention.CreateGuest(new Guid().ToString(), _httpClient);
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokensModel.AccessToken);
var user = await UserExtention.GetCurrentUser(_httpClient);
var roles = new object[1];
foreach (var role in user.Roles)
{
roles[0] = new
{
id = role.Id,
name = role.Name,
};
}
await LoginAsync("test@gmail.com", "Yuiop12345");
var user = await GetCurrentUserAsync();
var mutation = new
{
query = "mutation UpdateUserByAdmin($id: String!, $userDto: UserDtoInput!) { updateUserByAdmin(id: $id, userDto: $userDto) { tokens { accessToken, refreshToken }, user { guestId } }}",
query = @"
mutation UpdateUserByAdmin($id: String!, $userDto: UserDtoInput!) {
updateUserByAdmin(id: $id, userDto: $userDto) {
email,
phone
}
}",
variables = new
{
id = user.Id,
userDto = new
{
id = user.Id,
guestId = Guid.NewGuid().ToString(),
roles = roles,
refreshTokenExpiryDate = user.RefreshTokenExpiryDate
guestId = user.GuestId,
roles = user.Roles.Select(r => new { id = r.Id, name = r.Name }),
email = user.Email,
phone = "+12345678903",
}
}
};
var jsonPayload = JsonConvert.SerializeObject(mutation);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
var jsonObject = await SendGraphQlRequestAsync(mutation);
var updatedUser = (UserDto?) jsonObject?.data?.updateUserByAdmin?.ToObject<UserDto>();
using var response = await _httpClient.PostAsync("graphql", content);
response.EnsureSuccessStatusCode();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var responseString = await response.Content.ReadAsStringAsync();
var document = JsonConvert.DeserializeObject<dynamic>(responseString);
var accessTokenResult = (string)document.data.updateUserByAdmin.tokens.accessToken;
var refreshToken = (string)document.data.updateUserByAdmin.tokens.refreshToken;
var updatedUserGuestId = (Guid)document.data.updateUserByAdmin.user.guestId;
Assert.NotNull(accessTokenResult);
Assert.NotNull(refreshToken);
Assert.NotEqual(user.GuestId, updatedUserGuestId);
Assert.NotNull(updatedUser);
Assert.NotNull(updatedUser.Email);
Assert.Equal(user.Email, updatedUser.Email);
Assert.Equal("+12345678903", updatedUser.Phone);
}
[Fact]
public async Task GetUserAsync_ValidUserId_ReturnsUser()
{
var tokensModel = await AccessExtention.Login("mykhailo.bilodid@nure.ua", "Yuiop12345", _httpClient);
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokensModel.AccessToken);
var usersPage = await UserExtention.GetUsers(10, _httpClient);
await LoginAsync("admin@gmail.com", "Yuiop12345");
var query = new
{
query = "query User($id: String!) { user(id: $id) { id, email, phone }}",
query = @"
query User($id: String!) {
user(id: $id) {
id,
email
}
}",
variables = new
{
id = usersPage[0].Id,
id = "652c3b89ae02a3135d6409fc",
}
};
var jsonPayload = JsonConvert.SerializeObject(query);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
var jsonObject = await SendGraphQlRequestAsync(query);
var user = (UserDto?) jsonObject?.data?.user?.ToObject<UserDto>();
using var response = await _httpClient.PostAsync("graphql", content);
response.EnsureSuccessStatusCode();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var responseString = await response.Content.ReadAsStringAsync();
var document = JsonConvert.DeserializeObject<dynamic>(responseString);
var userResult = JsonConvert.DeserializeObject<UserDto>(document.data.user.ToString());
Assert.Equal(userResult.Id, usersPage[0].Id);
Assert.NotNull(user);
Assert.Equal("652c3b89ae02a3135d6409fc", user.Id);
Assert.Equal("test@gmail.com", user.Email);
}
[Fact]
public async Task GetUserAsync_InvalidUserId_ReturnsInternalServerError()
{
var tokensModel = await AccessExtention.Login("mykhailo.bilodid@nure.ua", "Yuiop12345", _httpClient);
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokensModel.AccessToken);
await LoginAsync("admin@gmail.com", "Yuiop12345");
var query = new
{
query = "query User($id: String!) { user(id: $id) { id, email, phone }}",
query = "query User($id: String!) { user(id: $id) { id }}",
variables = new
{
id = "error",
id = "invalid",
}
};
var jsonPayload = JsonConvert.SerializeObject(query);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
var jsonObject = await SendGraphQlRequestAsync(query);
var errors = (JArray?) jsonObject?.errors;
using var response = await _httpClient.PostAsync("graphql", content);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
Assert.NotNull(errors);
Assert.True(errors.Count > 0);
}
[Fact]
public async Task GetCurrentUserAsync_ValidCredentials_ReturnsCurrentUser()
public async Task GetCurrentUserAsync_Authorized_ReturnsCurrentUser()
{
var tokensModel = await AccessExtention.Login("mykhailo.bilodid@nure.ua", "Yuiop12345", _httpClient);
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokensModel.AccessToken);
await LoginAsync("admin@gmail.com", "Yuiop12345");
var query = new
{
query = "query CurrentUser { currentUser { id, email, phone }}",
variables = new { }
query = "query CurrentUser { currentUser { id, email, phone }}"
};
var jsonPayload = JsonConvert.SerializeObject(query);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
var jsonObject = await SendGraphQlRequestAsync(query);
var user = (UserDto?) jsonObject?.data?.currentUser?.ToObject<UserDto>();
using var response = await _httpClient.PostAsync("graphql", content);
response.EnsureSuccessStatusCode();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var responseString = await response.Content.ReadAsStringAsync();
var document = JsonConvert.DeserializeObject<dynamic>(responseString);
var user = JsonConvert.DeserializeObject<UserDto>(document.data.currentUser.ToString());
Assert.NotEmpty(user.Id);
Assert.NotEmpty(user.Email);
Assert.NotEmpty(user.Phone);
Assert.Equal(user.Email, "mykhailo.bilodid@nure.ua");
Assert.NotNull(user);
Assert.Equal("652c3b89ae02a3135d6408fc", user.Id);
Assert.Equal("admin@gmail.com", user.Email);
Assert.Equal("+12345678901", user.Phone);
}
[Fact]
public async Task GetUsersPageAsync_ValidPageNumberAndSize_ReturnsUsersPage()
{
var tokensModel = await AccessExtention.Login("mykhailo.bilodid@nure.ua", "Yuiop12345", _httpClient);
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokensModel.AccessToken);
await LoginAsync("admin@gmail.com", "Yuiop12345");
var query = new
{
query = "query UsersPage($pageNumber: Int!, $pageSize: Int!) { usersPage(pageNumber: $pageNumber, pageSize: $pageSize) { items { id, email, phone }}}",
query = @"
query UsersPage($pageNumber: Int!, $pageSize: Int!) {
usersPage(pageNumber: $pageNumber, pageSize: $pageSize) {
items { id, email, phone }
}
}",
variables = new
{
pageNumber = 1,
@ -224,17 +174,128 @@ public class UsersTests : IClassFixture<TestingFactory<Program>>
}
};
var jsonPayload = JsonConvert.SerializeObject(query);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
var jsonObject = await SendGraphQlRequestAsync(query);
var pagedList = (PagedList<UserDto>?) jsonObject?.data?.usersPage?.ToObject<PagedList<UserDto>>();
using var response = await _httpClient.PostAsync("graphql", content);
response.EnsureSuccessStatusCode();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(pagedList);
Assert.NotEmpty(pagedList.Items);
}
var responseString = await response.Content.ReadAsStringAsync();
var document = JsonConvert.DeserializeObject<dynamic>(responseString);
var items = document.data.usersPage.items;
Assert.NotEmpty(items);
[Fact]
public async Task AddToRoleAsync_ValidRoleName_ReturnsTokensModel()
{
await LoginAsync("admin@gmail.com", "Yuiop12345");
var mutation = new
{
query = @"
mutation AddToRole($roleName: String!, $userId: String!) {
addToRole(roleName: $roleName, userId: $userId) {
id, email, roles {
name
}
}
}",
variables = new
{
roleName = "Admin",
userId = "652c3b89ae02a3135d6409fc",
}
};
var jsonObject = await SendGraphQlRequestAsync(mutation);
var user = (UserDto?) jsonObject?.data?.addToRole?.ToObject<UserDto>();
Assert.NotNull(user);
Assert.Equal("652c3b89ae02a3135d6409fc", user.Id);
Assert.Equal("test@gmail.com", user.Email);
Assert.Contains(user.Roles, r => r.Name == "Admin");
}
[Fact]
public async Task AddToRoleAsync_NonExistingRole_ReturnsErrors()
{
await LoginAsync("admin@gmail.com", "Yuiop12345");
var mutation = new
{
query = @"
mutation AddToRole($roleName: String!, $userId: String!) {
addToRole(roleName: $roleName, userId: $userId) {
id, email, roles {
name
}
}
}",
variables = new
{
roleName = "NonExistingRole",
id = "652c3b89ae02a3135d6409fc",
}
};
var jsonObject = await SendGraphQlRequestAsync(mutation);
var errors = (JArray?) jsonObject?.errors;
Assert.NotNull(errors);
Assert.True(errors.Count > 0);
}
[Fact]
public async Task RemoveFromRoleAsync_ValidRoleName_ReturnsTokensModel()
{
await LoginAsync("admin@gmail.com", "Yuiop12345");
var mutation = new
{
query = @"
mutation RemoveFromRole($roleName: String!, $userId: String!) {
removeFromRole(roleName: $roleName, userId: $userId) {
id, email, roles {
name
}
}
}",
variables = new
{
roleName = "User",
userId = "652c3b89ae02a3135d6409fc",
}
};
var jsonObject = await SendGraphQlRequestAsync(mutation);
var user = (UserDto?) jsonObject?.data?.removeFromRole?.ToObject<UserDto>();
Assert.NotNull(user);
Assert.Equal("652c3b89ae02a3135d6409fc", user.Id);
Assert.Equal("test@gmail.com", user.Email);
Assert.DoesNotContain(user.Roles, r => r.Name == "User");
}
[Fact]
public async Task RemoveFromRoleAsync_NonExistingRole_ReturnsErrors()
{
await LoginAsync("admin@gmail.com", "Yuiop12345");
var mutation = new
{
query = @"
mutation RemoveFromRole($roleName: String!, $userId: String!) {
removeFromRole(roleName: $roleName, userId: $userId) {
id, email, roles {
name
}
}
}",
variables = new
{
roleName = "NonExistingRole",
userId = "652c3b89ae02a3135d6409fc",
}
};
var jsonObject = await SendGraphQlRequestAsync(mutation);
var errors = (JArray?) jsonObject?.errors;
Assert.NotNull(errors);
Assert.True(errors.Count > 0);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,4 @@
global using Xunit;
// Parallel running was disabled because it causes errors with the database access
[assembly: CollectionBehavior(DisableTestParallelization = true)]