From 9684bb3a344c029eb836c2011f6e3f96c798d66f Mon Sep 17 00:00:00 2001 From: "Jefferson L. da Silva" Date: Thu, 4 Dec 2025 20:05:44 -0300 Subject: [PATCH 1/3] Updates project to .NET 10 Migrates the project to .NET 10. This involves updating the target framework in project files and the Dockerfile. Adds a central package management file to manage package versions. Also adds a CI workflow to run unit tests on pull requests. Removes unused references and updates test projects to use MSTest runner. Updates logging implementation with logger enabled check. --- .github/workflows/workflow-ci.yml | 47 ++- Directory.Build.props | 10 + Directory.Packages.props | 67 ++-- .../BearerSecuritySchemeTransformerTests.cs | 2 +- .../Endpoints/LoginEndpointTests.cs | 2 +- .../InvoiceReminder.API.UnitTests.csproj | 11 +- .../BearerSecuritySchemeTransformer.cs | 7 +- InvoiceReminder.API/Dockerfile | 11 +- .../InvoiceReminder.API.csproj | 6 +- InvoiceReminder.API/InvoiceReminder.http | 359 +++++++++++++++++- .../AppServices/BaseAppServiceTests.cs | 2 +- ...voiceReminder.Application.UnitTests.csproj | 10 +- .../InvoiceReminder.Application.csproj | 6 - .../ViewModels/UserViewModel.cs | 3 + .../InvoiceReminder.ArchitectureTests.csproj | 12 +- .../InvoiceReminder.Authentication.csproj | 7 - .../InvoiceReminder.CrossCutting.IoC.csproj | 6 - .../InvoiceReminder.Data.csproj | 15 +- .../Repository/BaseRepository.cs | 3 +- .../Repository/EmailAuthTokenRepository.cs | 11 +- .../Repository/InvoiceRepository.cs | 11 +- .../Repository/JobScheduleRepository.cs | 11 +- .../ScanEmailDefinitionRepository.cs | 31 +- InvoiceReminder.Data/Repository/UnitOfWork.cs | 5 +- .../Repository/UserRepository.cs | 21 +- InvoiceReminder.Domain/Entities/User.cs | 8 + .../Extensions/EntityExtensions.cs | 22 ++ .../Extensions/UserExtensions.cs | 59 +-- .../InvoiceReminder.Domain.csproj | 7 - .../Extensions/EntityExtensionsTests.cs | 236 ++++++++++++ .../Extensions/UserExtensionsTests.cs | 8 +- ...ceReminder.DomainEntities.UnitTests.csproj | 13 +- .../BarcodeReaderServiceTests.cs | 2 +- ...Reminder.ExternalServices.UnitTests.csproj | 11 +- .../SendMessage/SendMessageServiceTests.cs | 38 +- .../TelegramBotBackgroundService.cs | 20 +- .../BarcodeReader/BarcodeReaderService.cs | 5 +- .../Gmail/GoogleOAuthService.cs | 19 +- .../InvoiceReminder.ExternalServices.csproj | 7 - .../SendMessage/SendMessageService.cs | 21 +- .../Telegram/TelegramMessageService.cs | 10 +- .../Data/Repository/BaseRepositoryTests.cs | 2 +- .../Data/Repository/UnitOfWorkTests.cs | 19 +- ...ceReminder.Infrastructure.UnitTests.csproj | 10 +- .../HostedService/QuartzHostedServiceTests.cs | 20 +- ...oiceReminder.JobScheduler.UnitTests.csproj | 10 +- .../JobSettings/CronJobTests.cs | 35 +- .../HostedService/QuartzHostedService.cs | 6 +- .../InvoiceReminder.JobScheduler.csproj | 6 - .../JobSettings/CronJob.cs | 6 +- .../InvoiceReminder.UnitTests.Assets.csproj | 6 - InvoiceReminder.sln | 5 +- 52 files changed, 979 insertions(+), 308 deletions(-) create mode 100644 Directory.Build.props create mode 100644 InvoiceReminder.Domain/Extensions/EntityExtensions.cs create mode 100644 InvoiceReminder.DomainEntities.UnitTests/Extensions/EntityExtensionsTests.cs diff --git a/.github/workflows/workflow-ci.yml b/.github/workflows/workflow-ci.yml index 49a2fa6..8fc162f 100644 --- a/.github/workflows/workflow-ci.yml +++ b/.github/workflows/workflow-ci.yml @@ -12,18 +12,55 @@ on: jobs: build: - runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 9.0.x + dotnet-version: 10.0.x + - name: Restore dependencies run: dotnet restore + - name: Build - run: dotnet build --no-restore - - name: Test - run: dotnet test --no-build --verbosity normal + run: dotnet build --no-restore --configuration Debug + + - name: Run tests (executables) + run: | + set -e + echo "🔍 Localizando projetos de teste..." + TEST_PROJECTS=$(find . -name "*.csproj" | grep -E "\.UnitTests|\.Tests" | grep -v "Assets") + + if [ -z "$TEST_PROJECTS" ]; then + echo "⚠️ Nenhum projeto de teste encontrado. Abortando." + exit 1 + fi + + FAIL=0 + + for PROJ in $TEST_PROJECTS; do + echo "▶️ Executando testes do projeto: $PROJ" + + BIN_PATH=$(dirname "$PROJ")/bin/Debug/net10.0 + EXE_NAME=$(basename "$PROJ" .csproj).exe + EXE="$BIN_PATH/$EXE_NAME" + + if [ ! -f "$EXE" ]; then + echo "❌ Não encontrei executável de testes: $EXE" + FAIL=1 + continue + fi + + echo "🚀 Rodando: $EXE" + "$EXE" --report-trx --results-directory "$BIN_PATH/TestResults" || FAIL=1 + done + + if [ $FAIL -ne 0 ]; then + echo "❌ Alguns testes falharam. Pipeline abortado." + exit 1 + fi + + echo "✅ Todos os testes passaram." diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..bfa8f03 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,10 @@ + + + net10.0 + latest + enable + disable + true + cb5b3f1e-1e38-47a5-bcd4-457b0c7f1d0a + + diff --git a/Directory.Packages.props b/Directory.Packages.props index 38603fd..31eb18a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,62 +3,59 @@ true true - + + - - - + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - + + - - + + + - - - + + - + - - - + diff --git a/InvoiceReminder.API.UnitTests/AuthenticationSetup/BearerSecuritySchemeTransformerTests.cs b/InvoiceReminder.API.UnitTests/AuthenticationSetup/BearerSecuritySchemeTransformerTests.cs index 893c0ad..03d6310 100644 --- a/InvoiceReminder.API.UnitTests/AuthenticationSetup/BearerSecuritySchemeTransformerTests.cs +++ b/InvoiceReminder.API.UnitTests/AuthenticationSetup/BearerSecuritySchemeTransformerTests.cs @@ -1,7 +1,7 @@ using InvoiceReminder.API.AuthenticationSetup; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.OpenApi; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using NSubstitute; using Shouldly; diff --git a/InvoiceReminder.API.UnitTests/Endpoints/LoginEndpointTests.cs b/InvoiceReminder.API.UnitTests/Endpoints/LoginEndpointTests.cs index 44d48f0..7b7a422 100644 --- a/InvoiceReminder.API.UnitTests/Endpoints/LoginEndpointTests.cs +++ b/InvoiceReminder.API.UnitTests/Endpoints/LoginEndpointTests.cs @@ -18,7 +18,7 @@ namespace InvoiceReminder.API.UnitTests.Endpoints; [TestClass] -public class LoginEndpointTests +public sealed class LoginEndpointTests { private readonly HttpClient _client; private readonly IJwtProvider _jwtProvider; diff --git a/InvoiceReminder.API.UnitTests/InvoiceReminder.API.UnitTests.csproj b/InvoiceReminder.API.UnitTests/InvoiceReminder.API.UnitTests.csproj index 3002931..cf1117e 100644 --- a/InvoiceReminder.API.UnitTests/InvoiceReminder.API.UnitTests.csproj +++ b/InvoiceReminder.API.UnitTests/InvoiceReminder.API.UnitTests.csproj @@ -1,16 +1,13 @@  - net9.0 - latest - enable - disable - cb5b3f1e-1e38-47a5-bcd4-457b0c7f1d0a - true + true + Exe + false @@ -30,8 +27,6 @@ - - all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/InvoiceReminder.API/AuthenticationSetup/BearerSecuritySchemeTransformer.cs b/InvoiceReminder.API/AuthenticationSetup/BearerSecuritySchemeTransformer.cs index d6d42e5..e3d1a10 100644 --- a/InvoiceReminder.API/AuthenticationSetup/BearerSecuritySchemeTransformer.cs +++ b/InvoiceReminder.API/AuthenticationSetup/BearerSecuritySchemeTransformer.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.OpenApi; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("InvoiceReminder.API.UnitTests")] @@ -35,7 +35,10 @@ CancellationToken cancellationToken }; document.Components ??= new OpenApiComponents(); - document.Components.SecuritySchemes = requirements; + document.Components.SecuritySchemes = requirements.ToDictionary( + kvp => kvp.Key, + kvp => (IOpenApiSecurityScheme)kvp.Value + ); } } } diff --git a/InvoiceReminder.API/Dockerfile b/InvoiceReminder.API/Dockerfile index 0975444..bad9d4d 100644 --- a/InvoiceReminder.API/Dockerfile +++ b/InvoiceReminder.API/Dockerfile @@ -1,19 +1,26 @@ # Acesse https://aka.ms/customizecontainer para saber como personalizar seu contêiner de depuração e como o Visual Studio usa este Dockerfile para criar suas imagens para uma depuração mais rápida. # Esta fase é usada durante a execução no VS no modo rápido (Padrão para a configuração de Depuração) -FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base ARG APP_UID=64198 USER ${APP_UID} WORKDIR /app EXPOSE 8080 EXPOSE 8081 +# Instala a biblioteca necessária para evitar erro de libgssapi_krb5.so.2 +USER root +RUN apt-get update && apt-get install -y \ + libgssapi-krb5-2 \ + && rm -rf /var/lib/apt/lists/* +USER ${APP_UID} # Esta fase é usada para compilar o projeto de serviço -FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build ARG BUILD_CONFIGURATION=Release # Adiciona o arquivo de versões centralizadas +COPY Directory.Build.props ./ COPY Directory.Packages.props ./ WORKDIR /src diff --git a/InvoiceReminder.API/InvoiceReminder.API.csproj b/InvoiceReminder.API/InvoiceReminder.API.csproj index 7cbe886..14517a2 100644 --- a/InvoiceReminder.API/InvoiceReminder.API.csproj +++ b/InvoiceReminder.API/InvoiceReminder.API.csproj @@ -1,10 +1,6 @@  - net9.0 - enable - disable - cb5b3f1e-1e38-47a5-bcd4-457b0c7f1d0a Linux @@ -22,7 +18,7 @@ all - runtime; build; native; contentfiles; analyzers; buildtransitive + runtime; build; native; analyzers; buildtransitive diff --git a/InvoiceReminder.API/InvoiceReminder.http b/InvoiceReminder.API/InvoiceReminder.http index 36240c1..c6e20d9 100644 --- a/InvoiceReminder.API/InvoiceReminder.http +++ b/InvoiceReminder.API/InvoiceReminder.http @@ -1,6 +1,363 @@ @InvoiceReminder_HostAddress = http://localhost:5216 -GET {{InvoiceReminder_HostAddress}}/weatherforecast/ +### ============================================ +### LOGIN ENDPOINTS +### ============================================ + +### Login +POST {{InvoiceReminder_HostAddress}}/api/login +Content-Type: application/json + +{ + "email": "user@example.com", + "password": "password123" +} + +### ============================================ +### USER ENDPOINTS +### ============================================ + +### Get All Users +GET {{InvoiceReminder_HostAddress}}/api/user +Accept: application/json +Authorization: Bearer your_jwt_token_here + +### + +### Get User by ID +GET {{InvoiceReminder_HostAddress}}/api/user/550e8400-e29b-41d4-a716-446655440000 +Accept: application/json +Authorization: Bearer your_jwt_token_here + +### + +### Get User by Email +GET {{InvoiceReminder_HostAddress}}/api/user/getby-email/user@example.com +Accept: application/json +Authorization: Bearer your_jwt_token_here + +### + +### Create User +POST {{InvoiceReminder_HostAddress}}/api/user +Content-Type: application/json +Authorization: Bearer your_jwt_token_here + +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "João Silva", + "email": "joao@example.com", + "password": "password123", + "createdAt": "2025-11-25T00:00:00Z", + "updatedAt": "2025-11-25T00:00:00Z" +} + +### + +### Create Multiple Users +POST {{InvoiceReminder_HostAddress}}/api/user/bulk-insert +Content-Type: application/json +Authorization: Bearer your_jwt_token_here + +[ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "João Silva", + "email": "joao@example.com", + "password": "password123", + "createdAt": "2025-11-25T00:00:00Z", + "updatedAt": "2025-11-25T00:00:00Z" + }, + { + "id": "550e8400-e29b-41d4-a716-446655440001", + "name": "Maria Santos", + "email": "maria@example.com", + "password": "password123", + "createdAt": "2025-11-25T00:00:00Z", + "updatedAt": "2025-11-25T00:00:00Z" + } +] + +### + +### Update User +PUT {{InvoiceReminder_HostAddress}}/api/user +Content-Type: application/json +Authorization: Bearer your_jwt_token_here + +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "João Silva Updated", + "email": "joao.updated@example.com", + "password": "newpassword123", + "createdAt": "2025-11-25T00:00:00Z", + "updatedAt": "2025-11-25T00:00:00Z" +} + +### + +### Delete User +DELETE {{InvoiceReminder_HostAddress}}/api/user +Content-Type: application/json +Authorization: Bearer your_jwt_token_here + +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "João Silva", + "email": "joao@example.com", + "password": "password123", + "createdAt": "2025-11-25T00:00:00Z", + "updatedAt": "2025-11-25T00:00:00Z" +} + +### + +### ============================================ +### INVOICE ENDPOINTS +### ============================================ + +### Get All Invoices +GET {{InvoiceReminder_HostAddress}}/api/invoice +Accept: application/json +Authorization: Bearer your_jwt_token_here + +### + +### Get Invoice by ID +GET {{InvoiceReminder_HostAddress}}/api/invoice/550e8400-e29b-41d4-a716-446655440000 +Accept: application/json +Authorization: Bearer your_jwt_token_here + +### + +### Get Invoice by Barcode +GET {{InvoiceReminder_HostAddress}}/api/invoice/getby-barcode/12345678901234567890123456789012345678901234 +Accept: application/json +Authorization: Bearer your_jwt_token_here + +### + +### Create Invoice +POST {{InvoiceReminder_HostAddress}}/api/invoice +Content-Type: application/json +Authorization: Bearer your_jwt_token_here + +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "bank": "Banco do Brasil", + "beneficiary": "João da Silva", + "amount": 100.00, + "barcode": "12345678901234567890123456789012345678901234", + "dueDate": "2025-12-25T00:00:00Z", + "createdAt": "2025-11-25T00:00:00Z", + "updatedAt": "2025-11-25T00:00:00Z" +} + +### + +### Update Invoice +PUT {{InvoiceReminder_HostAddress}}/api/invoice +Content-Type: application/json +Authorization: Bearer your_jwt_token_here + +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "bank": "Banco do Brasil Updated", + "beneficiary": "João da Silva", + "amount": 150.00, + "barcode": "12345678901234567890123456789012345678901244", + "dueDate": "2025-12-25T00:00:00Z", + "createdAt": "2025-11-25T00:00:00Z", + "updatedAt": "2025-11-25T00:00:00Z" +} + +### + +### Delete Invoice +DELETE {{InvoiceReminder_HostAddress}}/api/invoice +Content-Type: application/json +Authorization: Bearer your_jwt_token_here + +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "bank": "Banco do Brasil", + "beneficiary": "João da Silva", + "amount": 100.00, + "barcode": "12345678901234567890123456789012345678901244", + "dueDate": "2025-12-25T00:00:00Z", + "createdAt": "2025-11-25T00:00:00Z", + "updatedAt": "2025-11-25T00:00:00Z" +} + +### + +### ============================================ +### JOB SCHEDULE ENDPOINTS +### ============================================ + +### Get All Job Schedules +GET {{InvoiceReminder_HostAddress}}/api/job_schedule +Accept: application/json +Authorization: Bearer your_jwt_token_here + +### + +### Get Job Schedule by ID +GET {{InvoiceReminder_HostAddress}}/api/job_schedule/550e8400-e29b-41d4-a716-446655440000 +Accept: application/json +Authorization: Bearer your_jwt_token_here + +### + +### Get Job Schedules by User ID +GET {{InvoiceReminder_HostAddress}}/api/job_schedule/getby-userid/550e8400-e29b-41d4-a716-446655440000 +Accept: application/json +Authorization: Bearer your_jwt_token_here + +### + +### Create Job Schedule +POST {{InvoiceReminder_HostAddress}}/api/job_schedule +Content-Type: application/json +Authorization: Bearer your_jwt_token_here + +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "userId": "550e8400-e29b-41d4-a716-446655440000", + "cronExpression": "0 0 * * *", + "createdAt": "2025-11-25T00:00:00Z", + "updatedAt": "2025-11-25T00:00:00Z" +} + +### + +### Update Job Schedule +PUT {{InvoiceReminder_HostAddress}}/api/job_schedule +Content-Type: application/json +Authorization: Bearer your_jwt_token_here + +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "userId": "550e8400-e29b-41d4-a716-446655440000", + "cronExpression": "0 0 * * 1", + "createdAt": "2025-11-25T00:00:00Z", + "updatedAt": "2025-11-25T00:00:00Z" +} + +### + +### Delete Job Schedule +DELETE {{InvoiceReminder_HostAddress}}/api/job_schedule +Content-Type: application/json +Authorization: Bearer your_jwt_token_here + +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "userId": "550e8400-e29b-41d4-a716-446655440000", + "cronExpression": "0 0 * * *", + "createdAt": "2025-11-25T00:00:00Z", + "updatedAt": "2025-11-25T00:00:00Z" +} + +### + +### ============================================ +### SCAN EMAIL DEFINITION ENDPOINTS +### ============================================ + +### Get All Scan Email Definitions +GET {{InvoiceReminder_HostAddress}}/api/scan_email +Accept: application/json +Authorization: Bearer your_jwt_token_here + +### + +### Get Scan Email Definition by ID +GET {{InvoiceReminder_HostAddress}}/api/scan_email/550e8400-e29b-41d4-a716-446655440000 +Accept: application/json +Authorization: Bearer your_jwt_token_here + +### + +### Get Scan Email Definitions by User ID +GET {{InvoiceReminder_HostAddress}}/api/scan_email/getby-userid/550e8400-e29b-41d4-a716-446655440000 +Accept: application/json +Authorization: Bearer your_jwt_token_here + +### + +### Get Scan Email Definition by Sender Email Address +GET {{InvoiceReminder_HostAddress}}/api/scan_email/sender@example.com/550e8400-e29b-41d4-a716-446655440000 +Accept: application/json +Authorization: Bearer your_jwt_token_here + +### + +### Create Scan Email Definition +POST {{InvoiceReminder_HostAddress}}/api/scan_email +Content-Type: application/json +Authorization: Bearer your_jwt_token_here + +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "userId": "550e8400-e29b-41d4-a716-446655440000", + "senderEmailAddress": "sender@example.com", + "createdAt": "2025-11-25T00:00:00Z", + "updatedAt": "2025-11-25T00:00:00Z" +} + +### + +### Update Scan Email Definition +PUT {{InvoiceReminder_HostAddress}}/api/scan_email +Content-Type: application/json +Authorization: Bearer your_jwt_token_here + +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "userId": "550e8400-e29b-41d4-a716-446655440000", + "senderEmailAddress": "newemail@example.com", + "createdAt": "2025-11-25T00:00:00Z", + "updatedAt": "2025-11-25T00:00:00Z" +} + +### + +### Delete Scan Email Definition +DELETE {{InvoiceReminder_HostAddress}}/api/scan_email +Content-Type: application/json +Authorization: Bearer your_jwt_token_here + +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "userId": "550e8400-e29b-41d4-a716-446655440000", + "senderEmailAddress": "sender@example.com", + "createdAt": "2025-11-25T00:00:00Z", + "updatedAt": "2025-11-25T00:00:00Z" +} + +### + +### ============================================ +### GOOGLE OAUTH ENDPOINTS +### ============================================ + +### Get Authorization URL +GET {{InvoiceReminder_HostAddress}}/api/google_oauth/get-auth-url/550e8400-e29b-41d4-a716-446655440000 +Accept: application/json +Authorization: Bearer your_jwt_token_here + +### + +### Authorize Google OAuth +GET {{InvoiceReminder_HostAddress}}/api/google_oauth/authorize?state=550e8400-e29b-41d4-a716-446655440000&code=4/0AY0e-gxxxxxxxxxxxxxxxxxx +Accept: application/json + +### + +### Revoke Google OAuth Authorization +DELETE {{InvoiceReminder_HostAddress}}/api/google_oauth/revoke?id=550e8400-e29b-41d4-a716-446655440000 Accept: application/json +Authorization: Bearer your_jwt_token_here ### diff --git a/InvoiceReminder.Application.UnitTests/AppServices/BaseAppServiceTests.cs b/InvoiceReminder.Application.UnitTests/AppServices/BaseAppServiceTests.cs index f6bd0a0..7e257f1 100644 --- a/InvoiceReminder.Application.UnitTests/AppServices/BaseAppServiceTests.cs +++ b/InvoiceReminder.Application.UnitTests/AppServices/BaseAppServiceTests.cs @@ -7,7 +7,7 @@ namespace InvoiceReminder.Application.UnitTests.AppServices; [TestClass] -public class BaseAppServiceTests +public sealed class BaseAppServiceTests { private readonly BaseAppService _appService; private readonly IBaseRepository _repository; diff --git a/InvoiceReminder.Application.UnitTests/InvoiceReminder.Application.UnitTests.csproj b/InvoiceReminder.Application.UnitTests/InvoiceReminder.Application.UnitTests.csproj index 1841126..fa4d5f2 100644 --- a/InvoiceReminder.Application.UnitTests/InvoiceReminder.Application.UnitTests.csproj +++ b/InvoiceReminder.Application.UnitTests/InvoiceReminder.Application.UnitTests.csproj @@ -1,15 +1,13 @@  - net9.0 - latest - enable - disable - true + true + Exe + false @@ -27,8 +25,6 @@ - - all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/InvoiceReminder.Application/InvoiceReminder.Application.csproj b/InvoiceReminder.Application/InvoiceReminder.Application.csproj index 1ce1f69..90a6123 100644 --- a/InvoiceReminder.Application/InvoiceReminder.Application.csproj +++ b/InvoiceReminder.Application/InvoiceReminder.Application.csproj @@ -1,11 +1,5 @@  - - net9.0 - enable - disable - - 1701;1702;S2326 diff --git a/InvoiceReminder.Application/ViewModels/UserViewModel.cs b/InvoiceReminder.Application/ViewModels/UserViewModel.cs index c8bb9c5..d861f3b 100644 --- a/InvoiceReminder.Application/ViewModels/UserViewModel.cs +++ b/InvoiceReminder.Application/ViewModels/UserViewModel.cs @@ -26,5 +26,8 @@ public class UserViewModel : ViewModelDefaults public UserViewModel() { Id = Guid.NewGuid(); + Invoices = []; + JobSchedules = []; + ScanEmailDefinitions = []; } } diff --git a/InvoiceReminder.ArchitectureTests/InvoiceReminder.ArchitectureTests.csproj b/InvoiceReminder.ArchitectureTests/InvoiceReminder.ArchitectureTests.csproj index 4a33da4..e00d40d 100644 --- a/InvoiceReminder.ArchitectureTests/InvoiceReminder.ArchitectureTests.csproj +++ b/InvoiceReminder.ArchitectureTests/InvoiceReminder.ArchitectureTests.csproj @@ -1,17 +1,15 @@  - net9.0 - latest - enable - disable - true + true + Exe + false - + @@ -26,8 +24,6 @@ - - all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/InvoiceReminder.Authentication/InvoiceReminder.Authentication.csproj b/InvoiceReminder.Authentication/InvoiceReminder.Authentication.csproj index f61be14..6e98d95 100644 --- a/InvoiceReminder.Authentication/InvoiceReminder.Authentication.csproj +++ b/InvoiceReminder.Authentication/InvoiceReminder.Authentication.csproj @@ -1,12 +1,5 @@ - - net9.0 - enable - disable - 01c72e23-c6d4-4909-bba5-76c3a0cda739 - - diff --git a/InvoiceReminder.CrossCutting.IoC/InvoiceReminder.CrossCutting.IoC.csproj b/InvoiceReminder.CrossCutting.IoC/InvoiceReminder.CrossCutting.IoC.csproj index 4171320..0d983da 100644 --- a/InvoiceReminder.CrossCutting.IoC/InvoiceReminder.CrossCutting.IoC.csproj +++ b/InvoiceReminder.CrossCutting.IoC/InvoiceReminder.CrossCutting.IoC.csproj @@ -1,11 +1,5 @@ - - net9.0 - enable - disable - - diff --git a/InvoiceReminder.Data/InvoiceReminder.Data.csproj b/InvoiceReminder.Data/InvoiceReminder.Data.csproj index 5925ab1..05981c8 100644 --- a/InvoiceReminder.Data/InvoiceReminder.Data.csproj +++ b/InvoiceReminder.Data/InvoiceReminder.Data.csproj @@ -1,11 +1,4 @@ - - - - net9.0 - enable - disable - bd460ecb-03c4-4be2-afe0-e5f2ce4edbf0 - + @@ -13,12 +6,12 @@ - - + + all - runtime; build; native; contentfiles; analyzers; buildtransitive + runtime; build; native; analyzers; buildtransitive all diff --git a/InvoiceReminder.Data/Repository/BaseRepository.cs b/InvoiceReminder.Data/Repository/BaseRepository.cs index 236d3da..bd3c10c 100644 --- a/InvoiceReminder.Data/Repository/BaseRepository.cs +++ b/InvoiceReminder.Data/Repository/BaseRepository.cs @@ -1,4 +1,3 @@ -using EFCore.BulkExtensions; using InvoiceReminder.Data.Interfaces; using Microsoft.EntityFrameworkCore; using System.Linq.Expressions; @@ -34,7 +33,7 @@ public virtual async Task BulkInsertAsync(ICollection entities, Ca entity.GetType().GetProperty("UpdatedAt")?.SetValue(entity, DateTime.UtcNow); } - await _dbContext.BulkInsertAsync(entities, cancellationToken: cancellationToken); + await _dbContext.BulkInsertAsync(entities, cancellationToken); return entities.Count; } diff --git a/InvoiceReminder.Data/Repository/EmailAuthTokenRepository.cs b/InvoiceReminder.Data/Repository/EmailAuthTokenRepository.cs index 9f5b652..9b0646b 100644 --- a/InvoiceReminder.Data/Repository/EmailAuthTokenRepository.cs +++ b/InvoiceReminder.Data/Repository/EmailAuthTokenRepository.cs @@ -13,6 +13,7 @@ public class EmailAuthTokenRepository : BaseRepository _logger; + private const string LogExceptionMessage = "{ContextualInfo} - Exception: {Message}"; public EmailAuthTokenRepository(CoreDbContext dbContext, ILogger logger) : base(dbContext) { @@ -39,7 +40,10 @@ public async Task GetByUserIdAsync(Guid id, string tokenProvider var method = $"{nameof(EmailAuthTokenRepository)}.{nameof(GetByUserIdAsync)}"; var contextualInfo = $"Method {method} execution was interrupted by a CancellationToken Request..."; - _logger.LogWarning(ex, "{ContextualInfo} - Exception: {Message}", contextualInfo, ex.Message); + if (_logger.IsEnabled(LogLevel.Warning)) + { + _logger.LogWarning(ex, LogExceptionMessage, contextualInfo, ex.Message); + } throw new OperationCanceledException(contextualInfo, ex, cancellationToken); } @@ -48,7 +52,10 @@ public async Task GetByUserIdAsync(Guid id, string tokenProvider var method = $"{nameof(EmailAuthTokenRepository)}.{nameof(GetByUserIdAsync)}"; var contextualInfo = $"Exception raised while querying DB >> {method}(...)"; - _logger.LogError(ex, "{ContextualInfo} - Exception: {Message}", contextualInfo, ex.Message); + if (_logger.IsEnabled(LogLevel.Error)) + { + _logger.LogError(ex, LogExceptionMessage, contextualInfo, ex.Message); + } throw new DataLayerException(contextualInfo, ex); } diff --git a/InvoiceReminder.Data/Repository/InvoiceRepository.cs b/InvoiceReminder.Data/Repository/InvoiceRepository.cs index e69b677..f7fbcc6 100644 --- a/InvoiceReminder.Data/Repository/InvoiceRepository.cs +++ b/InvoiceReminder.Data/Repository/InvoiceRepository.cs @@ -13,6 +13,7 @@ public class InvoiceRepository : BaseRepository, IInvoic { private readonly IDbConnection _dbConnection; private readonly ILogger _logger; + private const string LogExceptionMessage = "{ContextualInfo} - Exception: {Message}"; public InvoiceRepository(CoreDbContext dbContext, ILogger logger) : base(dbContext) { @@ -36,7 +37,10 @@ public async Task GetByBarcodeAsync(string barcode, CancellationToken c var method = $"{nameof(InvoiceRepository)}.{nameof(GetByBarcodeAsync)}"; var contextualInfo = $"Method {method} execution was interrupted by a CancellationToken Request..."; - _logger.LogWarning(ex, "{ContextualInfo} - Exception: {Message}", contextualInfo, ex.Message); + if (_logger.IsEnabled(LogLevel.Warning)) + { + _logger.LogWarning(ex, LogExceptionMessage, contextualInfo, ex.Message); + } throw new OperationCanceledException(contextualInfo, ex, cancellationToken); } @@ -45,7 +49,10 @@ public async Task GetByBarcodeAsync(string barcode, CancellationToken c var method = $"{nameof(InvoiceRepository)}.{nameof(GetByBarcodeAsync)}"; var contextualInfo = $"Exception raised while querying DB >> {method}(...)"; - _logger.LogError(ex, "{ContextualInfo} - Exception: {Message}", contextualInfo, ex.Message); + if (_logger.IsEnabled(LogLevel.Error)) + { + _logger.LogError(ex, LogExceptionMessage, contextualInfo, ex.Message); + } throw new DataLayerException(contextualInfo, ex); } diff --git a/InvoiceReminder.Data/Repository/JobScheduleRepository.cs b/InvoiceReminder.Data/Repository/JobScheduleRepository.cs index 6c0037e..957e178 100644 --- a/InvoiceReminder.Data/Repository/JobScheduleRepository.cs +++ b/InvoiceReminder.Data/Repository/JobScheduleRepository.cs @@ -13,6 +13,7 @@ public class JobScheduleRepository : BaseRepository, { private readonly IDbConnection _dbConnection; private readonly ILogger _logger; + private const string LogExceptionMessage = "{ContextualInfo} - Exception: {Message}"; public JobScheduleRepository(CoreDbContext dbContext, ILogger logger) : base(dbContext) { @@ -36,7 +37,10 @@ public async Task> GetByUserIdAsync(Guid id, Cancellati var method = $"{nameof(JobScheduleRepository)}.{nameof(GetByUserIdAsync)}"; var contextualInfo = $"Method {method} execution was interrupted by a CancellationToken Request..."; - _logger.LogWarning(ex, "{ContextualInfo} - Exception: {Message}", contextualInfo, ex.Message); + if (_logger.IsEnabled(LogLevel.Warning)) + { + _logger.LogWarning(ex, LogExceptionMessage, contextualInfo, ex.Message); + } throw new OperationCanceledException(contextualInfo, ex, cancellationToken); } @@ -45,7 +49,10 @@ public async Task> GetByUserIdAsync(Guid id, Cancellati var method = $"{nameof(JobScheduleRepository)}.{nameof(GetByUserIdAsync)}"; var contextualInfo = $"Exception raised while querying DB >> {method}(...)"; - _logger.LogError(ex, "{ContextualInfo} - Exception: {Message}", contextualInfo, ex.Message); + if (_logger.IsEnabled(LogLevel.Error)) + { + _logger.LogError(ex, LogExceptionMessage, contextualInfo, ex.Message); + } throw new DataLayerException(contextualInfo, ex); } diff --git a/InvoiceReminder.Data/Repository/ScanEmailDefinitionRepository.cs b/InvoiceReminder.Data/Repository/ScanEmailDefinitionRepository.cs index 442e953..a8ecf20 100644 --- a/InvoiceReminder.Data/Repository/ScanEmailDefinitionRepository.cs +++ b/InvoiceReminder.Data/Repository/ScanEmailDefinitionRepository.cs @@ -13,6 +13,7 @@ public class ScanEmailDefinitionRepository : BaseRepository _logger; + private const string LogExceptionMessageTemplate = "{ContextualInfo} - Exception: {Message}"; public ScanEmailDefinitionRepository(CoreDbContext dbContext, ILogger logger) : base(dbContext) { @@ -40,7 +41,10 @@ public async Task GetBySenderBeneficiaryAsync(string benefi var method = $"{nameof(ScanEmailDefinitionRepository)}.{nameof(GetBySenderBeneficiaryAsync)}"; var contextualInfo = $"Method {method} execution was interrupted by a CancellationToken Request..."; - _logger.LogWarning(ex, "{ContextualInfo} - Exception: {Message}", contextualInfo, ex.Message); + if (_logger.IsEnabled(LogLevel.Warning)) + { + _logger.LogWarning(ex, LogExceptionMessageTemplate, contextualInfo, ex.Message); + } throw new OperationCanceledException(contextualInfo, ex, cancellationToken); } @@ -49,7 +53,10 @@ public async Task GetBySenderBeneficiaryAsync(string benefi var method = $"{nameof(ScanEmailDefinitionRepository)}.{nameof(GetBySenderBeneficiaryAsync)}"; var contextualInfo = $"Exception raised while querying DB >> {method}(...)"; - _logger.LogError(ex, "{ContextualInfo} - Exception: {Message}", contextualInfo, ex.Message); + if (_logger.IsEnabled(LogLevel.Error)) + { + _logger.LogError(ex, LogExceptionMessageTemplate, contextualInfo, ex.Message); + } throw new DataLayerException(contextualInfo, ex); } @@ -77,7 +84,10 @@ public async Task GetBySenderEmailAddressAsync(string email var method = $"{nameof(ScanEmailDefinitionRepository)}.{nameof(GetBySenderEmailAddressAsync)}"; var contextualInfo = $"Method {method} execution was interrupted by a CancellationToken Request..."; - _logger.LogWarning(ex, "{ContextualInfo} - Exception: {Message}", contextualInfo, ex.Message); + if (_logger.IsEnabled(LogLevel.Warning)) + { + _logger.LogWarning(ex, LogExceptionMessageTemplate, contextualInfo, ex.Message); + } throw new OperationCanceledException(contextualInfo, ex, cancellationToken); } @@ -86,7 +96,10 @@ public async Task GetBySenderEmailAddressAsync(string email var method = $"{nameof(ScanEmailDefinitionRepository)}.{nameof(GetBySenderEmailAddressAsync)}"; var contextualInfo = $"Exception raised while querying DB >> {method}(...)"; - _logger.LogError(ex, "{ContextualInfo} - Exception: {Message}", contextualInfo, ex.Message); + if (_logger.IsEnabled(LogLevel.Error)) + { + _logger.LogError(ex, LogExceptionMessageTemplate, contextualInfo, ex.Message); + } throw new DataLayerException(contextualInfo, ex); } @@ -113,7 +126,10 @@ public async Task> GetByUserIdAsync(Guid userId var method = $"{nameof(ScanEmailDefinitionRepository)}.{nameof(GetByUserIdAsync)}"; var contextualInfo = $"Method {method} execution was interrupted by a CancellationToken Request..."; - _logger.LogWarning(ex, "{ContextualInfo} - Exception: {Message}", contextualInfo, ex.Message); + if (_logger.IsEnabled(LogLevel.Warning)) + { + _logger.LogWarning(ex, LogExceptionMessageTemplate, contextualInfo, ex.Message); + } throw new OperationCanceledException(contextualInfo, ex, cancellationToken); } @@ -122,7 +138,10 @@ public async Task> GetByUserIdAsync(Guid userId var method = $"{nameof(ScanEmailDefinitionRepository)}.{nameof(GetByUserIdAsync)}"; var contextualInfo = $"Exception raised while querying DB >> {method}(...)"; - _logger.LogError(ex, "{ContextualInfo} - Exception: {Message}", contextualInfo, ex.Message); + if (_logger.IsEnabled(LogLevel.Error)) + { + _logger.LogError(ex, LogExceptionMessageTemplate, contextualInfo, ex.Message); + } throw new DataLayerException(contextualInfo, ex); } diff --git a/InvoiceReminder.Data/Repository/UnitOfWork.cs b/InvoiceReminder.Data/Repository/UnitOfWork.cs index c06896b..3091e9f 100644 --- a/InvoiceReminder.Data/Repository/UnitOfWork.cs +++ b/InvoiceReminder.Data/Repository/UnitOfWork.cs @@ -36,7 +36,10 @@ public async Task SaveChangesAsync(CancellationToken cancellationToken = default } catch (Exception ex) { - _logger.LogError(ex, "{Message}", ex.Message); + if (_logger.IsEnabled(LogLevel.Error)) + { + _logger.LogError(ex, "{Message}", ex.Message); + } await transaction.RollbackAsync(cancellationToken); diff --git a/InvoiceReminder.Data/Repository/UserRepository.cs b/InvoiceReminder.Data/Repository/UserRepository.cs index 712db83..245d09e 100644 --- a/InvoiceReminder.Data/Repository/UserRepository.cs +++ b/InvoiceReminder.Data/Repository/UserRepository.cs @@ -15,6 +15,7 @@ public class UserRepository : BaseRepository, IUserReposito private readonly IDbConnection _dbConnection; private readonly ILogger _logger; private readonly string _query; + private const string LogExceptionMessage = "{ContextualInfo} - Exception: {Message}"; public UserRepository(CoreDbContext dbContext, ILogger logger) : base(dbContext) { @@ -64,7 +65,10 @@ public async Task GetByEmailAsync(string value, CancellationToken cancella var method = $"{nameof(UserRepository)}.{nameof(GetByEmailAsync)}"; var contextualInfo = $"Method {method} execution was interrupted by a CancellationToken Request..."; - _logger.LogWarning(ex, "{ContextualInfo} - Exception: {Message}", contextualInfo, ex.Message); + if (_logger.IsEnabled(LogLevel.Warning)) + { + _logger.LogWarning(ex, LogExceptionMessage, contextualInfo, ex.Message); + } throw new OperationCanceledException(contextualInfo, ex, cancellationToken); } @@ -73,7 +77,10 @@ public async Task GetByEmailAsync(string value, CancellationToken cancella var method = $"{nameof(UserRepository)}.{nameof(GetByEmailAsync)}"; var contextualInfo = $"Exception raised while querying DB >> {method}(...)"; - _logger.LogError(ex, "{ContextualInfo} - Exception: {Message}", contextualInfo, ex.Message); + if (_logger.IsEnabled(LogLevel.Error)) + { + _logger.LogError(ex, LogExceptionMessage, contextualInfo, ex.Message); + } throw new DataLayerException(contextualInfo, ex); } @@ -112,7 +119,10 @@ public override async Task GetByIdAsync(Guid id, CancellationToken cancell var method = $"{nameof(UserRepository)}.{nameof(GetByIdAsync)}"; var contextualInfo = $"Method {method} execution was interrupted by a CancellationToken Request..."; - _logger.LogWarning(ex, "{ContextualInfo} - Exception: {Message}", contextualInfo, ex.Message); + if (_logger.IsEnabled(LogLevel.Warning)) + { + _logger.LogWarning(ex, LogExceptionMessage, contextualInfo, ex.Message); + } throw new OperationCanceledException(contextualInfo, ex, cancellationToken); } @@ -121,7 +131,10 @@ public override async Task GetByIdAsync(Guid id, CancellationToken cancell var method = $"{nameof(UserRepository)}.{nameof(GetByIdAsync)}"; var contextualInfo = $"Exception raised while querying DB >> {method}(...)"; - _logger.LogError(ex, "{ContextualInfo} - Exception: {Message}", contextualInfo, ex.Message); + if (_logger.IsEnabled(LogLevel.Error)) + { + _logger.LogError(ex, LogExceptionMessage, contextualInfo, ex.Message); + } throw new DataLayerException(contextualInfo, ex); } diff --git a/InvoiceReminder.Domain/Entities/User.cs b/InvoiceReminder.Domain/Entities/User.cs index c918111..fbf08a9 100644 --- a/InvoiceReminder.Domain/Entities/User.cs +++ b/InvoiceReminder.Domain/Entities/User.cs @@ -10,4 +10,12 @@ public class User : EntityDefaults public virtual ICollection Invoices { get; set; } public virtual ICollection JobSchedules { get; set; } public virtual ICollection ScanEmailDefinitions { get; set; } + + public User() + { + EmailAuthTokens = []; + Invoices = []; + JobSchedules = []; + ScanEmailDefinitions = []; + } } diff --git a/InvoiceReminder.Domain/Extensions/EntityExtensions.cs b/InvoiceReminder.Domain/Extensions/EntityExtensions.cs new file mode 100644 index 0000000..37005a1 --- /dev/null +++ b/InvoiceReminder.Domain/Extensions/EntityExtensions.cs @@ -0,0 +1,22 @@ +namespace InvoiceReminder.Domain.Extensions; + +public static class EntityExtensions +{ + public static void AddIfNotExists(this T entity, ICollection collection) where T : class + { + if (entity is null) + { + return; + } + + if (collection.FirstOrDefault(e => GetId(e).Equals(GetId(entity))) is null) + { + collection.Add(entity); + } + } + + private static Guid GetId(T entity) where T : class + { + return (Guid)typeof(T).GetProperty("Id")!.GetValue(entity)!; + } +} diff --git a/InvoiceReminder.Domain/Extensions/UserExtensions.cs b/InvoiceReminder.Domain/Extensions/UserExtensions.cs index ba7ba1c..abf484f 100644 --- a/InvoiceReminder.Domain/Extensions/UserExtensions.cs +++ b/InvoiceReminder.Domain/Extensions/UserExtensions.cs @@ -11,62 +11,19 @@ public static IDictionary Handle( { if (!result.TryGetValue(user.Id, out var existingUser)) { - user.Invoices ??= []; - user.JobSchedules ??= []; - user.EmailAuthTokens ??= []; - user.ScanEmailDefinitions ??= []; - - if (parameters.Invoice is not null && - user.Invoices.FirstOrDefault(i => i.Id == parameters.Invoice.Id) is null) - { - user.Invoices.Add(parameters.Invoice); - } - - if (parameters.JobSchedule is not null && - user.JobSchedules.FirstOrDefault(js => js.Id == parameters.JobSchedule.Id) is null) - { - user.JobSchedules.Add(parameters.JobSchedule); - } - - if (parameters.EmailAuthToken is not null && - user.EmailAuthTokens.FirstOrDefault(eat => eat.Id == parameters.EmailAuthToken.Id) is null) - { - user.EmailAuthTokens.Add(parameters.EmailAuthToken); - } - - if (parameters.ScanEmailDefinition is not null && - user.ScanEmailDefinitions.FirstOrDefault(sed => sed.Id == parameters.ScanEmailDefinition.Id) is null) - { - user.ScanEmailDefinitions.Add(parameters.ScanEmailDefinition); - } + parameters.Invoice.AddIfNotExists(user.Invoices); + parameters.JobSchedule.AddIfNotExists(user.JobSchedules); + parameters.EmailAuthToken.AddIfNotExists(user.EmailAuthTokens); + parameters.ScanEmailDefinition.AddIfNotExists(user.ScanEmailDefinitions); result.Add(user.Id, user); } else { - if (parameters.Invoice is not null && - existingUser.Invoices.FirstOrDefault(i => i.Id == parameters.Invoice.Id) is null) - { - existingUser.Invoices.Add(parameters.Invoice); - } - - if (parameters.JobSchedule is not null && - existingUser.JobSchedules.FirstOrDefault(js => js.Id == parameters.JobSchedule.Id) is null) - { - existingUser.JobSchedules.Add(parameters.JobSchedule); - } - - if (parameters.EmailAuthToken is not null && - existingUser.EmailAuthTokens.FirstOrDefault(eat => eat.Id == parameters.EmailAuthToken.Id) is null) - { - existingUser.EmailAuthTokens.Add(parameters.EmailAuthToken); - } - - if (parameters.ScanEmailDefinition is not null && - existingUser.ScanEmailDefinitions.FirstOrDefault(sed => sed.Id == parameters.ScanEmailDefinition.Id) is null) - { - existingUser.ScanEmailDefinitions.Add(parameters.ScanEmailDefinition); - } + parameters.Invoice.AddIfNotExists(existingUser.Invoices); + parameters.JobSchedule.AddIfNotExists(existingUser.JobSchedules); + parameters.EmailAuthToken.AddIfNotExists(existingUser.EmailAuthTokens); + parameters.ScanEmailDefinition.AddIfNotExists(existingUser.ScanEmailDefinitions); } return result; diff --git a/InvoiceReminder.Domain/InvoiceReminder.Domain.csproj b/InvoiceReminder.Domain/InvoiceReminder.Domain.csproj index b5adbc5..9690f6f 100644 --- a/InvoiceReminder.Domain/InvoiceReminder.Domain.csproj +++ b/InvoiceReminder.Domain/InvoiceReminder.Domain.csproj @@ -1,12 +1,5 @@ - - net9.0 - enable - disable - cb5b3f1e-1e38-47a5-bcd4-457b0c7f1d0a - - diff --git a/InvoiceReminder.DomainEntities.UnitTests/Extensions/EntityExtensionsTests.cs b/InvoiceReminder.DomainEntities.UnitTests/Extensions/EntityExtensionsTests.cs new file mode 100644 index 0000000..8661c98 --- /dev/null +++ b/InvoiceReminder.DomainEntities.UnitTests/Extensions/EntityExtensionsTests.cs @@ -0,0 +1,236 @@ +using InvoiceReminder.Domain.Extensions; +using Shouldly; + +namespace InvoiceReminder.DomainEntities.UnitTests.Extensions; + +[TestClass] +public sealed class EntityExtensionsTests +{ + private sealed class TestEntity + { + public Guid Id { get; init; } + public string Name { get; init; } = string.Empty; + } + + [TestMethod] + public void AddIfNotExists_WhenEntityIsNull_ShouldNotAddToCollection() + { + // Arrange + TestEntity entity = null; + var collection = new List(); + + // Act + entity.AddIfNotExists(collection); + + // Assert + collection.ShouldBeEmpty(); + } + + [TestMethod] + public void AddIfNotExists_WhenCollectionIsEmpty_ShouldAddEntity() + { + // Arrange + var entityId = Guid.NewGuid(); + var entity = new TestEntity { Id = entityId, Name = "Test Entity" }; + var collection = new List(); + + // Act + entity.AddIfNotExists(collection); + + // Assert + collection.ShouldSatisfyAllConditions(() => + { + _ = collection.ShouldHaveSingleItem(); + collection[0].ShouldBeEquivalentTo(entity); + collection[0].Id.ShouldBe(entityId); + }); + } + + [TestMethod] + public void AddIfNotExists_WhenEntityAlreadyExists_ShouldNotAddDuplicate() + { + // Arrange + var entityId = Guid.NewGuid(); + var existingEntity = new TestEntity { Id = entityId, Name = "Existing Entity" }; + var newEntity = new TestEntity { Id = entityId, Name = "New Entity" }; + var collection = new List { existingEntity }; + + // Act + newEntity.AddIfNotExists(collection); + + // Assert + collection.ShouldSatisfyAllConditions(() => + { + _ = collection.ShouldHaveSingleItem(); + collection[0].ShouldBeEquivalentTo(existingEntity); + collection[0].Name.ShouldBe("Existing Entity"); + }); + } + + [TestMethod] + public void AddIfNotExists_WhenEntityDoesNotExist_ShouldAddToNonEmptyCollection() + { + // Arrange + var existingId = Guid.NewGuid(); + var newId = Guid.NewGuid(); + var existingEntity = new TestEntity { Id = existingId, Name = "Existing Entity" }; + var newEntity = new TestEntity { Id = newId, Name = "New Entity" }; + var collection = new List { existingEntity }; + + // Act + newEntity.AddIfNotExists(collection); + + // Assert + collection.ShouldSatisfyAllConditions(() => + { + collection.Count.ShouldBe(2); + collection.ShouldContain(existingEntity); + collection.ShouldContain(newEntity); + }); + } + + [TestMethod] + public void AddIfNotExists_WhenMultipleEntitiesExist_ShouldFindExistingByIdOnly() + { + // Arrange + var targetId = Guid.NewGuid(); + var entity1 = new TestEntity { Id = Guid.NewGuid(), Name = "Entity 1" }; + var entity2 = new TestEntity { Id = targetId, Name = "Entity 2" }; + var entity3 = new TestEntity { Id = Guid.NewGuid(), Name = "Entity 3" }; + var duplicateEntity = new TestEntity { Id = targetId, Name = "Different Name" }; + var collection = new List { entity1, entity2, entity3 }; + + // Act + duplicateEntity.AddIfNotExists(collection); + + // Assert + collection.ShouldSatisfyAllConditions(() => + { + collection.Count.ShouldBe(3); + collection.Count(e => e.Id == targetId).ShouldBe(1); + }); + + } + + [TestMethod] + public void AddIfNotExists_WhenEntityWithNewIdAdded_ShouldIncreaseCollectionCount() + { + // Arrange + var collection = new List + { + new() { Id = Guid.NewGuid(), Name = "Entity 1" }, + new() { Id = Guid.NewGuid(), Name = "Entity 2" } + }; + var initialCount = collection.Count; + var newEntity = new TestEntity { Id = Guid.NewGuid(), Name = "Entity 3" }; + + // Act + newEntity.AddIfNotExists(collection); + + // Assert + collection.ShouldSatisfyAllConditions(() => + { + collection.Count.ShouldBe(initialCount + 1); + collection[^1].ShouldBe(newEntity); + }); + } + + [TestMethod] + public void AddIfNotExists_WhenMultipleCallsWithSameEntity_ShouldOnlyAddOnce() + { + // Arrange + var entityId = Guid.NewGuid(); + var entity = new TestEntity { Id = entityId, Name = "Test Entity" }; + var collection = new List(); + + // Act + entity.AddIfNotExists(collection); + entity.AddIfNotExists(collection); + entity.AddIfNotExists(collection); + + // Assert + collection.ShouldSatisfyAllConditions(() => + { + _ = collection.ShouldHaveSingleItem(); + collection[0].ShouldBeEquivalentTo(entity); + }); + } + + [TestMethod] + public void AddIfNotExists_WhenEntityIdIsEmptyGuid_ShouldAddEntity() + { + // Arrange + var entity = new TestEntity { Id = Guid.Empty, Name = "Entity with Empty ID" }; + var collection = new List(); + + // Act + entity.AddIfNotExists(collection); + + // Assert + collection.ShouldSatisfyAllConditions(() => + { + _ = collection.ShouldHaveSingleItem(); + collection[0].Id.ShouldBe(Guid.Empty); + }); + } + + [TestMethod] + public void AddIfNotExists_WhenEntityWithEmptyIdAlreadyExists_ShouldNotAddDuplicate() + { + // Arrange + var existingEntity = new TestEntity { Id = Guid.Empty, Name = "Existing Entity" }; + var newEntity = new TestEntity { Id = Guid.Empty, Name = "New Entity" }; + var collection = new List { existingEntity }; + + // Act + newEntity.AddIfNotExists(collection); + + // Assert + collection.ShouldSatisfyAllConditions(() => + { + _ = collection.ShouldHaveSingleItem(); + collection[0].Name.ShouldBe("Existing Entity"); + }); + } + + [TestMethod] + public void AddIfNotExists_WithHashSetCollection_ShouldWorkCorrectly() + { + // Arrange + var entityId = Guid.NewGuid(); + var entity = new TestEntity { Id = entityId, Name = "Test Entity" }; + var collection = new HashSet([new TestEntity { Id = Guid.NewGuid(), Name = "Existing" }]); + + // Act + entity.AddIfNotExists(collection); + + // Assert + collection.ShouldSatisfyAllConditions(() => + { + collection.Count.ShouldBe(2); + collection.ShouldContain(entity); + }); + } + + [TestMethod] + public void AddIfNotExists_WhenPreservingExistingEntityProperties_ShouldNotReplaceExistingEntity() + { + // Arrange + var entityId = Guid.NewGuid(); + var existingEntity = new TestEntity { Id = entityId, Name = "Original Name" }; + var newEntity = new TestEntity { Id = entityId, Name = "Updated Name" }; + var collection = new List { existingEntity }; + + // Act + newEntity.AddIfNotExists(collection); + + // Assert + collection.ShouldSatisfyAllConditions(() => + { + _ = collection.ShouldHaveSingleItem(); + collection[0].Name.ShouldBe("Original Name"); + collection[0].ShouldBe(existingEntity); + }); + } +} + diff --git a/InvoiceReminder.DomainEntities.UnitTests/Extensions/UserExtensionsTests.cs b/InvoiceReminder.DomainEntities.UnitTests/Extensions/UserExtensionsTests.cs index c0f8be1..38c707a 100644 --- a/InvoiceReminder.DomainEntities.UnitTests/Extensions/UserExtensionsTests.cs +++ b/InvoiceReminder.DomainEntities.UnitTests/Extensions/UserExtensionsTests.cs @@ -5,7 +5,7 @@ namespace InvoiceReminder.DomainEntities.UnitTests.Extensions; [TestClass] -public class UserExtensionsTests +public sealed class UserExtensionsTests { [TestMethod] public void Handle_NewUser_AddsUserToResult() @@ -435,16 +435,14 @@ public void Handle_ExistingUserWithSameScanEmailDefinition_DoesNotAddDuplicate() } [TestMethod] - public void Handle_NewUserWithNullCollections_InitializesCollections() + public void Handle_NewUserWithEmptyCollections_InitializesCollections() { // Arrange var result = new Dictionary(); var user = new User { Id = Guid.NewGuid(), - Name = "Test User", - JobSchedules = null, - ScanEmailDefinitions = null + Name = "Test User" }; var invoice = new Invoice { Id = Guid.NewGuid() }; var jobSchedule = new JobSchedule { Id = Guid.NewGuid() }; diff --git a/InvoiceReminder.DomainEntities.UnitTests/InvoiceReminder.DomainEntities.UnitTests.csproj b/InvoiceReminder.DomainEntities.UnitTests/InvoiceReminder.DomainEntities.UnitTests.csproj index dfaeee1..2c50dd8 100644 --- a/InvoiceReminder.DomainEntities.UnitTests/InvoiceReminder.DomainEntities.UnitTests.csproj +++ b/InvoiceReminder.DomainEntities.UnitTests/InvoiceReminder.DomainEntities.UnitTests.csproj @@ -1,18 +1,15 @@  - net9.0 - latest - enable - disable - cb5b3f1e-1e38-47a5-bcd4-457b0c7f1d0a - true + true + Exe + false - + @@ -25,8 +22,6 @@ - - all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/InvoiceReminder.ExternalServices.UnitTests/BarcodeReader/BarcodeReaderServiceTests.cs b/InvoiceReminder.ExternalServices.UnitTests/BarcodeReader/BarcodeReaderServiceTests.cs index 8b8ff92..95aa98a 100644 --- a/InvoiceReminder.ExternalServices.UnitTests/BarcodeReader/BarcodeReaderServiceTests.cs +++ b/InvoiceReminder.ExternalServices.UnitTests/BarcodeReader/BarcodeReaderServiceTests.cs @@ -11,7 +11,7 @@ namespace InvoiceReminder.ExternalServices.UnitTests.BarcodeReader; [TestClass] -public class BarcodeReaderServiceTests +public sealed class BarcodeReaderServiceTests { private readonly ILogger _logger; private readonly IInvoiceBarcodeHandler _barcodeHandler; diff --git a/InvoiceReminder.ExternalServices.UnitTests/InvoiceReminder.ExternalServices.UnitTests.csproj b/InvoiceReminder.ExternalServices.UnitTests/InvoiceReminder.ExternalServices.UnitTests.csproj index 6d07830..f7c68f8 100644 --- a/InvoiceReminder.ExternalServices.UnitTests/InvoiceReminder.ExternalServices.UnitTests.csproj +++ b/InvoiceReminder.ExternalServices.UnitTests/InvoiceReminder.ExternalServices.UnitTests.csproj @@ -1,16 +1,13 @@  - net9.0 - latest - enable - disable - cb5b3f1e-1e38-47a5-bcd4-457b0c7f1d0a - true + true + Exe + false @@ -27,8 +24,6 @@ - - all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/InvoiceReminder.ExternalServices.UnitTests/SendMessage/SendMessageServiceTests.cs b/InvoiceReminder.ExternalServices.UnitTests/SendMessage/SendMessageServiceTests.cs index 2fb893b..0276e4b 100644 --- a/InvoiceReminder.ExternalServices.UnitTests/SendMessage/SendMessageServiceTests.cs +++ b/InvoiceReminder.ExternalServices.UnitTests/SendMessage/SendMessageServiceTests.cs @@ -12,7 +12,7 @@ namespace InvoiceReminder.ExternalServices.UnitTests.SendMessage; [TestClass] -public class SendMessageServiceTests +public sealed class SendMessageServiceTests { private readonly ILogger _logger; private readonly IBarcodeReaderService _barcodeReader; @@ -112,18 +112,22 @@ public async Task SendMessage_Should_Log_Error_On_Exception() _ = _userRepository.GetByIdAsync(Arg.Any(), Arg.Any()) .Returns(Task.FromException(exception)); + _ = _logger.IsEnabled(Arg.Any()).Returns(true); + // Act && Assert _ = await Should.ThrowAsync(async () => await _sendMessageService.SendMessage(userId, TestContext.CancellationToken) ); - _logger.Received(1).Log( - LogLevel.Error, - Arg.Any(), - Arg.Is(o => o.ToString().Contains("User not found")), - exception, - Arg.Any>() - ); + if (_logger.IsEnabled(LogLevel.Error)) + { + var eventId = Arg.Any(); + var state = Arg.Is(o => o.ToString().Contains("User not found")); + var loggedException = Arg.Any(); + var formatter = Arg.Any>(); + + _logger.Received(1).Log(LogLevel.Error, eventId, state, loggedException, formatter); + } } [TestMethod] @@ -143,18 +147,22 @@ public async Task SendMessage_ShouldNot_SendMessages_When_UserHasNoAuthenticatio _ = _userRepository.GetByIdAsync(Arg.Any(), Arg.Any()) .Returns(Task.FromResult(user)); + _ = _logger.IsEnabled(Arg.Any()).Returns(true); + // Act var result = await _sendMessageService.SendMessage(userId, TestContext.CancellationToken); // Assert result.ShouldBe($"No Authentication Token found for userId: {userId}"); - _logger.Received(1).Log( - LogLevel.Warning, - Arg.Any(), - Arg.Is(o => o.ToString().Contains("No Authentication Token found")), - null, - Arg.Any>() - ); + if (_logger.IsEnabled(LogLevel.Warning)) + { + var eventId = Arg.Any(); + var state = Arg.Is(o => o.ToString().Contains("No Authentication Token found")); + var loggedException = Arg.Is(e => e == null); + var formatter = Arg.Any>(); + + _logger.Received(1).Log(LogLevel.Warning, eventId, state, loggedException, formatter); + } } } diff --git a/InvoiceReminder.ExternalServices/BackgroundServices/TelegramBotBackgroundService.cs b/InvoiceReminder.ExternalServices/BackgroundServices/TelegramBotBackgroundService.cs index ce2d421..3357974 100644 --- a/InvoiceReminder.ExternalServices/BackgroundServices/TelegramBotBackgroundService.cs +++ b/InvoiceReminder.ExternalServices/BackgroundServices/TelegramBotBackgroundService.cs @@ -97,16 +97,22 @@ public async Task HandleErrorAsync( { if (exception is ApiRequestException apiException) { - _logger.LogError( - exception, - "Telegram API Error: [{ErrorCode}] {Message}", - apiException.ErrorCode, - exception.Message - ); + if (_logger.IsEnabled(LogLevel.Error)) + { + _logger.LogError( + exception, + "Telegram API Error: [{ErrorCode}] {Message}", + apiException.ErrorCode, + exception.Message + ); + } } else { - _logger.LogError(exception, "Unexpected Error: {Message}", exception.Message); + if (_logger.IsEnabled(LogLevel.Error)) + { + _logger.LogError(exception, "Unexpected Error: {Message}", exception.Message); + } } await Task.CompletedTask; diff --git a/InvoiceReminder.ExternalServices/BarcodeReader/BarcodeReaderService.cs b/InvoiceReminder.ExternalServices/BarcodeReader/BarcodeReaderService.cs index 3f4538a..5b7971c 100644 --- a/InvoiceReminder.ExternalServices/BarcodeReader/BarcodeReaderService.cs +++ b/InvoiceReminder.ExternalServices/BarcodeReader/BarcodeReaderService.cs @@ -24,7 +24,10 @@ public Invoice ReadTextContentFromPdf(byte[] byteStream, string beneficiary, Inv { var exception = new ArgumentException("Empty document byte stream", nameof(byteStream)); - _logger.LogError(exception, "{Messagem}", exception.Message); + if (_logger.IsEnabled(LogLevel.Error)) + { + _logger.LogError(exception, "{Messagem}", exception.Message); + } throw exception; } diff --git a/InvoiceReminder.ExternalServices/Gmail/GoogleOAuthService.cs b/InvoiceReminder.ExternalServices/Gmail/GoogleOAuthService.cs index 82c03a5..5eea212 100644 --- a/InvoiceReminder.ExternalServices/Gmail/GoogleOAuthService.cs +++ b/InvoiceReminder.ExternalServices/Gmail/GoogleOAuthService.cs @@ -21,6 +21,7 @@ public class GoogleOAuthService : IGoogleOAuthService private readonly GoogleAuthorizationCodeFlow _flow; private readonly string _redirectUri; private readonly byte[] _key; + private const string AppKeysSection = "appKeys"; public GoogleOAuthService( IEmailAuthTokenRepository tokenRepository, @@ -32,17 +33,17 @@ public GoogleOAuthService( { ClientSecrets = new ClientSecrets { - ClientId = configurationService.GetSecret("appKeys", "googleOauthClientId"), - ClientSecret = configurationService.GetSecret("appKeys", "googleOauthClientSecret") + ClientId = configurationService.GetSecret(AppKeysSection, "googleOauthClientId"), + ClientSecret = configurationService.GetSecret(AppKeysSection, "googleOauthClientSecret") }, Scopes = [GmailService.Scope.GmailModify] }); - _key = Convert.FromBase64String(configurationService.GetSecret("appKeys", "tokenEncryptionKey")); + _key = Convert.FromBase64String(configurationService.GetSecret(AppKeysSection, "tokenEncryptionKey")); _logger = logger; _tokenRepository = tokenRepository; _unitOfWork = unitOfWork; - _redirectUri = configurationService.GetSecret("appKeys", "googleOauthRedirectUri"); + _redirectUri = configurationService.GetSecret(AppKeysSection, "googleOauthRedirectUri"); } public async Task AuthenticateAsync( @@ -112,7 +113,10 @@ public async Task> GrantAuthorizationAsync( } catch (Exception ex) { - _logger.LogError(ex, "Error granting authorization for user {UserId}", userId); + if (_logger.IsEnabled(LogLevel.Error)) + { + _logger.LogError(ex, "Error granting authorization for user {UserId}", userId); + } return Result.Failure("Error granting authorization for user"); } @@ -140,7 +144,10 @@ public async Task> RevokeAuthorizationAsync( } catch (Exception ex) { - _logger.LogError(ex, "Revoking Authorization Token failed: {Message}", ex.Message); + if (_logger.IsEnabled(LogLevel.Error)) + { + _logger.LogError(ex, "Revoking Authorization Token failed: {Message}", ex.Message); + } return Result.Failure("Error revoking Authorization Token."); } diff --git a/InvoiceReminder.ExternalServices/InvoiceReminder.ExternalServices.csproj b/InvoiceReminder.ExternalServices/InvoiceReminder.ExternalServices.csproj index a52251e..6b43a32 100644 --- a/InvoiceReminder.ExternalServices/InvoiceReminder.ExternalServices.csproj +++ b/InvoiceReminder.ExternalServices/InvoiceReminder.ExternalServices.csproj @@ -1,12 +1,5 @@ - - net9.0 - enable - disable - cb5b3f1e-1e38-47a5-bcd4-457b0c7f1d0a - - diff --git a/InvoiceReminder.ExternalServices/SendMessage/SendMessageService.cs b/InvoiceReminder.ExternalServices/SendMessage/SendMessageService.cs index a695138..9bd8a8a 100644 --- a/InvoiceReminder.ExternalServices/SendMessage/SendMessageService.cs +++ b/InvoiceReminder.ExternalServices/SendMessage/SendMessageService.cs @@ -15,6 +15,7 @@ public class SendMessageService : ISendMessageService private readonly ITelegramMessageService _telegramMessageService; private readonly IInvoiceRepository _invoiceRepository; private readonly IUserRepository _userRepository; + private const string LogExceptionMessage = "{ContextualInfo} - Exception: {Message}"; public SendMessageService( IBarcodeReaderService barcodeService, @@ -79,7 +80,10 @@ public async Task SendMessage(Guid userId, CancellationToken cancellatio var method = $"{nameof(SendMessageService)}.{nameof(SendMessage)}"; var contextualInfo = $"Method {method} execution was interrupted by a CancellationToken Request..."; - _logger.LogWarning(ex, "{ContextualInfo} - Exception: {Message}", contextualInfo, ex.Message); + if (_logger.IsEnabled(LogLevel.Warning)) + { + _logger.LogWarning(ex, LogExceptionMessage, contextualInfo, ex.Message); + } throw new OperationCanceledException(contextualInfo, ex, cancellationToken); } @@ -87,7 +91,10 @@ public async Task SendMessage(Guid userId, CancellationToken cancellatio { var contextualInfo = $"Error occurred while sending messages for userId: {userId}"; - _logger.LogError(ex, "{ContextualInfo} - Exception: {Message}", contextualInfo, ex.Message); + if (_logger.IsEnabled(LogLevel.Error)) + { + _logger.LogError(ex, LogExceptionMessage, contextualInfo, ex.Message); + } throw new InvalidOperationException(contextualInfo, ex); } @@ -103,7 +110,10 @@ public async Task SendMessage(Guid userId, CancellationToken cancellatio { message = $"User not found!"; - _logger.LogWarning("{Message}", message); + if (_logger.IsEnabled(LogLevel.Warning)) + { + _logger.LogWarning("{Message}", message); + } return (false, message); } @@ -112,7 +122,10 @@ public async Task SendMessage(Guid userId, CancellationToken cancellatio { message = $"No Authentication Token found for userId: {user.Id}"; - _logger.LogWarning("{Message}", message); + if (_logger.IsEnabled(LogLevel.Warning)) + { + _logger.LogWarning("{Message}", message); + } return (false, message); } diff --git a/InvoiceReminder.ExternalServices/Telegram/TelegramMessageService.cs b/InvoiceReminder.ExternalServices/Telegram/TelegramMessageService.cs index 4a14414..2b26055 100644 --- a/InvoiceReminder.ExternalServices/Telegram/TelegramMessageService.cs +++ b/InvoiceReminder.ExternalServices/Telegram/TelegramMessageService.cs @@ -31,11 +31,17 @@ public async Task SendMessageAsync(long chatId, string message, CancellationToke } catch (ApiRequestException ex) { - _logger.LogError(ex, "{Message}", $"Telegram API error: {ex.Message}"); + if (_logger.IsEnabled(LogLevel.Error)) + { + _logger.LogError(ex, "{Message}", $"Telegram API error: {ex.Message}"); + } } catch (Exception ex) { - _logger.LogError(ex, "{Message}", $"Unexpected error: {ex.Message}"); + if (_logger.IsEnabled(LogLevel.Error)) + { + _logger.LogError(ex, "{Message}", $"Unexpected error: {ex.Message}"); + } } } } diff --git a/InvoiceReminder.Infrastructure.UnitTests/Data/Repository/BaseRepositoryTests.cs b/InvoiceReminder.Infrastructure.UnitTests/Data/Repository/BaseRepositoryTests.cs index 0df5195..8b14a05 100644 --- a/InvoiceReminder.Infrastructure.UnitTests/Data/Repository/BaseRepositoryTests.cs +++ b/InvoiceReminder.Infrastructure.UnitTests/Data/Repository/BaseRepositoryTests.cs @@ -8,7 +8,7 @@ namespace InvoiceReminder.Infrastructure.UnitTests.Data.Repository; [TestClass] -public class BaseRepositoryTests +public sealed class BaseRepositoryTests { private readonly SqliteConnection _connection; private readonly DbContextOptions _contextOptions; diff --git a/InvoiceReminder.Infrastructure.UnitTests/Data/Repository/UnitOfWorkTests.cs b/InvoiceReminder.Infrastructure.UnitTests/Data/Repository/UnitOfWorkTests.cs index eb953dd..ccae27a 100644 --- a/InvoiceReminder.Infrastructure.UnitTests/Data/Repository/UnitOfWorkTests.cs +++ b/InvoiceReminder.Infrastructure.UnitTests/Data/Repository/UnitOfWorkTests.cs @@ -12,7 +12,7 @@ namespace InvoiceReminder.Infrastructure.UnitTests.Data.Repository { [TestClass] - public class UnitOfWorkTests + public sealed class UnitOfWorkTests { private readonly SqliteConnection _connection; private readonly DbContextOptions _contextOptions; @@ -67,6 +67,7 @@ public async Task SaveChangesAsync_Should_RollbackTransaction_LogError_AndThrowD var unitOfWork = CreateUnitOfWork(context); _ = context.Users.Add(new User { Id = Guid.NewGuid() }); + _ = _logger.IsEnabled(Arg.Any()).Returns(true); // Act var dataLayerException = await Should.ThrowAsync( @@ -76,13 +77,15 @@ public async Task SaveChangesAsync_Should_RollbackTransaction_LogError_AndThrowD // Assert context.Database.GetDbConnection().State.ShouldBe(ConnectionState.Closed); - _logger.Received(1).Log( - LogLevel.Error, - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any>() - ); + if (_logger.IsEnabled(LogLevel.Error)) + { + var eventId = Arg.Any(); + var state = Arg.Any(); + var exception = Arg.Any(); + var formatter = Arg.Any>(); + + _logger.Received(1).Log(LogLevel.Error, eventId, state, exception, formatter); + } _ = dataLayerException.ShouldNotBeNull(); _ = dataLayerException.InnerException.ShouldBeOfType(); diff --git a/InvoiceReminder.Infrastructure.UnitTests/InvoiceReminder.Infrastructure.UnitTests.csproj b/InvoiceReminder.Infrastructure.UnitTests/InvoiceReminder.Infrastructure.UnitTests.csproj index ecca0d4..4ef0b10 100644 --- a/InvoiceReminder.Infrastructure.UnitTests/InvoiceReminder.Infrastructure.UnitTests.csproj +++ b/InvoiceReminder.Infrastructure.UnitTests/InvoiceReminder.Infrastructure.UnitTests.csproj @@ -1,15 +1,13 @@  - net9.0 - latest - enable - disable - true + true + Exe + false @@ -31,8 +29,6 @@ - - all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/InvoiceReminder.JobScheduler.UnitTests/HostedService/QuartzHostedServiceTests.cs b/InvoiceReminder.JobScheduler.UnitTests/HostedService/QuartzHostedServiceTests.cs index 79e1a65..f0fdfd1 100644 --- a/InvoiceReminder.JobScheduler.UnitTests/HostedService/QuartzHostedServiceTests.cs +++ b/InvoiceReminder.JobScheduler.UnitTests/HostedService/QuartzHostedServiceTests.cs @@ -9,7 +9,7 @@ namespace InvoiceReminder.JobScheduler.UnitTests.HostedService; [TestClass] -public class QuartzHostedServiceTests +public sealed class QuartzHostedServiceTests { private readonly ILogger _logger; private readonly ISchedulerFactory _schedulerFactory; @@ -185,18 +185,22 @@ public async Task StartAsync_InvalidCronExpression_ShouldLogError() var service = new QuartzHostedService(_logger, _schedulerFactory, _jobFactory, schedules); + _ = _logger.IsEnabled(Arg.Any()).Returns(true); + // Act && Assert await service.StartAsync(TestContext.CancellationToken); _ = _scheduler.DidNotReceive().ScheduleJob(Arg.Any(), Arg.Any(), Arg.Any()); - _logger.Received(1).Log( - LogLevel.Error, - Arg.Any(), - Arg.Is(o => o.ToString().Contains("CronJob inválido:")), - Arg.Any(), - Arg.Any>() - ); + if (_logger.IsEnabled(LogLevel.Error)) + { + var eventId = Arg.Any(); + var state = Arg.Is(o => o.ToString().Contains("CronJob inválido:")); + var loggedException = Arg.Is(e => e == null); + var formatter = Arg.Any>(); + + _logger.Received(1).Log(LogLevel.Error, eventId, state, loggedException, formatter); + } _scheduler.Received(1).JobFactory = Arg.Is(_jobFactory); } diff --git a/InvoiceReminder.JobScheduler.UnitTests/InvoiceReminder.JobScheduler.UnitTests.csproj b/InvoiceReminder.JobScheduler.UnitTests/InvoiceReminder.JobScheduler.UnitTests.csproj index 4fb042f..618eb11 100644 --- a/InvoiceReminder.JobScheduler.UnitTests/InvoiceReminder.JobScheduler.UnitTests.csproj +++ b/InvoiceReminder.JobScheduler.UnitTests/InvoiceReminder.JobScheduler.UnitTests.csproj @@ -1,15 +1,13 @@  - net9.0 - latest - enable - disable - true + true + Exe + false @@ -26,8 +24,6 @@ - - all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/InvoiceReminder.JobScheduler.UnitTests/JobSettings/CronJobTests.cs b/InvoiceReminder.JobScheduler.UnitTests/JobSettings/CronJobTests.cs index 3e506b0..991cdc4 100644 --- a/InvoiceReminder.JobScheduler.UnitTests/JobSettings/CronJobTests.cs +++ b/InvoiceReminder.JobScheduler.UnitTests/JobSettings/CronJobTests.cs @@ -9,7 +9,7 @@ namespace InvoiceReminder.JobScheduler.UnitTests.JobSettings; [TestClass] -public class CronJobTests +public sealed class CronJobTests { private readonly ILogger _logger; private readonly IServiceScopeFactory _serviceScopeFactory; @@ -49,6 +49,8 @@ public async Task Execute_ShouldCreateScopeResolveServiceAndSendMessage() // Arrange var cronJob = new CronJob(_logger, _serviceScopeFactory); + _ = _logger.IsEnabled(Arg.Any()).Returns(true); + // Act await cronJob.Execute(_jobExecutionContext); @@ -57,12 +59,14 @@ public async Task Execute_ShouldCreateScopeResolveServiceAndSendMessage() _ = _sendMessageService.Received(1).SendMessage(Arg.Any(), Arg.Any()); - _logger.ReceivedWithAnyArgs(1).Log( - LogLevel.Information, - Arg.Any(), - Arg.Any(), - null, - Arg.Any>()); + if (_logger.IsEnabled(LogLevel.Information)) + { + var eventId = Arg.Any(); + var state = Arg.Any(); + var formatter = Arg.Any>(); + + _logger.ReceivedWithAnyArgs(1).Log(LogLevel.Information, eventId, state, null, formatter); + } } [TestMethod] @@ -95,6 +99,8 @@ public async Task Execute_SendMessageFails_ShouldNotThrowExceptionAndStillLog() _ = _sendMessageService.SendMessage(Arg.Any(), Arg.Any()) .Returns("Total messages sent: 0"); + _ = _logger.IsEnabled(Arg.Any()).Returns(true); + // Act await cronJob.Execute(_jobExecutionContext); @@ -102,11 +108,14 @@ public async Task Execute_SendMessageFails_ShouldNotThrowExceptionAndStillLog() _ = _serviceScopeFactory.Received(1).CreateScope(); _ = _sendMessageService.Received(1).SendMessage(Arg.Any(), Arg.Any()); - _logger.Received(1).Log( - LogLevel.Information, - Arg.Any(), - Arg.Is(o => o.ToString().Contains("Test Job Description triggered...")), - null, - Arg.Any>()); + if (_logger.IsEnabled(LogLevel.Information)) + { + var eventId = Arg.Any(); + var state = Arg.Is(o => o.ToString().Contains("Test Job Description triggered...")); + var loggedException = Arg.Is(e => e == null); + var formatter = Arg.Any>(); + + _logger.Received(1).Log(LogLevel.Information, eventId, state, loggedException, formatter); + } } } diff --git a/InvoiceReminder.JobScheduler/HostedService/QuartzHostedService.cs b/InvoiceReminder.JobScheduler/HostedService/QuartzHostedService.cs index d000d85..1a1f60e 100644 --- a/InvoiceReminder.JobScheduler/HostedService/QuartzHostedService.cs +++ b/InvoiceReminder.JobScheduler/HostedService/QuartzHostedService.cs @@ -43,7 +43,11 @@ public async Task StartAsync(CancellationToken cancellationToken) { if (!CronExpression.IsValidExpression(schedule.CronExpression)) { - _logger.LogError("CronJob inválido: {JobId}", schedule.Id); + if (_logger.IsEnabled(LogLevel.Error)) + { + _logger.LogError("CronJob inválido: {JobId}", schedule.Id); + } + continue; } diff --git a/InvoiceReminder.JobScheduler/InvoiceReminder.JobScheduler.csproj b/InvoiceReminder.JobScheduler/InvoiceReminder.JobScheduler.csproj index f47131c..dbcf503 100644 --- a/InvoiceReminder.JobScheduler/InvoiceReminder.JobScheduler.csproj +++ b/InvoiceReminder.JobScheduler/InvoiceReminder.JobScheduler.csproj @@ -1,11 +1,5 @@ - - net9.0 - enable - disable - - diff --git a/InvoiceReminder.JobScheduler/JobSettings/CronJob.cs b/InvoiceReminder.JobScheduler/JobSettings/CronJob.cs index 397cb86..0291503 100644 --- a/InvoiceReminder.JobScheduler/JobSettings/CronJob.cs +++ b/InvoiceReminder.JobScheduler/JobSettings/CronJob.cs @@ -25,7 +25,11 @@ public async Task Execute(IJobExecutionContext context) var service = scope.ServiceProvider.GetRequiredService(); var message = $"{DateTime.Now:HH:mm:ss} - {context.JobDetail.Description} triggered..."; - _logger.LogInformation("{Message}", message); + if (_logger.IsEnabled(LogLevel.Information)) + { + _logger.LogInformation("{Message}", message); + } + // possível uso de um agendamento de envio de mensagem para lembrete no dia do vencimento?... _ = await service.SendMessage(id); } diff --git a/InvoiceReminder.UnitTests.Assets/InvoiceReminder.UnitTests.Assets.csproj b/InvoiceReminder.UnitTests.Assets/InvoiceReminder.UnitTests.Assets.csproj index a691e82..6454eea 100644 --- a/InvoiceReminder.UnitTests.Assets/InvoiceReminder.UnitTests.Assets.csproj +++ b/InvoiceReminder.UnitTests.Assets/InvoiceReminder.UnitTests.Assets.csproj @@ -1,11 +1,5 @@  - - net9.0 - enable - disable - - diff --git a/InvoiceReminder.sln b/InvoiceReminder.sln index 1629398..6997efe 100644 --- a/InvoiceReminder.sln +++ b/InvoiceReminder.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.13.35931.197 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11222.15 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InvoiceReminder.API", "InvoiceReminder.API\InvoiceReminder.API.csproj", "{CA4D17D7-211B-4FFA-ADDF-FD8C1890708B}" EndProject @@ -62,6 +62,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Itens de Solução", "Itens .editorconfig = .editorconfig .env = .env CodeCoverage.runsettings = CodeCoverage.runsettings + Directory.Build.props = Directory.Build.props Directory.Packages.props = Directory.Packages.props docker-compose.yml = docker-compose.yml EndProjectSection From b392e76a454c95c857707f3f8f3a73e3e32a4d3a Mon Sep 17 00:00:00 2001 From: "Jefferson L. da Silva" Date: Thu, 4 Dec 2025 22:06:04 -0300 Subject: [PATCH 2/3] Removes unnecessary logger checks in unit tests Simplifies unit tests by removing redundant checks for logger enablement. This streamlines the tests and makes them more readable without affecting their functionality. --- .../Extensions/EntityExtensions.cs | 34 +++++++++++++++++-- .../SendMessage/SendMessageServiceTests.cs | 26 ++++++-------- .../Data/Repository/UnitOfWorkTests.cs | 17 ++++------ .../HostedService/QuartzHostedServiceTests.cs | 16 ++++----- .../JobSettings/CronJobTests.cs | 26 ++++++-------- 5 files changed, 64 insertions(+), 55 deletions(-) diff --git a/InvoiceReminder.Domain/Extensions/EntityExtensions.cs b/InvoiceReminder.Domain/Extensions/EntityExtensions.cs index 37005a1..6e592dc 100644 --- a/InvoiceReminder.Domain/Extensions/EntityExtensions.cs +++ b/InvoiceReminder.Domain/Extensions/EntityExtensions.cs @@ -9,14 +9,42 @@ public static void AddIfNotExists(this T entity, ICollection collection) w return; } - if (collection.FirstOrDefault(e => GetId(e).Equals(GetId(entity))) is null) + if (collection is HashSet hashSet) + { + _ = hashSet.Add(entity); + + return; + } + + if (collection.FirstOrDefault(e => EntityIdComparer.GetId(e).Equals(EntityIdComparer.GetId(entity))) is null) { collection.Add(entity); } } +} - private static Guid GetId(T entity) where T : class +internal sealed class EntityIdComparer : IEqualityComparer where T : class +{ + public bool Equals(T x, T y) { - return (Guid)typeof(T).GetProperty("Id")!.GetValue(entity)!; + return !(x == null || y == null) && GetId(x).Equals(GetId(y)); + } + + public int GetHashCode(T obj) + { + return obj == null ? 0 : GetId(obj).GetHashCode(); + } + + public static Guid GetId(T entity) + { + var property = typeof(T).GetProperty("Id") + ?? throw new InvalidOperationException($"Type {typeof(T).Name} does not have an 'Id' property."); + + var value = property.GetValue(entity) + ?? throw new InvalidOperationException($"The 'Id' property of {typeof(T).Name} is null."); + + return value is not Guid guidValue + ? throw new InvalidOperationException($"The 'Id' property of {typeof(T).Name} is not of type Guid.") + : guidValue; } } diff --git a/InvoiceReminder.ExternalServices.UnitTests/SendMessage/SendMessageServiceTests.cs b/InvoiceReminder.ExternalServices.UnitTests/SendMessage/SendMessageServiceTests.cs index 0276e4b..8dafc5c 100644 --- a/InvoiceReminder.ExternalServices.UnitTests/SendMessage/SendMessageServiceTests.cs +++ b/InvoiceReminder.ExternalServices.UnitTests/SendMessage/SendMessageServiceTests.cs @@ -119,15 +119,12 @@ public async Task SendMessage_Should_Log_Error_On_Exception() await _sendMessageService.SendMessage(userId, TestContext.CancellationToken) ); - if (_logger.IsEnabled(LogLevel.Error)) - { - var eventId = Arg.Any(); - var state = Arg.Is(o => o.ToString().Contains("User not found")); - var loggedException = Arg.Any(); - var formatter = Arg.Any>(); + var eventId = Arg.Any(); + var state = Arg.Is(o => o.ToString().Contains("User not found")); + var loggedException = Arg.Any(); + var formatter = Arg.Any>(); - _logger.Received(1).Log(LogLevel.Error, eventId, state, loggedException, formatter); - } + _logger.Received(1).Log(LogLevel.Error, eventId, state, loggedException, formatter); } [TestMethod] @@ -155,14 +152,11 @@ public async Task SendMessage_ShouldNot_SendMessages_When_UserHasNoAuthenticatio // Assert result.ShouldBe($"No Authentication Token found for userId: {userId}"); - if (_logger.IsEnabled(LogLevel.Warning)) - { - var eventId = Arg.Any(); - var state = Arg.Is(o => o.ToString().Contains("No Authentication Token found")); - var loggedException = Arg.Is(e => e == null); - var formatter = Arg.Any>(); + var eventId = Arg.Any(); + var state = Arg.Is(o => o.ToString().Contains("No Authentication Token found")); + var loggedException = Arg.Is(e => e == null); + var formatter = Arg.Any>(); - _logger.Received(1).Log(LogLevel.Warning, eventId, state, loggedException, formatter); - } + _logger.Received(1).Log(LogLevel.Warning, eventId, state, loggedException, formatter); } } diff --git a/InvoiceReminder.Infrastructure.UnitTests/Data/Repository/UnitOfWorkTests.cs b/InvoiceReminder.Infrastructure.UnitTests/Data/Repository/UnitOfWorkTests.cs index ccae27a..a74c583 100644 --- a/InvoiceReminder.Infrastructure.UnitTests/Data/Repository/UnitOfWorkTests.cs +++ b/InvoiceReminder.Infrastructure.UnitTests/Data/Repository/UnitOfWorkTests.cs @@ -77,19 +77,16 @@ public async Task SaveChangesAsync_Should_RollbackTransaction_LogError_AndThrowD // Assert context.Database.GetDbConnection().State.ShouldBe(ConnectionState.Closed); - if (_logger.IsEnabled(LogLevel.Error)) - { - var eventId = Arg.Any(); - var state = Arg.Any(); - var exception = Arg.Any(); - var formatter = Arg.Any>(); - - _logger.Received(1).Log(LogLevel.Error, eventId, state, exception, formatter); - } - _ = dataLayerException.ShouldNotBeNull(); _ = dataLayerException.InnerException.ShouldBeOfType(); dataLayerException.Message.ShouldContain("Exception raised while saving changes"); + + var eventId = Arg.Any(); + var state = Arg.Any(); + var exception = Arg.Any(); + var formatter = Arg.Any>(); + + _logger.Received(1).Log(LogLevel.Error, eventId, state, exception, formatter); } [TestMethod] diff --git a/InvoiceReminder.JobScheduler.UnitTests/HostedService/QuartzHostedServiceTests.cs b/InvoiceReminder.JobScheduler.UnitTests/HostedService/QuartzHostedServiceTests.cs index f0fdfd1..6655524 100644 --- a/InvoiceReminder.JobScheduler.UnitTests/HostedService/QuartzHostedServiceTests.cs +++ b/InvoiceReminder.JobScheduler.UnitTests/HostedService/QuartzHostedServiceTests.cs @@ -182,7 +182,6 @@ public async Task StartAsync_InvalidCronExpression_ShouldLogError() // Arrange var schedule = new JobSchedule { Id = Guid.NewGuid(), CronExpression = "invalid cron", UserId = Guid.NewGuid() }; var schedules = new List { schedule }; - var service = new QuartzHostedService(_logger, _schedulerFactory, _jobFactory, schedules); _ = _logger.IsEnabled(Arg.Any()).Returns(true); @@ -192,17 +191,14 @@ public async Task StartAsync_InvalidCronExpression_ShouldLogError() _ = _scheduler.DidNotReceive().ScheduleJob(Arg.Any(), Arg.Any(), Arg.Any()); - if (_logger.IsEnabled(LogLevel.Error)) - { - var eventId = Arg.Any(); - var state = Arg.Is(o => o.ToString().Contains("CronJob inválido:")); - var loggedException = Arg.Is(e => e == null); - var formatter = Arg.Any>(); + _scheduler.Received(1).JobFactory = Arg.Is(_jobFactory); - _logger.Received(1).Log(LogLevel.Error, eventId, state, loggedException, formatter); - } + var eventId = Arg.Any(); + var state = Arg.Is(o => o.ToString().Contains("CronJob inválido:")); + var loggedException = Arg.Is(e => e == null); + var formatter = Arg.Any>(); - _scheduler.Received(1).JobFactory = Arg.Is(_jobFactory); + _logger.Received(1).Log(LogLevel.Error, eventId, state, loggedException, formatter); } [TestMethod] diff --git a/InvoiceReminder.JobScheduler.UnitTests/JobSettings/CronJobTests.cs b/InvoiceReminder.JobScheduler.UnitTests/JobSettings/CronJobTests.cs index 991cdc4..35e930f 100644 --- a/InvoiceReminder.JobScheduler.UnitTests/JobSettings/CronJobTests.cs +++ b/InvoiceReminder.JobScheduler.UnitTests/JobSettings/CronJobTests.cs @@ -59,14 +59,11 @@ public async Task Execute_ShouldCreateScopeResolveServiceAndSendMessage() _ = _sendMessageService.Received(1).SendMessage(Arg.Any(), Arg.Any()); - if (_logger.IsEnabled(LogLevel.Information)) - { - var eventId = Arg.Any(); - var state = Arg.Any(); - var formatter = Arg.Any>(); - - _logger.ReceivedWithAnyArgs(1).Log(LogLevel.Information, eventId, state, null, formatter); - } + var eventId = Arg.Any(); + var state = Arg.Any(); + var formatter = Arg.Any>(); + + _logger.ReceivedWithAnyArgs(1).Log(LogLevel.Information, eventId, state, null, formatter); } [TestMethod] @@ -108,14 +105,11 @@ public async Task Execute_SendMessageFails_ShouldNotThrowExceptionAndStillLog() _ = _serviceScopeFactory.Received(1).CreateScope(); _ = _sendMessageService.Received(1).SendMessage(Arg.Any(), Arg.Any()); - if (_logger.IsEnabled(LogLevel.Information)) - { - var eventId = Arg.Any(); - var state = Arg.Is(o => o.ToString().Contains("Test Job Description triggered...")); - var loggedException = Arg.Is(e => e == null); - var formatter = Arg.Any>(); + var eventId = Arg.Any(); + var state = Arg.Is(o => o.ToString().Contains("Test Job Description triggered...")); + var loggedException = Arg.Is(e => e == null); + var formatter = Arg.Any>(); - _logger.Received(1).Log(LogLevel.Information, eventId, state, loggedException, formatter); - } + _logger.Received(1).Log(LogLevel.Information, eventId, state, loggedException, formatter); } } From 63d228de95d0c26c12ff47189b7213a6ad5a9a8a Mon Sep 17 00:00:00 2001 From: "Jefferson L. da Silva" Date: Thu, 4 Dec 2025 22:18:14 -0300 Subject: [PATCH 3/3] Avoids adding duplicate entities to collections Ensures that duplicate entities are not added to collections by checking for existing entities with the same ID. This prevents potential data integrity issues and improves the reliability of the application when dealing with collections of entities. --- InvoiceReminder.Domain/Extensions/EntityExtensions.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/InvoiceReminder.Domain/Extensions/EntityExtensions.cs b/InvoiceReminder.Domain/Extensions/EntityExtensions.cs index 6e592dc..d7ec13f 100644 --- a/InvoiceReminder.Domain/Extensions/EntityExtensions.cs +++ b/InvoiceReminder.Domain/Extensions/EntityExtensions.cs @@ -11,12 +11,15 @@ public static void AddIfNotExists(this T entity, ICollection collection) w if (collection is HashSet hashSet) { - _ = hashSet.Add(entity); + if (!hashSet.Any(e => EntityIdComparer.GetId(e).Equals(EntityIdComparer.GetId(entity)))) + { + _ = hashSet.Add(entity); + } return; } - if (collection.FirstOrDefault(e => EntityIdComparer.GetId(e).Equals(EntityIdComparer.GetId(entity))) is null) + if (!collection.Any(e => EntityIdComparer.GetId(e).Equals(EntityIdComparer.GetId(entity)))) { collection.Add(entity); }