diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 3075303..5ad2e4e 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -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" ] } } diff --git a/.vscode/settings.json b/.vscode/settings.json index cf3a569..9cd2af5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,6 @@ { + "files.exclude": { + "**/bin": true + }, "editor.formatOnType": true } \ No newline at end of file diff --git a/ShoppingAssistantApi.Api/Mutations/AccessMutation.cs b/ShoppingAssistantApi.Api/Mutations/AccessMutation.cs index 7abc641..df920c0 100644 --- a/ShoppingAssistantApi.Api/Mutations/AccessMutation.cs +++ b/ShoppingAssistantApi.Api/Mutations/AccessMutation.cs @@ -7,14 +7,14 @@ namespace ShoppingAssistantApi.Api.Mutations; public class AccessMutation { public Task LoginAsync(AccessUserModel login, CancellationToken cancellationToken, - [Service] IUserManager userManager) - => userManager.LoginAsync(login, cancellationToken); + [Service] IUserManager userManager) + => userManager.LoginAsync(login, cancellationToken); public Task AccessGuestAsync(AccessGuestModel guest, CancellationToken cancellationToken, - [Service] IUserManager userManager) - => userManager.AccessGuestAsync(guest, cancellationToken); + [Service] IUserManager userManager) + => userManager.AccessGuestAsync(guest, cancellationToken); - public Task RefreshUserTokenAsync(TokensModel model, CancellationToken cancellationToken, - [Service] ITokensService tokensService) - => tokensService.RefreshUserAsync(model, cancellationToken); + public Task RefreshAccessTokenAsync(TokensModel model, CancellationToken cancellationToken, + [Service] IUserManager userManager) + => userManager.RefreshAccessTokenAsync(model, cancellationToken); } \ No newline at end of file diff --git a/ShoppingAssistantApi.Api/Mutations/RolesMutation.cs b/ShoppingAssistantApi.Api/Mutations/RolesMutation.cs index a4c98bb..e8e2138 100644 --- a/ShoppingAssistantApi.Api/Mutations/RolesMutation.cs +++ b/ShoppingAssistantApi.Api/Mutations/RolesMutation.cs @@ -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 AddToRoleAsync(string roleName, string id, CancellationToken cancellationToken, - [Service] IUserManager userManager) - => userManager.AddToRoleAsync(roleName, id, cancellationToken); - - public Task RemoveFromRoleAsync(string roleName, string id, CancellationToken cancellationToken, - [Service] IUserManager userManager) - => userManager.RemoveFromRoleAsync(roleName, id, cancellationToken); - + [Authorize] public Task AddRole(RoleCreateDto roleDto, CancellationToken cancellationToken, [Service] IRolesService rolesService) => rolesService.AddRoleAsync(roleDto, cancellationToken); diff --git a/ShoppingAssistantApi.Api/Mutations/UsersMutation.cs b/ShoppingAssistantApi.Api/Mutations/UsersMutation.cs index 1185f97..6beaccf 100644 --- a/ShoppingAssistantApi.Api/Mutations/UsersMutation.cs +++ b/ShoppingAssistantApi.Api/Mutations/UsersMutation.cs @@ -10,11 +10,21 @@ public class UsersMutation { [Authorize] public Task UpdateUserAsync(UserDto userDto, CancellationToken cancellationToken, - [Service] IUserManager userManager) - => userManager.UpdateAsync(userDto, cancellationToken); + [Service] IUserManager userManager) + => userManager.UpdateAsync(userDto, cancellationToken); [Authorize] - public Task UpdateUserByAdminAsync(string id, UserDto userDto, CancellationToken cancellationToken, - [Service] IUserManager userManager) - => userManager.UpdateUserByAdminAsync(id, userDto, cancellationToken); + public Task UpdateUserByAdminAsync(string id, UserDto userDto, CancellationToken cancellationToken, + [Service] IUserManager userManager) + => userManager.UpdateUserByAdminAsync(id, userDto, cancellationToken); + + [Authorize] + public Task AddToRoleAsync(string roleName, string userId, CancellationToken cancellationToken, + [Service] IUserManager userManager) + => userManager.AddToRoleAsync(roleName, userId, cancellationToken); + + [Authorize] + public Task RemoveFromRoleAsync(string roleName, string userId, CancellationToken cancellationToken, + [Service] IUserManager userManager) + => userManager.RemoveFromRoleAsync(roleName, userId, cancellationToken); } \ No newline at end of file diff --git a/ShoppingAssistantApi.Api/Mutations/WishlistsMutation.cs b/ShoppingAssistantApi.Api/Mutations/WishlistsMutation.cs index 9c195fb..f3ff806 100644 --- a/ShoppingAssistantApi.Api/Mutations/WishlistsMutation.cs +++ b/ShoppingAssistantApi.Api/Mutations/WishlistsMutation.cs @@ -7,11 +7,19 @@ namespace ShoppingAssistantApi.Api.Mutations; [ExtendObjectType(OperationTypeNames.Mutation)] public class WishlistsMutation { - public Task StartPersonalWishlist(WishlistCreateDto dto, CancellationToken cancellationToken, - [Service] IWishlistsService wishlistsService) + public Task StartPersonalWishlistAsync(WishlistCreateDto dto, CancellationToken cancellationToken, + [Service] IWishlistsService wishlistsService) => wishlistsService.StartPersonalWishlistAsync(dto, cancellationToken); - public Task AddMessageToPersonalWishlist(string wishlistId, MessageCreateDto dto, CancellationToken cancellationToken, - [Service] IWishlistsService wishlistsService) + public Task AddMessageToPersonalWishlistAsync(string wishlistId, MessageCreateDto dto, CancellationToken cancellationToken, + [Service] IWishlistsService wishlistsService) => wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, dto, cancellationToken); + + public Task AddProductToPersonalWishlistAsync(string wishlistId, ProductCreateDto dto, CancellationToken cancellationToken, + [Service] IWishlistsService wishlistsService) + => wishlistsService.AddProductToPersonalWishlistAsync(wishlistId, dto, cancellationToken); + + public Task DeletePersonalWishlistAsync(string wishlistId, CancellationToken cancellationToken, + [Service] IWishlistsService wishlistsService) + => wishlistsService.DeletePersonalWishlistAsync(wishlistId, cancellationToken); } diff --git a/ShoppingAssistantApi.Api/Program.cs b/ShoppingAssistantApi.Api/Program.cs index 79395dd..e7f574b 100644 --- a/ShoppingAssistantApi.Api/Program.cs +++ b/ShoppingAssistantApi.Api/Program.cs @@ -37,14 +37,12 @@ app.AddGlobalUserMiddleware(); app.MapGraphQL(); app.MapControllers(); -/* -using var scope = app.Services.CreateScope(); -var serviceProvider = scope.ServiceProvider; -using var cancellationTokenSource = new CancellationTokenSource(); -var cancellationToken = cancellationTokenSource.Token; -var initializer = new DbInitialaizer(serviceProvider); -initializer.InitialaizeDb(cancellationToken); -*/ + +// using var scope = app.Services.CreateScope(); +// var serviceProvider = scope.ServiceProvider; +// var initializer = new DbInitialaizer(serviceProvider); +// await initializer.InitialaizeDb(CancellationToken.None); + app.Run(); public partial class Program { } \ No newline at end of file diff --git a/ShoppingAssistantApi.Api/Queries/WishlistsQuery.cs b/ShoppingAssistantApi.Api/Queries/WishlistsQuery.cs index 389864f..d3d36be 100644 --- a/ShoppingAssistantApi.Api/Queries/WishlistsQuery.cs +++ b/ShoppingAssistantApi.Api/Queries/WishlistsQuery.cs @@ -9,12 +9,22 @@ namespace ShoppingAssistantApi.Api.Queries; public class WishlistsQuery { [Authorize] - public Task> GetPersonalWishlistsPageAsync(int pageNumber, int pageSize, CancellationToken cancellationToken, - [Service] IWishlistsService wishlistsService) - => wishlistsService.GetPersonalWishlistsPageAsync(pageNumber, pageSize, cancellationToken); + public Task> GetPersonalWishlistsPageAsync(int pageNumber, int pageSize, + CancellationToken cancellationToken, [Service] IWishlistsService wishlistsService) + => wishlistsService.GetPersonalWishlistsPageAsync(pageNumber, pageSize, cancellationToken); [Authorize] public Task GetPersonalWishlistAsync(string wishlistId, CancellationToken cancellationToken, - [Service] IWishlistsService wishlistsService) - => wishlistsService.GetPersonalWishlistAsync(wishlistId, cancellationToken); + [Service] IWishlistsService wishlistsService) + => wishlistsService.GetPersonalWishlistAsync(wishlistId, cancellationToken); + + [Authorize] + public Task> GetMessagesPageFromPersonalWishlistAsync(string wishlistId, int pageNumber, int pageSize, + CancellationToken cancellationToken, [Service] IWishlistsService wishlistsService) + => wishlistsService.GetMessagesPageFromPersonalWishlistAsync(wishlistId, pageNumber, pageSize, cancellationToken); + + [Authorize] + public Task> GetProductsPageFromPersonalWishlistAsync(string wishlistId, int pageNumber, int pageSize, + CancellationToken cancellationToken, [Service] IWishlistsService wishlistsService) + => wishlistsService.GetProductsPageFromPersonalWishlistAsync(wishlistId, pageNumber, pageSize, cancellationToken); } diff --git a/ShoppingAssistantApi.Application/IRepositories/IBaseRepository.cs b/ShoppingAssistantApi.Application/IRepositories/IBaseRepository.cs index f2a2ceb..ebcb860 100644 --- a/ShoppingAssistantApi.Application/IRepositories/IBaseRepository.cs +++ b/ShoppingAssistantApi.Application/IRepositories/IBaseRepository.cs @@ -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 where TEntity : EntityBase { Task AddAsync(TEntity entity, CancellationToken cancellationToken); + Task GetOneAsync(ObjectId id, CancellationToken cancellationToken); + + Task GetOneAsync(Expression> predicate, CancellationToken cancellationToken); + Task> GetPageAsync(int pageNumber, int pageSize, CancellationToken cancellationToken); Task> GetPageAsync(int pageNumber, int pageSize, Expression> predicate, CancellationToken cancellationToken); diff --git a/ShoppingAssistantApi.Application/IRepositories/IMessagerepository.cs b/ShoppingAssistantApi.Application/IRepositories/IMessagerepository.cs index 5387549..3d0483e 100644 --- a/ShoppingAssistantApi.Application/IRepositories/IMessagerepository.cs +++ b/ShoppingAssistantApi.Application/IRepositories/IMessagerepository.cs @@ -1,5 +1,9 @@ +using System.Linq.Expressions; using ShoppingAssistantApi.Domain.Entities; namespace ShoppingAssistantApi.Application.IRepositories; -public interface IMessagesRepository : IBaseRepository { } +public interface IMessagesRepository : IBaseRepository +{ + Task> GetPageStartingFromEndAsync(int pageNumber, int pageSize, Expression> predicate, CancellationToken cancellationToken); +} diff --git a/ShoppingAssistantApi.Application/IRepositories/IProductsRepository.cs b/ShoppingAssistantApi.Application/IRepositories/IProductsRepository.cs new file mode 100644 index 0000000..d5c6c02 --- /dev/null +++ b/ShoppingAssistantApi.Application/IRepositories/IProductsRepository.cs @@ -0,0 +1,5 @@ +using ShoppingAssistantApi.Domain.Entities; + +namespace ShoppingAssistantApi.Application.IRepositories; + +public interface IProductsRepository : IBaseRepository { } diff --git a/ShoppingAssistantApi.Application/IRepositories/IRefreshTokensRepository.cs b/ShoppingAssistantApi.Application/IRepositories/IRefreshTokensRepository.cs new file mode 100644 index 0000000..492e62e --- /dev/null +++ b/ShoppingAssistantApi.Application/IRepositories/IRefreshTokensRepository.cs @@ -0,0 +1,8 @@ +using ShoppingAssistantApi.Domain.Entities; + +namespace ShoppingAssistantApi.Application.IRepositories; + +public interface IRefreshTokensRepository : IBaseRepository +{ + +} diff --git a/ShoppingAssistantApi.Application/IServices/IWishlistService.cs b/ShoppingAssistantApi.Application/IServices/IWishlistService.cs index 5bfae6a..d95a874 100644 --- a/ShoppingAssistantApi.Application/IServices/IWishlistService.cs +++ b/ShoppingAssistantApi.Application/IServices/IWishlistService.cs @@ -13,4 +13,12 @@ public interface IWishlistsService Task> GetPersonalWishlistsPageAsync(int pageNumber, int pageSize, CancellationToken cancellationToken); Task GetPersonalWishlistAsync(string wishlistId, CancellationToken cancellationToken); + + Task> GetMessagesPageFromPersonalWishlistAsync(string wishlistId, int pageNumber, int pageSize, CancellationToken cancellationToken); + + Task AddProductToPersonalWishlistAsync(string wishlistId, ProductCreateDto dto, CancellationToken cancellationToken); + + Task> GetProductsPageFromPersonalWishlistAsync(string wishlistId, int pageNumber, int pageSize, CancellationToken cancellationToken); + + Task DeletePersonalWishlistAsync(string wishlistId, CancellationToken cancellationToken); } diff --git a/ShoppingAssistantApi.Application/IServices/Identity/ITokensService.cs b/ShoppingAssistantApi.Application/IServices/Identity/ITokensService.cs index 2dda569..a6f14d7 100644 --- a/ShoppingAssistantApi.Application/IServices/Identity/ITokensService.cs +++ b/ShoppingAssistantApi.Application/IServices/Identity/ITokensService.cs @@ -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 RefreshUserAsync(TokensModel tokensModel, CancellationToken cancellationToken); + ClaimsPrincipal GetPrincipalFromExpiredToken(string token); } \ No newline at end of file diff --git a/ShoppingAssistantApi.Application/IServices/Identity/IUsersManager.cs b/ShoppingAssistantApi.Application/IServices/Identity/IUsersManager.cs index 72c84ae..4a07f20 100644 --- a/ShoppingAssistantApi.Application/IServices/Identity/IUsersManager.cs +++ b/ShoppingAssistantApi.Application/IServices/Identity/IUsersManager.cs @@ -10,11 +10,13 @@ public interface IUserManager Task LoginAsync(AccessUserModel login, CancellationToken cancellationToken); - Task AddToRoleAsync(string roleName, string id, CancellationToken cancellationToken); + Task AddToRoleAsync(string roleName, string userId, CancellationToken cancellationToken); - Task RemoveFromRoleAsync(string roleName, string id, CancellationToken cancellationToken); + Task RemoveFromRoleAsync(string roleName, string userId, CancellationToken cancellationToken); Task UpdateAsync(UserDto userDto, CancellationToken cancellationToken); - Task UpdateUserByAdminAsync(string id, UserDto userDto, CancellationToken cancellationToken); + Task UpdateUserByAdminAsync(string id, UserDto userDto, CancellationToken cancellationToken); + + Task RefreshAccessTokenAsync(TokensModel tokensModel, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/ShoppingAssistantApi.Application/MappingProfiles/ProductProfile.cs b/ShoppingAssistantApi.Application/MappingProfiles/ProductProfile.cs new file mode 100644 index 0000000..92ab66f --- /dev/null +++ b/ShoppingAssistantApi.Application/MappingProfiles/ProductProfile.cs @@ -0,0 +1,15 @@ +using AutoMapper; +using ShoppingAssistantApi.Application.Models.CreateDtos; +using ShoppingAssistantApi.Application.Models.Dtos; +using ShoppingAssistantApi.Domain.Entities; + +namespace ShoppingAssistantApi.Application.MappingProfiles; +public class ProductProfile : Profile +{ + public ProductProfile() + { + CreateMap().ReverseMap(); + + CreateMap().ReverseMap(); + } +} diff --git a/ShoppingAssistantApi.Application/Models/CreateDtos/ProductCreateDto.cs b/ShoppingAssistantApi.Application/Models/CreateDtos/ProductCreateDto.cs new file mode 100644 index 0000000..015706c --- /dev/null +++ b/ShoppingAssistantApi.Application/Models/CreateDtos/ProductCreateDto.cs @@ -0,0 +1,16 @@ +namespace ShoppingAssistantApi.Application.Models.CreateDtos; + +public class ProductCreateDto +{ + public required string Url { get; set; } + + public required string Name { get; set; } + + public required string Description { get; set; } + + public required double Rating { get; set; } + + public required string[] ImagesUrls { get; set; } + + public required bool WasOpened { get; set; } +} diff --git a/ShoppingAssistantApi.Application/Models/Dtos/MessageDto.cs b/ShoppingAssistantApi.Application/Models/Dtos/MessageDto.cs index a33c92e..9225d00 100644 --- a/ShoppingAssistantApi.Application/Models/Dtos/MessageDto.cs +++ b/ShoppingAssistantApi.Application/Models/Dtos/MessageDto.cs @@ -8,5 +8,5 @@ public class MessageDto public required string Role { get; set; } - public string? CreatedById { get; set; } = null; + public required string CreatedById { get; set; } } diff --git a/ShoppingAssistantApi.Application/Models/Dtos/ProductDto.cs b/ShoppingAssistantApi.Application/Models/Dtos/ProductDto.cs new file mode 100644 index 0000000..1697cd6 --- /dev/null +++ b/ShoppingAssistantApi.Application/Models/Dtos/ProductDto.cs @@ -0,0 +1,20 @@ +namespace ShoppingAssistantApi.Application.Models.Dtos; + +public class ProductDto +{ + public required string Id { get; set; } + + public required string Url { get; set; } + + public required string Name { get; set; } + + public required string Description { get; set; } + + public required double Rating { get; set; } + + public required string[] ImagesUrls { get; set; } + + public required bool WasOpened { get; set; } + + public required string WishlistId { get; set; } +} diff --git a/ShoppingAssistantApi.Application/Models/Dtos/WishlistDto.cs b/ShoppingAssistantApi.Application/Models/Dtos/WishlistDto.cs index 9398c26..cbb2cf6 100644 --- a/ShoppingAssistantApi.Application/Models/Dtos/WishlistDto.cs +++ b/ShoppingAssistantApi.Application/Models/Dtos/WishlistDto.cs @@ -8,5 +8,5 @@ public class WishlistDto public required string Type { get; set; } - public string CreatedById { get; set; } = null!; + public required string CreatedById { get; set; } } diff --git a/ShoppingAssistantApi.Domain/Entities/Message.cs b/ShoppingAssistantApi.Domain/Entities/Message.cs index 8a50457..1d9328e 100644 --- a/ShoppingAssistantApi.Domain/Entities/Message.cs +++ b/ShoppingAssistantApi.Domain/Entities/Message.cs @@ -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 ObjectId WishlistId { get; set; } } diff --git a/ShoppingAssistantApi.Domain/Entities/Product.cs b/ShoppingAssistantApi.Domain/Entities/Product.cs new file mode 100644 index 0000000..2085293 --- /dev/null +++ b/ShoppingAssistantApi.Domain/Entities/Product.cs @@ -0,0 +1,22 @@ +using MongoDB.Bson; +using ShoppingAssistantApi.Domain.Common; + +namespace ShoppingAssistantApi.Domain.Entities; + +public class Product : EntityBase +{ + + public string Url { get; set; } + + public string Name { get; set; } + + public string Description { get; set; } + + public double Rating { get; set; } + + public string[] ImagesUrls { get; set; } + + public bool WasOpened { get; set; } + + public ObjectId WishlistId { get; set; } +} diff --git a/ShoppingAssistantApi.Domain/Entities/RefreshToken.cs b/ShoppingAssistantApi.Domain/Entities/RefreshToken.cs new file mode 100644 index 0000000..e36974b --- /dev/null +++ b/ShoppingAssistantApi.Domain/Entities/RefreshToken.cs @@ -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; } +} diff --git a/ShoppingAssistantApi.Domain/Entities/User.cs b/ShoppingAssistantApi.Domain/Entities/User.cs index 7f5a0b1..27e928b 100644 --- a/ShoppingAssistantApi.Domain/Entities/User.cs +++ b/ShoppingAssistantApi.Domain/Entities/User.cs @@ -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; } } diff --git a/ShoppingAssistantApi.Domain/Entities/Wishlist.cs b/ShoppingAssistantApi.Domain/Entities/Wishlist.cs index 11fe978..4dee7fc 100644 --- a/ShoppingAssistantApi.Domain/Entities/Wishlist.cs +++ b/ShoppingAssistantApi.Domain/Entities/Wishlist.cs @@ -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? Messages { get; set; } } diff --git a/ShoppingAssistantApi.Infrastructure/Services/Identity/TokensService.cs b/ShoppingAssistantApi.Infrastructure/Services/Identity/TokensService.cs index 2ea9728..b50f302 100644 --- a/ShoppingAssistantApi.Infrastructure/Services/Identity/TokensService.cs +++ b/ShoppingAssistantApi.Infrastructure/Services/Identity/TokensService.cs @@ -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 logger) + public TokensService( + IConfiguration configuration, + ILogger logger) { this._configuration = configuration; - this._usersRepository = usersRepository; this._logger = logger; } - public async Task 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 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("JsonWebTokenKeys:ValidIssuer"), audience: _configuration.GetValue("JsonWebTokenKeys:ValidAudience"), - expires: DateTime.UtcNow.AddMinutes(5), + expires: DateTime.UtcNow.AddMinutes(15), claims: claims, signingCredentials: signinCredentials ); diff --git a/ShoppingAssistantApi.Infrastructure/Services/Identity/UserManager.cs b/ShoppingAssistantApi.Infrastructure/Services/Identity/UserManager.cs index 571ad9c..7720477 100644 --- a/ShoppingAssistantApi.Infrastructure/Services/Identity/UserManager.cs +++ b/ShoppingAssistantApi.Infrastructure/Services/Identity/UserManager.cs @@ -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 logger, IPasswordHasher passwordHasher, ITokensService tokensService, IMapper mapper, IRolesRepository rolesRepository) + public UserManager( + IUsersRepository usersRepository, + IPasswordHasher passwordHasher, + ITokensService tokensService, + IRolesRepository rolesRepository, + IRefreshTokensRepository refreshTokensRepository, + IMapper mapper, + ILogger 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 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(); @@ -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 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 }, + 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 }, - 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 AddToRoleAsync(string roleName, string id, CancellationToken cancellationToken) + public async Task 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 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(); } - 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.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(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 RemoveFromRoleAsync(string roleName, string id, CancellationToken cancellationToken) + public async Task 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(); } - 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(); } 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(updatedUser); - return tokens; + this._logger.LogInformation($"Removed Role: {roleName} from User with Id: {userId}."); + + return userDto; } public async Task 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(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(); } - 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("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("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(user) }; + var updatedUserDto = this._mapper.Map(updatedUser); + + this._logger.LogInformation($"Update user with id: {GlobalUser.Id}."); + + return new UpdateUserModel() + { + Tokens = tokens, + User = updatedUserDto + }; } - public async Task UpdateUserByAdminAsync(string id, UserDto userDto, CancellationToken cancellationToken) + public async Task 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(); } + 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(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(user) }; + return updatedUserDto; } - private string GetRefreshToken() + private async Task 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() { - 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("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("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}$"; diff --git a/ShoppingAssistantApi.Infrastructure/Services/RolesService.cs b/ShoppingAssistantApi.Infrastructure/Services/RolesService.cs index 7881ec4..d30bc3b 100644 --- a/ShoppingAssistantApi.Infrastructure/Services/RolesService.cs +++ b/ShoppingAssistantApi.Infrastructure/Services/RolesService.cs @@ -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(entity); } public async Task> GetRolesPageAsync(int pageNumber, int pageSize, CancellationToken cancellationToken) { var entities = await this._repository.GetPageAsync(pageNumber, pageSize, cancellationToken); - var dtos = this._mapper.Map>(entities); var count = await this._repository.GetTotalCountAsync(); + var dtos = this._mapper.Map>(entities); + return new PagedList(dtos, pageNumber, pageSize, count); } } diff --git a/ShoppingAssistantApi.Infrastructure/Services/ServiceBase.cs b/ShoppingAssistantApi.Infrastructure/Services/ServiceBase.cs new file mode 100644 index 0000000..3e1381f --- /dev/null +++ b/ShoppingAssistantApi.Infrastructure/Services/ServiceBase.cs @@ -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."); + } +} diff --git a/ShoppingAssistantApi.Infrastructure/Services/WishlistsService.cs b/ShoppingAssistantApi.Infrastructure/Services/WishlistsService.cs index 7e1fc40..5dd2ae7 100644 --- a/ShoppingAssistantApi.Infrastructure/Services/WishlistsService.cs +++ b/ShoppingAssistantApi.Infrastructure/Services/WishlistsService.cs @@ -18,12 +18,15 @@ public class WishlistsService : IWishlistsService private readonly IMessagesRepository _messagesRepository; + private readonly IProductsRepository _productsRepository; + private readonly IMapper _mapper; - public WishlistsService(IWishlistsRepository wishlistRepository, IMessagesRepository messageRepository, IMapper mapper) + public WishlistsService(IWishlistsRepository wishlistRepository, IMessagesRepository messageRepository, IProductsRepository productRepository, IMapper mapper) { _wishlistsRepository = wishlistRepository; _messagesRepository = messageRepository; + _productsRepository = productRepository; _mapper = mapper; } @@ -47,6 +50,8 @@ public class WishlistsService : IWishlistsService { Text = dto.FirstMessageText, Role = MessageRoles.User.ToString(), + CreatedById = (ObjectId) GlobalUser.Id, + CreatedDateUtc = DateTime.UtcNow, WishlistId = createdWishlist.Id }; var createdMessage = await _messagesRepository.AddAsync(newMessage, cancellationToken); @@ -62,17 +67,13 @@ public class WishlistsService : IWishlistsService { throw new InvalidDataException("Provided id is invalid."); } - newMessage.WishlistId = wishlistObjectId; + newMessage.Role = MessageRoles.User.ToString(); newMessage.CreatedById = (ObjectId) GlobalUser.Id; newMessage.CreatedDateUtc = DateTime.UtcNow; + newMessage.WishlistId = wishlistObjectId; - var relatedWishlist = await _wishlistsRepository.GetWishlistAsync(x => x.Id == wishlistObjectId && x.CreatedById == GlobalUser.Id, cancellationToken); - - if (relatedWishlist == null) - { - throw new UnAuthorizedException(); - } + await TryGetPersonalWishlist(wishlistObjectId, cancellationToken); var createdMessage = await _messagesRepository.AddAsync(newMessage, cancellationToken); @@ -81,7 +82,7 @@ public class WishlistsService : IWishlistsService public async Task> GetPersonalWishlistsPageAsync(int pageNumber, int pageSize, CancellationToken cancellationToken) { - var entities = await _wishlistsRepository.GetPageAsync(pageNumber, pageSize, cancellationToken); + var entities = await _wishlistsRepository.GetPageAsync(pageNumber, pageSize, x => x.CreatedById == GlobalUser.Id, cancellationToken); var dtos = _mapper.Map>(entities); var count = await _wishlistsRepository.GetTotalCountAsync(); return new PagedList(dtos, pageNumber, pageSize, count); @@ -93,15 +94,95 @@ public class WishlistsService : IWishlistsService { throw new InvalidDataException("Provided id is invalid."); } - var entity = await _wishlistsRepository.GetWishlistAsync(x => x.Id == wishlistObjectId && x.CreatedById == GlobalUser.Id, cancellationToken); - Console.WriteLine(" WISHLIST: " + entity.CreatedById + " " + GlobalUser.Id); - - if (entity == null) - { - throw new UnAuthorizedException(); - } + var entity = await TryGetPersonalWishlist(wishlistObjectId, cancellationToken); return _mapper.Map(entity); } + + public async Task> GetMessagesPageFromPersonalWishlistAsync(string wishlistId, int pageNumber, int pageSize, CancellationToken cancellationToken) + { + if (!ObjectId.TryParse(wishlistId, out var wishlistObjectId)) + { + throw new InvalidDataException("Provided id is invalid."); + } + + await TryGetPersonalWishlist(wishlistObjectId, cancellationToken); + + var entities = await _messagesRepository.GetPageStartingFromEndAsync(pageNumber, pageSize, x => x.WishlistId == wishlistObjectId, cancellationToken); + + var dtos = _mapper.Map>(entities); + var count = await _messagesRepository.GetCountAsync(x => x.WishlistId == wishlistObjectId, cancellationToken); + return new PagedList(dtos, pageNumber, pageSize, count); + } + + public async Task AddProductToPersonalWishlistAsync(string wishlistId, ProductCreateDto dto, CancellationToken cancellationToken) + { + var newProduct = _mapper.Map(dto); + + if (!ObjectId.TryParse(wishlistId, out var wishlistObjectId)) + { + throw new InvalidDataException("Provided id is invalid."); + } + + await TryGetPersonalWishlist(wishlistObjectId, cancellationToken); + + newProduct.CreatedById = (ObjectId) GlobalUser.Id; + newProduct.CreatedDateUtc = DateTime.UtcNow; + newProduct.WishlistId = wishlistObjectId; + + var createdProduct = await _productsRepository.AddAsync(newProduct, cancellationToken); + + return _mapper.Map(createdProduct); + } + + public async Task> GetProductsPageFromPersonalWishlistAsync(string wishlistId, int pageNumber, int pageSize, CancellationToken cancellationToken) + { + if (!ObjectId.TryParse(wishlistId, out var wishlistObjectId)) + { + throw new InvalidDataException("Provided id is invalid."); + } + + await TryGetPersonalWishlist(wishlistObjectId, cancellationToken); + + var entities = await _productsRepository.GetPageAsync(pageNumber, pageSize, x => x.WishlistId == wishlistObjectId, cancellationToken); + + var dtos = _mapper.Map>(entities); + var count = await _productsRepository.GetCountAsync(x => x.WishlistId == wishlistObjectId, cancellationToken); + return new PagedList(dtos, pageNumber, pageSize, count); + } + + public async Task DeletePersonalWishlistAsync(string wishlistId, CancellationToken cancellationToken) + { + if (!ObjectId.TryParse(wishlistId, out var wishlistObjectId)) + { + throw new InvalidDataException("Provided id is invalid."); + } + + var entity = await TryGetPersonalWishlist(wishlistObjectId, cancellationToken); + + entity.LastModifiedById = GlobalUser.Id; + entity.LastModifiedDateUtc = DateTime.UtcNow; + + await _wishlistsRepository.DeleteAsync(entity, cancellationToken); + + return _mapper.Map(entity); + } + + private async Task TryGetPersonalWishlist(ObjectId wishlistId, CancellationToken cancellationToken) + { + var entity = await _wishlistsRepository.GetWishlistAsync(x => x.Id == wishlistId, cancellationToken); + + if (entity.CreatedById != GlobalUser.Id) + { + throw new UnAuthorizedException(); + } + + if (entity == null) + { + throw new EntityNotFoundException(); + } + + return entity; + } } diff --git a/ShoppingAssistantApi.Persistance/Database/MongoDbContext.cs b/ShoppingAssistantApi.Persistance/Database/MongoDbContext.cs index ea3f3a3..ac3876e 100644 --- a/ShoppingAssistantApi.Persistance/Database/MongoDbContext.cs +++ b/ShoppingAssistantApi.Persistance/Database/MongoDbContext.cs @@ -16,4 +16,6 @@ public class MongoDbContext } public IMongoDatabase Db => this._db; + + public MongoClient Client => this._client; } \ No newline at end of file diff --git a/ShoppingAssistantApi.Persistance/PersistanceExtentions/DbInitialaizer.cs b/ShoppingAssistantApi.Persistance/PersistanceExtentions/DbInitialaizer.cs index a63c8f7..f627b46 100644 --- a/ShoppingAssistantApi.Persistance/PersistanceExtentions/DbInitialaizer.cs +++ b/ShoppingAssistantApi.Persistance/PersistanceExtentions/DbInitialaizer.cs @@ -1,42 +1,34 @@ using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using MongoDB.Bson; using MongoDB.Driver; -using ShoppingAssistantApi.Application.GlobalInstances; -using ShoppingAssistantApi.Application.IServices; using ShoppingAssistantApi.Application.IServices.Identity; -using ShoppingAssistantApi.Application.Models.CreateDtos; -using ShoppingAssistantApi.Application.Models.Dtos; -using ShoppingAssistantApi.Application.Models.Identity; using ShoppingAssistantApi.Domain.Entities; using ShoppingAssistantApi.Domain.Enums; +using ShoppingAssistantApi.Infrastructure.Services.Identity; using ShoppingAssistantApi.Persistance.Database; namespace ShoppingAssistantApi.Persistance.PersistanceExtentions; public class DbInitialaizer { - private readonly IUsersService _usersService; - - private readonly IUserManager _userManager; - - private readonly IRolesService _rolesService; - - private readonly ITokensService _tokensService; - private readonly IMongoCollection _userCollection; + private readonly IMongoCollection _roleCollection; + private readonly IMongoCollection _wishlistCollection; - public IEnumerable Roles { get; set; } + private readonly IMongoCollection _productCollection; + + private readonly PasswordHasher passwordHasher; public DbInitialaizer(IServiceProvider serviceProvider) { - _usersService = serviceProvider.GetService(); - _rolesService = serviceProvider.GetService(); - _userManager = serviceProvider.GetService(); - _tokensService = serviceProvider.GetService(); + passwordHasher = new PasswordHasher(new Logger(new LoggerFactory())); _wishlistCollection = serviceProvider.GetService().Db.GetCollection("Wishlists"); _userCollection = serviceProvider.GetService().Db.GetCollection("Users"); + _roleCollection = serviceProvider.GetService().Db.GetCollection("Roles"); + _productCollection = serviceProvider.GetService().Db.GetCollection("Product"); } public async Task InitialaizeDb(CancellationToken cancellationToken) @@ -44,127 +36,145 @@ public class DbInitialaizer await AddRoles(cancellationToken); await AddUsers(cancellationToken); await AddWishlistsWithMessages(cancellationToken); + await AddProducts(cancellationToken); } public async Task AddUsers(CancellationToken cancellationToken) { - var guestModel1 = new AccessGuestModel + var userRole = await (await _roleCollection.FindAsync(x => x.Name.Equals("User"))).FirstAsync(); + var guestRole = await (await _roleCollection.FindAsync(x => x.Name.Equals("Guest"))).FirstAsync(); + var adminRole = await (await _roleCollection.FindAsync(x => x.Name.Equals("Admin"))).FirstAsync(); + + var users = new User[] { - GuestId = Guid.NewGuid(), + new User + { + Id = ObjectId.Parse("6533bb29c8c22b038c71cf46"), + GuestId = Guid.NewGuid(), + Roles = new List {guestRole}, + CreatedById = ObjectId.Parse("6533bb29c8c22b038c71cf46"), + CreatedDateUtc = DateTime.UtcNow, + LastModifiedById = ObjectId.Parse("6533bb29c8c22b038c71cf46"), + LastModifiedDateUtc = DateTime.UtcNow, + IsDeleted = false + }, + + new User + { + Id = ObjectId.Parse("6533bde5755745116be42ce7"), + GuestId = Guid.NewGuid(), + Roles = new List + { + guestRole, + userRole + }, + Phone = "+380953326869", + Email = "mykhailo.bilodid@nure.ua", + PasswordHash = this.passwordHasher.Hash("Yuiop12345"), + CreatedById = ObjectId.Parse("6533bde5755745116be42ce7"), + CreatedDateUtc = DateTime.UtcNow, + LastModifiedById = ObjectId.Parse("6533bde5755745116be42ce7"), + LastModifiedDateUtc = DateTime.UtcNow, + IsDeleted = false + }, + + new User + { + Id = ObjectId.Parse("6533bded80fbc6e96250575b"), + GuestId = Guid.NewGuid(), + Roles = new List + { + guestRole, + userRole, + adminRole + }, + Phone = "+380953826869", + Email = "shopping.assistant.team@gmail.com", + PasswordHash = this.passwordHasher.Hash("Yuiop12345"), + CreatedById = ObjectId.Parse("6533bded80fbc6e96250575b"), + CreatedDateUtc = DateTime.UtcNow, + LastModifiedById = ObjectId.Parse("6533bded80fbc6e96250575b"), + LastModifiedDateUtc = DateTime.UtcNow, + IsDeleted = false }, + + new User + { + Id = ObjectId.Parse("6533bdf9efaca5bb0894f992"), + GuestId = Guid.NewGuid(), + Roles = new List + { + guestRole, + userRole + }, + Phone = "+380983326869", + Email = "vitalii.krasnorutski@nure.ua", + PasswordHash = this.passwordHasher.Hash("Yuiop12345"), + CreatedById = ObjectId.Parse("6533bdf9efaca5bb0894f992"), + CreatedDateUtc = DateTime.UtcNow, + LastModifiedById = ObjectId.Parse("6533bdf9efaca5bb0894f992"), + LastModifiedDateUtc = DateTime.UtcNow, + IsDeleted = false }, + + new User + { + Id = ObjectId.Parse("6533be06d1b78a76c664ddae"), + GuestId = Guid.NewGuid(), + Roles = new List + { + guestRole, + userRole + }, + Phone = "+380953326888", + Email = "serhii.shchoholiev@nure.ua", + PasswordHash = this.passwordHasher.Hash("Yuiop12345"), + CreatedById = ObjectId.Parse("6533be06d1b78a76c664ddae"), + CreatedDateUtc = DateTime.UtcNow, + LastModifiedById = ObjectId.Parse("6533be06d1b78a76c664ddae"), + LastModifiedDateUtc = DateTime.UtcNow, + IsDeleted = false } }; - var guestModel2 = new AccessGuestModel - { - GuestId = Guid.NewGuid(), - }; - - var guestModel3 = new AccessGuestModel - { - GuestId = Guid.NewGuid(), - }; - - var guestModel4 = new AccessGuestModel - { - GuestId = Guid.NewGuid(), - }; - - var guestModel5 = new AccessGuestModel - { - GuestId = Guid.NewGuid(), - }; - - Task.WaitAll( - _userManager.AccessGuestAsync(guestModel1, cancellationToken), - _userManager.AccessGuestAsync(guestModel2, cancellationToken), - _userManager.AccessGuestAsync(guestModel3, cancellationToken), - _userManager.AccessGuestAsync(guestModel4, cancellationToken), - _userManager.AccessGuestAsync(guestModel5, cancellationToken) - ); - - var guests = await _usersService.GetUsersPageAsync(1, 4, cancellationToken); - var guestsResult = guests.Items.ToList(); - - var user1 = new UserDto - { - Id = guestsResult[0].Id, - GuestId = guestsResult[0].GuestId, - Roles = guestsResult[0].Roles, - Phone = "+380953326869", - Email = "mykhailo.bilodid@nure.ua", - Password = "Yuiop12345", - RefreshToken = _tokensService.GenerateRefreshToken(), - RefreshTokenExpiryDate = DateTime.Now.AddDays(7), - }; - - var user2 = new UserDto - { - Id = guestsResult[1].Id, - GuestId = guestsResult[1].GuestId, - Roles = guestsResult[1].Roles, - Phone = "+380953326888", - Email = "serhii.shchoholiev@nure.ua", - Password = "Yuiop12345", - RefreshToken = _tokensService.GenerateRefreshToken(), - RefreshTokenExpiryDate = DateTime.Now.AddDays(7), - }; - - var user3 = new UserDto - { - Id = guestsResult[2].Id, - GuestId = guestsResult[2].GuestId, - Roles = guestsResult[2].Roles, - Phone = "+380983326869", - Email = "vitalii.krasnorutski@nure.ua", - Password = "Yuiop12345", - RefreshToken = _tokensService.GenerateRefreshToken(), - RefreshTokenExpiryDate = DateTime.Now.AddDays(7), - }; - - var user4 = new UserDto - { - Id = guestsResult[3].Id, - GuestId = guestsResult[3].GuestId, - Roles = guestsResult[3].Roles, - Phone = "+380953826869", - Email = "shopping.assistant.team@gmail.com", - Password = "Yuiop12345", - RefreshToken = _tokensService.GenerateRefreshToken(), - RefreshTokenExpiryDate = DateTime.Now.AddDays(7), - }; - - GlobalUser.Id = ObjectId.Parse(user1.Id); - await _userManager.UpdateAsync(user1, cancellationToken); - - GlobalUser.Id = ObjectId.Parse(user2.Id); - await _userManager.UpdateAsync(user2, cancellationToken); - - GlobalUser.Id = ObjectId.Parse(user3.Id); - await _userManager.UpdateAsync(user3, cancellationToken); - - GlobalUser.Id = ObjectId.Parse(user4.Id); - await _userManager.UpdateAsync(user4, cancellationToken); + await _userCollection.InsertManyAsync(users); } public async Task AddRoles(CancellationToken cancellationToken) { - var role1 = new RoleCreateDto + var roles = new Role[] { - Name = "User" - }; + new Role + { + Id = ObjectId.Parse("6533b5882e7867b8b21e7b27"), + Name = "User", + CreatedById = ObjectId.Parse("6533bded80fbc6e96250575b"), + CreatedDateUtc = DateTime.UtcNow, + LastModifiedById = ObjectId.Parse("6533bded80fbc6e96250575b"), + LastModifiedDateUtc = DateTime.UtcNow, + IsDeleted = false + }, - var role2 = new RoleCreateDto - { - Name = "Admin" - }; + new Role + { + Id = ObjectId.Parse("6533b591a7f31776cd2d50fc"), + Name = "Guest", + CreatedById = ObjectId.Parse("6533bded80fbc6e96250575b"), + CreatedDateUtc = DateTime.UtcNow, + LastModifiedById = ObjectId.Parse("6533bded80fbc6e96250575b"), + LastModifiedDateUtc = DateTime.UtcNow, + IsDeleted = false + }, - var role3 = new RoleCreateDto - { - Name = "Guest" + new Role + { + Id = ObjectId.Parse("6533b59d1b09ab2618af5ff3"), + Name = "Admin", + CreatedById = ObjectId.Parse("6533bded80fbc6e96250575b"), + CreatedDateUtc = DateTime.UtcNow, + LastModifiedById = ObjectId.Parse("6533bded80fbc6e96250575b"), + LastModifiedDateUtc = DateTime.UtcNow, + IsDeleted = false + }, }; - - var dto1 = await _rolesService.AddRoleAsync(role1, cancellationToken); - var dto2 = await _rolesService.AddRoleAsync(role2, cancellationToken); - var dto3 = await _rolesService.AddRoleAsync(role3, cancellationToken); + await _roleCollection.InsertManyAsync(roles); } public async Task AddWishlistsWithMessages(CancellationToken cancellationToken) @@ -186,11 +196,17 @@ public class DbInitialaizer { Text = "Prompt", Role = MessageRoles.User.ToString(), + WishlistId = ObjectId.Parse("ab79cde6f69abcd3efab65cd"), + CreatedById = user1.Id, + CreatedDateUtc = DateTime.UtcNow }, new Message { Text = "Answer", Role = MessageRoles.Application.ToString(), + WishlistId = ObjectId.Parse("ab79cde6f69abcd3efab65cd"), + CreatedById = user1.Id, + CreatedDateUtc = DateTime.UtcNow }, } }, @@ -206,6 +222,9 @@ public class DbInitialaizer { Text = "Prompt", Role = MessageRoles.User.ToString(), + WishlistId = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab"), + CreatedById = user1.Id, + CreatedDateUtc = DateTime.UtcNow } } } @@ -213,4 +232,83 @@ public class DbInitialaizer await _wishlistCollection.InsertManyAsync(wishlists); } + + public async Task AddProducts(CancellationToken cancellationToken) + { + var products = new Product[] + { + new Product() + { + Name = "Thermaltake Glacier 360 Liquid-Cooled PC", + Description = "Cool PC for any task!", + Rating = 4.3, + Url = "https://www.amazon.com/Thermaltake-Liquid-Cooled-ToughRAM-Computer-S3WT-B550-G36-LCS/dp" + + "/B09FYNM2GW/ref=sr_1_1?crid=391KAS4JFJSFF&keywords=gaming%2Bpc&qid=1697132083&sprefix=gaming%2Bpc%2Caps%2C209&sr=8-1&th=1", + ImagesUrls = new string[] + { + "https://m.media-amazon.com/images/I/61cXu9yGldL._AC_SL1200_.jpg", + "https://m.media-amazon.com/images/I/615gxSGp42L._AC_SL1200_.jpg" + }, + CreatedDateUtc = DateTime.UtcNow, + WasOpened = false, + WishlistId = ObjectId.Parse("ab79cde6f69abcd3efab65cd") + }, + + new Product() + { + Name = "Apple MagSafe Battery Pack", + Description = "Portable Charger with Fast Charging Capability, Power Bank Compatible with iPhone", + Rating = 4.3, + Url = "https://www.amazon.com/Apple-MJWY3AM-A-MagSafe-Battery/dp/" + + "B099BWY7WT/ref=sr_1_1?keywords=apple+power+bank&qid=1697375350&sr=8-1", + ImagesUrls = new string[] + { + "https://m.media-amazon.com/images/I/418SjFMB1wL._AC_SX679_.jpg", + "https://m.media-amazon.com/images/I/51v4pgChtLL._AC_SX679_.jpg", + "https://m.media-amazon.com/images/I/61mJ0z7uYQL._AC_SX679_.jpg" + }, + CreatedDateUtc = DateTime.UtcNow, + WasOpened = false, + WishlistId = ObjectId.Parse("ab79cde6f69abcd3efab65cd") + }, + + new Product() + { + Name = "Logitech K400 Plus Wireless Touch With Easy Media Control and Built-in Touchpad", + Description = "Reliable membrane keyboard with touchpad!", + Rating = 4.5, + Url = "https://www.amazon.com/Logitech-Wireless-Keyboard-Touchpad-PC-connected/dp/B014EUQOGK/" + + "ref=sr_1_11?crid=BU2PHZKHKD65&keywords=keyboard+wireless&qid=1697375559&sprefix=keyboard+wir%2Caps%2C195&sr=8-11", + ImagesUrls = new string[] + { + "https://m.media-amazon.com/images/I/51yjnWJ5urL._AC_SX466_.jpg", + "https://m.media-amazon.com/images/I/71al70zP7QL._AC_SX466_.jpg", + "https://m.media-amazon.com/images/I/71+JXDDY01L._AC_SX466_.jpg" + }, + CreatedDateUtc = DateTime.UtcNow, + WasOpened = false, + WishlistId = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab") + }, + + new Product() + { + Name = "Logitech MX Anywhere 2S Wireless Mouse Use On Any Surface", + Description = "Cross computer control: Game changing capacity to navigate seamlessly on three computers," + + " and copy paste text, images, and files from one to the other using Logitech Flow", + Rating = 4.6, + Url = "https://www.amazon.com/Logitech-Hyper-Fast-Scrolling-Rechargeable-Computers/dp/B08P2JFPQC/ref=sr_1_8?" + + "crid=2BL6Z14W2TPP3&keywords=mouse%2Bwireless&qid=1697375784&sprefix=mousewireless%2Caps%2C197&sr=8-8&th=1", + ImagesUrls = new string[] + { + "https://m.media-amazon.com/images/I/6170mJHIsYL._AC_SX466_.jpg", + "https://m.media-amazon.com/images/I/71a5As76MDL._AC_SX466_.jpg" + }, + CreatedDateUtc = DateTime.UtcNow, + WasOpened = false, + WishlistId = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab") + } + }; + + await _productCollection.InsertManyAsync(products); + } } diff --git a/ShoppingAssistantApi.Persistance/PersistanceExtentions/RepositoriesExtention.cs b/ShoppingAssistantApi.Persistance/PersistanceExtentions/RepositoriesExtention.cs index e48ae73..3da2a49 100644 --- a/ShoppingAssistantApi.Persistance/PersistanceExtentions/RepositoriesExtention.cs +++ b/ShoppingAssistantApi.Persistance/PersistanceExtentions/RepositoriesExtention.cs @@ -13,8 +13,10 @@ public static class RepositoriesExtention services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } diff --git a/ShoppingAssistantApi.Persistance/Repositories/BaseRepository.cs b/ShoppingAssistantApi.Persistance/Repositories/BaseRepository.cs index 565112e..8a128ff 100644 --- a/ShoppingAssistantApi.Persistance/Repositories/BaseRepository.cs +++ b/ShoppingAssistantApi.Persistance/Repositories/BaseRepository.cs @@ -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 where TEntity : EntityBase +public abstract class BaseRepository : IBaseRepository where TEntity : EntityBase { protected MongoDbContext _db; @@ -17,6 +19,16 @@ public abstract class BaseRepository where TEntity : EntityBase this._collection = _db.Db.GetCollection(collectionName); } + public async Task GetOneAsync(ObjectId id, CancellationToken cancellationToken) + { + return await this._collection.Find(x => x.Id == id).FirstOrDefaultAsync(cancellationToken); + } + + public async Task GetOneAsync(Expression> predicate, CancellationToken cancellationToken) + { + return await this._collection.Find(predicate).FirstOrDefaultAsync(cancellationToken); + } + public async Task AddAsync(TEntity entity, CancellationToken cancellationToken) { await this._collection.InsertOneAsync(entity, new InsertOneOptions(), cancellationToken); @@ -69,4 +81,4 @@ public abstract class BaseRepository where TEntity : EntityBase return await this._collection.FindOneAndUpdateAsync( Builders.Filter.Eq(e => e.Id, entity.Id), updateDefinition, options, cancellationToken); } -} \ No newline at end of file +} diff --git a/ShoppingAssistantApi.Persistance/Repositories/MessagesRepository.cs b/ShoppingAssistantApi.Persistance/Repositories/MessagesRepository.cs index 06481f6..55734c9 100644 --- a/ShoppingAssistantApi.Persistance/Repositories/MessagesRepository.cs +++ b/ShoppingAssistantApi.Persistance/Repositories/MessagesRepository.cs @@ -1,3 +1,5 @@ +using System.Linq.Expressions; +using MongoDB.Driver; using ShoppingAssistantApi.Application.IRepositories; using ShoppingAssistantApi.Domain.Entities; using ShoppingAssistantApi.Persistance.Database; @@ -7,4 +9,13 @@ namespace ShoppingAssistantApi.Persistance.Repositories; public class MessagesRepository : BaseRepository, IMessagesRepository { public MessagesRepository(MongoDbContext db) : base(db, "Messages") { } + + public async Task> GetPageStartingFromEndAsync(int pageNumber, int pageSize, Expression> predicate, CancellationToken cancellationToken) + { + return await _collection.Find(predicate) + .SortByDescending(x => x.CreatedDateUtc) + .Skip((pageNumber - 1) * pageSize) + .Limit(pageSize) + .ToListAsync(cancellationToken); + } } diff --git a/ShoppingAssistantApi.Persistance/Repositories/ProductRepository.cs b/ShoppingAssistantApi.Persistance/Repositories/ProductRepository.cs new file mode 100644 index 0000000..3e0863a --- /dev/null +++ b/ShoppingAssistantApi.Persistance/Repositories/ProductRepository.cs @@ -0,0 +1,10 @@ +using ShoppingAssistantApi.Application.IRepositories; +using ShoppingAssistantApi.Domain.Entities; +using ShoppingAssistantApi.Persistance.Database; + +namespace ShoppingAssistantApi.Persistance.Repositories; + +public class ProductsRepository : BaseRepository, IProductsRepository +{ + public ProductsRepository(MongoDbContext db) : base(db, "Products") { } +} diff --git a/ShoppingAssistantApi.Persistance/Repositories/RefreshTokensRepository.cs b/ShoppingAssistantApi.Persistance/Repositories/RefreshTokensRepository.cs new file mode 100644 index 0000000..0228cf9 --- /dev/null +++ b/ShoppingAssistantApi.Persistance/Repositories/RefreshTokensRepository.cs @@ -0,0 +1,10 @@ +using ShoppingAssistantApi.Application.IRepositories; +using ShoppingAssistantApi.Domain.Entities; +using ShoppingAssistantApi.Persistance.Database; + +namespace ShoppingAssistantApi.Persistance.Repositories; + +public class RefreshTokensRepository : BaseRepository, IRefreshTokensRepository +{ + public RefreshTokensRepository(MongoDbContext db) : base(db, "RefreshTokens") { } +} diff --git a/ShoppingAssistantApi.Persistance/Repositories/UsersRepository.cs b/ShoppingAssistantApi.Persistance/Repositories/UsersRepository.cs index 198bc08..39b4ab5 100644 --- a/ShoppingAssistantApi.Persistance/Repositories/UsersRepository.cs +++ b/ShoppingAssistantApi.Persistance/Repositories/UsersRepository.cs @@ -27,8 +27,6 @@ public class UsersRepository : BaseRepository, IUsersRepository var updateDefinition = Builders.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, IUsersRepository return await this._collection.FindOneAndUpdateAsync( Builders.Filter.Eq(u => u.Id, user.Id), updateDefinition, options, cancellationToken); - } - } diff --git a/ShoppingAssistantApi.Persistance/ShoppingAssistantApi.Persistance.csproj b/ShoppingAssistantApi.Persistance/ShoppingAssistantApi.Persistance.csproj index 951f76f..743a04f 100644 --- a/ShoppingAssistantApi.Persistance/ShoppingAssistantApi.Persistance.csproj +++ b/ShoppingAssistantApi.Persistance/ShoppingAssistantApi.Persistance.csproj @@ -14,6 +14,7 @@ + diff --git a/ShoppingAssistantApi.Tests/ShoppingAssistantApi.Tests.csproj b/ShoppingAssistantApi.Tests/ShoppingAssistantApi.Tests.csproj index a7e4074..c36f3b4 100644 --- a/ShoppingAssistantApi.Tests/ShoppingAssistantApi.Tests.csproj +++ b/ShoppingAssistantApi.Tests/ShoppingAssistantApi.Tests.csproj @@ -10,7 +10,6 @@ - diff --git a/ShoppingAssistantApi.Tests/TestExtentions/AccessExtention.cs b/ShoppingAssistantApi.Tests/TestExtentions/AccessExtention.cs deleted file mode 100644 index 16e0a03..0000000 --- a/ShoppingAssistantApi.Tests/TestExtentions/AccessExtention.cs +++ /dev/null @@ -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 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(responseString); - - return new TokensModel - { - AccessToken = (string)document.data.login.accessToken, - RefreshToken = (string)document.data.login.refreshToken - }; - } - - public static async Task 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(responseString); - - return new TokensModel - { - AccessToken = (string)document.data.accessGuest.accessToken, - RefreshToken = (string)document.data.accessGuest.refreshToken - }; - } -} \ No newline at end of file diff --git a/ShoppingAssistantApi.Tests/TestExtentions/DbInitializer.cs b/ShoppingAssistantApi.Tests/TestExtentions/DbInitializer.cs new file mode 100644 index 0000000..811cce6 --- /dev/null +++ b/ShoppingAssistantApi.Tests/TestExtentions/DbInitializer.cs @@ -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("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(new LoggerFactory())); + var usersCollection = _dbContext.Db.GetCollection("Users"); + + var testUser = new User + { + Id = ObjectId.Parse("652c3b89ae02a3135d6409fc"), + Email = "test@gmail.com", + Phone = "+380953326869", + Roles = new List { 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 { 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 { 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("Wishlists"); + var usersCollection = _dbContext.Db.GetCollection("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("Messages"); + var usersCollection = _dbContext.Db.GetCollection("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("Products"); + var usersCollection = _dbContext.Db.GetCollection("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); + } +} diff --git a/ShoppingAssistantApi.Tests/TestExtentions/TestingFactory.cs b/ShoppingAssistantApi.Tests/TestExtentions/TestingFactory.cs index a8407df..a63a957 100644 --- a/ShoppingAssistantApi.Tests/TestExtentions/TestingFactory.cs +++ b/ShoppingAssistantApi.Tests/TestExtentions/TestingFactory.cs @@ -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 : WebApplicationFactory 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() - { - { "ConnectionStrings:MongoDb", _runner.ConnectionString } - }) - .Build(); + var dbConfig = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary() + { + { "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(); + + var initialaizer = new DbInitializer(mongodbContext); + initialaizer.InitializeDb(); + + _isDataInitialaized = true; } protected override void Dispose(bool disposing) { - _runner.Dispose(); + _runner?.Dispose(); base.Dispose(disposing); } } \ No newline at end of file diff --git a/ShoppingAssistantApi.Tests/TestExtentions/UserExtention.cs b/ShoppingAssistantApi.Tests/TestExtentions/UserExtention.cs deleted file mode 100644 index 269ff8a..0000000 --- a/ShoppingAssistantApi.Tests/TestExtentions/UserExtention.cs +++ /dev/null @@ -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 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(responseString); - return JsonConvert.DeserializeObject(document.data.currentUser.ToString()); - } - - public static async Task> 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(responseString); - return JsonConvert.DeserializeObject>(document.data.usersPage.items.ToString()); - } -} \ No newline at end of file diff --git a/ShoppingAssistantApi.Tests/Tests/AccessTests.cs b/ShoppingAssistantApi.Tests/Tests/AccessTests.cs index 65f29f4..0a5195c 100644 --- a/ShoppingAssistantApi.Tests/Tests/AccessTests.cs +++ b/ShoppingAssistantApi.Tests/Tests/AccessTests.cs @@ -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> +// TODO: make errors test more descrptive +public class AccessTests : TestsBase { - private readonly HttpClient _httpClient; - public AccessTests(TestingFactory 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> } }; - 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(); - 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(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> } }; - 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> } }; - 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> } } }; + + var jsonObject = await SendGraphQlRequestAsync(mutation); + var tokens = (TokensModel?) jsonObject?.data?.login?.ToObject(); - 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(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(); - 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(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 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(); + + return tokens; } } \ No newline at end of file diff --git a/ShoppingAssistantApi.Tests/Tests/RolesTests.cs b/ShoppingAssistantApi.Tests/Tests/RolesTests.cs index 29b3774..2e52ef1 100644 --- a/ShoppingAssistantApi.Tests/Tests/RolesTests.cs +++ b/ShoppingAssistantApi.Tests/Tests/RolesTests.cs @@ -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> +// TODO: make errors test more descrptive +public class RolesTests : TestsBase { - private readonly HttpClient _httpClient; - public RolesTests(TestingFactory 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(); + + 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(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(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> { 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> pageSize = 3 } }; + + var jsonObject = await SendGraphQlRequestAsync(query); + var pagedList = (PagedList?) jsonObject?.data?.rolesPage?.ToObject>(); - 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(responseString); - - var items = document.data.rolesPage.items; - Assert.NotEmpty(items); + Assert.NotNull(pagedList); + Assert.NotEmpty(pagedList.Items); } } \ No newline at end of file diff --git a/ShoppingAssistantApi.Tests/Tests/TestsBase.cs b/ShoppingAssistantApi.Tests/Tests/TestsBase.cs new file mode 100644 index 0000000..d5d7962 --- /dev/null +++ b/ShoppingAssistantApi.Tests/Tests/TestsBase.cs @@ -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> +{ + private protected HttpClient _httpClient; + + public TestsBase(TestingFactory 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 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(responseString); + + return jsonObject; + } + + public async Task 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(); + + return user; + } +} \ No newline at end of file diff --git a/ShoppingAssistantApi.Tests/Tests/UsersTests.cs b/ShoppingAssistantApi.Tests/Tests/UsersTests.cs index 8c0e7d1..1d34597 100644 --- a/ShoppingAssistantApi.Tests/Tests/UsersTests.cs +++ b/ShoppingAssistantApi.Tests/Tests/UsersTests.cs @@ -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> +// TODO: make errors test more descrptive +public class UsersTests : TestsBase { - private readonly HttpClient _httpClient; - public UsersTests(TestingFactory 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(); + var updatedUser = (UserDto?) jsonObject?.data?.updateUser?.user?.ToObject(); - 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(responseString); - - var accessTokenResult = (string)document.data.updateUser.tokens.accessToken; - var refreshTokenResult = (string)document.data.updateUser.tokens.refreshToken; - var userResult = JsonConvert.DeserializeObject(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(); - 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(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(); - 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(responseString); - var userResult = JsonConvert.DeserializeObject(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(); - 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(responseString); - - var user = JsonConvert.DeserializeObject(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> } }; - var jsonPayload = JsonConvert.SerializeObject(query); - var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); + var jsonObject = await SendGraphQlRequestAsync(query); + var pagedList = (PagedList?) jsonObject?.data?.usersPage?.ToObject>(); - 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(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(); + + 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(); + + 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); } } diff --git a/ShoppingAssistantApi.Tests/Tests/WishlistsTests.cs b/ShoppingAssistantApi.Tests/Tests/WishlistsTests.cs index d153a33..5d989b7 100644 --- a/ShoppingAssistantApi.Tests/Tests/WishlistsTests.cs +++ b/ShoppingAssistantApi.Tests/Tests/WishlistsTests.cs @@ -1,40 +1,47 @@ using ShoppingAssistantApi.Tests.TestExtentions; -using System.Net.Http.Headers; -using System.Net; -using System.Text; -using Xunit; -using Newtonsoft.Json; using ShoppingAssistantApi.Domain.Enums; +using ShoppingAssistantApi.Application.Models.Dtos; +using ShoppingAssistantApi.Application.Paging; +using Newtonsoft.Json.Linq; namespace ShoppingAssistantApi.Tests.Tests; -[Collection("Tests")] -public class WishlistsTests : IClassFixture> +public class WishlistsTests : TestsBase { - private readonly HttpClient _httpClient; + // From DbInitializer + private const string TestingUserId = "652c3b89ae02a3135d6418fc"; - private const string WISHLIST_TESTING_USER_EMAIL = "shopping.assistant.team@gmail.com"; + private const string TestingUserEmail = "wishlists@gmail.com"; - private const string WISHLIST_TESTING_USER_PASSWORD = "Yuiop12345"; + private const string TestingUserPassword = "Yuiop12345"; + + private const string TestingNotExistingWishlistId = "1234567890abcdef12345678"; + + private const string TestingValidWishlistName = "Gaming PC"; + + private const WishlistTypes TestingValidWishlistType = WishlistTypes.Product; + + private const string TestingUnauthorizedWishlistId = "ab6c2c2d9edf39abcd1ef9ab"; + + private const string TestingValidWishlistId = "ab79cde6f69abcd3efab65cd"; - private const string TESTING_WISHLIST_ID = "ab79cde6f69abcd3efab65cd"; public WishlistsTests(TestingFactory factory) - { - _httpClient = factory.CreateClient(); - factory.InitialaizeData().GetAwaiter().GetResult(); - } + : base(factory) + { } [Fact] - public async Task StartPersonalWishlistAsync_ValidWishlistModel_ReturnsNewWishlistModels() + public async Task StartPersonalWishlist_ValidWishlist_ReturnsNewWishlist() { - var tokensModel = await AccessExtention.Login(WISHLIST_TESTING_USER_EMAIL, WISHLIST_TESTING_USER_PASSWORD, _httpClient); - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokensModel.AccessToken); - var user = await UserExtention.GetCurrentUser(_httpClient); - + await LoginAsync(TestingUserEmail, TestingUserPassword); var mutation = new { - query = "mutation startPersonalWishlist($dto: WishlistCreateDtoInput!) { startPersonalWishlist (dto: $dto) { id, name, type, createdById } }", + query = @" + mutation startPersonalWishlist($dto: WishlistCreateDtoInput!) { + startPersonalWishlist (dto: $dto) { + id, name, type, createdById + } + }", variables = new { dto = new @@ -45,36 +52,27 @@ public class WishlistsTests : IClassFixture> } }; - var jsonPayload = JsonConvert.SerializeObject(mutation); - var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); + var jsonObject = await SendGraphQlRequestAsync(mutation); + var wishlist = (WishlistDto?) jsonObject?.data?.startPersonalWishlist?.ToObject(); - 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(responseString); - - var wishlistId = (string) document.data.startPersonalWishlist.id; - var wishlistCreatedById = (string) document.data.startPersonalWishlist.createdById; - var wishlistType = (string) document.data.startPersonalWishlist.type; - var wishlistName = (string) document.data.startPersonalWishlist.name; - - Assert.Equal(user.Id, wishlistCreatedById); - Assert.Equal(WishlistTypes.Product.ToString(), wishlistType); - Assert.Equal($"{WishlistTypes.Product} Search", wishlistName); + Assert.NotNull(wishlist); + Assert.Equal(TestingUserId, wishlist.CreatedById); + Assert.Equal(WishlistTypes.Product.ToString(), wishlist.Type); + Assert.Equal($"{WishlistTypes.Product} Search", wishlist.Name); } [Fact] public async Task GetPersonalWishlistsPage_ValidPageNumberAndSize_ReturnsPage() { - var tokensModel = await AccessExtention.Login(WISHLIST_TESTING_USER_EMAIL, WISHLIST_TESTING_USER_PASSWORD, _httpClient); - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokensModel.AccessToken); - var user = await UserExtention.GetCurrentUser(_httpClient); - + await LoginAsync(TestingUserEmail, TestingUserPassword); var query = new { - query = "query personalWishlistsPage($pageNumber: Int!, $pageSize: Int!) { personalWishlistsPage(pageNumber: $pageNumber, pageSize: $pageSize) { items { createdById, id, name, type } } }", + query = @" + query personalWishlistsPage($pageNumber: Int!, $pageSize: Int!) { + personalWishlistsPage(pageNumber: $pageNumber, pageSize: $pageSize) { + items { createdById, id, name, type } + } + }", variables = new { pageNumber = 1, @@ -82,111 +80,223 @@ public class WishlistsTests : IClassFixture> } }; - var jsonPayload = JsonConvert.SerializeObject(query); - var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); + var jsonObject = await SendGraphQlRequestAsync(query); + var pagedList = (PagedList?) jsonObject?.data?.personalWishlistsPage?.ToObject>(); - 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(responseString); - - var personalWishlistsPageItems = Enumerable.ToList(document.data.personalWishlistsPage.items); - var personalWishlistCreatedById = (string) personalWishlistsPageItems[0].createdById; - - Assert.NotEmpty(personalWishlistsPageItems); - Assert.Equal(user.Id, personalWishlistCreatedById); + Assert.NotNull(pagedList); + Assert.NotEmpty(pagedList.Items); } [Fact] - public async Task GetPersonalWishlist_ValidWishlistIdOrAuthorizedAccess_ReturnsWishlistDto() + public async Task GetPersonalWishlist_ValidWishlistIdOrAuthorizedAccess_ReturnsWishlist() { - var tokensModel = await AccessExtention.Login(WISHLIST_TESTING_USER_EMAIL, WISHLIST_TESTING_USER_PASSWORD, _httpClient); - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokensModel.AccessToken); - var user = await UserExtention.GetCurrentUser(_httpClient); - + await LoginAsync(TestingUserEmail, TestingUserPassword); var query = new { - query = "query personalWishlist($wishlistId: String!) { personalWishlist(wishlistId: $wishlistId) { createdById, id, name, type } }", + query = @" + query personalWishlist($wishlistId: String!) { + personalWishlist(wishlistId: $wishlistId) { + createdById, id, name, type + } + }", variables = new { - wishlistId = TESTING_WISHLIST_ID + wishlistId = TestingValidWishlistId } }; - var jsonPayload = JsonConvert.SerializeObject(query); - var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); + var jsonObject = await SendGraphQlRequestAsync(query); + var wishlist = (WishlistDto?) jsonObject?.data?.personalWishlist?.ToObject(); - 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(responseString); - - var personalWishlistId = (string) document.data.personalWishlist.id; - var personalWishlistName = (string) document.data.personalWishlist.name; - var personalWishlistType = (string) document.data.personalWishlist.type; - var personalWishlistCreatedById = (string) document.data.personalWishlist.createdById; - - Assert.Equal(TESTING_WISHLIST_ID, personalWishlistId); - Assert.Equal("Gaming PC", personalWishlistName); - Assert.Equal(WishlistTypes.Product.ToString(), personalWishlistType); - Assert.Equal(user.Id, personalWishlistCreatedById); + Assert.NotNull(wishlist); + Assert.Equal(TestingValidWishlistName, wishlist.Name); + Assert.Equal(TestingValidWishlistType.ToString(), wishlist.Type); + Assert.Equal(TestingUserId, wishlist.CreatedById); } [Fact] - public async Task AddMessageToPersonalWishlist_ValidMessageModel_ReturnsNewMessageModel() + public async Task AddMessageToPersonalWishlist_ValidMessage_ReturnsNewMessage() { - var tokensModel = await AccessExtention.Login(WISHLIST_TESTING_USER_EMAIL, WISHLIST_TESTING_USER_PASSWORD, _httpClient); - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokensModel.AccessToken); - var user = await UserExtention.GetCurrentUser(_httpClient); - - const string MESSAGE_TEXT = "Second Message"; - + await LoginAsync(TestingUserEmail, TestingUserPassword); + const string MessageText = "Second Message"; var mutation = new { - query = "mutation addMessageToPersonalWishlist($wishlistId: String!, $dto: MessageCreateDtoInput!) { addMessageToPersonalWishlist (wishlistId: $wishlistId, dto: $dto) { role, text, createdById } }", + query = @" + mutation addMessageToPersonalWishlist($wishlistId: String!, $dto: MessageCreateDtoInput!) { + addMessageToPersonalWishlist(wishlistId: $wishlistId, dto: $dto) { + role, text, createdById + } + }", variables = new { - wishlistId = TESTING_WISHLIST_ID, + wishlistId = TestingValidWishlistId, dto = new { - text = MESSAGE_TEXT + text = MessageText } } }; - var jsonPayload = JsonConvert.SerializeObject(mutation); - var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); + var jsonObject = await SendGraphQlRequestAsync(mutation); + var message = (MessageDto?) jsonObject?.data?.addMessageToPersonalWishlist?.ToObject(); - 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(responseString); - - var messageRole = (string) document.data.addMessageToPersonalWishlist.role; - var messageText = (string) document.data.addMessageToPersonalWishlist.text; - var messageCreatedById = (string) document.data.addMessageToPersonalWishlist.createdById; - - Assert.Equal(MessageRoles.User.ToString(), messageRole); - Assert.Equal(MESSAGE_TEXT, messageText); - Assert.Equal(user.Id, messageCreatedById); + Assert.NotNull(message); + Assert.Equal(MessageRoles.User.ToString(), message.Role); + Assert.Equal(MessageText, message.Text); + Assert.Equal(TestingUserId, message.CreatedById); } [Fact] - public async Task StartPersonalWishlistAsync_InvalidWishlistModel_ReturnsInternalServerError() + public async Task GetMessagesPageFromPersonalWishlist_ValidPageNumberAndSizeValidWishlistIdOrAuthorizedAccess_ReturnsPage() { - var tokensModel = await AccessExtention.Login(WISHLIST_TESTING_USER_EMAIL, WISHLIST_TESTING_USER_PASSWORD, _httpClient); - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokensModel.AccessToken); - var user = await UserExtention.GetCurrentUser(_httpClient); - + await LoginAsync(TestingUserEmail, TestingUserPassword); var mutation = new { - query = "mutation startPersonalWishlist($dto: WishlistCreateDtoInput!) { startPersonalWishlist (dto: $dto) { id, name, type, createdById } }", + query = @" + query messagesPageFromPersonalWishlist($wishlistId: String!, $pageNumber: Int!, $pageSize: Int!) { + messagesPageFromPersonalWishlist (wishlistId: $wishlistId, pageNumber: $pageNumber, pageSize: $pageSize) { + hasNextPage, + hasPreviousPage, + items { id, text, role, createdById }, + pageNumber, + pageSize, + totalItems, + totalPages + } + }", + variables = new + { + wishlistId = TestingValidWishlistId, + pageNumber = 1, + pageSize = 2 + } + }; + + var jsonObject = await SendGraphQlRequestAsync(mutation); + var pagedList = (PagedList?) jsonObject?.data?.messagesPageFromPersonalWishlist?.ToObject>(); + + Assert.NotNull(pagedList); + Assert.NotEmpty(pagedList.Items); + Assert.Equal("Message 6", pagedList.Items.FirstOrDefault()?.Text); + Assert.Equal(MessageRoles.Application.ToString(), pagedList.Items.FirstOrDefault()?.Role); + } + + [Fact] + public async Task AddProductToPersonalWishlist_ValidProduct_ReturnsNewProduct() + { + await LoginAsync(TestingUserEmail, TestingUserPassword); + var mutation = new + { + query = @" + mutation addProductToPersonalWishlist($wishlistId: String!, $dto: ProductCreateDtoInput!) { + addProductToPersonalWishlist (wishlistId: $wishlistId, dto: $dto) { + url, name, description, rating, imagesUrls, wasOpened + } + }", + variables = new + { + wishlistId = TestingValidWishlistId, + dto = new + { + url = "https://www.amazon.com/url", + name = "Generic name", + description = "Generic description", + rating = 4.8, + imagesUrls = new string[] + { + "https://www.amazon.com/image-url-1", + "https://www.amazon.com/image-url-2" + }, + wasOpened = false + } + } + }; + + var jsonObject = await SendGraphQlRequestAsync(mutation); + var product = (ProductDto?) jsonObject?.data?.addProductToPersonalWishlist?.ToObject(); + + Assert.NotNull(product); + Assert.Equal("https://www.amazon.com/url", product.Url); + Assert.Equal("Generic name", product.Name); + Assert.Equal("Generic description", product.Description); + Assert.Equal(4.8, product.Rating); + Assert.Equal("https://www.amazon.com/image-url-1", product.ImagesUrls[0]); + } + + [Fact] + public async Task GetProductsPageFromPersonalWishlist_ValidPageNumberAndSizeValidWishlistIdOrAuthorizedAccess_ReturnsPage() + { + await LoginAsync(TestingUserEmail, TestingUserPassword); + var mutation = new + { + query = @" + query productsPageFromPersonalWishlist($wishlistId: String!, $pageNumber: Int!, $pageSize: Int!) { + productsPageFromPersonalWishlist (wishlistId: $wishlistId, pageNumber: $pageNumber, pageSize: $pageSize) { + hasNextPage, + hasPreviousPage, + items { id, url, name, description, rating, imagesUrls, wasOpened, wishlistId }, + pageNumber, + pageSize, + totalItems, + totalPages + } + }", + variables = new + { + wishlistId = TestingValidWishlistId, + pageNumber = 1, + pageSize = 2 + } + }; + + var jsonObject = await SendGraphQlRequestAsync(mutation); + var pagedList = (PagedList?) jsonObject?.data?.productsPageFromPersonalWishlist?.ToObject>(); + + Assert.NotNull(pagedList); + Assert.Equal("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 ", pagedList.Items.LastOrDefault()?.Name); + Assert.Equal(TestingValidWishlistId, pagedList.Items.LastOrDefault()?.WishlistId); + } + + [Fact] + public async Task DeletePersonalWishlist_ValidWishlistIdOrAuthorizedAccess_ReturnsWishlist() + { + await LoginAsync(TestingUserEmail, TestingUserPassword); + var mutation = new + { + query = @" + mutation deletePersonalWishlist($wishlistId: String!) { + deletePersonalWishlist (wishlistId: $wishlistId) { + createdById, id, name, type + } + }", + variables = new + { + wishlistId = TestingValidWishlistId + } + }; + + var jsonObject = await SendGraphQlRequestAsync(mutation); + var wishlist = (WishlistDto?) jsonObject?.data?.deletePersonalWishlist?.ToObject(); + + Assert.NotNull(wishlist); + Assert.Equal(TestingValidWishlistId, wishlist.Id); + Assert.Equal(TestingValidWishlistName, wishlist.Name); + Assert.Equal(TestingValidWishlistType.ToString(), wishlist.Type); + Assert.Equal(TestingUserId, wishlist.CreatedById); + } + + [Fact] + public async Task StartPersonalWishlist_InvalidWishlist_ReturnsErrors() + { + await LoginAsync(TestingUserEmail, TestingUserPassword); + var mutation = new + { + query = @" + mutation startPersonalWishlist($dto: WishlistCreateDtoInput!) { + startPersonalWishlist (dto: $dto) { + id, name, type, createdById + } + }", variables = new { dto = new @@ -197,85 +307,701 @@ public class WishlistsTests : IClassFixture> } }; - 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 GetPersonalWishlist_InvalidWishlistId_ReturnsInternalServerError() + public async Task GetPersonalWishlistsPage_PageNumberGreaterThanAvailablePages_ReturnsEmptyList() { - var tokensModel = await AccessExtention.Login(WISHLIST_TESTING_USER_EMAIL, WISHLIST_TESTING_USER_PASSWORD, _httpClient); - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokensModel.AccessToken); - var user = await UserExtention.GetCurrentUser(_httpClient); - - var query = new - { - query = "query personalWishlist($wishlistId: String!) { personalWishlist(wishlistId: $wishlistId) { createdById, id, name, type } }", - variables = new - { - wishlistId = "1234567890abcdef12345678" // Invalid wishlistId - } - }; - - var jsonPayload = JsonConvert.SerializeObject(query); - 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 GetPersonalWishlist_UnAuthorizedAccess_ReturnsInternalServerError() - { - var tokensModel = await AccessExtention.Login(WISHLIST_TESTING_USER_EMAIL, WISHLIST_TESTING_USER_PASSWORD, _httpClient); - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokensModel.AccessToken); - var user = await UserExtention.GetCurrentUser(_httpClient); - - var query = new - { - query = "query personalWishlist($wishlistId: String!) { personalWishlist(wishlistId: $wishlistId) { createdById, id, name, type } }", - variables = new - { - wishlistId = "ab6c2c2d9edf39abcd1ef9ab" // Other user's wishlist - } - }; - - var jsonPayload = JsonConvert.SerializeObject(query); - 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 AddMessageToPersonalWishlist_InvalidMessageModel_ReturnsInternalServerError() - { - var tokensModel = await AccessExtention.Login(WISHLIST_TESTING_USER_EMAIL, WISHLIST_TESTING_USER_PASSWORD, _httpClient); - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokensModel.AccessToken); - var user = await UserExtention.GetCurrentUser(_httpClient); - - const string MESSAGE_TEXT = "Second Message"; - + await LoginAsync(TestingUserEmail, TestingUserPassword); var mutation = new { - query = "mutation addMessageToPersonalWishlist($wishlistId: String!, $dto: MessageCreateDtoInput!) { addMessageToPersonalWishlist (wishlistId: $wishlistId, dto: $dto) { role, text, createdById } }", + query = @" + query personalWishlistsPage($pageNumber: Int!, $pageSize: Int!) { + personalWishlistsPage(pageNumber: $pageNumber, pageSize: $pageSize) { + items { createdById, id, name, type } + } + }", variables = new { - wishlistId = "8125jad7g12", // Invalid wishlistId + pageNumber = 100, + pageSize = 2 + } + }; + + var jsonObject = await SendGraphQlRequestAsync(mutation); + var pagedList = (PagedList?) jsonObject?.data?.personalWishlistsPage?.ToObject>(); + + Assert.NotNull(pagedList); + Assert.Empty(pagedList.Items); + } + + [Fact] + public async Task GetPersonalWishlistsPage_PageNumberLessThan1_ReturnsError() + { + await LoginAsync(TestingUserEmail, TestingUserPassword); + var mutation = new + { + query = @" + query personalWishlistsPage($pageNumber: Int!, $pageSize: Int!) { + personalWishlistsPage(pageNumber: $pageNumber, pageSize: $pageSize) { + items { createdById, id, name, type } + } + }", + variables = new + { + pageNumber = 0, + pageSize = 1 + } + }; + + var jsonObject = await SendGraphQlRequestAsync(mutation); + var errors = (JArray?) jsonObject?.errors; + + Assert.NotNull(errors); + Assert.True(errors.Count > 0); + } + + [Fact] + public async Task GetPersonalWishlistsPage_PageSizeGreaterThanAvailableEntities_ReturnsPage() + { + await LoginAsync(TestingUserEmail, TestingUserPassword); + var mutation = new + { + query = @" + query personalWishlistsPage($pageNumber: Int!, $pageSize: Int!) { + personalWishlistsPage(pageNumber: $pageNumber, pageSize: $pageSize) { + items { createdById, id, name, type } + } + }", + variables = new + { + pageNumber = 1, + pageSize = 100 + } + }; + + var jsonObject = await SendGraphQlRequestAsync(mutation); + var pagedList = (PagedList?) jsonObject?.data?.personalWishlistsPage?.ToObject>(); + + Assert.NotNull(pagedList); + Assert.NotEmpty(pagedList.Items); + Assert.Equal(TestingUserId, pagedList.Items.FirstOrDefault()?.CreatedById); + } + + [Fact] + public async Task GetPersonalWishlistsPage_PageSizeLessThan0_ReturnsPage() + { + await LoginAsync(TestingUserEmail, TestingUserPassword); + var mutation = new + { + query = @" + query personalWishlistsPage($pageNumber: Int!, $pageSize: Int!) { + personalWishlistsPage(pageNumber: $pageNumber, pageSize: $pageSize) { + items { createdById, id, name, type } + } + }", + variables = new + { + pageNumber = 1, + pageSize = -1 + } + }; + + var jsonObject = await SendGraphQlRequestAsync(mutation); + var pagedList = (PagedList?) jsonObject?.data?.personalWishlistsPage?.ToObject>(); + + Assert.NotNull(pagedList); + Assert.NotEmpty(pagedList.Items); + Assert.Equal(TestingUserId, pagedList.Items.FirstOrDefault()?.CreatedById); + } + + [Fact] + public async Task GetPersonalWishlist_NotExistingWishlistId_ReturnsErrors() + { + await LoginAsync(TestingUserEmail, TestingUserPassword); + var query = new + { + query = @" + query personalWishlist($wishlistId: String!) { + personalWishlist(wishlistId: $wishlistId) { + createdById, id, name, type + } + }", + variables = new + { + wishlistId = TestingNotExistingWishlistId + } + }; + + var jsonObject = await SendGraphQlRequestAsync(query); + var errors = (JArray?) jsonObject?.errors; + + Assert.NotNull(errors); + Assert.True(errors.Count > 0); + } + + [Fact] + public async Task GetMessagesPageFromPersonalWishlist_PageNumberGreaterThanAvailablePages_ReturnsEmptyList() + { + await LoginAsync(TestingUserEmail, TestingUserPassword); + var mutation = new + { + query = @" + query messagesPageFromPersonalWishlist($wishlistId: String!, $pageNumber: Int!, $pageSize: Int!) { + messagesPageFromPersonalWishlist (wishlistId: $wishlistId, pageNumber: $pageNumber, pageSize: $pageSize) { + hasNextPage, + hasPreviousPage, + items { id, text, role, createdById }, + pageNumber, + pageSize, + totalItems, + totalPages + } + }", + variables = new + { + wishlistId = TestingValidWishlistId, + pageNumber = 100, + pageSize = 2 + } + }; + + var jsonObject = await SendGraphQlRequestAsync(mutation); + var pagedList = (PagedList?) jsonObject?.data?.messagesPageFromPersonalWishlist?.ToObject>(); + + Assert.NotNull(pagedList); + Assert.Empty(pagedList.Items); + } + + [Fact] + public async Task GetMessagesPageFromPersonalWishlist_PageNumberLessThan1_ReturnsError() + { + await LoginAsync(TestingUserEmail, TestingUserPassword); + var mutation = new + { + query = @" + query messagesPageFromPersonalWishlist($wishlistId: String!, $pageNumber: Int!, $pageSize: Int!) { + messagesPageFromPersonalWishlist (wishlistId: $wishlistId, pageNumber: $pageNumber, pageSize: $pageSize) { + hasNextPage, + hasPreviousPage, + items { id, text, role, createdById }, + pageNumber, + pageSize, + totalItems, + totalPages + } + }", + variables = new + { + wishlistId = TestingValidWishlistId, + pageNumber = 0, + pageSize = 2 + } + }; + + var jsonObject = await SendGraphQlRequestAsync(mutation); + var errors = (JArray?) jsonObject?.errors; + + Assert.NotNull(errors); + Assert.True(errors.Count > 0); + } + + [Fact] + public async Task GetMessagesPageFromPersonalWishlist_PageSizeGreaterThanAvailableEntities_ReturnsPage() + { + await LoginAsync(TestingUserEmail, TestingUserPassword); + var mutation = new + { + query = @" + query messagesPageFromPersonalWishlist($wishlistId: String!, $pageNumber: Int!, $pageSize: Int!) { + messagesPageFromPersonalWishlist (wishlistId: $wishlistId, pageNumber: $pageNumber, pageSize: $pageSize) { + hasNextPage, + hasPreviousPage, + items { id, text, role, createdById }, + pageNumber, + pageSize, + totalItems, + totalPages + } + }", + variables = new + { + wishlistId = TestingValidWishlistId, + pageNumber = 1, + pageSize = 10 + } + }; + + var jsonObject = await SendGraphQlRequestAsync(mutation); + var pagedList = (PagedList?) jsonObject?.data?.messagesPageFromPersonalWishlist?.ToObject>(); + + Assert.NotNull(pagedList); + Assert.Equal("Message 6", pagedList.Items.FirstOrDefault()?.Text); + Assert.Equal(MessageRoles.Application.ToString(), pagedList.Items.FirstOrDefault()?.Role); + } + + [Fact] + public async Task GetMessagesPageFromPersonalWishlist_PageSizeLessThan0_ReturnsPage() + { + await LoginAsync(TestingUserEmail, TestingUserPassword); + var mutation = new + { + query = @" + query messagesPageFromPersonalWishlist($wishlistId: String!, $pageNumber: Int!, $pageSize: Int!) { + messagesPageFromPersonalWishlist (wishlistId: $wishlistId, pageNumber: $pageNumber, pageSize: $pageSize) { + hasNextPage, + hasPreviousPage, + items { id, text, role, createdById }, + pageNumber, + pageSize, + totalItems, + totalPages + } + }", + variables = new + { + wishlistId = TestingValidWishlistId, + pageNumber = 1, + pageSize = -2 + } + }; + + var jsonObject = await SendGraphQlRequestAsync(mutation); + var pagedList = (PagedList?) jsonObject?.data?.messagesPageFromPersonalWishlist?.ToObject>(); + + Assert.NotNull(pagedList); + Assert.Equal("Message 6", pagedList.Items.FirstOrDefault()?.Text); + Assert.Equal(MessageRoles.Application.ToString(), pagedList.Items.FirstOrDefault()?.Role); + } + + [Fact] + public async Task GetMessagesPageFromPersonalWishlist_NotExistingWishlistId_ReturnsError() + { + await LoginAsync(TestingUserEmail, TestingUserPassword); + var mutation = new + { + query = @" + query messagesPageFromPersonalWishlist($wishlistId: String!, $pageNumber: Int!, $pageSize: Int!) { + messagesPageFromPersonalWishlist (wishlistId: $wishlistId, pageNumber: $pageNumber, pageSize: $pageSize) { + hasNextPage, + hasPreviousPage, + items { id, text, role, createdById }, + pageNumber, + pageSize, + totalItems, + totalPages + } + }", + variables = new + { + wishlistId = TestingNotExistingWishlistId, + pageNumber = 1, + pageSize = 2 + } + }; + + var jsonObject = await SendGraphQlRequestAsync(mutation); + var errors = (JArray?) jsonObject?.errors; + + Assert.NotNull(errors); + Assert.True(errors.Count > 0); + } + + [Fact] + public async Task GetMessagesPageFromPersonalWishlist_OtherUserWishlistId_ReturnsError() + { + await LoginAsync(TestingUserEmail, TestingUserPassword); + var mutation = new + { + query = @" + query messagesPageFromPersonalWishlist($wishlistId: String!, $pageNumber: Int!, $pageSize: Int!) { + messagesPageFromPersonalWishlist (wishlistId: $wishlistId, pageNumber: $pageNumber, pageSize: $pageSize) { + hasNextPage, + hasPreviousPage, + items { id, text, role, createdById }, + pageNumber, + pageSize, + totalItems, + totalPages + } + }", + variables = new + { + wishlistId = TestingUnauthorizedWishlistId, + pageNumber = 1, + pageSize = 2 + } + }; + + var jsonObject = await SendGraphQlRequestAsync(mutation); + var errors = (JArray?) jsonObject?.errors; + + Assert.NotNull(errors); + Assert.True(errors.Count > 0); + } + + [Fact] + public async Task AddProductToPersonalWishlist_NotExistingWishlistId_RturnsError() + { + await LoginAsync(TestingUserEmail, TestingUserPassword); + var mutation = new + { + query = @" + mutation addProductToPersonalWishlist($wishlistId: String!, $dto: ProductCreateDtoInput!) { + addProductToPersonalWishlist (wishlistId: $wishlistId, dto: $dto) { + url, name, description, rating, imagesUrls, wasOpened + } + }", + variables = new + { + wishlistId = TestingNotExistingWishlistId, dto = new { - text = MESSAGE_TEXT, + url = "https://www.amazon.com/url", + name = "Generic name", + description = "Generic description", + rating = 4.8, + imagesUrls = new string[] + { + "https://www.amazon.com/image-url-1", + "https://www.amazon.com/image-url-2" + }, + wasOpened = false } } }; - 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 AddProductToPersonalWishlist_OtherUserWishlistId_RturnsError() + { + await LoginAsync(TestingUserEmail, TestingUserPassword); + var mutation = new + { + query = @" + mutation addProductToPersonalWishlist($wishlistId: String!, $dto: ProductCreateDtoInput!) { + addProductToPersonalWishlist (wishlistId: $wishlistId, dto: $dto) { + url, name, description, rating, imagesUrls, wasOpened + } + }", + variables = new + { + wishlistId = TestingUnauthorizedWishlistId, + dto = new + { + url = "https://www.amazon.com/url", + name = "Generic name", + description = "Generic description", + rating = 4.8, + imagesUrls = new string[] + { + "https://www.amazon.com/image-url-1", + "https://www.amazon.com/image-url-2" + }, + wasOpened = false + } + } + }; + + var jsonObject = await SendGraphQlRequestAsync(mutation); + var errors = (JArray?) jsonObject?.errors; + + Assert.NotNull(errors); + Assert.True(errors.Count > 0); + } + + [Fact] + public async Task GetProductsPageFromPersonalWishlist_PageNumberGreaterThanAvailablePages_ReturnsEmptyList() + { + await LoginAsync(TestingUserEmail, TestingUserPassword); + var mutation = new + { + query = @" + query productsPageFromPersonalWishlist($wishlistId: String!, $pageNumber: Int!, $pageSize: Int!) { + productsPageFromPersonalWishlist (wishlistId: $wishlistId, pageNumber: $pageNumber, pageSize: $pageSize) { + hasNextPage, + hasPreviousPage, + items { id, url, name, description, rating, imagesUrls, wasOpened, wishlistId }, + pageNumber, + pageSize, + totalItems, + totalPages + } + }", + variables = new + { + wishlistId = TestingValidWishlistId, + pageNumber = 100, + pageSize = 2 + } + }; + + var jsonObject = await SendGraphQlRequestAsync(mutation); + var pagedList = (PagedList?) jsonObject?.data?.productsPageFromPersonalWishlist?.ToObject>(); + + Assert.NotNull(pagedList); + Assert.Empty(pagedList.Items); + } + + [Fact] + public async Task GetProductsPageFromPersonalWishlist_PageNumberLessThan1_ReturnsError() + { + await LoginAsync(TestingUserEmail, TestingUserPassword); + var mutation = new + { + query = @" + query productsPageFromPersonalWishlist($wishlistId: String!, $pageNumber: Int!, $pageSize: Int!) { + productsPageFromPersonalWishlist (wishlistId: $wishlistId, pageNumber: $pageNumber, pageSize: $pageSize) { + hasNextPage, + hasPreviousPage, + items { id, url, name, description, rating, imagesUrls, wasOpened, wishlistId }, + pageNumber, + pageSize, + totalItems, + totalPages + } + }", + variables = new + { + wishlistId = TestingValidWishlistId, + pageNumber = 0, + pageSize = 2 + } + }; + + var jsonObject = await SendGraphQlRequestAsync(mutation); + var errors = (JArray?) jsonObject?.errors; + + Assert.NotNull(errors); + Assert.True(errors.Count > 0); + } + + [Fact] + public async Task GetProductsPageFromPersonalWishlist_PageSizeGreaterThanAvailableEntities_ReturnsPage() + { + await LoginAsync(TestingUserEmail, TestingUserPassword); + var mutation = new + { + query = "query productsPageFromPersonalWishlist($wishlistId: String!, $pageNumber: Int!, $pageSize: Int!) { productsPageFromPersonalWishlist (wishlistId: $wishlistId, pageNumber: $pageNumber, pageSize: $pageSize) { hasNextPage, hasPreviousPage, items { id, url, name, description, rating, imagesUrls, wasOpened, wishlistId }, pageNumber, pageSize, totalItems, totalPages } }", + variables = new + { + wishlistId = TestingValidWishlistId, + pageNumber = 1, + pageSize = 100 + } + }; + + var jsonObject = await SendGraphQlRequestAsync(mutation); + var pagedList = (PagedList?) jsonObject?.data?.productsPageFromPersonalWishlist?.ToObject>(); + + Assert.NotNull(pagedList); + + Assert.Equal("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 ", pagedList.Items.ToList()[1].Name); + Assert.Equal(TestingValidWishlistId, pagedList.Items.ToList()[1].WishlistId); + } + + [Fact] + public async Task GetProductsPageFromPersonalWishlist_PageSizeLessThan0_ReturnsPage() + { + await LoginAsync(TestingUserEmail, TestingUserPassword); + var mutation = new + { + query = "query productsPageFromPersonalWishlist($wishlistId: String!, $pageNumber: Int!, $pageSize: Int!) { productsPageFromPersonalWishlist (wishlistId: $wishlistId, pageNumber: $pageNumber, pageSize: $pageSize) { hasNextPage, hasPreviousPage, items { id, url, name, description, rating, imagesUrls, wasOpened, wishlistId }, pageNumber, pageSize, totalItems, totalPages } }", + variables = new + { + wishlistId = TestingValidWishlistId, + pageNumber = 1, + pageSize = -2 + } + }; + + var jsonObject = await SendGraphQlRequestAsync(mutation); + var pagedList = (PagedList?) jsonObject?.data?.productsPageFromPersonalWishlist?.ToObject>(); + + Assert.NotNull(pagedList); + + Assert.Equal("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 ", pagedList.Items.ToList()[1].Name); + Assert.Equal(TestingValidWishlistId, pagedList.Items.ToList()[1].WishlistId); + } + + [Fact] + public async Task GetPersonalWishlist_OtherUserWishlistId_ReturnsErrors() + { + await LoginAsync(TestingUserEmail, TestingUserPassword); + var query = new + { + query = @" + query personalWishlist($wishlistId: String!) { + personalWishlist(wishlistId: $wishlistId) { + createdById, id, name, type + } + }", + variables = new + { + wishlistId = TestingUnauthorizedWishlistId + } + }; + + var jsonObject = await SendGraphQlRequestAsync(query); + var errors = (JArray?) jsonObject?.errors; + + Assert.NotNull(errors); + Assert.True(errors.Count > 0); + } + + [Fact] + public async Task GetProductsPageFromPersonalWishlist_NotExistingWishlistId_ReturnsError() + { + await LoginAsync(TestingUserEmail, TestingUserPassword); + var mutation = new + { + query = @" + query productsPageFromPersonalWishlist($wishlistId: String!, $pageNumber: Int!, $pageSize: Int!) { + productsPageFromPersonalWishlist (wishlistId: $wishlistId, pageNumber: $pageNumber, pageSize: $pageSize) { + hasNextPage, + hasPreviousPage, + items { id, url, name, description, rating, imagesUrls, wasOpened, wishlistId }, + pageNumber, + pageSize, + totalItems, + totalPages + } + }", + variables = new + { + wishlistId = TestingNotExistingWishlistId, + pageNumber = 0, + pageSize = 2 + } + }; + + var jsonObject = await SendGraphQlRequestAsync(mutation); + var errors = (JArray?) jsonObject?.errors; + + Assert.NotNull(errors); + Assert.True(errors.Count > 0); + } + + [Fact] + public async Task GetProductsPageFromPersonalWishlist_OtherUserWishlistId_ReturnsError() + { + await LoginAsync(TestingUserEmail, TestingUserPassword); + var mutation = new + { + query = @" + query productsPageFromPersonalWishlist($wishlistId: String!, $pageNumber: Int!, $pageSize: Int!) { + productsPageFromPersonalWishlist (wishlistId: $wishlistId, pageNumber: $pageNumber, pageSize: $pageSize) { + hasNextPage, + hasPreviousPage, + items { id, url, name, description, rating, imagesUrls, wasOpened, wishlistId }, + pageNumber, + pageSize, + totalItems, + totalPages + } + }", + variables = new + { + wishlistId = TestingUnauthorizedWishlistId, + pageNumber = 0, + pageSize = 2 + } + }; + + var jsonObject = await SendGraphQlRequestAsync(mutation); + var errors = (JArray?) jsonObject?.errors; + + Assert.NotNull(errors); + Assert.True(errors.Count > 0); + } + + [Fact] + public async Task DeletePersonalWishlist_NotExistingWishlistId_ReturnsError() + { + await LoginAsync(TestingUserEmail, TestingUserPassword); + var mutation = new + { + query = @" + mutation deletePersonalWishlist($wishlistId: String!) { + deletePersonalWishlist (wishlistId: $wishlistId) { + createdById, id, name, type + } + }", + variables = new + { + wishlistId = TestingNotExistingWishlistId + } + }; + + var jsonObject = await SendGraphQlRequestAsync(mutation); + var errors = (JArray?) jsonObject?.errors; + + Assert.NotNull(errors); + Assert.True(errors.Count > 0); + } + + [Fact] + public async Task DeletePersonalWishlist_OtherUserWishlistId_ReturnsError() + { + await LoginAsync(TestingUserEmail, TestingUserPassword); + var mutation = new + { + query = @" + mutation deletePersonalWishlist($wishlistId: String!) { + deletePersonalWishlist (wishlistId: $wishlistId) { + createdById, id, name, type + } + }", + variables = new + { + wishlistId = TestingUnauthorizedWishlistId + } + }; + + var jsonObject = await SendGraphQlRequestAsync(mutation); + var errors = (JArray?) jsonObject?.errors; + + Assert.NotNull(errors); + Assert.True(errors.Count > 0); + } + + [Fact] + public async Task AddMessageToPersonalWishlist_InvalidMessage_ReturnsErrors() + { + await LoginAsync(TestingUserEmail, TestingUserPassword); + var mutation = new + { + query = @" + mutation addMessageToPersonalWishlist($wishlistId: String!, $dto: MessageCreateDtoInput!) { + addMessageToPersonalWishlist (wishlistId: $wishlistId, dto: $dto) { + role, text, createdById + } + }", + variables = new + { + wishlistId = TestingNotExistingWishlistId, + dto = new + { + text = "random text", + } + } + }; + + var jsonObject = await SendGraphQlRequestAsync(mutation); + var errors = (JArray?) jsonObject?.errors; + + Assert.NotNull(errors); + Assert.True(errors.Count > 0); } } diff --git a/ShoppingAssistantApi.Tests/Usings.cs b/ShoppingAssistantApi.Tests/Usings.cs new file mode 100644 index 0000000..4f1bf08 --- /dev/null +++ b/ShoppingAssistantApi.Tests/Usings.cs @@ -0,0 +1,4 @@ +global using Xunit; + +// Parallel running was disabled because it causes errors with the database access +[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file