diff --git a/tests/CommonTestsUtilities/Entities/RecipeBuilder.cs b/tests/CommonTestsUtilities/Entities/RecipeBuilder.cs new file mode 100644 index 0000000..d475af7 --- /dev/null +++ b/tests/CommonTestsUtilities/Entities/RecipeBuilder.cs @@ -0,0 +1,52 @@ +using Bogus; +using MyRecipes.Domain.Entities; +using MyRecipes.Domain.Enums; + +namespace CommonTestsUtilities.Entities; +public class RecipeBuilder +{ + public static IList Collecion(User user, uint count = 2) + { + var list = new List(); + + if (count == 0) + count = 1; + + var recipeId = 1; + + for (int i = 0; i < count; i++) + { + var fakeRecipe = Build(user); + fakeRecipe.Id = recipeId++; + list.Add(fakeRecipe); + } + + return list; + } + + public static Recipe Build(User user) + { + return new Faker() + .RuleFor(r => r.Id, () => 1) + .RuleFor(r => r.Title, (f) => f.Lorem.Word()) + .RuleFor(r => r.CookingTime, (f) => f.PickRandom()) + .RuleFor(r => r.Difficulty, (f) => f.PickRandom()) + .RuleFor(r => r.Ingredients, (f) => f.Make(1, () => new Ingredient + { + Id = 1, + Item = f.Commerce.ProductName() + })) + .RuleFor(r => r.Instructions, (f) => f.Make(1, () => new Instruction + { + Id = 1, + Step = 1, + Text = f.Lorem.Paragraph() + })) + .RuleFor(r => r.DishTypes, (f) => f.Make(1, () => new MyRecipes.Domain.Entities.DishType + { + Id = 1, + Type = f.PickRandom() + })) + .RuleFor(r => r.UserId, () => user.Id); + } +} diff --git a/tests/CommonTestsUtilities/Repositories/RecipeReadOnlyRepositoryBuilder.cs b/tests/CommonTestsUtilities/Repositories/RecipeReadOnlyRepositoryBuilder.cs new file mode 100644 index 0000000..7c62155 --- /dev/null +++ b/tests/CommonTestsUtilities/Repositories/RecipeReadOnlyRepositoryBuilder.cs @@ -0,0 +1,22 @@ +using Moq; +using MyRecipes.Domain.Dtos; +using MyRecipes.Domain.Entities; +using MyRecipes.Domain.Repositories.Recipe; + +namespace CommonTestsUtilities.Repositories; +public class RecipeReadOnlyRepositoryBuilder +{ + private readonly Mock _repository; + + public RecipeReadOnlyRepositoryBuilder(Mock repository) => _repository = repository; + + public RecipeReadOnlyRepositoryBuilder Filter(User user, IList recipes) + { + _repository.Setup(r => r.Filter(user, It.IsAny())).ReturnsAsync(recipes); + return this; + } + + public RecipeReadOnlyRepositoryBuilder() => _repository = new Mock(); + + public IRecipeReadOnlyRepository Build() => _repository.Object; +} diff --git a/tests/CommonTestsUtilities/Requests/RequestFilterRecipeJsonBuilder.cs b/tests/CommonTestsUtilities/Requests/RequestFilterRecipeJsonBuilder.cs new file mode 100644 index 0000000..c8c8975 --- /dev/null +++ b/tests/CommonTestsUtilities/Requests/RequestFilterRecipeJsonBuilder.cs @@ -0,0 +1,16 @@ +using Bogus; +using MyRecipes.Communication.Enums; +using MyRecipes.Communication.Requests; + +namespace CommonTestsUtilities.Requests; +public class RequestFilterRecipeJsonBuilder +{ + public static RequestFilterRecipeJson Build() + { + return new Faker() + .RuleFor(r => r.CookingTimes, f => f.Make(1, () => f.PickRandom())) + .RuleFor(r => r.Difficulties, f => f.Make(1, () => f.PickRandom())) + .RuleFor(r => r.DishTypes, f => f.Make(1, () => f.PickRandom())) + .RuleFor(r => r.RecipeTitle_Ingredient, f => f.Lorem.Word()); + } +} diff --git a/tests/UseCases.Tests/Recipe/Filter/FilterRecipeUseCaseTest.cs b/tests/UseCases.Tests/Recipe/Filter/FilterRecipeUseCaseTest.cs new file mode 100644 index 0000000..fe4d343 --- /dev/null +++ b/tests/UseCases.Tests/Recipe/Filter/FilterRecipeUseCaseTest.cs @@ -0,0 +1,61 @@ +using CommonTestsUtilities.Entities; +using CommonTestsUtilities.LoggedUser; +using CommonTestsUtilities.Mapper; +using CommonTestsUtilities.Repositories; +using CommonTestsUtilities.Requests; +using FluentAssertions; +using MyRecipes.Application.UseCases.Recipe.Filter; +using MyRecipes.Communication.Enums; +using MyRecipes.Exceptions; +using MyRecipes.Exceptions.ExceptionsBase; + +namespace UseCases.Tests.Recipe.Filter; +public class FilterRecipeUseCaseTest +{ + [Fact] + public async Task Success() + { + var (user, _) = UserBuilder.Build(); + + var request = RequestFilterRecipeJsonBuilder.Build(); + var recipes = RecipeBuilder.Collecion(user); + + var useCase = CreateUseCase(user, recipes); + + var result = await useCase.Execute(request); + + result.Should().NotBeNull(); + result.Recipes.Should().NotBeNullOrEmpty(); + result.Recipes.Should().HaveCount(recipes.Count); + } + + [Fact] + public async Task Error_CookintTime_Invalid() + { + var (user, _) = UserBuilder.Build(); + + var recipes = RecipeBuilder.Collecion(user); + + var request = RequestFilterRecipeJsonBuilder.Build(); + request.CookingTimes.Add((CookingTime)99999); + + + var useCase = CreateUseCase(user, recipes); + + Func act = async () => await useCase.Execute(request); + + (await act.Should().ThrowAsync()) + .Where(e => e.ErrorMessages.Count == 1 && + e.ErrorMessages.Contains(ResourceMessagesExceptions.COOKING_TIME_NOT_SUPPORTED)); + } + + private static FilterRecipeUseCase CreateUseCase(MyRecipes.Domain.Entities.User user, + IList recipes) + { + var mapper = MapperBuilder.Build(); + var loggedUser = LoggedUserBuilder.Build(user); + var repository = new RecipeReadOnlyRepositoryBuilder().Filter(user, recipes).Build(); + + return new FilterRecipeUseCase(loggedUser, mapper, repository); + } +} diff --git a/tests/Validators.Tests/Recipe/Filter/FilterRecipeValidatorTest.cs b/tests/Validators.Tests/Recipe/Filter/FilterRecipeValidatorTest.cs new file mode 100644 index 0000000..784693a --- /dev/null +++ b/tests/Validators.Tests/Recipe/Filter/FilterRecipeValidatorTest.cs @@ -0,0 +1,66 @@ +using CommonTestsUtilities.Requests; +using FluentAssertions; +using MyRecipes.Application.UseCases.Recipe.Filter; +using MyRecipes.Communication.Enums; +using MyRecipes.Exceptions; + +namespace Validators.Tests.Recipe.Filter; +public class FilterRecipeValidatorTest +{ + [Fact] + public void Success() + { + var validator = new FilterRecipeValidator(); + + var request = RequestFilterRecipeJsonBuilder.Build(); + + var result = validator.Validate(request); + + result.IsValid.Should().BeTrue(); + } + + [Fact] + public void Error_Invalid_Cooking_Time() + { + var validator = new FilterRecipeValidator(); + + var request = RequestFilterRecipeJsonBuilder.Build(); + request.CookingTimes.Add((CookingTime)1000); + + var result = validator.Validate(request); + + result.IsValid.Should().BeFalse(); + result.Errors.Should().ContainSingle() + .And.Contain(e => e.ErrorMessage.Equals(ResourceMessagesExceptions.COOKING_TIME_NOT_SUPPORTED)); + } + + [Fact] + public void Error_Invalid_Difficulty() + { + var validator = new FilterRecipeValidator(); + + var request = RequestFilterRecipeJsonBuilder.Build(); + request.Difficulties.Add((Difficulty)1000); + + var result = validator.Validate(request); + + result.IsValid.Should().BeFalse(); + result.Errors.Should().ContainSingle() + .And.Contain(e => e.ErrorMessage.Equals(ResourceMessagesExceptions.DIFFICULTY_LEVEL_NOT_SUPPORTED)); + } + + [Fact] + public void Error_Invalid_DishType() + { + var validator = new FilterRecipeValidator(); + + var request = RequestFilterRecipeJsonBuilder.Build(); + request.DishTypes.Add((DishType)1000); + + var result = validator.Validate(request); + + result.IsValid.Should().BeFalse(); + result.Errors.Should().ContainSingle() + .And.Contain(e => e.ErrorMessage.Equals(ResourceMessagesExceptions.DISH_TYPE_NOT_SUPPORTED)); + } +} diff --git a/tests/WebApi.Tests/CustomWebApplicationFactory.cs b/tests/WebApi.Tests/CustomWebApplicationFactory.cs index c7a373a..bc1155a 100644 --- a/tests/WebApi.Tests/CustomWebApplicationFactory.cs +++ b/tests/WebApi.Tests/CustomWebApplicationFactory.cs @@ -3,12 +3,14 @@ using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using MyRecipes.Domain.Enums; using MyRecipes.Infrastructure.DataAccess; namespace WebApi.Tests; public class CustomWebApplicationFactory : WebApplicationFactory { + private MyRecipes.Domain.Entities.Recipe _recipe = default!; private MyRecipes.Domain.Entities.User _user = default!; private string _password = string.Empty; @@ -43,10 +45,20 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) public string GetPassword() => _password; public Guid GetUserIdentifier() => _user.UserIdentifier; + public string GetRecipeTitle() => _recipe.Title; + public Difficulty GetRecipeDifficulty() => _recipe.Difficulty!.Value; + public CookingTime GetRecipeCookingTime() => _recipe.CookingTime!.Value; + public IList GetDishTypes() => [.. _recipe.DishTypes.Select(c => c.Type)]; + private void StartDatabase(MyRecipesDbContext dbContext) { (_user, _password) = UserBuilder.Build(); + + _recipe = RecipeBuilder.Build(_user); + dbContext.Users.Add(_user); + dbContext.Recipes.Add(_recipe); + dbContext.SaveChanges(); } } diff --git a/tests/WebApi.Tests/Recipe/Filter/FilterRecipeInvalidTokenTest.cs b/tests/WebApi.Tests/Recipe/Filter/FilterRecipeInvalidTokenTest.cs new file mode 100644 index 0000000..3a19b50 --- /dev/null +++ b/tests/WebApi.Tests/Recipe/Filter/FilterRecipeInvalidTokenTest.cs @@ -0,0 +1,44 @@ +using CommonTestsUtilities.Requests; +using CommonTestsUtilities.Tokens; +using FluentAssertions; +using System.Net; + +namespace WebApi.Tests.Recipe.Filter; +public class FilterRecipeInvalidTokenTest : MyRecipesClassFixture +{ + private const string METHOD = "recipes/filter"; + public FilterRecipeInvalidTokenTest(CustomWebApplicationFactory factory) : base(factory) + { + } + + [Fact] + public async Task Error_Token_Invalid() + { + var request = RequestFilterRecipeJsonBuilder.Build(); + + var response = await DoPost(method: METHOD, request: request, token: "invalidToken"); + + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task Error_Without_Token() + { + var request = RequestFilterRecipeJsonBuilder.Build(); + + var response = await DoPost(method: METHOD, request: request, token: string.Empty); + + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task Error_Token_With_User_NotFound() + { + var request = RequestFilterRecipeJsonBuilder.Build(); + var token = JwtTokenGeneratorBuilder.Build().Generate(Guid.NewGuid()); + + var response = await DoPost(method: METHOD, request: request, token: token); + + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } +} diff --git a/tests/WebApi.Tests/Recipe/Filter/FilterRecipeTest.cs b/tests/WebApi.Tests/Recipe/Filter/FilterRecipeTest.cs new file mode 100644 index 0000000..feabe95 --- /dev/null +++ b/tests/WebApi.Tests/Recipe/Filter/FilterRecipeTest.cs @@ -0,0 +1,91 @@ +using CommonTestsUtilities.Requests; +using CommonTestsUtilities.Tokens; +using FluentAssertions; +using MyRecipes.Communication.Requests; +using MyRecipes.Exceptions; +using System.Globalization; +using System.Net; +using System.Text.Json; +using WebApi.Tests.InlineData; + +namespace WebApi.Tests.Recipe.Filter; +public class FilterRecipeTest : MyRecipesClassFixture +{ + private const string METHOD = "recipes/filter"; + + private readonly Guid _userIdentifier; + private readonly string _recipeTitle; + private readonly MyRecipes.Domain.Enums.CookingTime _recipeCookingTime; + private readonly MyRecipes.Domain.Enums.Difficulty _recipeDifficultyLevel; + private readonly IList _recipeDishTypes; + + + public FilterRecipeTest(CustomWebApplicationFactory factory) : base(factory) + { + _userIdentifier = factory.GetUserIdentifier(); + + _recipeTitle = factory.GetRecipeTitle(); + _recipeCookingTime = factory.GetRecipeCookingTime(); + _recipeDifficultyLevel = factory.GetRecipeDifficulty(); + _recipeDishTypes = factory.GetDishTypes(); + } + + [Fact] + public async Task Success() + { + var request = new RequestFilterRecipeJson + { + CookingTimes = [(MyRecipes.Communication.Enums.CookingTime)_recipeCookingTime], + Difficulties = [(MyRecipes.Communication.Enums.Difficulty)_recipeDifficultyLevel], + DishTypes = [.. _recipeDishTypes.Select(dishType => (MyRecipes.Communication.Enums.DishType)dishType)], + RecipeTitle_Ingredient = _recipeTitle, + }; + + var token = JwtTokenGeneratorBuilder.Build().Generate(_userIdentifier); + + var response = await DoPost(method: METHOD, request: request, token: token); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + + using var resopnseBody = await response.Content.ReadAsStreamAsync(); + var responseData = await JsonDocument.ParseAsync(resopnseBody); + + responseData.RootElement.GetProperty("recipes").EnumerateArray().Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task Success_NoContent() + { + var request = RequestFilterRecipeJsonBuilder.Build(); + request.RecipeTitle_Ingredient = "recipe-not-found"; + + var token = JwtTokenGeneratorBuilder.Build().Generate(_userIdentifier); + + var response = await DoPost(method: METHOD, request: request, token: token); + + response.StatusCode.Should().Be(HttpStatusCode.NoContent); + } + + [Theory] + [ClassData(typeof(CultureInlineDataTest))] + public async Task Error_CookingTime_Invalid(string culture) + { + var request = RequestFilterRecipeJsonBuilder.Build(); + request.CookingTimes.Add((MyRecipes.Communication.Enums.CookingTime)999); + var token = JwtTokenGeneratorBuilder.Build().Generate(_userIdentifier); + + var result = await DoPost(method: METHOD, request: request, token: token, culture: culture); + + result.StatusCode.Should().Be(HttpStatusCode.BadRequest); + + await using var responseBody = await result.Content.ReadAsStreamAsync(); + + var responseData = await JsonDocument.ParseAsync(responseBody); + + var errors = responseData.RootElement.GetProperty("errors").EnumerateArray(); + + var expectedMessage = ResourceMessagesExceptions.ResourceManager.GetString("COOKING_TIME_NOT_SUPPORTED", new CultureInfo(culture)); + + errors.Should().HaveCount(1).And.Contain(c => c.GetString()!.Equals(expectedMessage)); + } +}