From 85b4c40d4af51abbfcb43efa0c197e9f68817c5e Mon Sep 17 00:00:00 2001 From: Vanderlan Gomes Date: Tue, 14 Nov 2023 21:48:16 -0300 Subject: [PATCH] Add API Tests (#28) --- .github/workflows/sonar.yml | 41 +++-- .gitignore | 1 + README.md | 12 +- docker-compose.yaml | 14 ++ .../Configuration/HealthCheckConfiguration.cs | 2 +- .../Controllers/V1/UsersController.cs | 13 ++ src/Orion.Api/Models/RefreshTokenModel.cs | 7 - src/Orion.Api/Models/UserLoginModel.cs | 13 -- src/Orion.Api/Orion.Api.csproj | 6 +- src/Orion.Api/appsettings.Development.json | 6 +- src/Orion.Api/appsettings.Production.json | 6 +- src/Orion.Api/appsettings.Test.json | 8 +- .../UserChangePasswordRequest.cs | 11 ++ .../UserUpdatePasswordRequestHandler.cs | 30 ++++ .../UserUpdateRequestValidator.cs | 25 +++ .../UserCreate/UserCreateRequestValidator.cs | 9 +- .../UserDelete/UserDeleteRequestValidator.cs | 20 --- .../UserUpdate/UserUpdateRequestValidator.cs | 9 +- .../UserPasswordChangedNotification.cs | 9 + .../UserPasswordChangedNotificationHandler.cs | 29 ++++ .../Orion.Application.Core.csproj | 4 +- .../UserGetByIdRequestValidator.cs | 20 --- .../UserGetById/UserGetByIdResponse.cs | 5 +- .../Messages/MessagesKeys.cs | 5 + .../Resources/OrionResources.en-US.resx | 12 ++ .../Resources/OrionResources.pt-BR.resx | 12 ++ .../AuthenticationConfiguration.cs | 2 +- .../Entities/Enuns/UserProfile.cs | 4 +- .../Extensions/EnumExtensions.cs | 2 +- .../Services/Interfaces/IUserService.cs | 1 + src/Orion.Domain.Core/Services/UserService.cs | 32 +++- src/Orion.Infra.Data/Context/DataContext.cs | 7 +- .../Migrations/20231030205851_Initial.cs | 3 +- .../Migrations/DataContextModelSnapshot.cs | 6 +- .../Generic/BaseEntityRepository.cs | 6 +- src/Orion.Infra.Data/UnitOfWork/UnitOfWork.cs | 14 +- tests/Orion.Test/Api/V1/AuthApiTest.cs | 99 +++++++++++ tests/Orion.Test/Api/V1/UsersApiTest.cs | 158 ++++++++++++++++++ .../Configuration/ApiTestInitializer.cs | 77 --------- .../DependencyInjectionSetupFixture.cs | 24 --- .../Faker/RefreshTokenFaker.cs | 2 +- .../{ => Configuration}/Faker/UserFaker.cs | 16 +- .../Configuration/TestBoostrapper.cs | 16 -- .../Services/BaseService/BaseServiceTest.cs | 15 -- .../Domain/Services/UserServiceTest.cs | 35 +--- .../Setup/IntegrationTestsBootstrapper.cs | 86 ++++++++++ .../Setup/IntegrationTestsFixture.cs | 86 ++++++++++ tests/Orion.Test/Orion.Test.csproj | 17 +- .../Controllers/AuthControllerTest.cs | 18 +- .../BaseController/BaseControllerTest.cs | 4 +- .../Controllers/UsersControllerTest.cs | 12 +- 51 files changed, 722 insertions(+), 349 deletions(-) delete mode 100644 src/Orion.Api/Models/RefreshTokenModel.cs delete mode 100644 src/Orion.Api/Models/UserLoginModel.cs create mode 100644 src/Orion.Application.Core/Commands/UserChangePassword/UserChangePasswordRequest.cs create mode 100644 src/Orion.Application.Core/Commands/UserChangePassword/UserUpdatePasswordRequestHandler.cs create mode 100644 src/Orion.Application.Core/Commands/UserChangePassword/UserUpdateRequestValidator.cs delete mode 100644 src/Orion.Application.Core/Commands/UserDelete/UserDeleteRequestValidator.cs create mode 100644 src/Orion.Application.Core/Notifications/UserPasswordChanged/UserPasswordChangedNotification.cs create mode 100644 src/Orion.Application.Core/Notifications/UserPasswordChanged/UserPasswordChangedNotificationHandler.cs delete mode 100644 src/Orion.Application.Core/Queries/UserGetById/UserGetByIdRequestValidator.cs create mode 100644 tests/Orion.Test/Api/V1/AuthApiTest.cs create mode 100644 tests/Orion.Test/Api/V1/UsersApiTest.cs delete mode 100644 tests/Orion.Test/Configuration/ApiTestInitializer.cs delete mode 100644 tests/Orion.Test/Configuration/DependencyInjectionSetupFixture.cs rename tests/Orion.Test/{ => Configuration}/Faker/RefreshTokenFaker.cs (91%) rename tests/Orion.Test/{ => Configuration}/Faker/UserFaker.cs (72%) delete mode 100644 tests/Orion.Test/Configuration/TestBoostrapper.cs delete mode 100644 tests/Orion.Test/Domain/Services/BaseService/BaseServiceTest.cs rename tests/Orion.Test/{ => Integration}/Domain/Services/UserServiceTest.cs (90%) create mode 100644 tests/Orion.Test/Integration/Setup/IntegrationTestsBootstrapper.cs create mode 100644 tests/Orion.Test/Integration/Setup/IntegrationTestsFixture.cs rename tests/Orion.Test/{Api => Unit}/Controllers/AuthControllerTest.cs (89%) rename tests/Orion.Test/{Api => Unit}/Controllers/BaseController/BaseControllerTest.cs (56%) rename tests/Orion.Test/{Api => Unit}/Controllers/UsersControllerTest.cs (92%) diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 83374b4..d7b9a1c 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -5,10 +5,20 @@ on: - master pull_request: types: [opened, synchronize, reopened] +env: + ASPNETCORE_ENVIRONMENT: Test jobs: build: name: Build - runs-on: windows-latest + runs-on: ubuntu-latest + services: + sqlserver: + image: mcr.microsoft.com/mssql/server:2019-latest + ports: + - 1433:1433 + env: + ACCEPT_EULA: Y + SA_PASSWORD: SqlServer2019! steps: - name: Set up JDK 17 uses: actions/setup-java@v3 @@ -18,33 +28,20 @@ jobs: - uses: actions/checkout@v2 with: fetch-depth: 0 - - name: Cache SonarCloud packages - uses: actions/cache@v1 - with: - path: ~\sonar\cache - key: ${{ runner.os }}-sonar - restore-keys: ${{ runner.os }}-sonar - - name: Cache SonarCloud scanner - id: cache-sonar-scanner - uses: actions/cache@v1 - with: - path: .\.sonar\scanner - key: ${{ runner.os }}-sonar-scanner - restore-keys: ${{ runner.os }}-sonar-scanner - name: Install SonarCloud scanner - if: steps.cache-sonar-scanner.outputs.cache-hit != 'true' - shell: powershell run: | - New-Item -Path .\.sonar\scanner -ItemType Directory - dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner + dotnet tool install --global dotnet-sonarscanner --version 5.14.0 + - name: Install EF Core Tools + run: | + dotnet tool install --global dotnet-ef --version 7.0.14 - name: Build and analyze env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - shell: powershell run: | - .\.sonar\scanner\dotnet-sonarscanner begin /k:"vanderlan_Orion-Api" /o:"vanderlan-gomes" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="opencover.xml" + dotnet ef database update -p src/Orion.Infra.Data -s src/Orion.Api --verbose + dotnet sonarscanner begin /k:"vanderlan_Orion-Api" /o:"vanderlan-gomes" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="opencover.xml" dotnet build Orion.sln dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=coverage /p:Exclude=[xunit.*]* Orion.sln - cp tests\Orion.Test\coverage.opencover.xml opencover.xml - .\.sonar\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" \ No newline at end of file + cp tests/Orion.Test/coverage.opencover.xml opencover.xml + dotnet sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" diff --git a/.gitignore b/.gitignore index 3d99822..06dfc73 100644 --- a/.gitignore +++ b/.gitignore @@ -263,3 +263,4 @@ __pycache__/ /VBaseProject.Test/coverage.json /.editorconfig /.sonarqube +/tests/Orion.Test/coverage diff --git a/README.md b/README.md index ffb5576..fc54424 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ # **Orion Api** [![Build](https://github.com/vanderlan/Orion-Api/actions/workflows/sonar.yml/badge.svg)](https://github.com/vanderlan/Orion-Api/actions/workflows/sonar.yml) -[![Coverage Status](https://coveralls.io/repos/github/vanderlan/Orion-Api/badge.svg)](https://coveralls.io/github/vanderlan/Orion-Api) -[![GitHub release](https://img.shields.io/github/release/vanderlan/Orion-Api.svg)](https://GitHub.com/vanderlan/Orion-Api/) -[![GitHub repo size](https://img.shields.io/github/repo-size/vanderlan/Orion-Api)](https://github.com/vanderlan/Orion-Api) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=vanderlan_Orion-Api&metric=coverage)](https://sonarcloud.io/summary/new_code?id=vanderlan_Orion-Api) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=vanderlan_Orion-Api&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=vanderlan_Orion-Api) +[![Technical Debt](https://sonarcloud.io/api/project_badges/measure?project=vanderlan_Orion-Api&metric=sqale_index)](https://sonarcloud.io/summary/new_code?id=vanderlan_Orion-Api) +[![Manteinability](https://api.codeclimate.com/v1/badges/76a30970ddd45c75129b/maintainability)](https://codeclimate.com/github/vanderlan/Orion-Api/maintainability) +[![GitHub release](https://img.shields.io/github/release/vanderlan/Orion-Api.svg)](https://github.com/vanderlan/Orion-Api/releases) **About this Project** @@ -50,7 +52,7 @@ The main objective is to start projects with a clean and simple architecture, wi **CI & CD** - 1. Automated tests; + 1. Unit, Integration and Api Tests; 2. Continuous Integration (GitHub CI); 3. Continuous Delivery (GitHub CI); 4. DockerHub Integration; @@ -72,4 +74,4 @@ The main objective is to start projects with a clean and simple architecture, wi dotnet ef migrations add MigrationName -p Orion.Infra.Data -s Orion.Api dotnet ef database update -p Orion.Infra.Data -s Orion.Api --verbose -Author: https://github.com/vanderlan \ No newline at end of file +*author:* https://github.com/vanderlan \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 07847a6..b8816bb 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -8,6 +8,20 @@ services: ports: - "5010:80" container_name: orion-api + depends_on: + - sqlserver + networks: + - backend-network + + sqlserver: + image: mcr.microsoft.com/mssql/server:2019-latest + environment: + SA_PASSWORD: "SqlServer2019!" + ACCEPT_EULA: "Y" + MSSQL_PID: "Developer" + ports: + - "1434:1433" + container_name: sqlserver networks: - backend-network diff --git a/src/Orion.Api/Configuration/HealthCheckConfiguration.cs b/src/Orion.Api/Configuration/HealthCheckConfiguration.cs index 0f76074..ba52c0b 100644 --- a/src/Orion.Api/Configuration/HealthCheckConfiguration.cs +++ b/src/Orion.Api/Configuration/HealthCheckConfiguration.cs @@ -15,7 +15,7 @@ public static void ConfigureHealthCheck(this IApplicationBuilder app) public static void AddApplicationHealthChecks(this IServiceCollection services, IConfiguration configuration) { - services.AddHealthChecks().AddSqlServer(configuration["DatabaseOptions:ConnectionString"], + services.AddHealthChecks().AddSqlServer(configuration["ConnectionStrings:OrionDatabase"], tags: new[] { "database" diff --git a/src/Orion.Api/Controllers/V1/UsersController.cs b/src/Orion.Api/Controllers/V1/UsersController.cs index 819e5e5..bc1ac88 100644 --- a/src/Orion.Api/Controllers/V1/UsersController.cs +++ b/src/Orion.Api/Controllers/V1/UsersController.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc; using Orion.Api.Attributes; using Orion.Api.Controllers.Base; +using Orion.Application.Core.Commands.UserChangePassword; using Orion.Application.Core.Commands.UserCreate; using Orion.Application.Core.Commands.UserDelete; using Orion.Application.Core.Commands.UserUpdate; @@ -83,4 +84,16 @@ public async Task Delete(string id) return NoContent(); } + + [HttpPatch("Me/PasswordChange")] + [SwaggerResponse((int)HttpStatusCode.BadRequest, "A error response with the error description", typeof(ExceptionResponse))] + [SwaggerResponse((int)HttpStatusCode.Accepted)] + [SwaggerResponse((int)HttpStatusCode.NotFound)] + [SwaggerResponse((int)HttpStatusCode.Unauthorized)] + public async Task PatchChangePassword([FromBody] UserChangePasswordRequest userChangePasswordRequest) + { + await Mediator.Send(userChangePasswordRequest); + + return Accepted(); + } } diff --git a/src/Orion.Api/Models/RefreshTokenModel.cs b/src/Orion.Api/Models/RefreshTokenModel.cs deleted file mode 100644 index a1bee63..0000000 --- a/src/Orion.Api/Models/RefreshTokenModel.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Orion.Api.Models; - -public class RefreshTokenModel -{ - public string RefreshToken { get; set; } - public string Token { get; set; } -} diff --git a/src/Orion.Api/Models/UserLoginModel.cs b/src/Orion.Api/Models/UserLoginModel.cs deleted file mode 100644 index 3ed57b0..0000000 --- a/src/Orion.Api/Models/UserLoginModel.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Orion.Api.Models; - -public class UserLoginModel -{ - [Required] - [EmailAddress] - public string Email { get; set; } - - [Required] - public string Password { get; set; } -} diff --git a/src/Orion.Api/Orion.Api.csproj b/src/Orion.Api/Orion.Api.csproj index 6c75ea4..16f619a 100644 --- a/src/Orion.Api/Orion.Api.csproj +++ b/src/Orion.Api/Orion.Api.csproj @@ -13,10 +13,6 @@ - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - @@ -26,7 +22,7 @@ - + diff --git a/src/Orion.Api/appsettings.Development.json b/src/Orion.Api/appsettings.Development.json index 21ec9c5..33908bd 100644 --- a/src/Orion.Api/appsettings.Development.json +++ b/src/Orion.Api/appsettings.Development.json @@ -6,11 +6,9 @@ "Microsoft": "Information" } }, - - "DatabaseOptions": { - "ConnectionString": "Data Source=10.0.0.90,1433;Initial Catalog=Orion;User ID=sa;Password=123;TrustServerCertificate=True" + "ConnectionStrings": { + "OrionDatabase": "Data Source=localhost,1433;Initial Catalog=Orion;User ID=Orion;Password=123;TrustServerCertificate=True" }, - "JwtOptions": { "SymmetricSecurityKey": "5cCI6IkpXVCJ9.eyJlbWFpbCI6InZhbmRlcmxhbi5nc0BnbWFpbC5jb20iLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJhZG1p", "Issuer": "Orion API", diff --git a/src/Orion.Api/appsettings.Production.json b/src/Orion.Api/appsettings.Production.json index cffddb4..3e5c31e 100644 --- a/src/Orion.Api/appsettings.Production.json +++ b/src/Orion.Api/appsettings.Production.json @@ -6,11 +6,9 @@ "Microsoft": "Information" } }, - - "DatabaseOptions": { - "ConnectionString": "Data Source=10.0.0.90,1433;Initial Catalog=Orion;User ID=sa;Password=123;TrustServerCertificate=True" + "ConnectionStrings": { + "OrionDatabase": "Data Source=localhost,1433;Initial Catalog=Orion;User ID=Orion;Password=123;TrustServerCertificate=True" }, - "JwtOptions": { "SymmetricSecurityKey": "5cCI6IkpXVCJ9.eyJlbWFpbCI6InZhbmRlcmxhbi5nc0BnbWFpbC5jb20iLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJhZG1p", "Issuer": "Orion API", diff --git a/src/Orion.Api/appsettings.Test.json b/src/Orion.Api/appsettings.Test.json index fa32847..91afafa 100644 --- a/src/Orion.Api/appsettings.Test.json +++ b/src/Orion.Api/appsettings.Test.json @@ -6,11 +6,9 @@ "Microsoft": "Information" } }, - - "DatabaseOptions": { - "ConnectionString": "Data Source=localhost,1433;Initial Catalog=Orion;User ID=sa;Password=123;TrustServerCertificate=True" + "ConnectionStrings": { + "OrionDatabase": "Data Source=localhost,1433;Initial Catalog=Orion;User ID=sa;Password=SqlServer2019!;TrustServerCertificate=True" }, - "JwtOptions": { "SymmetricSecurityKey": "5cCI6IkpXVCJ9.eyJlbWFpbCI6InZhbmRlcmxhbi5nc0BnbWFpbC5jb20iLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJhZG1p", "Issuer": "Orion API", @@ -20,4 +18,4 @@ "ElasticConfiguration": { "Uri": "http://localhost:9200" } -} +} \ No newline at end of file diff --git a/src/Orion.Application.Core/Commands/UserChangePassword/UserChangePasswordRequest.cs b/src/Orion.Application.Core/Commands/UserChangePassword/UserChangePasswordRequest.cs new file mode 100644 index 0000000..444eda5 --- /dev/null +++ b/src/Orion.Application.Core/Commands/UserChangePassword/UserChangePasswordRequest.cs @@ -0,0 +1,11 @@ +using MediatR; + +namespace Orion.Application.Core.Commands.UserChangePassword +{ + public class UserChangePasswordRequest : IRequest + { + public string CurrentPassword { get; set; } + public string NewPassword { get; set; } + public string NewPasswordConfirm { get; set; } + } +} diff --git a/src/Orion.Application.Core/Commands/UserChangePassword/UserUpdatePasswordRequestHandler.cs b/src/Orion.Application.Core/Commands/UserChangePassword/UserUpdatePasswordRequestHandler.cs new file mode 100644 index 0000000..9545182 --- /dev/null +++ b/src/Orion.Application.Core/Commands/UserChangePassword/UserUpdatePasswordRequestHandler.cs @@ -0,0 +1,30 @@ +using MediatR; +using Orion.Application.Core.Notifications.UserPasswordChanged; +using Orion.Domain.Core.Authentication; +using Orion.Domain.Core.Services.Interfaces; + +namespace Orion.Application.Core.Commands.UserChangePassword +{ + public class UserUpdatePasswordRequestHandler : IRequestHandler + { + private readonly IUserService _userService; + private readonly ICurrentUser _currentUser; + private readonly IMediator _mediator; + + public UserUpdatePasswordRequestHandler(IUserService userService, ICurrentUser currentUser, IMediator mediator) + { + _userService = userService; + _currentUser = currentUser; + _mediator = mediator; + } + + public async Task Handle(UserChangePasswordRequest request, CancellationToken cancellationToken) + { + await _userService.ChangePasswordAsync(_currentUser.Id, request.CurrentPassword, request.NewPassword); + + await _mediator.Publish(new UserPasswordChangedNotification(), cancellationToken); + + return Unit.Value; + } + } +} diff --git a/src/Orion.Application.Core/Commands/UserChangePassword/UserUpdateRequestValidator.cs b/src/Orion.Application.Core/Commands/UserChangePassword/UserUpdateRequestValidator.cs new file mode 100644 index 0000000..957fd56 --- /dev/null +++ b/src/Orion.Application.Core/Commands/UserChangePassword/UserUpdateRequestValidator.cs @@ -0,0 +1,25 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; +using Orion.Croscutting.Resources; +using static Orion.Croscutting.Resources.Messages.MessagesKeys; + +namespace Orion.Application.Core.Commands.UserChangePassword; + +public class UserChangePasswordRequestValidator : AbstractValidator +{ + public UserChangePasswordRequestValidator(IStringLocalizer stringLocalizer) + { + + RuleFor(c => c.CurrentPassword) + .NotEmpty().WithMessage(stringLocalizer[UserMessages.EmptyPasword]); + + RuleFor(c => c.NewPassword) + .NotEmpty().WithMessage(stringLocalizer[UserMessages.EmptyNewPasword]); + + RuleFor(c => c.NewPasswordConfirm) + .NotEmpty().WithMessage(stringLocalizer[UserMessages.EmptyNewPaswordConfirmation]); + + RuleFor(x => x.NewPassword).Equal(x => x.NewPasswordConfirm) + .WithMessage(stringLocalizer[UserMessages.PaswordAndConfirmationDifferent]); + } +} diff --git a/src/Orion.Application.Core/Commands/UserCreate/UserCreateRequestValidator.cs b/src/Orion.Application.Core/Commands/UserCreate/UserCreateRequestValidator.cs index ccf7685..ede9f8c 100644 --- a/src/Orion.Application.Core/Commands/UserCreate/UserCreateRequestValidator.cs +++ b/src/Orion.Application.Core/Commands/UserCreate/UserCreateRequestValidator.cs @@ -14,15 +14,12 @@ public UserCreateRequestValidator(IStringLocalizer stringLocaliz .WithMessage(stringLocalizer[UserMessages.NullEntity]); RuleFor(c => c.Name) - .NotEmpty().WithMessage(stringLocalizer[UserMessages.EmptyName]) - .NotNull().WithMessage(stringLocalizer[UserMessages.EmptyName]); + .NotEmpty().WithMessage(stringLocalizer[UserMessages.EmptyName]); RuleFor(c => c.Email) - .NotEmpty().WithMessage(stringLocalizer[UserMessages.EmptyEmail]) - .NotNull().WithMessage(stringLocalizer[UserMessages.EmptyEmail]); + .NotEmpty().WithMessage(stringLocalizer[UserMessages.EmptyEmail]); RuleFor(c => c.Password) - .NotEmpty().WithMessage(stringLocalizer[UserMessages.EmptyPasword]) - .NotNull().WithMessage(stringLocalizer[UserMessages.EmptyPasword]); + .NotEmpty().WithMessage(stringLocalizer[UserMessages.EmptyPasword]); } } diff --git a/src/Orion.Application.Core/Commands/UserDelete/UserDeleteRequestValidator.cs b/src/Orion.Application.Core/Commands/UserDelete/UserDeleteRequestValidator.cs deleted file mode 100644 index 1642a52..0000000 --- a/src/Orion.Application.Core/Commands/UserDelete/UserDeleteRequestValidator.cs +++ /dev/null @@ -1,20 +0,0 @@ -using FluentValidation; -using Microsoft.Extensions.Localization; -using Orion.Croscutting.Resources; -using static Orion.Croscutting.Resources.Messages.MessagesKeys; - -namespace Orion.Application.Core.Commands.UserDelete; - -public class UserDeleteRequestValidator : AbstractValidator -{ - public UserDeleteRequestValidator(IStringLocalizer stringLocalizer) - { - RuleFor(c => c) - .NotNull() - .WithMessage(stringLocalizer[UserMessages.NullEntity]); - - RuleFor(c => c.PublicId) - .NotEmpty().WithMessage(stringLocalizer[UserMessages.EmptyId]) - .NotNull().WithMessage(stringLocalizer[UserMessages.EmptyId]); - } -} diff --git a/src/Orion.Application.Core/Commands/UserUpdate/UserUpdateRequestValidator.cs b/src/Orion.Application.Core/Commands/UserUpdate/UserUpdateRequestValidator.cs index b87dd20..3bd7c86 100644 --- a/src/Orion.Application.Core/Commands/UserUpdate/UserUpdateRequestValidator.cs +++ b/src/Orion.Application.Core/Commands/UserUpdate/UserUpdateRequestValidator.cs @@ -14,15 +14,12 @@ public UserUpdateRequestValidator(IStringLocalizer stringLocaliz .WithMessage(stringLocalizer[UserMessages.NullEntity]); RuleFor(c => c.Name) - .NotEmpty().WithMessage(stringLocalizer[UserMessages.EmptyName]) - .NotNull().WithMessage(stringLocalizer[UserMessages.EmptyName]); + .NotEmpty().WithMessage(stringLocalizer[UserMessages.EmptyName]); RuleFor(c => c.Email) - .NotEmpty().WithMessage(stringLocalizer[UserMessages.EmptyEmail]) - .NotNull().WithMessage(stringLocalizer[UserMessages.EmptyEmail]); + .NotEmpty().WithMessage(stringLocalizer[UserMessages.EmptyEmail]); RuleFor(c => c.PublicId) - .NotEmpty().WithMessage(stringLocalizer[UserMessages.EmptyId]) - .NotNull().WithMessage(stringLocalizer[UserMessages.EmptyId]); + .NotEmpty().WithMessage(stringLocalizer[UserMessages.EmptyId]); } } diff --git a/src/Orion.Application.Core/Notifications/UserPasswordChanged/UserPasswordChangedNotification.cs b/src/Orion.Application.Core/Notifications/UserPasswordChanged/UserPasswordChangedNotification.cs new file mode 100644 index 0000000..545cf59 --- /dev/null +++ b/src/Orion.Application.Core/Notifications/UserPasswordChanged/UserPasswordChangedNotification.cs @@ -0,0 +1,9 @@ +using MediatR; + +namespace Orion.Application.Core.Notifications.UserPasswordChanged +{ + public class UserPasswordChangedNotification : INotification + { + + } +} diff --git a/src/Orion.Application.Core/Notifications/UserPasswordChanged/UserPasswordChangedNotificationHandler.cs b/src/Orion.Application.Core/Notifications/UserPasswordChanged/UserPasswordChangedNotificationHandler.cs new file mode 100644 index 0000000..1df9f79 --- /dev/null +++ b/src/Orion.Application.Core/Notifications/UserPasswordChanged/UserPasswordChangedNotificationHandler.cs @@ -0,0 +1,29 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Orion.Domain.Core.Authentication; + +namespace Orion.Application.Core.Notifications.UserPasswordChanged +{ + public class UserPasswordChangedNotificationHandler : INotificationHandler + { + private readonly ILogger _logger; + private readonly ICurrentUser _currentUser; + + public UserPasswordChangedNotificationHandler(ILogger logger, ICurrentUser currentUser) + { + _logger = logger; + _currentUser = currentUser; + } + + public async Task Handle(UserPasswordChangedNotification notification, CancellationToken cancellationToken) + { + _logger.LogInformation("A notification {notificationType} has been received. Notification details: {notification}. Action performed by: {currentUserName}", + nameof(UserPasswordChangedNotification), + JsonConvert.SerializeObject(notification, Formatting.Indented), + _currentUser.ToString()); + + await Task.CompletedTask; + } + } +} diff --git a/src/Orion.Application.Core/Orion.Application.Core.csproj b/src/Orion.Application.Core/Orion.Application.Core.csproj index 1cadd51..a3c061b 100644 --- a/src/Orion.Application.Core/Orion.Application.Core.csproj +++ b/src/Orion.Application.Core/Orion.Application.Core.csproj @@ -10,10 +10,10 @@ - + - + diff --git a/src/Orion.Application.Core/Queries/UserGetById/UserGetByIdRequestValidator.cs b/src/Orion.Application.Core/Queries/UserGetById/UserGetByIdRequestValidator.cs deleted file mode 100644 index 25e2f44..0000000 --- a/src/Orion.Application.Core/Queries/UserGetById/UserGetByIdRequestValidator.cs +++ /dev/null @@ -1,20 +0,0 @@ -using FluentValidation; -using Microsoft.Extensions.Localization; -using Orion.Croscutting.Resources; -using static Orion.Croscutting.Resources.Messages.MessagesKeys; - -namespace Orion.Application.Core.Queries.UserGetById; - -public class UserGetByIdRequestValidator : AbstractValidator -{ - public UserGetByIdRequestValidator(IStringLocalizer stringLocalizer) - { - RuleFor(c => c) - .NotNull() - .WithMessage(stringLocalizer[UserMessages.NullEntity]); - - RuleFor(c => c.PublicId) - .NotEmpty().WithMessage(stringLocalizer[UserMessages.EmptyId]) - .NotNull().WithMessage(stringLocalizer[UserMessages.EmptyId]); - } -} diff --git a/src/Orion.Application.Core/Queries/UserGetById/UserGetByIdResponse.cs b/src/Orion.Application.Core/Queries/UserGetById/UserGetByIdResponse.cs index 937a155..3ac684f 100644 --- a/src/Orion.Application.Core/Queries/UserGetById/UserGetByIdResponse.cs +++ b/src/Orion.Application.Core/Queries/UserGetById/UserGetByIdResponse.cs @@ -1,4 +1,5 @@ using Orion.Domain.Core.Entities; +using Orion.Domain.Core.Entities.Enuns; namespace Orion.Application.Core.Queries.UserGetById; @@ -7,6 +8,7 @@ public class UserGetByIdResponse public string PublicId { get; set; } public string Name { get; set; } public string Email { get; set; } + public UserProfile Profile { get; set; } public DateTime LastUpdated { get; set; } public DateTime CreatedAt { get; set; } @@ -21,7 +23,8 @@ public static explicit operator UserGetByIdResponse(User user) Name = user.Name, Email = user.Email, LastUpdated = user.LastUpdated, - CreatedAt = user.CreatedAt + CreatedAt = user.CreatedAt, + Profile = user.Profile }; } diff --git a/src/Orion.Croscutting.Resources/Messages/MessagesKeys.cs b/src/Orion.Croscutting.Resources/Messages/MessagesKeys.cs index f40b6cd..0933068 100644 --- a/src/Orion.Croscutting.Resources/Messages/MessagesKeys.cs +++ b/src/Orion.Croscutting.Resources/Messages/MessagesKeys.cs @@ -24,5 +24,10 @@ public static class UserMessages public const string EmptyEmail = "User.EmptyEmail"; public const string EmptyId = "User.EmptyId"; public const string EmailExists = "User.EmailExists"; + public const string InvalidPassword = "User.InvalidPassword"; + + public const string EmptyNewPasword = "User.InvalidNewPassword"; + public const string EmptyNewPaswordConfirmation = "User.EmptyNewPaswordConfirmation"; + public const string PaswordAndConfirmationDifferent = "User.PaswordConfirmationDifferent"; } } diff --git a/src/Orion.Croscutting.Resources/Resources/OrionResources.en-US.resx b/src/Orion.Croscutting.Resources/Resources/OrionResources.en-US.resx index e385aea..32819c9 100644 --- a/src/Orion.Croscutting.Resources/Resources/OrionResources.en-US.resx +++ b/src/Orion.Croscutting.Resources/Resources/OrionResources.en-US.resx @@ -147,4 +147,16 @@ Is necessary to inform the Id + + The password entered is incorrect + + + Is necessary to inform the New Password Confirmation + + + Is necessary to inform the New Password + + + New Password and Confirmation must be equal + \ No newline at end of file diff --git a/src/Orion.Croscutting.Resources/Resources/OrionResources.pt-BR.resx b/src/Orion.Croscutting.Resources/Resources/OrionResources.pt-BR.resx index f7ee555..44b1734 100644 --- a/src/Orion.Croscutting.Resources/Resources/OrionResources.pt-BR.resx +++ b/src/Orion.Croscutting.Resources/Resources/OrionResources.pt-BR.resx @@ -147,4 +147,16 @@ É necessário informar o Id + + A senha informada está incorreta + + + É necessário informar a Confirmação da Nova Senha + + + É necessário informar a Nova Senha + + + A nova senha e a confiração da senha precisam ser iguais + \ No newline at end of file diff --git a/src/Orion.Domain.Core/Authentication/AuthenticationConfiguration.cs b/src/Orion.Domain.Core/Authentication/AuthenticationConfiguration.cs index 08f9e75..5f53e4c 100644 --- a/src/Orion.Domain.Core/Authentication/AuthenticationConfiguration.cs +++ b/src/Orion.Domain.Core/Authentication/AuthenticationConfiguration.cs @@ -5,6 +5,6 @@ public static class AuthorizationConfiguration public static class Roles { public const string Admin = "admin"; - public const string Customer = "customer"; + public const string Operator = "operator"; } } diff --git a/src/Orion.Domain.Core/Entities/Enuns/UserProfile.cs b/src/Orion.Domain.Core/Entities/Enuns/UserProfile.cs index 67a7baf..611840d 100644 --- a/src/Orion.Domain.Core/Entities/Enuns/UserProfile.cs +++ b/src/Orion.Domain.Core/Entities/Enuns/UserProfile.cs @@ -8,6 +8,6 @@ public enum UserProfile [Description(Roles.Admin)] Admin = 1, - [Description(Roles.Customer)] - Customer = 2 + [Description(Roles.Operator)] + Operator = 2 } diff --git a/src/Orion.Domain.Core/Extensions/EnumExtensions.cs b/src/Orion.Domain.Core/Extensions/EnumExtensions.cs index 137e845..b852055 100644 --- a/src/Orion.Domain.Core/Extensions/EnumExtensions.cs +++ b/src/Orion.Domain.Core/Extensions/EnumExtensions.cs @@ -7,7 +7,7 @@ public static class EnumExtensions { public static string Description(this T source) { - var fieldInfo = source.GetType().GetField(source.ToString()); + var fieldInfo = source.GetType().GetField(source.ToString() ?? string.Empty); var attributes = (DescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false); diff --git a/src/Orion.Domain.Core/Services/Interfaces/IUserService.cs b/src/Orion.Domain.Core/Services/Interfaces/IUserService.cs index 659f155..527793a 100644 --- a/src/Orion.Domain.Core/Services/Interfaces/IUserService.cs +++ b/src/Orion.Domain.Core/Services/Interfaces/IUserService.cs @@ -11,4 +11,5 @@ public interface IUserService : IBaseService Task<(User User, RefreshToken RefreshToken)> SignInWithCredentialsAsync(string email, string password); Task<(User User, RefreshToken RefreshToken)> SignInWithRefreshTokenAsync(string refreshToken, string expiredToken); Task> ListPaginateAsync(UserFilter filter); + Task ChangePasswordAsync(string userId, string currentPassword, string newPassword); } diff --git a/src/Orion.Domain.Core/Services/UserService.cs b/src/Orion.Domain.Core/Services/UserService.cs index 93a03f8..94dead1 100644 --- a/src/Orion.Domain.Core/Services/UserService.cs +++ b/src/Orion.Domain.Core/Services/UserService.cs @@ -29,7 +29,7 @@ public UserService(IUnitOfWork unitOfWork, IStringLocalizer reso public async Task AddAsync(User user) { - await ValidateUser(user, validatePassword: true); + await ValidateUser(user); user.Password = user.Password.ToSha512(); @@ -41,6 +41,11 @@ public async Task AddAsync(User user) public async Task DeleteAsync(string publicId) { + var user = await FindByIdAsync(publicId); + + if (user == null) + throw new NotFoundException(publicId); + await _unitOfWork.UserRepository.DeleteAsync(publicId); await _unitOfWork.CommitAsync(); } @@ -67,10 +72,11 @@ public async Task UpdateAsync(User user) { var entitySaved = await FindByIdAsync(user.PublicId); - await ValidateUser(user, validatePassword: false); + await ValidateUser(user); entitySaved.Email = user.Email; entitySaved.Name = user.Name; + entitySaved.Profile = user.Profile; _unitOfWork.UserRepository.Update(entitySaved); @@ -142,11 +148,8 @@ private string GetClaimFromJwtToken(string jtwToken, string claimName) } } - private async Task ValidateUser(User user, bool validatePassword) + private async Task ValidateUser(User user) { - if (string.IsNullOrEmpty(user.Password) && validatePassword) - throw new BusinessException(_messages[UserMessages.EmptyPasword]); - var userFound = await _unitOfWork.UserRepository.FindByEmailAsync(user.Email); if (userFound != null && userFound.PublicId != user.PublicId) @@ -157,4 +160,21 @@ public async Task> ListPaginateAsync(UserFilter filter) { return await _unitOfWork.UserRepository.ListPaginateAsync(filter); } + + public async Task ChangePasswordAsync(string userId, string currentPassword, string newPassword) + { + var user = await FindByIdAsync(userId); + + if (user == null) + throw new NotFoundException(userId); + + if(user.Password != currentPassword.ToSha512()) + throw new BusinessException(_messages[UserMessages.InvalidPassword]); + + user.Password = newPassword.ToSha512(); + + _unitOfWork.UserRepository.Update(user); + + await _unitOfWork.CommitAsync(); + } } diff --git a/src/Orion.Infra.Data/Context/DataContext.cs b/src/Orion.Infra.Data/Context/DataContext.cs index 53920ff..5c36227 100644 --- a/src/Orion.Infra.Data/Context/DataContext.cs +++ b/src/Orion.Infra.Data/Context/DataContext.cs @@ -17,11 +17,6 @@ public DataContext(IConfiguration configuration) : base(GetDefaultOptions(config } - public DataContext(DbContextOptions options) : base(options) - { - - } - #region DBSet public DbSet Users { get; set; } public DbSet RefreshTokens { get; set; } @@ -29,7 +24,7 @@ public DataContext(DbContextOptions options) : base(options) private static DbContextOptions GetDefaultOptions(IConfiguration configuration) { - var connectionString = configuration.GetSection("DatabaseOptions:ConnectionString").Value; + var connectionString = configuration.GetSection("ConnectionStrings:OrionDatabase").Value; return new DbContextOptionsBuilder().UseSqlServer(connectionString).Options; } diff --git a/src/Orion.Infra.Data/Migrations/20231030205851_Initial.cs b/src/Orion.Infra.Data/Migrations/20231030205851_Initial.cs index 0491516..509afb2 100644 --- a/src/Orion.Infra.Data/Migrations/20231030205851_Initial.cs +++ b/src/Orion.Infra.Data/Migrations/20231030205851_Initial.cs @@ -1,11 +1,12 @@ using System; +using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable namespace Orion.Infra.Data.Migrations { - /// + [ExcludeFromCodeCoverage] public partial class Initial : Migration { /// diff --git a/src/Orion.Infra.Data/Migrations/DataContextModelSnapshot.cs b/src/Orion.Infra.Data/Migrations/DataContextModelSnapshot.cs index a8f1907..0acb3da 100644 --- a/src/Orion.Infra.Data/Migrations/DataContextModelSnapshot.cs +++ b/src/Orion.Infra.Data/Migrations/DataContextModelSnapshot.cs @@ -1,15 +1,15 @@ // -using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Orion.Infra.Data.Context; +using System; +using System.Diagnostics.CodeAnalysis; #nullable disable namespace Orion.Infra.Data.Migrations { + [ExcludeFromCodeCoverage] [DbContext(typeof(DataContext))] partial class DataContextModelSnapshot : ModelSnapshot { diff --git a/src/Orion.Infra.Data/Repository/Generic/BaseEntityRepository.cs b/src/Orion.Infra.Data/Repository/Generic/BaseEntityRepository.cs index c3a147c..d45c5ff 100644 --- a/src/Orion.Infra.Data/Repository/Generic/BaseEntityRepository.cs +++ b/src/Orion.Infra.Data/Repository/Generic/BaseEntityRepository.cs @@ -1,13 +1,11 @@ using Microsoft.EntityFrameworkCore; +using Orion.Domain.Core.Entities; +using Orion.Infra.Data.Context; using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; -using Orion.Domain.Core.Entities; -using Orion.Domain.Core.Filters; -using Orion.Domain.Core.ValueObjects.Pagination; -using Orion.Infra.Data.Context; namespace Orion.Infra.Data.Repository.Generic; diff --git a/src/Orion.Infra.Data/UnitOfWork/UnitOfWork.cs b/src/Orion.Infra.Data/UnitOfWork/UnitOfWork.cs index 8b2bea1..f531b41 100644 --- a/src/Orion.Infra.Data/UnitOfWork/UnitOfWork.cs +++ b/src/Orion.Infra.Data/UnitOfWork/UnitOfWork.cs @@ -16,12 +16,7 @@ public class UnitOfWork : IUnitOfWork public UnitOfWork(IConfiguration configuration) { - DbContext = new DataContext(GetOptions(configuration.GetSection("DatabaseOptions:ConnectionString").Value)); - } - - public UnitOfWork(DbContextOptions dbContextOptions) - { - DbContext = new DataContext(dbContextOptions); + DbContext = new DataContext(configuration); } private IUserRepository _userRepository; @@ -34,12 +29,7 @@ public async Task CommitAsync() { await DbContext.SaveChangesAsync(); } - - private static DbContextOptions GetOptions(string connection) - { - return SqlServerDbContextOptionsExtensions.UseSqlServer(new DbContextOptionsBuilder(), connection).Options; - } - + public void DiscardChanges() { foreach (var entry in DbContext.ChangeTracker.Entries().Where(e => e.State != EntityState.Unchanged)) diff --git a/tests/Orion.Test/Api/V1/AuthApiTest.cs b/tests/Orion.Test/Api/V1/AuthApiTest.cs new file mode 100644 index 0000000..2298822 --- /dev/null +++ b/tests/Orion.Test/Api/V1/AuthApiTest.cs @@ -0,0 +1,99 @@ +using Orion.Application.Core.Commands.UserCreate; +using Orion.Test.Configuration.Faker; +using Orion.Test.Integration.Setup; +using System.Net; +using System.Threading.Tasks; +using Orion.Application.Core.Commands.LoginWithCredentials; +using Orion.Application.Core.Commands.LoginWithRefreshToken; +using Xunit; + +namespace Orion.Test.Api.V1 +{ + public class AuthApiTest : IntegrationTestsBootstrapper + { + public AuthApiTest(IntegrationTestsFixture fixture) : base(fixture) + { + LoginWithDefaultUser(); + } + + [Theory] + [InlineData("invalid@email.com", "1233")] + [InlineData(null, "")] + [InlineData(null, "passs-invalid")] + public async Task AuthUser_WithCredentialsInvalid_ReturnsUnauthorized(string email, string password) + { + //arrange + var request = new LoginWithCredentialsRequest() + { + Email = email, + Password = password + }; + + //act + var httpResponse = await HttpClient.PostAsync("/api/Auth/Login", GetStringContent(request)); + + //assert + Assert.Equal(HttpStatusCode.Unauthorized, httpResponse.StatusCode); + } + + [Theory] + [InlineData("invalid-refresh-token", "1233")] + [InlineData(null, "")] + [InlineData(null, "invalid-expired-token")] + public async Task AuthUser_WithRefreshTokenInvalid_ReturnsUnauthorized(string refreshToken, string token) + { + //arrange + var request = new LoginWithRefreshTokenRequest() + { + RefreshToken = refreshToken, + Token = token + }; + + //act + var httpResponse = await HttpClient.PostAsync("/api/Auth/RefreshToken", GetStringContent(request)); + + //assert + Assert.Equal(HttpStatusCode.Unauthorized, httpResponse.StatusCode); + } + + [Fact] + public async Task AuthUser_WithValidRefreshToken_ReturnsNewToken() + { + //arrange + var user = UserFaker.GetUserCreateRequest(); + + var userCreated = await CreateUserAsync(user); + + var httpClient = IntegrationTestsFixture.GetNewHttpClient(); + + var tokenResult = AuthUser(userCreated.Email, user.Password); + + var refreshTokenRequest = new LoginWithRefreshTokenRequest + { + RefreshToken = tokenResult.RefreshToken, + Token = tokenResult.Token + }; + + //act + var httpResponseRefreshToken = await httpClient.PostAsync("/api/Auth/RefreshToken", GetStringContent(refreshTokenRequest)); + + var refreshTokenResponse = await GetResultContentAsync(httpResponseRefreshToken); + + //assert + Assert.Equal(HttpStatusCode.OK, httpResponseRefreshToken.StatusCode); + Assert.NotNull(refreshTokenResponse.Token); + Assert.NotNull(refreshTokenResponse.RefreshToken); + } + + private async Task CreateUserAsync(UserCreateRequest userCreateRequest = null) + { + userCreateRequest ??= UserFaker.GetUserCreateRequest(); + + var httpResponsePost = await AuthenticatedHttpClient.PostAsync("/api/Users", GetStringContent(userCreateRequest)); + + Assert.Equal(HttpStatusCode.Created, httpResponsePost.StatusCode); + + return await GetResultContentAsync(httpResponsePost); + } + } +} diff --git a/tests/Orion.Test/Api/V1/UsersApiTest.cs b/tests/Orion.Test/Api/V1/UsersApiTest.cs new file mode 100644 index 0000000..761707c --- /dev/null +++ b/tests/Orion.Test/Api/V1/UsersApiTest.cs @@ -0,0 +1,158 @@ +using Orion.Application.Core.Commands.UserCreate; +using Orion.Test.Configuration.Faker; +using Orion.Test.Integration.Setup; +using System; +using System.Net; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Orion.Application.Core.Commands.UserChangePassword; +using Xunit; + +namespace Orion.Test.Api.V1 +{ + public class UsersApiTest : IntegrationTestsBootstrapper + { + public UsersApiTest(IntegrationTestsFixture fixture) : base(fixture) + { + LoginWithDefaultUser(); + } + + [Fact] + public async Task PostUser_WithValidData_CreateAUser() + { + //arrange + var request = UserFaker.GetUserCreateRequest(); + + //act + var httpResponse = await AuthenticatedHttpClient.PostAsync("/api/Users", GetStringContent(request)); + + var userCreated = await GetResultContentAsync(httpResponse); + + //assert + Assert.Equal(HttpStatusCode.Created, httpResponse.StatusCode); + + Assert.NotNull(userCreated); + Assert.Equal(request.Email, userCreated.Email); + Assert.Equal(request.Name, userCreated.Name); + Assert.Equal(request.Profile, userCreated.Profile); + Assert.True(userCreated.CreatedAt > DateTime.MinValue); + Assert.True(userCreated.LastUpdated > DateTime.MinValue); + Assert.NotNull(userCreated.PublicId); + } + + [Fact] + public async Task PutUser_WithValidData_UpdateUser() + { + //arrange + var userCreated = await CreateUserAsync(); + + var userUpdateRequest = UserFaker.GetUserUpdateRequest(); + userUpdateRequest.PublicId = userCreated.PublicId; + + //act + var httpResponsePut = await AuthenticatedHttpClient.PutAsync($"/api/Users/{userCreated.PublicId}", GetStringContent(userUpdateRequest)); + + Assert.Equal(HttpStatusCode.Accepted, httpResponsePut.StatusCode); + + var userGetHttpResponse = await AuthenticatedHttpClient.GetAsync($"/api/Users/{userCreated.PublicId}"); + + var user = await GetResultContentAsync(userGetHttpResponse); + + //assert + Assert.NotNull(user); + Assert.Equal(userUpdateRequest.Email, user.Email); + Assert.Equal(userUpdateRequest.Name, user.Name); + Assert.Equal(userUpdateRequest.Profile, user.Profile); + Assert.True(user.LastUpdated > userCreated.LastUpdated); + Assert.NotNull(user.PublicId); + } + + [Fact] + public async Task DeleteUser_WithValidIdData_DeleteTheUser() + { + //arrange + var userCreated = await CreateUserAsync(); + + //act + var httpResponsePut = await AuthenticatedHttpClient.DeleteAsync($"/api/Users/{userCreated.PublicId}"); + + Assert.Equal(HttpStatusCode.NoContent, httpResponsePut.StatusCode); + + var userGetHttpResponse = await AuthenticatedHttpClient.GetAsync($"/api/Users/{userCreated.PublicId}"); + + //assert + Assert.Equal(HttpStatusCode.NotFound, userGetHttpResponse.StatusCode); + } + + [Fact] + public async Task DeleteUser_WithInvalidId_ReturnsNotFound() + { + //arrange & act + var httpResponseDelete = await AuthenticatedHttpClient.DeleteAsync($"/api/Users/{Guid.NewGuid()}"); + + //assert + Assert.Equal(HttpStatusCode.NotFound, httpResponseDelete.StatusCode); + } + + [Theory] + [InlineData("123", "12345", "30298302")] + [InlineData("", "12345", "30298302")] + [InlineData("", "12345", null)] + [InlineData("", "", null)] + [InlineData("123", null, null)] + public async Task UpdateUserPassword_WithInvalidPasswordsAndConfirmation_ReturnsBadRequest(string currentPass, string newPass, string newPassConfirm) + { + //arrange + var changePasswordRequest = new UserChangePasswordRequest + { + CurrentPassword = currentPass, + NewPassword = newPass, + NewPasswordConfirm = newPassConfirm + }; + + //act + var httpResponsePatch = await AuthenticatedHttpClient.PatchAsync("/api/Users/Me/PasswordChange", GetStringContent(changePasswordRequest)); + + Assert.Equal(HttpStatusCode.BadRequest, httpResponsePatch.StatusCode); + } + + [Fact] + public async Task UpdateUserPassword_WithValidPasswordsAndConfirmation_ReturnsAcepted() + { + //arrange + var user = UserFaker.GetUserCreateRequest(); + + var userCreated = await CreateUserAsync(user); + + var changePasswordRequest = new UserChangePasswordRequest + { + CurrentPassword = user.Password, + NewPassword = "Ab647477382", + NewPasswordConfirm = "Ab647477382" + }; + + var httpClient = IntegrationTestsFixture.GetNewHttpClient(); + + var tokenResult = AuthUser(userCreated.Email, user.Password); + + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResult.Token); + + //act + var httpResponsePatch = await httpClient.PatchAsync("/api/Users/Me/PasswordChange", GetStringContent(changePasswordRequest)); + + //assert + Assert.Equal(HttpStatusCode.Accepted, httpResponsePatch.StatusCode); + } + + private async Task CreateUserAsync(UserCreateRequest userCreateRequest = null) + { + userCreateRequest ??= UserFaker.GetUserCreateRequest(); + + var httpResponsePost = await AuthenticatedHttpClient.PostAsync("/api/Users", GetStringContent(userCreateRequest)); + + Assert.Equal(HttpStatusCode.Created, httpResponsePost.StatusCode); + + return await GetResultContentAsync(httpResponsePost); + } + } +} diff --git a/tests/Orion.Test/Configuration/ApiTestInitializer.cs b/tests/Orion.Test/Configuration/ApiTestInitializer.cs deleted file mode 100644 index f6c2ec5..0000000 --- a/tests/Orion.Test/Configuration/ApiTestInitializer.cs +++ /dev/null @@ -1,77 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.Extensions.Configuration; -using Newtonsoft.Json; -using Orion.Api.Models; -using System; -using System.Net.Http; -using System.Text; - -namespace Orion.Test.Configuration; - -public abstract class ApiTestInitializer : IDisposable -{ - protected string AuthToken; - protected readonly HttpClient Client; - protected readonly HttpClient AuthenticatedClient; - protected IServiceProvider ServiceProvider { get; private set; } - - public ApiTestInitializer() - { - var appFactory = new WebApplicationFactory() - .WithWebHostBuilder(builder => - { - var config = new ConfigurationBuilder() - .AddJsonFile("appsettings.Test.json", optional: false, reloadOnChange: true) - .Build(); - - builder - .UseConfiguration(config); - }); - ServiceProvider = appFactory.Services; - Client = appFactory.CreateClient(); - AuthenticatedClient = appFactory.CreateClient(); - } - - public void AuthUser() - { - var result = Client.PostAsync("/api/Auth/Login", GetStringContent( - new UserLoginModel { - Email = "vanderlan.gs@gmail.com", - Password = "123" - })) - .GetAwaiter().GetResult(); - - var content = result.Content.ReadAsStringAsync().GetAwaiter().GetResult(); - - var tokenResult = JsonConvert.DeserializeObject(content); - - AuthToken = tokenResult.Token; - } - - protected static StringContent GetStringContent(object obj) - { - return new StringContent(JsonConvert.SerializeObject(obj), Encoding.Default, "application/json"); - } - - private bool _disposedValue = false; - - protected virtual void Dispose(bool disposing) - { - if (!_disposedValue) - { - _disposedValue = true; - } - } - - ~ApiTestInitializer() - { - Dispose(false); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } -} diff --git a/tests/Orion.Test/Configuration/DependencyInjectionSetupFixture.cs b/tests/Orion.Test/Configuration/DependencyInjectionSetupFixture.cs deleted file mode 100644 index 84fdf18..0000000 --- a/tests/Orion.Test/Configuration/DependencyInjectionSetupFixture.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Orion.Infra.Data.UnitOfWork; -using Orion.Domain.Core.Repositories.UnitOfWork; -using Orion.Croscutting.Ioc.Dependencies; - -namespace Orion.Test.Configuration; - -public class DependencyInjectionSetupFixture -{ - public ServiceProvider ServiceProvider { get; private set; } - - public DependencyInjectionSetupFixture() - { - var serviceCollection = new ServiceCollection(); - - serviceCollection.AddDomainServices(); - - serviceCollection.AddTransient(s => new UnitOfWork(TestBootstrapper.GetInMemoryDbContextOptions())); - serviceCollection.AddLogging(); - serviceCollection.AddLocalization(options => options.ResourcesPath = @"Resources"); - - ServiceProvider = serviceCollection.BuildServiceProvider(); - } -} diff --git a/tests/Orion.Test/Faker/RefreshTokenFaker.cs b/tests/Orion.Test/Configuration/Faker/RefreshTokenFaker.cs similarity index 91% rename from tests/Orion.Test/Faker/RefreshTokenFaker.cs rename to tests/Orion.Test/Configuration/Faker/RefreshTokenFaker.cs index 56d1689..cca185d 100644 --- a/tests/Orion.Test/Faker/RefreshTokenFaker.cs +++ b/tests/Orion.Test/Configuration/Faker/RefreshTokenFaker.cs @@ -2,7 +2,7 @@ using Bogus; using Orion.Domain.Core.Entities; -namespace Orion.Test.Faker; +namespace Orion.Test.Configuration.Faker; public static class RefreshTokenFaker { diff --git a/tests/Orion.Test/Faker/UserFaker.cs b/tests/Orion.Test/Configuration/Faker/UserFaker.cs similarity index 72% rename from tests/Orion.Test/Faker/UserFaker.cs rename to tests/Orion.Test/Configuration/Faker/UserFaker.cs index 0051e60..ae8ff4d 100644 --- a/tests/Orion.Test/Faker/UserFaker.cs +++ b/tests/Orion.Test/Configuration/Faker/UserFaker.cs @@ -4,11 +4,23 @@ using Orion.Application.Core.Commands.UserUpdate; using Orion.Domain.Core.Entities; using Orion.Domain.Core.Entities.Enuns; +using Orion.Domain.Core.Extensions; -namespace Orion.Test.Faker; +namespace Orion.Test.Configuration.Faker; public static class UserFaker { + public static User GetDefaultSystemUser() + { + return new Faker() + .RuleFor(o => o.Name, f => f.Name.FullName()) + .RuleFor(o => o.Email, f => f.Internet.Email()) + .RuleFor(o => o.Password, f => "123".ToSha512()) + .RuleFor(o => o.Profile, f => UserProfile.Admin) + .RuleFor(o => o.PublicId, f => Guid.NewGuid().ToString()) + .Generate(); + } + public static User Get() { return new Faker() @@ -29,7 +41,7 @@ public static UserCreateRequest GetUserCreateRequest() .RuleFor(o => o.Profile, f => UserProfile.Admin) .Generate(); } - + public static UserUpdateRequest GetUserUpdateRequest() { return new Faker() diff --git a/tests/Orion.Test/Configuration/TestBoostrapper.cs b/tests/Orion.Test/Configuration/TestBoostrapper.cs deleted file mode 100644 index 5f061f7..0000000 --- a/tests/Orion.Test/Configuration/TestBoostrapper.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Orion.Infra.Data.Context; - -namespace Orion.Test.Configuration; - -public class TestBootstrapper -{ - public static DbContextOptions GetInMemoryDbContextOptions(string dbName = "Test_DB") - { - var context = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: dbName) - .EnableDetailedErrors(true); - - return context.Options; - } -} diff --git a/tests/Orion.Test/Domain/Services/BaseService/BaseServiceTest.cs b/tests/Orion.Test/Domain/Services/BaseService/BaseServiceTest.cs deleted file mode 100644 index 526443e..0000000 --- a/tests/Orion.Test/Domain/Services/BaseService/BaseServiceTest.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Orion.Test.Configuration; -using Xunit; - -namespace Orion.Test.Domain.Services.BaseService; - -public class BaseServiceTest : IClassFixture -{ - protected readonly ServiceProvider ServiceProvider; - - public BaseServiceTest(DependencyInjectionSetupFixture fixture) - { - ServiceProvider = fixture.ServiceProvider; - } -} diff --git a/tests/Orion.Test/Domain/Services/UserServiceTest.cs b/tests/Orion.Test/Integration/Domain/Services/UserServiceTest.cs similarity index 90% rename from tests/Orion.Test/Domain/Services/UserServiceTest.cs rename to tests/Orion.Test/Integration/Domain/Services/UserServiceTest.cs index dd578b5..dd3f5b5 100644 --- a/tests/Orion.Test/Domain/Services/UserServiceTest.cs +++ b/tests/Orion.Test/Integration/Domain/Services/UserServiceTest.cs @@ -1,28 +1,23 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; -using Orion.Api.Configuration; -using Orion.Domain.Core.Entities; +using Orion.Croscutting.Resources; using Orion.Domain.Core.Exceptions; using Orion.Domain.Core.Extensions; +using Orion.Domain.Core.Filters; using Orion.Domain.Core.Services.Interfaces; -using Orion.Croscutting.Resources; -using Orion.Test.Configuration; -using Orion.Test.Domain.Services.BaseService; -using System; +using Orion.Test.Configuration.Faker; +using Orion.Test.Integration.Setup; using System.Collections.Generic; using System.Threading.Tasks; -using Orion.Application.Core.Commands.LoginWithCredentials; -using Orion.Domain.Core.Filters; -using Orion.Test.Faker; using Xunit; using static Orion.Croscutting.Resources.Messages.MessagesKeys; -namespace Orion.Test.Domain.Services; +namespace Orion.Test.Integration.Domain.Services; -public class UserServiceTest : BaseServiceTest +public class UserServiceTest : IntegrationTestsBootstrapper { - public UserServiceTest(DependencyInjectionSetupFixture fixture) : base(fixture) + public UserServiceTest(IntegrationTestsFixture fixture) : base(fixture) { } @@ -96,20 +91,6 @@ public async Task ListPaginateAsync_WithFilterByName_GetAllMatchedUsers() await userService.DeleteAsync(userFound.PublicId); } - [Fact] - public async Task AddAsync_WithInvalidData_ThrowsBusinessException() - { - //arrange - using var scope = ServiceProvider.CreateScope(); - var userService = scope.ServiceProvider.GetService(); - - var user = UserFaker.Get(); - user.Password = null; - - //act & assert - await Assert.ThrowsAsync(() => userService.AddAsync(user)); - } - [Fact] public async Task DeleteAsync_WithExistantId_RemoveUserAsSuccess() { @@ -221,7 +202,7 @@ public async Task SignInWithRefreshTokenAsync_WithInvalidToken_ThrowsUnauthorize var userFound = await userService.FindByIdAsync(userAdded.PublicId); Assert.NotNull(userFound); - + //act var exeption = await Assert.ThrowsAsync(() => userService.SignInWithRefreshTokenAsync(refreshToken, token)); diff --git a/tests/Orion.Test/Integration/Setup/IntegrationTestsBootstrapper.cs b/tests/Orion.Test/Integration/Setup/IntegrationTestsBootstrapper.cs new file mode 100644 index 0000000..7fe2c08 --- /dev/null +++ b/tests/Orion.Test/Integration/Setup/IntegrationTestsBootstrapper.cs @@ -0,0 +1,86 @@ +using Newtonsoft.Json; +using Orion.Api.Models; +using Orion.Domain.Core.Entities; +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Orion.Application.Core.Commands.LoginWithCredentials; +using Xunit; + +namespace Orion.Test.Integration.Setup; + +public abstract class IntegrationTestsBootstrapper : IClassFixture, IDisposable +{ + protected readonly HttpClient HttpClient; + protected readonly HttpClient AuthenticatedHttpClient; + private readonly User _defaultSystemUser; + protected readonly IntegrationTestsFixture IntegrationTestsFixture; + + protected IServiceProvider ServiceProvider { get; private set; } + + protected IntegrationTestsBootstrapper(IntegrationTestsFixture fixture) + { + HttpClient = fixture.HttpClient; + AuthenticatedHttpClient = fixture.AuthenticatedHttpClient; + _defaultSystemUser = fixture.DefaultSystemUser; + ServiceProvider = fixture.ServiceProvider; + IntegrationTestsFixture = fixture; + } + + protected void LoginWithDefaultUser() + { + var tokenResult = AuthUser(_defaultSystemUser.Email, "123"); + + AuthenticatedHttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResult.Token); + } + + protected UserApiTokenModel AuthUser(string email, string password) + { + var result = HttpClient.PostAsync("/api/Auth/Login", GetStringContent( + new LoginWithCredentialsRequest + { + Email = email, + Password = password + })) + .GetAwaiter().GetResult(); + + var content = result.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + + return JsonConvert.DeserializeObject(content); + } + + protected static StringContent GetStringContent(object obj) + { + return new StringContent(JsonConvert.SerializeObject(obj), Encoding.Default, "application/json"); + } + + private bool _disposedValue; + + private void Dispose(bool disposing) + { + if (!_disposedValue) + { + _disposedValue = true; + } + } + + ~IntegrationTestsBootstrapper() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected static async Task GetResultContentAsync(HttpResponseMessage httpResponse) + { + var content = await httpResponse.Content.ReadAsStringAsync(); + + return JsonConvert.DeserializeObject(content); + } +} diff --git a/tests/Orion.Test/Integration/Setup/IntegrationTestsFixture.cs b/tests/Orion.Test/Integration/Setup/IntegrationTestsFixture.cs new file mode 100644 index 0000000..1d9d358 --- /dev/null +++ b/tests/Orion.Test/Integration/Setup/IntegrationTestsFixture.cs @@ -0,0 +1,86 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Orion.Domain.Core.Entities; +using Orion.Domain.Core.Repositories.UnitOfWork; +using Orion.Test.Configuration.Faker; +using System; +using System.Net.Http; + +namespace Orion.Test.Integration.Setup +{ + public class IntegrationTestsFixture + { + public readonly HttpClient HttpClient; + public readonly HttpClient AuthenticatedHttpClient; + public readonly User DefaultSystemUser; + public readonly IServiceProvider ServiceProvider; + private readonly IUnitOfWork _unitOfWork; + private readonly SqlConnection _sqlConnection; + private IConfiguration _configuration; + private WebApplicationFactory AppFactory; + + public IntegrationTestsFixture() + { + var appFactory = new WebApplicationFactory() + .WithWebHostBuilder(builder => + { + var config = new ConfigurationBuilder() + .AddJsonFile("appsettings.Test.json", optional: false, reloadOnChange: true) + .Build(); + + builder.UseConfiguration(config); + + _configuration = config; + }); + + ServiceProvider = appFactory.Services; + + _unitOfWork = appFactory.Services.GetRequiredService(); + + HttpClient = appFactory.CreateClient(); + AuthenticatedHttpClient = appFactory.CreateClient(); + + DefaultSystemUser = UserFaker.GetDefaultSystemUser(); + + _sqlConnection = new SqlConnection(_configuration["ConnectionStrings:OrionDatabase"]); + + AppFactory = appFactory; + + BeforeEachTest(); + } + + public HttpClient GetNewHttpClient() + { + return AppFactory.CreateClient(); + } + + private void BeforeEachTest() + { + var tablesToTruncate = new[] { "User", "RefreshToken" }; + + lock (_sqlConnection) + { + using (_sqlConnection) + { + _sqlConnection.Open(); + + foreach (var table in tablesToTruncate) + { + var command = new SqlCommand($"TRUNCATE TABLE dbo.[{table}]", _sqlConnection); + + command.ExecuteNonQuery(); + } + + lock (_unitOfWork) + { + _unitOfWork.UserRepository.AddAsync(DefaultSystemUser).GetAwaiter().GetResult(); + _unitOfWork.CommitAsync().GetAwaiter().GetResult(); + } + } + } + } + } +} diff --git a/tests/Orion.Test/Orion.Test.csproj b/tests/Orion.Test/Orion.Test.csproj index 4237770..f07d28e 100644 --- a/tests/Orion.Test/Orion.Test.csproj +++ b/tests/Orion.Test/Orion.Test.csproj @@ -5,30 +5,21 @@ false - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/tests/Orion.Test/Api/Controllers/AuthControllerTest.cs b/tests/Orion.Test/Unit/Controllers/AuthControllerTest.cs similarity index 89% rename from tests/Orion.Test/Api/Controllers/AuthControllerTest.cs rename to tests/Orion.Test/Unit/Controllers/AuthControllerTest.cs index 730d356..b3dc2c2 100644 --- a/tests/Orion.Test/Api/Controllers/AuthControllerTest.cs +++ b/tests/Orion.Test/Unit/Controllers/AuthControllerTest.cs @@ -9,15 +9,15 @@ using Orion.Application.Core.Commands.LoginWithRefreshToken; using Orion.Domain.Core.Entities; using Orion.Domain.Core.Entities.Enuns; -using Orion.Test.Api.Controllers.BaseController; -using Orion.Test.Faker; +using Orion.Test.Configuration.Faker; +using Orion.Test.Unit.Controllers.BaseController; using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Xunit; -namespace Orion.Test.Api.Controllers; +namespace Orion.Test.Unit.Controllers; public class AuthControllerTest : BaseControllerTest { @@ -80,12 +80,12 @@ public async Task RefreshToken_WithValidRefreshToken_ReturnsNewToken() { //arrange & act var (token, _) = AuthenticationConfiguration.CreateToken(new LoginWithCredentialsResponse - { - Email = _validUser.Email, - Name = _validUser.Name, - PublicId = _validUser.PublicId, - Profile = UserProfile.Admin - }, + { + Email = _validUser.Email, + Name = _validUser.Name, + PublicId = _validUser.PublicId, + Profile = UserProfile.Admin + }, _configuration); var result = await _authController.RefreshToken(new LoginWithRefreshTokenRequest { RefreshToken = _validRefreshToken.Refreshtoken, Token = token }); diff --git a/tests/Orion.Test/Api/Controllers/BaseController/BaseControllerTest.cs b/tests/Orion.Test/Unit/Controllers/BaseController/BaseControllerTest.cs similarity index 56% rename from tests/Orion.Test/Api/Controllers/BaseController/BaseControllerTest.cs rename to tests/Orion.Test/Unit/Controllers/BaseController/BaseControllerTest.cs index eb29efd..1d09df9 100644 --- a/tests/Orion.Test/Api/Controllers/BaseController/BaseControllerTest.cs +++ b/tests/Orion.Test/Unit/Controllers/BaseController/BaseControllerTest.cs @@ -1,9 +1,9 @@ -namespace Orion.Test.Api.Controllers.BaseController; +namespace Orion.Test.Unit.Controllers.BaseController; public class BaseControllerTest { public BaseControllerTest() { - + } } diff --git a/tests/Orion.Test/Api/Controllers/UsersControllerTest.cs b/tests/Orion.Test/Unit/Controllers/UsersControllerTest.cs similarity index 92% rename from tests/Orion.Test/Api/Controllers/UsersControllerTest.cs rename to tests/Orion.Test/Unit/Controllers/UsersControllerTest.cs index bdeb326..b8a0435 100644 --- a/tests/Orion.Test/Api/Controllers/UsersControllerTest.cs +++ b/tests/Orion.Test/Unit/Controllers/UsersControllerTest.cs @@ -6,14 +6,14 @@ using Orion.Application.Core.Queries.UserGetPaginated; using Orion.Domain.Core.Entities; using Orion.Domain.Core.ValueObjects.Pagination; -using Orion.Test.Api.Controllers.BaseController; -using Orion.Test.Faker; +using Orion.Test.Configuration.Faker; +using Orion.Test.Unit.Controllers.BaseController; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Xunit; -namespace Orion.Test.Api.Controllers; +namespace Orion.Test.Unit.Controllers; public class UsersControllerTestTest : BaseControllerTest { @@ -110,11 +110,11 @@ private void SetupMediatorMock() var userListPaginated = new PagedList(userList, 4); - mediatorMock.Setup(x => x.Send(It.IsAny(),It.IsAny())) + mediatorMock.Setup(x => x.Send(It.IsAny(), It.IsAny())) .ReturnsAsync(userListPaginated); - mediatorMock.Setup(x => x.Send(It.IsAny(),It.IsAny())) - .ReturnsAsync((UserGetByIdResponse) _validUser); + mediatorMock.Setup(x => x.Send(It.IsAny(), It.IsAny())) + .ReturnsAsync((UserGetByIdResponse)_validUser); _usersController = new UsersController(mediatorMock.Object); }