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