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..d7ec13f --- /dev/null +++ b/InvoiceReminder.Domain/Extensions/EntityExtensions.cs @@ -0,0 +1,53 @@ +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 is HashSet hashSet) + { + if (!hashSet.Any(e => EntityIdComparer.GetId(e).Equals(EntityIdComparer.GetId(entity)))) + { + _ = hashSet.Add(entity); + } + + return; + } + + if (!collection.Any(e => EntityIdComparer.GetId(e).Equals(EntityIdComparer.GetId(entity)))) + { + collection.Add(entity); + } + } +} + +internal sealed class EntityIdComparer : IEqualityComparer where T : class +{ + public bool Equals(T x, T y) + { + 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.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..8dafc5c 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,19 @@ 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>() - ); + 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 +144,19 @@ 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>() - ); + 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..a74c583 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,17 +77,16 @@ 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>() - ); - _ = 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.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..6655524 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; @@ -182,23 +182,23 @@ 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); + // 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>() - ); - _scheduler.Received(1).JobFactory = Arg.Is(_jobFactory); + + 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); } [TestMethod] 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..35e930f 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,11 @@ 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>()); + 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 +96,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 +105,11 @@ 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>()); + 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