diff --git a/Directory.Build.props b/Directory.Build.props
index 7be86f068..0678a494d 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -6,6 +6,7 @@
net9.0
enable
enable
+ false
false
@@ -61,6 +62,10 @@
$(NoWarn);CA2213
$(NoWarn);CA1816
+
+ $(NoWarn);CA1016
+ $(NoWarn);S3904
+
$(NoWarn);CA1304
$(NoWarn);CA1305
diff --git a/README.md b/README.md
index 408477efe..1a238d658 100644
--- a/README.md
+++ b/README.md
@@ -170,12 +170,18 @@ MeAjudaAi/
│ ├── Bootstrapper/ # API service bootstrapper
│ │ └── MeAjudaAi.ApiService/ # Ponto de entrada da API
│ ├── Modules/ # Módulos de domínio
-│ │ └── Users/ # Módulo de usuários
+│ │ ├── Users/ # Módulo de usuários
+│ │ │ ├── API/ # Endpoints e controllers
+│ │ │ ├── Application/ # Use cases e handlers CQRS
+│ │ │ ├── Domain/ # Entidades, value objects, eventos
+│ │ │ ├── Infrastructure/ # Persistência e serviços externos
+│ │ │ └── Tests/ # Testes do módulo
+│ │ └── Providers/ # Módulo de prestadores
│ │ ├── API/ # Endpoints e controllers
│ │ ├── Application/ # Use cases e handlers CQRS
│ │ ├── Domain/ # Entidades, value objects, eventos
-│ │ ├── Infrastructure/ # Persistência e serviços externos
-│ │ └── Tests/ # Testes do módulo
+│ │ ├── Infrastructure/ # Persistência e event handlers
+│ │ └── Tests/ # Testes unitários e integração
│ └── Shared/ # Componentes compartilhados
│ └── MeAjudaAi.Shared/ # Abstrações e utilities
├── tests/ # Testes de integração
@@ -193,6 +199,12 @@ MeAjudaAi/
- **Features**: Registro, login, perfis, papéis (cliente, prestador, admin)
- **Integração**: Keycloak para autenticação OAuth2/OIDC
+### 🏢 Módulo Providers
+- **Domain**: Gestão de prestadores de serviços e verificação
+- **Features**: Cadastro, perfis empresariais, documentos, qualificações, status de verificação
+- **Eventos**: Sistema completo de eventos de domínio e integração para comunicação inter-modular
+- **Arquitetura**: Clean Architecture com CQRS, DDD e event-driven design
+
### 🔮 Módulos Futuros
- **Services**: Catálogo de serviços e categorias
- **Bookings**: Agendamentos e reservas
@@ -251,6 +263,21 @@ dotnet test src/Modules/Users/Tests/
- **Value Objects**: Para conceitos de domínio imutáveis
- **Aggregates**: Para consistência transacional
+#### Implementação de Eventos - Módulo Providers
+
+O módulo Providers implementa um sistema completo de eventos para comunicação inter-modular:
+
+**Domain Events:**
+- `ProviderRegisteredDomainEvent` - Novo prestador cadastrado
+- `ProviderDeletedDomainEvent` - Prestador removido do sistema
+- `ProviderVerificationStatusUpdatedDomainEvent` - Status de verificação alterado
+- `ProviderProfileUpdatedDomainEvent` - Perfil do prestador atualizado
+
+**Integration Events:**
+- Conversão automática via Domain Event Handlers
+- Publicação em message bus para outros módulos
+- Suporte completo a event sourcing e auditoria
+
### Estrutura de Commits
```bash
diff --git a/src/Bootstrapper/MeAjudaAi.ApiService/Middlewares/RateLimitingMiddleware.cs b/src/Bootstrapper/MeAjudaAi.ApiService/Middlewares/RateLimitingMiddleware.cs
index 358767775..35aeb1dd3 100644
--- a/src/Bootstrapper/MeAjudaAi.ApiService/Middlewares/RateLimitingMiddleware.cs
+++ b/src/Bootstrapper/MeAjudaAi.ApiService/Middlewares/RateLimitingMiddleware.cs
@@ -30,11 +30,18 @@ private sealed class Counter
}
public async Task InvokeAsync(HttpContext context)
{
+ var currentOptions = options.CurrentValue;
+
+ // Bypass rate limiting if explicitly disabled
+ if (!currentOptions.General.Enabled)
+ {
+ await next(context);
+ return;
+ }
+
var clientIp = GetClientIpAddress(context);
var isAuthenticated = context.User.Identity?.IsAuthenticated == true;
- var currentOptions = options.CurrentValue;
-
// Check IP whitelist first - bypass rate limiting if IP is whitelisted
if (currentOptions.General.EnableIpWhitelist &&
currentOptions.General.WhitelistedIps.Contains(clientIp))
diff --git a/src/Bootstrapper/MeAjudaAi.ApiService/Options/RateLimitOptions.cs b/src/Bootstrapper/MeAjudaAi.ApiService/Options/RateLimitOptions.cs
index 041c64834..f5552a51a 100644
--- a/src/Bootstrapper/MeAjudaAi.ApiService/Options/RateLimitOptions.cs
+++ b/src/Bootstrapper/MeAjudaAi.ApiService/Options/RateLimitOptions.cs
@@ -67,6 +67,7 @@ public class RoleLimits
public class GeneralSettings
{
+ public bool Enabled { get; set; } = true;
[Range(1, 86400)] public int WindowInSeconds { get; set; } = 60;
public bool EnableIpWhitelist { get; set; } = false;
public List WhitelistedIps { get; set; } = [];
diff --git a/src/Modules/Providers/API/Extensions.cs b/src/Modules/Providers/API/Extensions.cs
index 154193579..d6db04f33 100644
--- a/src/Modules/Providers/API/Extensions.cs
+++ b/src/Modules/Providers/API/Extensions.cs
@@ -5,6 +5,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace MeAjudaAi.Modules.Providers.API;
@@ -54,6 +55,13 @@ private static void EnsureDatabaseMigrations(WebApplication app)
var context = scope.ServiceProvider.GetService();
if (context == null) return;
+ // Em ambiente de teste E2E, pular migrações automáticas - elas são gerenciadas pelo TestContainer
+ if (app.Environment.IsEnvironment("Test") || app.Environment.IsEnvironment("Testing"))
+ {
+ return;
+ }
+
+ // Em produção, usar migrações normais
context.Database.Migrate();
}
catch (Exception ex)
diff --git a/src/Modules/Providers/Application/Handlers/Commands/AddDocumentCommandHandler.cs b/src/Modules/Providers/Application/Handlers/Commands/AddDocumentCommandHandler.cs
index 2be818a30..5317052a0 100644
--- a/src/Modules/Providers/Application/Handlers/Commands/AddDocumentCommandHandler.cs
+++ b/src/Modules/Providers/Application/Handlers/Commands/AddDocumentCommandHandler.cs
@@ -14,7 +14,7 @@ namespace MeAjudaAi.Modules.Providers.Application.Handlers.Commands;
///
/// Repositório para acesso aos dados
/// Logger estruturado
-internal sealed class AddDocumentCommandHandler(
+public sealed class AddDocumentCommandHandler(
IProviderRepository providerRepository,
ILogger logger
) : ICommandHandler>
diff --git a/src/Modules/Providers/Application/Handlers/Commands/AddQualificationCommandHandler.cs b/src/Modules/Providers/Application/Handlers/Commands/AddQualificationCommandHandler.cs
index a3353bc3b..6550e4a60 100644
--- a/src/Modules/Providers/Application/Handlers/Commands/AddQualificationCommandHandler.cs
+++ b/src/Modules/Providers/Application/Handlers/Commands/AddQualificationCommandHandler.cs
@@ -14,7 +14,7 @@ namespace MeAjudaAi.Modules.Providers.Application.Handlers.Commands;
///
/// Repositório para acesso aos dados
/// Logger estruturado
-internal sealed class AddQualificationCommandHandler(
+public sealed class AddQualificationCommandHandler(
IProviderRepository providerRepository,
ILogger logger
) : ICommandHandler>
diff --git a/src/Modules/Providers/Application/Handlers/Commands/CreateProviderCommandHandler.cs b/src/Modules/Providers/Application/Handlers/Commands/CreateProviderCommandHandler.cs
index 53e1ce6c6..b38b70af2 100644
--- a/src/Modules/Providers/Application/Handlers/Commands/CreateProviderCommandHandler.cs
+++ b/src/Modules/Providers/Application/Handlers/Commands/CreateProviderCommandHandler.cs
@@ -18,7 +18,7 @@ namespace MeAjudaAi.Modules.Providers.Application.Handlers.Commands;
///
/// Repositório para persistência de prestadores de serviços
/// Logger estruturado para auditoria e debugging
-internal sealed class CreateProviderCommandHandler(
+public sealed class CreateProviderCommandHandler(
IProviderRepository providerRepository,
ILogger logger
) : ICommandHandler>
diff --git a/src/Modules/Providers/Application/Handlers/Commands/DeleteProviderCommandHandler.cs b/src/Modules/Providers/Application/Handlers/Commands/DeleteProviderCommandHandler.cs
index 0797792d3..1c5455ee0 100644
--- a/src/Modules/Providers/Application/Handlers/Commands/DeleteProviderCommandHandler.cs
+++ b/src/Modules/Providers/Application/Handlers/Commands/DeleteProviderCommandHandler.cs
@@ -14,7 +14,7 @@ namespace MeAjudaAi.Modules.Providers.Application.Handlers.Commands;
/// Repositório para acesso aos dados
/// Provedor de data/hora para auditoria
/// Logger estruturado
-internal sealed class DeleteProviderCommandHandler(
+public sealed class DeleteProviderCommandHandler(
IProviderRepository providerRepository,
IDateTimeProvider dateTimeProvider,
ILogger logger
diff --git a/src/Modules/Providers/Application/Handlers/Commands/RemoveDocumentCommandHandler.cs b/src/Modules/Providers/Application/Handlers/Commands/RemoveDocumentCommandHandler.cs
index fffc0c74d..be53aae62 100644
--- a/src/Modules/Providers/Application/Handlers/Commands/RemoveDocumentCommandHandler.cs
+++ b/src/Modules/Providers/Application/Handlers/Commands/RemoveDocumentCommandHandler.cs
@@ -14,7 +14,7 @@ namespace MeAjudaAi.Modules.Providers.Application.Handlers.Commands;
///
/// Repositório para acesso aos dados
/// Logger estruturado
-internal sealed class RemoveDocumentCommandHandler(
+public sealed class RemoveDocumentCommandHandler(
IProviderRepository providerRepository,
ILogger logger
) : ICommandHandler>
diff --git a/src/Modules/Providers/Application/Handlers/Commands/RemoveQualificationCommandHandler.cs b/src/Modules/Providers/Application/Handlers/Commands/RemoveQualificationCommandHandler.cs
index 18483ad45..9db55d90b 100644
--- a/src/Modules/Providers/Application/Handlers/Commands/RemoveQualificationCommandHandler.cs
+++ b/src/Modules/Providers/Application/Handlers/Commands/RemoveQualificationCommandHandler.cs
@@ -14,7 +14,7 @@ namespace MeAjudaAi.Modules.Providers.Application.Handlers.Commands;
///
/// Repositório para acesso aos dados
/// Logger estruturado
-internal sealed class RemoveQualificationCommandHandler(
+public sealed class RemoveQualificationCommandHandler(
IProviderRepository providerRepository,
ILogger logger
) : ICommandHandler>
diff --git a/src/Modules/Providers/Application/Handlers/Commands/UpdateProviderProfileCommandHandler.cs b/src/Modules/Providers/Application/Handlers/Commands/UpdateProviderProfileCommandHandler.cs
index 725241fae..57bf9e0ec 100644
--- a/src/Modules/Providers/Application/Handlers/Commands/UpdateProviderProfileCommandHandler.cs
+++ b/src/Modules/Providers/Application/Handlers/Commands/UpdateProviderProfileCommandHandler.cs
@@ -12,7 +12,7 @@ namespace MeAjudaAi.Modules.Providers.Application.Handlers.Commands;
///
/// Handler responsável por processar comandos de atualização de perfil do prestador de serviços.
///
-internal sealed class UpdateProviderProfileCommandHandler(
+public sealed class UpdateProviderProfileCommandHandler(
IProviderRepository providerRepository,
ILogger logger
) : ICommandHandler>
diff --git a/src/Modules/Providers/Application/Handlers/Commands/UpdateVerificationStatusCommandHandler.cs b/src/Modules/Providers/Application/Handlers/Commands/UpdateVerificationStatusCommandHandler.cs
index 11dd7707d..fa1581b16 100644
--- a/src/Modules/Providers/Application/Handlers/Commands/UpdateVerificationStatusCommandHandler.cs
+++ b/src/Modules/Providers/Application/Handlers/Commands/UpdateVerificationStatusCommandHandler.cs
@@ -14,7 +14,7 @@ namespace MeAjudaAi.Modules.Providers.Application.Handlers.Commands;
///
/// Repositório para acesso aos dados
/// Logger estruturado
-internal sealed class UpdateVerificationStatusCommandHandler(
+public sealed class UpdateVerificationStatusCommandHandler(
IProviderRepository providerRepository,
ILogger logger
) : ICommandHandler>
diff --git a/src/Modules/Providers/Application/Handlers/Queries/GetProviderByDocumentQueryHandler.cs b/src/Modules/Providers/Application/Handlers/Queries/GetProviderByDocumentQueryHandler.cs
index 33ea019a9..f2241bf73 100644
--- a/src/Modules/Providers/Application/Handlers/Queries/GetProviderByDocumentQueryHandler.cs
+++ b/src/Modules/Providers/Application/Handlers/Queries/GetProviderByDocumentQueryHandler.cs
@@ -15,7 +15,7 @@ namespace MeAjudaAi.Modules.Providers.Application.Handlers.Queries;
/// Implementa a lógica de negócio para buscar prestadores utilizando número de documento,
/// integrando com o repositório de dados e aplicando as regras de mapeamento necessárias.
///
-internal sealed class GetProviderByDocumentQueryHandler(
+public sealed class GetProviderByDocumentQueryHandler(
IProviderRepository providerRepository,
ILogger logger)
: IQueryHandler>
diff --git a/src/Modules/Providers/Application/Handlers/Queries/GetProviderByIdQueryHandler.cs b/src/Modules/Providers/Application/Handlers/Queries/GetProviderByIdQueryHandler.cs
index 2031d9a7b..a57fc344d 100644
--- a/src/Modules/Providers/Application/Handlers/Queries/GetProviderByIdQueryHandler.cs
+++ b/src/Modules/Providers/Application/Handlers/Queries/GetProviderByIdQueryHandler.cs
@@ -14,7 +14,7 @@ namespace MeAjudaAi.Modules.Providers.Application.Handlers.Queries;
///
/// Repositório para acesso aos dados
/// Logger estruturado
-internal sealed class GetProviderByIdQueryHandler(
+public sealed class GetProviderByIdQueryHandler(
IProviderRepository providerRepository,
ILogger logger
) : IQueryHandler>
diff --git a/src/Modules/Providers/Application/Handlers/Queries/GetProviderByUserIdQueryHandler.cs b/src/Modules/Providers/Application/Handlers/Queries/GetProviderByUserIdQueryHandler.cs
index de219d7e3..506364f5c 100644
--- a/src/Modules/Providers/Application/Handlers/Queries/GetProviderByUserIdQueryHandler.cs
+++ b/src/Modules/Providers/Application/Handlers/Queries/GetProviderByUserIdQueryHandler.cs
@@ -13,7 +13,7 @@ namespace MeAjudaAi.Modules.Providers.Application.Handlers.Queries;
///
/// Repositório para acesso aos dados
/// Logger estruturado
-internal sealed class GetProviderByUserIdQueryHandler(
+public sealed class GetProviderByUserIdQueryHandler(
IProviderRepository providerRepository,
ILogger logger
) : IQueryHandler>
diff --git a/src/Modules/Providers/Application/Handlers/Queries/GetProvidersByCityQueryHandler.cs b/src/Modules/Providers/Application/Handlers/Queries/GetProvidersByCityQueryHandler.cs
index 990a701a9..2bf99da7e 100644
--- a/src/Modules/Providers/Application/Handlers/Queries/GetProvidersByCityQueryHandler.cs
+++ b/src/Modules/Providers/Application/Handlers/Queries/GetProvidersByCityQueryHandler.cs
@@ -13,7 +13,7 @@ namespace MeAjudaAi.Modules.Providers.Application.Handlers.Queries;
///
/// Repositório para acesso aos dados
/// Logger estruturado
-internal sealed class GetProvidersByCityQueryHandler(
+public sealed class GetProvidersByCityQueryHandler(
IProviderRepository providerRepository,
ILogger logger
) : IQueryHandler>>
diff --git a/src/Modules/Providers/Application/Handlers/Queries/GetProvidersByIdsQueryHandler.cs b/src/Modules/Providers/Application/Handlers/Queries/GetProvidersByIdsQueryHandler.cs
index 4253da956..8acace723 100644
--- a/src/Modules/Providers/Application/Handlers/Queries/GetProvidersByIdsQueryHandler.cs
+++ b/src/Modules/Providers/Application/Handlers/Queries/GetProvidersByIdsQueryHandler.cs
@@ -8,7 +8,7 @@
namespace MeAjudaAi.Modules.Providers.Application.Handlers.Queries;
-internal sealed class GetProvidersByIdsQueryHandler(
+public sealed class GetProvidersByIdsQueryHandler(
IProviderRepository providerRepository,
ILogger logger
) : IQueryHandler>>
diff --git a/src/Modules/Providers/Application/Handlers/Queries/GetProvidersByStateQueryHandler.cs b/src/Modules/Providers/Application/Handlers/Queries/GetProvidersByStateQueryHandler.cs
index 90a3660b5..16638e22d 100644
--- a/src/Modules/Providers/Application/Handlers/Queries/GetProvidersByStateQueryHandler.cs
+++ b/src/Modules/Providers/Application/Handlers/Queries/GetProvidersByStateQueryHandler.cs
@@ -8,7 +8,7 @@
namespace MeAjudaAi.Modules.Providers.Application.Handlers.Queries;
-internal sealed class GetProvidersByStateQueryHandler(
+public sealed class GetProvidersByStateQueryHandler(
IProviderRepository providerRepository,
ILogger logger
) : IQueryHandler>>
diff --git a/src/Modules/Providers/Application/Handlers/Queries/GetProvidersByTypeQueryHandler.cs b/src/Modules/Providers/Application/Handlers/Queries/GetProvidersByTypeQueryHandler.cs
index 306bb45b6..8da91e016 100644
--- a/src/Modules/Providers/Application/Handlers/Queries/GetProvidersByTypeQueryHandler.cs
+++ b/src/Modules/Providers/Application/Handlers/Queries/GetProvidersByTypeQueryHandler.cs
@@ -8,7 +8,7 @@
namespace MeAjudaAi.Modules.Providers.Application.Handlers.Queries;
-internal sealed class GetProvidersByTypeQueryHandler(
+public sealed class GetProvidersByTypeQueryHandler(
IProviderRepository providerRepository,
ILogger logger
) : IQueryHandler>>
diff --git a/src/Modules/Providers/Application/Handlers/Queries/GetProvidersByVerificationStatusQueryHandler.cs b/src/Modules/Providers/Application/Handlers/Queries/GetProvidersByVerificationStatusQueryHandler.cs
index bd540452b..afbacfabd 100644
--- a/src/Modules/Providers/Application/Handlers/Queries/GetProvidersByVerificationStatusQueryHandler.cs
+++ b/src/Modules/Providers/Application/Handlers/Queries/GetProvidersByVerificationStatusQueryHandler.cs
@@ -8,7 +8,7 @@
namespace MeAjudaAi.Modules.Providers.Application.Handlers.Queries;
-internal sealed class GetProvidersByVerificationStatusQueryHandler(
+public sealed class GetProvidersByVerificationStatusQueryHandler(
IProviderRepository providerRepository,
ILogger logger
) : IQueryHandler>>
diff --git a/src/Modules/Providers/Application/Handlers/Queries/GetProvidersQueryHandler.cs b/src/Modules/Providers/Application/Handlers/Queries/GetProvidersQueryHandler.cs
index 5372084ac..71892aa62 100644
--- a/src/Modules/Providers/Application/Handlers/Queries/GetProvidersQueryHandler.cs
+++ b/src/Modules/Providers/Application/Handlers/Queries/GetProvidersQueryHandler.cs
@@ -71,8 +71,7 @@ public async Task>> HandleAsync(
"Erro ao buscar prestadores - Página: {Page}, Filtros: Nome='{Name}', Tipo={Type}, Status={Status}",
query.Page, query.Name, query.Type, query.VerificationStatus);
- return Result>.Failure(Error.Internal(
- "Erro interno ao buscar prestadores"));
+ return Result>.Failure(Error.Internal("Erro interno ao buscar prestadores"));
}
}
}
diff --git a/src/Modules/Providers/Application/MeAjudaAi.Modules.Providers.Application.csproj b/src/Modules/Providers/Application/MeAjudaAi.Modules.Providers.Application.csproj
index 91920d2c4..2efeb1e47 100644
--- a/src/Modules/Providers/Application/MeAjudaAi.Modules.Providers.Application.csproj
+++ b/src/Modules/Providers/Application/MeAjudaAi.Modules.Providers.Application.csproj
@@ -10,9 +10,6 @@
<_Parameter1>MeAjudaAi.Modules.Providers.Tests
-
- <_Parameter1>DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7
-
diff --git a/src/Modules/Providers/Domain/Entities/Provider.cs b/src/Modules/Providers/Domain/Entities/Provider.cs
index 5407cb15f..9ab0f62d5 100644
--- a/src/Modules/Providers/Domain/Entities/Provider.cs
+++ b/src/Modules/Providers/Domain/Entities/Provider.cs
@@ -80,9 +80,9 @@ public sealed class Provider : AggregateRoot
private Provider() { }
///
- /// Construtor interno para testes que permite especificar o ID.
+ /// Construtor para testes que permite especificar o ID.
///
- internal Provider(
+ public Provider(
ProviderId id,
Guid userId,
string name,
@@ -158,7 +158,26 @@ public void UpdateProfile(string name, BusinessProfile businessProfile, string?
if (IsDeleted)
throw new ProviderDomainException("Cannot update deleted provider");
- Name = name.Trim();
+ // Track which fields are being updated
+ var updatedFields = new List();
+
+ var newName = name.Trim();
+ if (Name != newName)
+ updatedFields.Add("Name");
+
+ if (!BusinessProfile.ContactInfo.Email.Equals(businessProfile.ContactInfo.Email, StringComparison.OrdinalIgnoreCase))
+ updatedFields.Add("Email");
+
+ if (BusinessProfile.LegalName != businessProfile.LegalName)
+ updatedFields.Add("LegalName");
+
+ if (BusinessProfile.FantasyName != businessProfile.FantasyName)
+ updatedFields.Add("FantasyName");
+
+ if (BusinessProfile.Description != businessProfile.Description)
+ updatedFields.Add("Description");
+
+ Name = newName;
BusinessProfile = businessProfile;
MarkAsUpdated();
@@ -167,7 +186,8 @@ public void UpdateProfile(string name, BusinessProfile businessProfile, string?
1,
Name,
BusinessProfile.ContactInfo.Email,
- updatedBy));
+ updatedBy,
+ updatedFields.ToArray()));
}
///
diff --git a/src/Modules/Providers/Domain/Events/ProviderProfileUpdatedDomainEvent.cs b/src/Modules/Providers/Domain/Events/ProviderProfileUpdatedDomainEvent.cs
index e32f863e7..52c636930 100644
--- a/src/Modules/Providers/Domain/Events/ProviderProfileUpdatedDomainEvent.cs
+++ b/src/Modules/Providers/Domain/Events/ProviderProfileUpdatedDomainEvent.cs
@@ -10,10 +10,12 @@ namespace MeAjudaAi.Modules.Providers.Domain.Events;
/// Novo nome do prestador de serviços
/// Novo email de contato
/// Quem fez a atualização
+/// Lista dos campos que foram atualizados
public record ProviderProfileUpdatedDomainEvent(
Guid AggregateId,
int Version,
string Name,
string Email,
- string? UpdatedBy
+ string? UpdatedBy,
+ string[] UpdatedFields
) : DomainEvent(AggregateId, Version);
diff --git a/src/Modules/Providers/Infrastructure/Events/Handlers/ProviderDeletedDomainEventHandler.cs b/src/Modules/Providers/Infrastructure/Events/Handlers/ProviderDeletedDomainEventHandler.cs
new file mode 100644
index 000000000..a4b6892c1
--- /dev/null
+++ b/src/Modules/Providers/Infrastructure/Events/Handlers/ProviderDeletedDomainEventHandler.cs
@@ -0,0 +1,54 @@
+using MeAjudaAi.Modules.Providers.Domain.Events;
+using MeAjudaAi.Modules.Providers.Infrastructure.Events.Mappers;
+using MeAjudaAi.Modules.Providers.Infrastructure.Persistence;
+using MeAjudaAi.Shared.Events;
+using MeAjudaAi.Shared.Messaging;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace MeAjudaAi.Modules.Providers.Infrastructure.Events.Handlers;
+
+///
+/// Manipula eventos de domínio ProviderDeletedDomainEvent e publica eventos de integração.
+///
+public sealed class ProviderDeletedDomainEventHandler(
+ IMessageBus messageBus,
+ ProvidersDbContext context,
+ ILogger logger) : IEventHandler
+{
+ ///
+ /// Processa o evento de domínio de prestador excluído de forma assíncrona.
+ ///
+ public async Task HandleAsync(ProviderDeletedDomainEvent domainEvent, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ logger.LogInformation("Handling ProviderDeletedDomainEvent for provider {ProviderId}", domainEvent.AggregateId);
+
+ // Buscar apenas o UserId do provider, mesmo se soft-deleted ou removido
+ var userId = await context.Providers
+ .AsNoTracking()
+ .IgnoreQueryFilters()
+ .Where(p => p.Id.Value == domainEvent.AggregateId)
+ .Select(p => (Guid?)p.UserId)
+ .FirstOrDefaultAsync(cancellationToken);
+
+ if (userId.HasValue)
+ {
+ var integrationEvent = domainEvent.ToIntegrationEvent(userId.Value);
+ await messageBus.PublishAsync(integrationEvent, cancellationToken: cancellationToken);
+
+ logger.LogInformation("Successfully published ProviderDeleted integration event for provider {ProviderId}", domainEvent.AggregateId);
+ }
+ else
+ {
+ logger.LogWarning("Provider {ProviderId} not found when handling ProviderDeletedDomainEvent", domainEvent.AggregateId);
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error handling ProviderDeletedDomainEvent for provider {ProviderId}", domainEvent.AggregateId);
+ throw;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Modules/Providers/Infrastructure/Events/Handlers/ProviderProfileUpdatedDomainEventHandler.cs b/src/Modules/Providers/Infrastructure/Events/Handlers/ProviderProfileUpdatedDomainEventHandler.cs
new file mode 100644
index 000000000..95833ad33
--- /dev/null
+++ b/src/Modules/Providers/Infrastructure/Events/Handlers/ProviderProfileUpdatedDomainEventHandler.cs
@@ -0,0 +1,54 @@
+using MeAjudaAi.Modules.Providers.Domain.Events;
+using MeAjudaAi.Modules.Providers.Infrastructure.Events.Mappers;
+using MeAjudaAi.Modules.Providers.Infrastructure.Persistence;
+using MeAjudaAi.Shared.Events;
+using MeAjudaAi.Shared.Messaging;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace MeAjudaAi.Modules.Providers.Infrastructure.Events.Handlers;
+
+///
+/// Manipula eventos de domínio ProviderProfileUpdatedDomainEvent e publica eventos de integração.
+///
+public sealed class ProviderProfileUpdatedDomainEventHandler(
+ IMessageBus messageBus,
+ ProvidersDbContext context,
+ ILogger logger) : IEventHandler
+{
+ ///
+ /// Processa o evento de domínio de perfil de prestador atualizado de forma assíncrona.
+ ///
+ public async Task HandleAsync(ProviderProfileUpdatedDomainEvent domainEvent, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ logger.LogInformation("Handling ProviderProfileUpdatedDomainEvent for provider {ProviderId}", domainEvent.AggregateId);
+
+ // Busca apenas o UserId do prestador
+ var userId = await context.Providers
+ .AsNoTracking()
+ .Where(p => p.Id == new Domain.ValueObjects.ProviderId(domainEvent.AggregateId))
+ .Select(p => (Guid?)p.UserId)
+ .FirstOrDefaultAsync(cancellationToken);
+
+ if (!userId.HasValue)
+ {
+ logger.LogWarning("Provider {ProviderId} not found when handling ProviderProfileUpdatedDomainEvent", domainEvent.AggregateId);
+ return;
+ }
+
+ // Cria evento de integração para sistemas externos usando mapper
+ var integrationEvent = domainEvent.ToIntegrationEvent(userId.Value, domainEvent.UpdatedFields);
+
+ await messageBus.PublishAsync(integrationEvent, cancellationToken: cancellationToken);
+
+ logger.LogInformation("Successfully published ProviderProfileUpdated integration event for provider {ProviderId}", domainEvent.AggregateId);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error handling ProviderProfileUpdatedDomainEvent for provider {ProviderId}", domainEvent.AggregateId);
+ throw;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Modules/Providers/Infrastructure/Events/Handlers/ProviderRegisteredDomainEventHandler.cs b/src/Modules/Providers/Infrastructure/Events/Handlers/ProviderRegisteredDomainEventHandler.cs
new file mode 100644
index 000000000..7a0554302
--- /dev/null
+++ b/src/Modules/Providers/Infrastructure/Events/Handlers/ProviderRegisteredDomainEventHandler.cs
@@ -0,0 +1,60 @@
+using MeAjudaAi.Modules.Providers.Domain.Events;
+using MeAjudaAi.Modules.Providers.Infrastructure.Events.Mappers;
+using MeAjudaAi.Modules.Providers.Infrastructure.Persistence;
+using MeAjudaAi.Shared.Events;
+using MeAjudaAi.Shared.Messaging;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace MeAjudaAi.Modules.Providers.Infrastructure.Events.Handlers;
+
+///
+/// Manipula eventos de domínio ProviderRegisteredDomainEvent e publica eventos de integração.
+///
+///
+/// Responsável por converter eventos de domínio em eventos de integração para comunicação
+/// entre módulos. Quando um prestador é registrado no domínio, este handler busca os dados
+/// atualizados e publica um evento de integração para notificar outros sistemas.
+///
+public sealed class ProviderRegisteredDomainEventHandler(
+ IMessageBus messageBus,
+ ProvidersDbContext context,
+ ILogger logger) : IEventHandler
+{
+ ///
+ /// Processa o evento de domínio de prestador registrado de forma assíncrona.
+ ///
+ /// Evento de domínio contendo dados do prestador registrado
+ /// Token de cancelamento
+ /// Task representando a operação assíncrona
+ public async Task HandleAsync(ProviderRegisteredDomainEvent domainEvent, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ logger.LogInformation("Handling ProviderRegisteredDomainEvent for provider {ProviderId}", domainEvent.AggregateId);
+
+ // Busca o prestador para garantir que temos os dados mais recentes
+ var provider = await context.Providers
+ .AsNoTracking()
+ .FirstOrDefaultAsync(p => p.Id == new Domain.ValueObjects.ProviderId(domainEvent.AggregateId), cancellationToken);
+
+ if (provider == null)
+ {
+ logger.LogWarning("Provider {ProviderId} not found when handling ProviderRegisteredDomainEvent", domainEvent.AggregateId);
+ return;
+ }
+
+ // Cria evento de integração para sistemas externos usando mapper
+ var integrationEvent = domainEvent.ToIntegrationEvent();
+
+ await messageBus.PublishAsync(integrationEvent, cancellationToken: cancellationToken);
+
+ logger.LogInformation("Successfully published ProviderRegistered integration event for provider {ProviderId}", domainEvent.AggregateId);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error handling ProviderRegisteredDomainEvent for provider {ProviderId}", domainEvent.AggregateId);
+ throw;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Modules/Providers/Infrastructure/Events/Handlers/ProviderVerificationStatusUpdatedDomainEventHandler.cs b/src/Modules/Providers/Infrastructure/Events/Handlers/ProviderVerificationStatusUpdatedDomainEventHandler.cs
new file mode 100644
index 000000000..c076113d0
--- /dev/null
+++ b/src/Modules/Providers/Infrastructure/Events/Handlers/ProviderVerificationStatusUpdatedDomainEventHandler.cs
@@ -0,0 +1,51 @@
+using MeAjudaAi.Modules.Providers.Domain.Events;
+using MeAjudaAi.Modules.Providers.Infrastructure.Events.Mappers;
+using MeAjudaAi.Modules.Providers.Infrastructure.Persistence;
+using MeAjudaAi.Shared.Events;
+using MeAjudaAi.Shared.Messaging;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace MeAjudaAi.Modules.Providers.Infrastructure.Events.Handlers;
+
+///
+/// Manipula eventos de domínio ProviderVerificationStatusUpdatedDomainEvent e publica eventos de integração.
+///
+public sealed class ProviderVerificationStatusUpdatedDomainEventHandler(
+ IMessageBus messageBus,
+ ProvidersDbContext context,
+ ILogger logger) : IEventHandler
+{
+ ///
+ /// Processa o evento de domínio de status de verificação atualizado de forma assíncrona.
+ ///
+ public async Task HandleAsync(ProviderVerificationStatusUpdatedDomainEvent domainEvent, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ logger.LogInformation("Handling ProviderVerificationStatusUpdatedDomainEvent for provider {ProviderId}", domainEvent.AggregateId);
+
+ // Busca o prestador para garantir que temos os dados mais recentes
+ var provider = await context.Providers
+ .FirstOrDefaultAsync(p => p.Id == new Domain.ValueObjects.ProviderId(domainEvent.AggregateId), cancellationToken);
+
+ if (provider == null)
+ {
+ logger.LogWarning("Provider {ProviderId} not found when handling ProviderVerificationStatusUpdatedDomainEvent", domainEvent.AggregateId);
+ return;
+ }
+
+ // Cria evento de integração para sistemas externos usando mapper
+ var integrationEvent = domainEvent.ToIntegrationEvent(provider.UserId, provider.Name);
+
+ await messageBus.PublishAsync(integrationEvent, cancellationToken: cancellationToken);
+
+ logger.LogInformation("Successfully published ProviderVerificationStatusUpdated integration event for provider {ProviderId}", domainEvent.AggregateId);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error handling ProviderVerificationStatusUpdatedDomainEvent for provider {ProviderId}", domainEvent.AggregateId);
+ throw;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Modules/Providers/Infrastructure/Events/Mappers/ProviderEventMappers.cs b/src/Modules/Providers/Infrastructure/Events/Mappers/ProviderEventMappers.cs
new file mode 100644
index 000000000..72e29c2be
--- /dev/null
+++ b/src/Modules/Providers/Infrastructure/Events/Mappers/ProviderEventMappers.cs
@@ -0,0 +1,76 @@
+using MeAjudaAi.Modules.Providers.Domain.Events;
+using MeAjudaAi.Shared.Messaging.Messages.Providers;
+
+namespace MeAjudaAi.Modules.Providers.Infrastructure.Events.Mappers;
+
+///
+/// Mappers para converter Domain Events em Integration Events do módulo Providers.
+///
+public static class ProviderEventMappers
+{
+ private const string ModuleName = "Providers";
+
+ ///
+ /// Converte ProviderRegisteredDomainEvent para ProviderRegisteredIntegrationEvent.
+ ///
+ public static ProviderRegisteredIntegrationEvent ToIntegrationEvent(this ProviderRegisteredDomainEvent domainEvent)
+ {
+ return new ProviderRegisteredIntegrationEvent(
+ Source: ModuleName,
+ ProviderId: domainEvent.AggregateId,
+ UserId: domainEvent.UserId,
+ Name: domainEvent.Name,
+ ProviderType: domainEvent.Type.ToString(),
+ Email: domainEvent.Email,
+ RegisteredAt: DateTime.UtcNow
+ );
+ }
+
+ ///
+ /// Converte ProviderDeletedDomainEvent para ProviderDeletedIntegrationEvent.
+ ///
+ public static ProviderDeletedIntegrationEvent ToIntegrationEvent(this ProviderDeletedDomainEvent domainEvent, Guid userId)
+ {
+ return new ProviderDeletedIntegrationEvent(
+ Source: ModuleName,
+ ProviderId: domainEvent.AggregateId,
+ UserId: userId,
+ Name: domainEvent.Name,
+ Reason: "Provider deleted",
+ DeletedAt: DateTime.UtcNow,
+ DeletedBy: domainEvent.DeletedBy
+ );
+ }
+
+ ///
+ /// Converte ProviderVerificationStatusUpdatedDomainEvent para ProviderVerificationStatusUpdatedIntegrationEvent.
+ ///
+ public static ProviderVerificationStatusUpdatedIntegrationEvent ToIntegrationEvent(this ProviderVerificationStatusUpdatedDomainEvent domainEvent, Guid userId, string providerName)
+ {
+ return new ProviderVerificationStatusUpdatedIntegrationEvent(
+ Source: ModuleName,
+ ProviderId: domainEvent.AggregateId,
+ UserId: userId,
+ Name: providerName,
+ PreviousStatus: domainEvent.PreviousStatus.ToString(),
+ NewStatus: domainEvent.NewStatus.ToString(),
+ UpdatedBy: domainEvent.UpdatedBy
+ );
+ }
+
+ ///
+ /// Converte ProviderProfileUpdatedDomainEvent para ProviderProfileUpdatedIntegrationEvent.
+ ///
+ public static ProviderProfileUpdatedIntegrationEvent ToIntegrationEvent(this ProviderProfileUpdatedDomainEvent domainEvent, Guid userId, string[] updatedFields)
+ {
+ return new ProviderProfileUpdatedIntegrationEvent(
+ Source: ModuleName,
+ ProviderId: domainEvent.AggregateId,
+ UserId: userId,
+ Name: domainEvent.Name,
+ UpdatedFields: updatedFields,
+ UpdatedBy: domainEvent.UpdatedBy,
+ NewEmail: domainEvent.Email
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Modules/Providers/Infrastructure/Extensions.cs b/src/Modules/Providers/Infrastructure/Extensions.cs
index 5e0b965b0..98d11c90a 100644
--- a/src/Modules/Providers/Infrastructure/Extensions.cs
+++ b/src/Modules/Providers/Infrastructure/Extensions.cs
@@ -1,8 +1,11 @@
using MeAjudaAi.Modules.Providers.Application.Services.Interfaces;
+using MeAjudaAi.Modules.Providers.Domain.Events;
using MeAjudaAi.Modules.Providers.Domain.Repositories;
+using MeAjudaAi.Modules.Providers.Infrastructure.Events.Handlers;
using MeAjudaAi.Modules.Providers.Infrastructure.Persistence;
using MeAjudaAi.Modules.Providers.Infrastructure.Persistence.Repositories;
using MeAjudaAi.Modules.Providers.Infrastructure.Queries;
+using MeAjudaAi.Shared.Events;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -78,6 +81,23 @@ public static IServiceCollection AddInfrastructure(
// Registro do serviço de consultas
services.AddScoped();
+ // Registro dos Event Handlers
+ services.AddEventHandlers();
+
+ return services;
+ }
+
+ ///
+ /// Adiciona os Event Handlers do módulo Providers.
+ ///
+ private static IServiceCollection AddEventHandlers(this IServiceCollection services)
+ {
+ // Event Handlers específicos do módulo Providers
+ services.AddScoped, ProviderRegisteredDomainEventHandler>();
+ services.AddScoped, ProviderDeletedDomainEventHandler>();
+ services.AddScoped, ProviderVerificationStatusUpdatedDomainEventHandler>();
+ services.AddScoped, ProviderProfileUpdatedDomainEventHandler>();
+
return services;
}
}
diff --git a/src/Modules/Providers/Infrastructure/MeAjudaAi.Modules.Providers.Infrastructure.csproj b/src/Modules/Providers/Infrastructure/MeAjudaAi.Modules.Providers.Infrastructure.csproj
index 9cfde0545..06c4adede 100644
--- a/src/Modules/Providers/Infrastructure/MeAjudaAi.Modules.Providers.Infrastructure.csproj
+++ b/src/Modules/Providers/Infrastructure/MeAjudaAi.Modules.Providers.Infrastructure.csproj
@@ -6,6 +6,12 @@
enable
+
+
+ <_Parameter1>MeAjudaAi.Modules.Providers.Tests
+
+
+
diff --git a/src/Modules/Providers/Infrastructure/Persistence/Configurations/ProviderConfiguration.cs b/src/Modules/Providers/Infrastructure/Persistence/Configurations/ProviderConfiguration.cs
index c13149790..7302dd38c 100644
--- a/src/Modules/Providers/Infrastructure/Persistence/Configurations/ProviderConfiguration.cs
+++ b/src/Modules/Providers/Infrastructure/Persistence/Configurations/ProviderConfiguration.cs
@@ -49,6 +49,14 @@ public void Configure(EntityTypeBuilder builder)
builder.Property(p => p.DeletedAt)
.HasColumnName("deleted_at");
+ // Configuração das propriedades de auditoria da BaseEntity
+ builder.Property(p => p.CreatedAt)
+ .IsRequired()
+ .HasColumnName("created_at");
+
+ builder.Property(p => p.UpdatedAt)
+ .HasColumnName("updated_at");
+
// Configuração do BusinessProfile como owned builder
builder.OwnsOne(p => p.BusinessProfile, bp =>
{
@@ -150,6 +158,8 @@ public void Configure(EntityTypeBuilder builder)
doc.HasKey("ProviderId", "Id");
doc.ToTable("document", "providers");
doc.WithOwner().HasForeignKey("ProviderId");
+ doc.Property("ProviderId").HasColumnName("provider_id");
+ doc.Property("Id").HasColumnName("id");
doc.HasIndex("ProviderId", "DocumentType").IsUnique();
});
@@ -182,6 +192,8 @@ public void Configure(EntityTypeBuilder builder)
qual.HasKey("ProviderId", "Id");
qual.ToTable("qualification", "providers");
qual.WithOwner().HasForeignKey("ProviderId");
+ qual.Property("ProviderId").HasColumnName("provider_id");
+ qual.Property("Id").HasColumnName("id");
});
// Índices
diff --git a/src/Modules/Providers/Infrastructure/Persistence/Migrations/20251111020609_FixColumnNamingToSnakeCase.Designer.cs b/src/Modules/Providers/Infrastructure/Persistence/Migrations/20251111020609_FixColumnNamingToSnakeCase.Designer.cs
new file mode 100644
index 000000000..b300d597f
--- /dev/null
+++ b/src/Modules/Providers/Infrastructure/Persistence/Migrations/20251111020609_FixColumnNamingToSnakeCase.Designer.cs
@@ -0,0 +1,322 @@
+//
+using System;
+using MeAjudaAi.Modules.Providers.Infrastructure.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace MeAjudaAi.Modules.Providers.Infrastructure.Persistence.Migrations
+{
+ [DbContext(typeof(ProvidersDbContext))]
+ [Migration("20251111020609_FixColumnNamingToSnakeCase")]
+ partial class FixColumnNamingToSnakeCase
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasDefaultSchema("providers")
+ .HasAnnotation("ProductVersion", "9.0.9")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("MeAjudaAi.Modules.Providers.Domain.Entities.Provider", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("IsDeleted")
+ .HasColumnType("boolean")
+ .HasColumnName("is_deleted");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("name");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasColumnName("type");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("updated_at");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("VerificationStatus")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasColumnName("verification_status");
+
+ b.HasKey("Id");
+
+ b.HasIndex("IsDeleted")
+ .HasDatabaseName("ix_providers_is_deleted");
+
+ b.HasIndex("Name")
+ .HasDatabaseName("ix_providers_name");
+
+ b.HasIndex("Type")
+ .HasDatabaseName("ix_providers_type");
+
+ b.HasIndex("UserId")
+ .IsUnique()
+ .HasDatabaseName("ix_providers_user_id");
+
+ b.HasIndex("VerificationStatus")
+ .HasDatabaseName("ix_providers_verification_status");
+
+ b.ToTable("providers", "providers");
+ });
+
+ modelBuilder.Entity("MeAjudaAi.Modules.Providers.Domain.Entities.Provider", b =>
+ {
+ b.OwnsOne("MeAjudaAi.Modules.Providers.Domain.ValueObjects.BusinessProfile", "BusinessProfile", b1 =>
+ {
+ b1.Property("ProviderId")
+ .HasColumnType("uuid");
+
+ b1.Property("Description")
+ .HasMaxLength(1000)
+ .HasColumnType("character varying(1000)")
+ .HasColumnName("description");
+
+ b1.Property("FantasyName")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("fantasy_name");
+
+ b1.Property("LegalName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("legal_name");
+
+ b1.HasKey("ProviderId");
+
+ b1.ToTable("providers", "providers");
+
+ b1.WithOwner()
+ .HasForeignKey("ProviderId");
+
+ b1.OwnsOne("MeAjudaAi.Modules.Providers.Domain.ValueObjects.Address", "PrimaryAddress", b2 =>
+ {
+ b2.Property("BusinessProfileProviderId")
+ .HasColumnType("uuid");
+
+ b2.Property("City")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("city");
+
+ b2.Property("Complement")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("complement");
+
+ b2.Property("Country")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("country");
+
+ b2.Property("Neighborhood")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("neighborhood");
+
+ b2.Property("Number")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasColumnName("number");
+
+ b2.Property("State")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("state");
+
+ b2.Property("Street")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("street");
+
+ b2.Property("ZipCode")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasColumnName("zip_code");
+
+ b2.HasKey("BusinessProfileProviderId");
+
+ b2.ToTable("providers", "providers");
+
+ b2.WithOwner()
+ .HasForeignKey("BusinessProfileProviderId");
+ });
+
+ b1.OwnsOne("MeAjudaAi.Modules.Providers.Domain.ValueObjects.ContactInfo", "ContactInfo", b2 =>
+ {
+ b2.Property("BusinessProfileProviderId")
+ .HasColumnType("uuid");
+
+ b2.Property("Email")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)")
+ .HasColumnName("email");
+
+ b2.Property("PhoneNumber")
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasColumnName("phone_number");
+
+ b2.Property("Website")
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)")
+ .HasColumnName("website");
+
+ b2.HasKey("BusinessProfileProviderId");
+
+ b2.ToTable("providers", "providers");
+
+ b2.WithOwner()
+ .HasForeignKey("BusinessProfileProviderId");
+ });
+
+ b1.Navigation("ContactInfo")
+ .IsRequired();
+
+ b1.Navigation("PrimaryAddress")
+ .IsRequired();
+ });
+
+ b.OwnsMany("MeAjudaAi.Modules.Providers.Domain.ValueObjects.Document", "Documents", b1 =>
+ {
+ b1.Property("ProviderId")
+ .HasColumnType("uuid")
+ .HasColumnName("provider_id");
+
+ b1.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id"));
+
+ b1.Property("DocumentType")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasColumnName("document_type");
+
+ b1.Property("IsPrimary")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(false)
+ .HasColumnName("is_primary");
+
+ b1.Property("Number")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("number");
+
+ b1.HasKey("ProviderId", "Id");
+
+ b1.HasIndex("ProviderId", "DocumentType")
+ .IsUnique();
+
+ b1.ToTable("document", "providers");
+
+ b1.WithOwner()
+ .HasForeignKey("ProviderId");
+ });
+
+ b.OwnsMany("MeAjudaAi.Modules.Providers.Domain.ValueObjects.Qualification", "Qualifications", b1 =>
+ {
+ b1.Property("ProviderId")
+ .HasColumnType("uuid")
+ .HasColumnName("provider_id");
+
+ b1.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id"));
+
+ b1.Property("Description")
+ .HasMaxLength(1000)
+ .HasColumnType("character varying(1000)")
+ .HasColumnName("description");
+
+ b1.Property("DocumentNumber")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("document_number");
+
+ b1.Property("ExpirationDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiration_date");
+
+ b1.Property("IssueDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("issue_date");
+
+ b1.Property("IssuingOrganization")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("issuing_organization");
+
+ b1.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("name");
+
+ b1.HasKey("ProviderId", "Id");
+
+ b1.ToTable("qualification", "providers");
+
+ b1.WithOwner()
+ .HasForeignKey("ProviderId");
+ });
+
+ b.Navigation("BusinessProfile")
+ .IsRequired();
+
+ b.Navigation("Documents");
+
+ b.Navigation("Qualifications");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Modules/Providers/Infrastructure/Persistence/Migrations/20251111020609_FixColumnNamingToSnakeCase.cs b/src/Modules/Providers/Infrastructure/Persistence/Migrations/20251111020609_FixColumnNamingToSnakeCase.cs
new file mode 100644
index 000000000..9d0cf544a
--- /dev/null
+++ b/src/Modules/Providers/Infrastructure/Persistence/Migrations/20251111020609_FixColumnNamingToSnakeCase.cs
@@ -0,0 +1,162 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace MeAjudaAi.Modules.Providers.Infrastructure.Persistence.Migrations
+{
+ ///
+ public partial class FixColumnNamingToSnakeCase : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "FK_document_providers_ProviderId",
+ schema: "providers",
+ table: "document");
+
+ migrationBuilder.DropForeignKey(
+ name: "FK_qualification_providers_ProviderId",
+ schema: "providers",
+ table: "qualification");
+
+ migrationBuilder.RenameColumn(
+ name: "Id",
+ schema: "providers",
+ table: "qualification",
+ newName: "id");
+
+ migrationBuilder.RenameColumn(
+ name: "ProviderId",
+ schema: "providers",
+ table: "qualification",
+ newName: "provider_id");
+
+ migrationBuilder.RenameColumn(
+ name: "UpdatedAt",
+ schema: "providers",
+ table: "providers",
+ newName: "updated_at");
+
+ migrationBuilder.RenameColumn(
+ name: "CreatedAt",
+ schema: "providers",
+ table: "providers",
+ newName: "created_at");
+
+ migrationBuilder.RenameColumn(
+ name: "Id",
+ schema: "providers",
+ table: "document",
+ newName: "id");
+
+ migrationBuilder.RenameColumn(
+ name: "ProviderId",
+ schema: "providers",
+ table: "document",
+ newName: "provider_id");
+
+ migrationBuilder.RenameIndex(
+ name: "IX_document_ProviderId_document_type",
+ schema: "providers",
+ table: "document",
+ newName: "IX_document_provider_id_document_type");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_document_providers_provider_id",
+ schema: "providers",
+ table: "document",
+ column: "provider_id",
+ principalSchema: "providers",
+ principalTable: "providers",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_qualification_providers_provider_id",
+ schema: "providers",
+ table: "qualification",
+ column: "provider_id",
+ principalSchema: "providers",
+ principalTable: "providers",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "FK_document_providers_provider_id",
+ schema: "providers",
+ table: "document");
+
+ migrationBuilder.DropForeignKey(
+ name: "FK_qualification_providers_provider_id",
+ schema: "providers",
+ table: "qualification");
+
+ migrationBuilder.RenameColumn(
+ name: "id",
+ schema: "providers",
+ table: "qualification",
+ newName: "Id");
+
+ migrationBuilder.RenameColumn(
+ name: "provider_id",
+ schema: "providers",
+ table: "qualification",
+ newName: "ProviderId");
+
+ migrationBuilder.RenameColumn(
+ name: "updated_at",
+ schema: "providers",
+ table: "providers",
+ newName: "UpdatedAt");
+
+ migrationBuilder.RenameColumn(
+ name: "created_at",
+ schema: "providers",
+ table: "providers",
+ newName: "CreatedAt");
+
+ migrationBuilder.RenameColumn(
+ name: "id",
+ schema: "providers",
+ table: "document",
+ newName: "Id");
+
+ migrationBuilder.RenameColumn(
+ name: "provider_id",
+ schema: "providers",
+ table: "document",
+ newName: "ProviderId");
+
+ migrationBuilder.RenameIndex(
+ name: "IX_document_provider_id_document_type",
+ schema: "providers",
+ table: "document",
+ newName: "IX_document_ProviderId_document_type");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_document_providers_ProviderId",
+ schema: "providers",
+ table: "document",
+ column: "ProviderId",
+ principalSchema: "providers",
+ principalTable: "providers",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_qualification_providers_ProviderId",
+ schema: "providers",
+ table: "qualification",
+ column: "ProviderId",
+ principalSchema: "providers",
+ principalTable: "providers",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ }
+ }
+}
diff --git a/src/Modules/Providers/Infrastructure/Persistence/Migrations/ProvidersDbContextModelSnapshot.cs b/src/Modules/Providers/Infrastructure/Persistence/Migrations/ProvidersDbContextModelSnapshot.cs
index abea3494f..fbd9eb083 100644
--- a/src/Modules/Providers/Infrastructure/Persistence/Migrations/ProvidersDbContextModelSnapshot.cs
+++ b/src/Modules/Providers/Infrastructure/Persistence/Migrations/ProvidersDbContextModelSnapshot.cs
@@ -30,7 +30,8 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.HasColumnName("id");
b.Property("CreatedAt")
- .HasColumnType("timestamp with time zone");
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
b.Property("DeletedAt")
.HasColumnType("timestamp with time zone")
@@ -53,7 +54,8 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.HasColumnName("type");
b.Property("UpdatedAt")
- .HasColumnType("timestamp with time zone");
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("updated_at");
b.Property("UserId")
.HasColumnType("uuid")
@@ -215,11 +217,13 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.OwnsMany("MeAjudaAi.Modules.Providers.Domain.ValueObjects.Document", "Documents", b1 =>
{
b1.Property("ProviderId")
- .HasColumnType("uuid");
+ .HasColumnType("uuid")
+ .HasColumnName("provider_id");
b1.Property("Id")
.ValueGeneratedOnAdd()
- .HasColumnType("integer");
+ .HasColumnType("integer")
+ .HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id"));
@@ -255,11 +259,13 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.OwnsMany("MeAjudaAi.Modules.Providers.Domain.ValueObjects.Qualification", "Qualifications", b1 =>
{
b1.Property("ProviderId")
- .HasColumnType("uuid");
+ .HasColumnType("uuid")
+ .HasColumnName("provider_id");
b1.Property("Id")
.ValueGeneratedOnAdd()
- .HasColumnType("integer");
+ .HasColumnType("integer")
+ .HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id"));
diff --git a/src/Modules/Providers/Infrastructure/Persistence/ProvidersDbContext.cs b/src/Modules/Providers/Infrastructure/Persistence/ProvidersDbContext.cs
index 8d992fb60..fd2781577 100644
--- a/src/Modules/Providers/Infrastructure/Persistence/ProvidersDbContext.cs
+++ b/src/Modules/Providers/Infrastructure/Persistence/ProvidersDbContext.cs
@@ -11,12 +11,15 @@ namespace MeAjudaAi.Modules.Providers.Infrastructure.Persistence;
/// Implementa o padrão DbContext do Entity Framework Core para persistência
/// das entidades do domínio de prestadores de serviços.
///
-///
-/// Inicializa uma nova instância do contexto.
-///
-/// Opções de configuração do contexto
-public class ProvidersDbContext(DbContextOptions options) : DbContext(options)
+public class ProvidersDbContext : DbContext
{
+ ///
+ /// Inicializa uma nova instância do contexto.
+ ///
+ /// Opções de configuração do contexto
+ public ProvidersDbContext(DbContextOptions options) : base(options)
+ {
+ }
///
/// Conjunto de dados para prestadores de serviços.
///
diff --git a/src/Modules/Providers/Infrastructure/Queries/ProviderQueryService.cs b/src/Modules/Providers/Infrastructure/Queries/ProviderQueryService.cs
index ed44c3521..146f3739e 100644
--- a/src/Modules/Providers/Infrastructure/Queries/ProviderQueryService.cs
+++ b/src/Modules/Providers/Infrastructure/Queries/ProviderQueryService.cs
@@ -14,8 +14,14 @@ namespace MeAjudaAi.Modules.Providers.Infrastructure.Queries;
/// Implementa consultas complexas e paginadas específicas da infraestrutura
/// que não fazem parte do domínio principal.
///
-public sealed class ProviderQueryService(ProvidersDbContext context) : IProviderQueryService
+public sealed class ProviderQueryService : IProviderQueryService
{
+ private readonly ProvidersDbContext _context;
+
+ public ProviderQueryService(ProvidersDbContext context)
+ {
+ _context = context;
+ }
///
/// Busca prestadores de serviços com paginação e filtros opcionais.
@@ -35,7 +41,7 @@ public async Task> GetProvidersAsync(
if (pageSize < 1)
throw new ArgumentOutOfRangeException(nameof(pageSize), "PageSize must be greater than 0");
- var query = context.Providers
+ var query = _context.Providers
.Include(p => p.Documents)
.Include(p => p.Qualifications)
.Where(p => !p.IsDeleted);
@@ -72,8 +78,8 @@ public async Task> GetProvidersAsync(
return new PagedResult(
providers,
- totalCount,
page,
- pageSize);
+ pageSize,
+ totalCount);
}
}
diff --git a/src/Modules/Providers/Tests/Builders/ProviderBuilder.cs b/src/Modules/Providers/Tests/Builders/ProviderBuilder.cs
index fe76dbe79..6edd1bb20 100644
--- a/src/Modules/Providers/Tests/Builders/ProviderBuilder.cs
+++ b/src/Modules/Providers/Tests/Builders/ProviderBuilder.cs
@@ -26,9 +26,9 @@ public ProviderBuilder()
{
Provider provider;
+ // Se um ID específico foi fornecido, usa o construtor interno para testes
if (_providerId != null)
{
- // Usa o construtor interno para testes quando um ID específico é fornecido
provider = new Provider(
_providerId,
_userId ?? f.Random.Guid(),
@@ -39,7 +39,7 @@ public ProviderBuilder()
}
else
{
- // Usa o construtor público normal
+ // Usa o construtor público que gera um novo ID
provider = new Provider(
_userId ?? f.Random.Guid(),
_name ?? f.Company.CompanyName(),
diff --git a/src/Modules/Providers/Tests/Infrastructure/TestInfrastructureExtensions.cs b/src/Modules/Providers/Tests/Infrastructure/TestInfrastructureExtensions.cs
index 09cc1a892..df553b6a5 100644
--- a/src/Modules/Providers/Tests/Infrastructure/TestInfrastructureExtensions.cs
+++ b/src/Modules/Providers/Tests/Infrastructure/TestInfrastructureExtensions.cs
@@ -39,12 +39,7 @@ public static IServiceCollection AddProvidersTestInfrastructure(
// Para testes, usar implementação simples sem dependências complexas
services.AddSingleton();
- // Configurar banco de dados específico do módulo Providers
- services.AddTestDatabase(
- options.Database,
- "MeAjudaAi.Modules.Providers.Infrastructure");
-
- // Configurar DbContext específico
+ // Configurar DbContext específico para PostgreSQL com TestContainers (isolado por teste)
services.AddDbContext((serviceProvider, dbOptions) =>
{
var container = serviceProvider.GetRequiredService();
@@ -61,7 +56,7 @@ public static IServiceCollection AddProvidersTestInfrastructure(
// Suprimir warnings de pending model changes em testes
warnings.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning);
});
- });
+ }, ServiceLifetime.Scoped); // Garantir que seja Scoped
// Adicionar repositórios específicos do Providers
services.AddScoped();
diff --git a/src/Modules/Providers/Tests/Infrastructure/TestProviderQueryService.cs b/src/Modules/Providers/Tests/Infrastructure/TestProviderQueryService.cs
new file mode 100644
index 000000000..ea200bf5b
--- /dev/null
+++ b/src/Modules/Providers/Tests/Infrastructure/TestProviderQueryService.cs
@@ -0,0 +1,82 @@
+using MeAjudaAi.Modules.Providers.Application.Services.Interfaces;
+using MeAjudaAi.Modules.Providers.Domain.Entities;
+using MeAjudaAi.Modules.Providers.Domain.Enums;
+using MeAjudaAi.Modules.Providers.Infrastructure.Persistence;
+using MeAjudaAi.Shared.Contracts;
+using Microsoft.EntityFrameworkCore;
+
+namespace MeAjudaAi.Modules.Providers.Tests.Infrastructure;
+
+///
+/// Versão de teste do ProviderQueryService usando construtor tradicional
+///
+public sealed class TestProviderQueryService : IProviderQueryService
+{
+ private readonly ProvidersDbContext _context;
+
+ public TestProviderQueryService(ProvidersDbContext context)
+ {
+ _context = context;
+ }
+
+ ///
+ /// Busca prestadores de serviços com paginação e filtros opcionais.
+ ///
+ public async Task> GetProvidersAsync(
+ int page = 1,
+ int pageSize = 20,
+ string? nameFilter = null,
+ EProviderType? typeFilter = null,
+ EVerificationStatus? verificationStatusFilter = null,
+ CancellationToken cancellationToken = default)
+ {
+ // Valida parâmetros de paginação
+ if (page < 1)
+ throw new ArgumentOutOfRangeException(nameof(page), "Page must be greater than 0");
+
+ if (pageSize < 1)
+ throw new ArgumentOutOfRangeException(nameof(pageSize), "PageSize must be greater than 0");
+
+ var query = _context.Providers
+ .AsNoTracking()
+ .Include(p => p.Documents)
+ .Include(p => p.Qualifications)
+ .Where(p => !p.IsDeleted);
+
+ // Aplica filtro por nome (busca parcial, case-insensitive)
+ if (!string.IsNullOrWhiteSpace(nameFilter))
+ {
+ query = query.Where(p => EF.Functions.ILike(p.Name, $"%{nameFilter}%"));
+ }
+
+ // Aplica filtro por tipo
+ if (typeFilter.HasValue)
+ {
+ query = query.Where(p => p.Type == typeFilter.Value);
+ }
+
+ // Aplica filtro por status de verificação
+ if (verificationStatusFilter.HasValue)
+ {
+ query = query.Where(p => p.VerificationStatus == verificationStatusFilter.Value);
+ }
+
+ // Ordena por data de criação (mais recentes primeiro) com ID como tiebreaker para determinismo
+ query = query.OrderByDescending(p => p.CreatedAt).ThenByDescending(p => p.Id);
+
+ // Conta total de registros
+ var totalCount = await query.CountAsync(cancellationToken);
+
+ // Aplica paginação
+ var providers = await query
+ .Skip((page - 1) * pageSize)
+ .Take(pageSize)
+ .ToListAsync(cancellationToken);
+
+ return new PagedResult(
+ providers,
+ totalCount,
+ page,
+ pageSize);
+ }
+}
\ No newline at end of file
diff --git a/src/Modules/Providers/Tests/Integration/ProviderQueryServiceIntegrationTests.cs b/src/Modules/Providers/Tests/Integration/ProviderQueryServiceIntegrationTests.cs
index 8494fee41..71b8417ab 100644
--- a/src/Modules/Providers/Tests/Integration/ProviderQueryServiceIntegrationTests.cs
+++ b/src/Modules/Providers/Tests/Integration/ProviderQueryServiceIntegrationTests.cs
@@ -107,11 +107,25 @@ await CreateProviderAsync(
public async Task GetProvidersAsync_EmptyDatabase_ShouldReturnEmptyResult()
{
// Arrange
- await CleanupDatabase(); // Garantir isolamento
+ await ForceCleanDatabase();
+
var queryService = GetService();
-
- // Act
- var result = await queryService.GetProvidersAsync(page: 1, pageSize: 10);
+
+ // Act - teste com banco vazio usando container-backed services
+ var resultWithoutFilter = await queryService.GetProvidersAsync(page: 1, pageSize: 10);
+
+ // Assert - o banco deve estar vazio
+ resultWithoutFilter.Should().NotBeNull();
+ resultWithoutFilter.Items.Should().BeEmpty();
+ resultWithoutFilter.TotalCount.Should().Be(0);
+ resultWithoutFilter.TotalPages.Should().Be(0);
+
+ // Act - Agora com filtro único que não existe
+ var uniqueFilter = $"VERY_UNIQUE_TEST_FILTER__{Guid.NewGuid():N}";
+ var result = await queryService.GetProvidersAsync(
+ page: 1,
+ pageSize: 10,
+ nameFilter: uniqueFilter);
// Assert
result.Should().NotBeNull();
@@ -120,9 +134,4 @@ public async Task GetProvidersAsync_EmptyDatabase_ShouldReturnEmptyResult()
result.TotalPages.Should().Be(0);
}
- protected override async Task OnDisposeAsync()
- {
- await CleanupDatabase();
- await base.OnDisposeAsync();
- }
}
diff --git a/src/Modules/Providers/Tests/Integration/ProviderRepositoryIntegrationTests.cs b/src/Modules/Providers/Tests/Integration/ProviderRepositoryIntegrationTests.cs
index 35f5a2f39..045eb66aa 100644
--- a/src/Modules/Providers/Tests/Integration/ProviderRepositoryIntegrationTests.cs
+++ b/src/Modules/Providers/Tests/Integration/ProviderRepositoryIntegrationTests.cs
@@ -115,8 +115,19 @@ public async Task UpdateAsync_WithValidChanges_ShouldPersistChanges()
// Act
var updatedBusinessProfile = new BusinessProfile(
"New Name Legal",
- provider.BusinessProfile.ContactInfo,
- provider.BusinessProfile.PrimaryAddress,
+ new ContactInfo(
+ provider.BusinessProfile.ContactInfo.Email,
+ provider.BusinessProfile.ContactInfo.PhoneNumber,
+ provider.BusinessProfile.ContactInfo.Website),
+ new Address(
+ provider.BusinessProfile.PrimaryAddress.Street,
+ provider.BusinessProfile.PrimaryAddress.Number,
+ provider.BusinessProfile.PrimaryAddress.Neighborhood,
+ provider.BusinessProfile.PrimaryAddress.City,
+ provider.BusinessProfile.PrimaryAddress.State,
+ provider.BusinessProfile.PrimaryAddress.ZipCode,
+ provider.BusinessProfile.PrimaryAddress.Country,
+ provider.BusinessProfile.PrimaryAddress.Complement),
description: "Updated description");
provider.UpdateProfile("New Name", updatedBusinessProfile);
await repository.UpdateAsync(provider);
diff --git a/src/Modules/Providers/Tests/Integration/ProvidersIntegrationTestBase.cs b/src/Modules/Providers/Tests/Integration/ProvidersIntegrationTestBase.cs
index 9e890d9c8..1d20cbcb3 100644
--- a/src/Modules/Providers/Tests/Integration/ProvidersIntegrationTestBase.cs
+++ b/src/Modules/Providers/Tests/Integration/ProvidersIntegrationTestBase.cs
@@ -4,33 +4,48 @@
using MeAjudaAi.Modules.Providers.Infrastructure.Persistence;
using MeAjudaAi.Modules.Providers.Tests.Infrastructure;
using MeAjudaAi.Shared.Tests.Infrastructure;
+using MeAjudaAi.Shared.Tests.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Xunit;
+using Testcontainers.PostgreSql;
namespace MeAjudaAi.Modules.Providers.Tests.Integration;
///
/// Classe base para testes de integração específicos do módulo Providers.
+/// Usa isolamento completo com database único por classe de teste.
///
-public abstract class ProvidersIntegrationTestBase : IntegrationTestBase
+public abstract class ProvidersIntegrationTestBase : IAsyncLifetime
{
+ private PostgreSqlContainer? _container;
+ private ServiceProvider? _serviceProvider;
+ private readonly string _testClassId;
+
+ protected ProvidersIntegrationTestBase()
+ {
+ _testClassId = $"{GetType().Name}_{Guid.NewGuid():N}";
+ }
+
///
/// Configurações padrão para testes do módulo Providers
///
- protected override TestInfrastructureOptions GetTestOptions()
+ protected TestInfrastructureOptions GetTestOptions()
{
return new TestInfrastructureOptions
{
Database = new TestDatabaseOptions
{
- DatabaseName = $"MeAjudaAi", // Usar banco de desenvolvimento
- Username = "postgres",
- Password = "development123",
- Schema = "providers"
+ DatabaseName = $"providers_test_{_testClassId}",
+ Username = "test_user",
+ Password = "test_password",
+ Schema = "providers",
+ UseInMemoryDatabase = false
},
Cache = new TestCacheOptions
{
- Enabled = false // Não usa cache por padrão
+ Enabled = false
},
ExternalServices = new TestExternalServicesOptions
{
@@ -40,22 +55,69 @@ protected override TestInfrastructureOptions GetTestOptions()
};
}
+ ///
+ /// Inicialização executada antes de cada classe de teste
+ ///
+ public async ValueTask InitializeAsync()
+ {
+ // Criar container PostgreSQL isolado para esta classe de teste
+ var options = GetTestOptions();
+
+ _container = new PostgreSqlBuilder()
+ .WithImage(options.Database.PostgresImage)
+ .WithDatabase(options.Database.DatabaseName)
+ .WithUsername(options.Database.Username)
+ .WithPassword(options.Database.Password)
+ .WithPortBinding(0, true) // Porta aleatória
+ .Build();
+
+ await _container.StartAsync();
+
+ // Configurar serviços com container isolado
+ var services = new ServiceCollection();
+
+ // Registrar o container específico
+ services.AddSingleton(_container);
+
+ // Configurar logging otimizado
+ services.AddLogging(builder =>
+ {
+ builder.ConfigureTestLogging();
+ });
+
+ // Configurar serviços específicos do módulo
+ ConfigureModuleServices(services, options);
+
+ _serviceProvider = services.BuildServiceProvider();
+
+ // Inicializar banco de dados
+ await InitializeDatabaseAsync();
+ }
+
///
/// Configura serviços específicos do módulo Providers
///
- protected override void ConfigureModuleServices(IServiceCollection services, TestInfrastructureOptions options)
+ private void ConfigureModuleServices(IServiceCollection services, TestInfrastructureOptions options)
{
services.AddProvidersTestInfrastructure(options);
}
-
+
///
- /// Setup específico do módulo Providers (configurações adicionais se necessário)
+ /// Inicializa o banco de dados isolado
///
- protected override async Task OnModuleInitializeAsync(IServiceProvider serviceProvider)
+ private async Task InitializeDatabaseAsync()
{
- // Qualquer setup específico adicional do módulo Providers pode ser feito aqui
- // As migrações são aplicadas automaticamente pelo sistema de auto-descoberta
- await Task.CompletedTask;
+ var dbContext = _serviceProvider!.GetRequiredService();
+
+ // Criar banco e esquema sem executar migrations
+ await dbContext.Database.EnsureCreatedAsync();
+
+ // Verificar isolamento
+ var count = await dbContext.Providers.CountAsync();
+ if (count > 0)
+ {
+ throw new InvalidOperationException($"Database isolation failed for '{GetTestOptions().Database.DatabaseName}': found {count} existing providers in new database");
+ }
}
///
@@ -92,6 +154,7 @@ protected static BusinessProfile CreateTestBusinessProfile(string email = "test@
///
/// Limpa dados das tabelas para isolamento entre testes
+ /// Usando banco isolado, cleanup é mais simples e confiável
///
protected async Task CleanupDatabase()
{
@@ -99,49 +162,109 @@ protected async Task CleanupDatabase()
try
{
- // Use EF Core change tracking for safer cleanup
- // Only providers table is exposed as DbSet, child entities are accessed through navigation properties
- var providers = await dbContext.Providers
- .Include(p => p.Documents)
- .Include(p => p.Qualifications)
- .ToListAsync();
-
- if (providers.Any())
- {
- dbContext.Providers.RemoveRange(providers);
- await dbContext.SaveChangesAsync();
- }
+ // Com banco isolado, podemos usar TRUNCATE com segurança
+ await dbContext.Database.ExecuteSqlRawAsync("TRUNCATE TABLE providers.providers CASCADE;");
}
catch (Exception ex)
{
- Console.WriteLine($"EF Core cleanup failed: {ex.Message}. Trying raw SQL...");
+ // Fallback para DELETE se TRUNCATE falhar
+ var logger = GetService>();
+ logger.LogWarning(ex, "TRUNCATE failed: {Message}. Using DELETE fallback...", ex.Message);
+
+ await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM providers.qualification;");
+ await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM providers.document;");
+ await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM providers.providers;");
+ }
+
+ // Verificar se limpeza foi bem-sucedida
+ var remainingCount = await dbContext.Providers.CountAsync();
+ if (remainingCount > 0)
+ {
+ throw new InvalidOperationException($"Database cleanup failed: {remainingCount} providers remain");
+ }
+ }
- // Fallback to raw SQL if EF Core fails
- try
- {
- // Use correct table names (lowercase without quotes for most cases)
- await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM providers.qualification;");
- await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM providers.document;");
- await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM providers.providers;");
- }
- catch (Exception ex2)
- {
- Console.WriteLine($"Raw SQL cleanup failed: {ex2.Message}. Trying TRUNCATE...");
-
- // Se DELETE falhar, tentar TRUNCATE com cascata
- try
- {
- await dbContext.Database.ExecuteSqlRawAsync("TRUNCATE TABLE providers.qualification, providers.document, providers.providers RESTART IDENTITY CASCADE;");
- }
- catch (Exception ex3)
- {
- Console.WriteLine($"TRUNCATE failed: {ex3.Message}. Recreating database...");
-
- // Se ainda falhar, recriar o schema
- await dbContext.Database.EnsureDeletedAsync();
- await dbContext.Database.EnsureCreatedAsync();
- }
- }
+ ///
+ /// Força limpeza mais agressiva do banco de dados
+ /// Com isolamento completo, é mais simples e confiável
+ ///
+ protected async Task ForceCleanDatabase()
+ {
+ var dbContext = GetService();
+
+ try
+ {
+ // Estratégia 1: TRUNCATE CASCADE
+ await dbContext.Database.ExecuteSqlRawAsync("TRUNCATE TABLE providers.providers CASCADE;");
+ return;
+ }
+ catch (Exception ex)
+ {
+ var logger = GetService>();
+ logger.LogWarning(ex, "TRUNCATE failed: {Message}. Trying DELETE...", ex.Message);
+ }
+
+ try
+ {
+ // Estratégia 2: DELETE em ordem reversa
+ await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM providers.qualification;");
+ await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM providers.document;");
+ await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM providers.providers;");
+ return;
+ }
+ catch (Exception ex)
+ {
+ var logger = GetService>();
+ logger.LogError(ex, "DELETE failed: {Message}. Recreating database...", ex.Message);
+ }
+
+ // Estratégia 3: Recriar database
+ await dbContext.Database.EnsureDeletedAsync();
+ await dbContext.Database.EnsureCreatedAsync();
+ }
+
+ ///
+ /// Limpeza executada após cada classe de teste
+ ///
+ public async ValueTask DisposeAsync()
+ {
+ if (_serviceProvider != null)
+ {
+ await _serviceProvider.DisposeAsync();
+ }
+
+ if (_container != null)
+ {
+ await _container.StopAsync();
+ await _container.DisposeAsync();
}
}
+
+ ///
+ /// Obtém um serviço do provider isolado
+ ///
+ protected T GetService() where T : notnull
+ {
+ if (_serviceProvider == null)
+ throw new InvalidOperationException("Service provider not initialized");
+ return _serviceProvider.GetRequiredService();
+ }
+
+ ///
+ /// Obtém um serviço de um escopo específico
+ ///
+ protected T GetScopedService(IServiceScope scope) where T : notnull
+ {
+ return scope.ServiceProvider.GetRequiredService();
+ }
+
+ ///
+ /// Cria um escopo de serviços para o teste
+ ///
+ protected IServiceScope CreateScope()
+ {
+ if (_serviceProvider == null)
+ throw new InvalidOperationException("Service provider not initialized");
+ return _serviceProvider.CreateScope();
+ }
}
diff --git a/src/Modules/Providers/Tests/Unit/Application/Queries/GetProviderByIdQueryHandlerTests.cs b/src/Modules/Providers/Tests/Unit/Application/Queries/GetProviderByIdQueryHandlerTests.cs
index edf5001c1..b91f21574 100644
--- a/src/Modules/Providers/Tests/Unit/Application/Queries/GetProviderByIdQueryHandlerTests.cs
+++ b/src/Modules/Providers/Tests/Unit/Application/Queries/GetProviderByIdQueryHandlerTests.cs
@@ -128,13 +128,14 @@ private static Provider CreateValidProvider(ProviderId? providerId = null)
contactInfo: contactInfo,
primaryAddress: address);
- // Se um ProviderId específico foi fornecido, usa o construtor interno para testes
+ // Se um ProviderId específico foi fornecido, usa o construtor que aceita ProviderId explícito
+ // e não emite eventos de domínio
if (providerId != null)
{
return new Provider(providerId, userId, name, type, businessProfile);
}
- // Caso contrário, usa o construtor público normal
+ // Caso contrário, usa o construtor público normal que gera ProviderId automaticamente
return new Provider(userId, name, type, businessProfile);
}
}
diff --git a/src/Modules/Users/API/Extensions.cs b/src/Modules/Users/API/Extensions.cs
index aa0afa6e3..b933730f2 100644
--- a/src/Modules/Users/API/Extensions.cs
+++ b/src/Modules/Users/API/Extensions.cs
@@ -6,6 +6,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace MeAjudaAi.Modules.Users.API;
@@ -57,6 +58,12 @@ private static void EnsureDatabaseMigrations(WebApplication app)
// Só aplica migrações se não estivermos em ambiente de testes unitários
if (app?.Services == null) return;
+ // Em ambiente de teste E2E, pular migrações automáticas - elas são gerenciadas pelo TestContainer
+ if (app.Environment.IsEnvironment("Test") || app.Environment.IsEnvironment("Testing"))
+ {
+ return;
+ }
+
try
{
// Criar um escopo para obter o context e aplicar migrações
diff --git a/src/Modules/Users/Application/Handlers/Commands/ChangeUserEmailCommandHandler.cs b/src/Modules/Users/Application/Handlers/Commands/ChangeUserEmailCommandHandler.cs
index 170e14ae4..ae1ae5419 100644
--- a/src/Modules/Users/Application/Handlers/Commands/ChangeUserEmailCommandHandler.cs
+++ b/src/Modules/Users/Application/Handlers/Commands/ChangeUserEmailCommandHandler.cs
@@ -31,7 +31,7 @@ namespace MeAjudaAi.Modules.Users.Application.Handlers.Commands;
///
/// Repositório para operações de usuário
/// Logger estruturado para auditoria detalhada
-internal sealed class ChangeUserEmailCommandHandler(
+public sealed class ChangeUserEmailCommandHandler(
IUserRepository userRepository,
ILogger logger
) : ICommandHandler>
diff --git a/src/Modules/Users/Application/Handlers/Commands/ChangeUserUsernameCommandHandler.cs b/src/Modules/Users/Application/Handlers/Commands/ChangeUserUsernameCommandHandler.cs
index 904f85cf6..41ebf1b72 100644
--- a/src/Modules/Users/Application/Handlers/Commands/ChangeUserUsernameCommandHandler.cs
+++ b/src/Modules/Users/Application/Handlers/Commands/ChangeUserUsernameCommandHandler.cs
@@ -35,7 +35,7 @@ namespace MeAjudaAi.Modules.Users.Application.Handlers.Commands;
/// Repositório para operações de usuário
/// Provedor de data/hora para testabilidade
/// Logger estruturado para auditoria detalhada
-internal sealed class ChangeUserUsernameCommandHandler(
+public sealed class ChangeUserUsernameCommandHandler(
IUserRepository userRepository,
IDateTimeProvider dateTimeProvider,
ILogger logger
diff --git a/src/Modules/Users/Application/Handlers/Commands/CreateUserCommandHandler.cs b/src/Modules/Users/Application/Handlers/Commands/CreateUserCommandHandler.cs
index bb999f737..743ad7c9b 100644
--- a/src/Modules/Users/Application/Handlers/Commands/CreateUserCommandHandler.cs
+++ b/src/Modules/Users/Application/Handlers/Commands/CreateUserCommandHandler.cs
@@ -88,7 +88,7 @@ public async Task> HandleAsync(
stopwatch.Stop();
logger.LogError(ex, "Unexpected error creating user for email {Email} after {ElapsedMs}ms",
command.Email, stopwatch.ElapsedMilliseconds);
- return Result.Failure($"Failed to create user: {ex.Message}");
+ return Result.Failure("Failed to create user due to an unexpected error");
}
}
diff --git a/src/Modules/Users/Application/Handlers/Commands/UpdateUserProfileCommandHandler.cs b/src/Modules/Users/Application/Handlers/Commands/UpdateUserProfileCommandHandler.cs
index a2c330f30..a329b1178 100644
--- a/src/Modules/Users/Application/Handlers/Commands/UpdateUserProfileCommandHandler.cs
+++ b/src/Modules/Users/Application/Handlers/Commands/UpdateUserProfileCommandHandler.cs
@@ -23,7 +23,7 @@ namespace MeAjudaAi.Modules.Users.Application.Handlers.Commands;
/// Repositório para persistência de usuários
/// Serviço de cache para invalidação
/// Logger estruturado para auditoria e debugging
-internal sealed class UpdateUserProfileCommandHandler(
+public sealed class UpdateUserProfileCommandHandler(
IUserRepository userRepository,
IUsersCacheService usersCacheService,
ILogger logger
diff --git a/src/Modules/Users/Application/Handlers/Queries/GetUserByEmailQueryHandler.cs b/src/Modules/Users/Application/Handlers/Queries/GetUserByEmailQueryHandler.cs
index 7e62c7649..b78acd747 100644
--- a/src/Modules/Users/Application/Handlers/Queries/GetUserByEmailQueryHandler.cs
+++ b/src/Modules/Users/Application/Handlers/Queries/GetUserByEmailQueryHandler.cs
@@ -54,9 +54,23 @@ public async Task> HandleAsync(
try
{
+ // Validate and create email value object
+ Email email;
+ if (!Email.IsValid(query.Email))
+ {
+ logger.LogWarning(
+ "Invalid email format. CorrelationId: {CorrelationId}, Email: {Email}",
+ correlationId, query.Email);
+ return Result.Failure(Error.BadRequest("Invalid email format"));
+ }
+ else
+ {
+ email = new Email(query.Email);
+ }
+
// Busca o usuário pelo email utilizando value object
var user = await userRepository.GetByEmailAsync(
- new Email(query.Email), cancellationToken);
+ email, cancellationToken);
if (user == null)
{
@@ -79,7 +93,7 @@ public async Task> HandleAsync(
"Failed to retrieve user by email. CorrelationId: {CorrelationId}, Email: {Email}",
correlationId, query.Email);
- return Result.Failure($"Failed to retrieve user: {ex.Message}");
+ return Result.Failure(Error.Internal("Failed to retrieve user"));
}
}
}
diff --git a/src/Modules/Users/Application/Handlers/Queries/GetUserByIdQueryHandler.cs b/src/Modules/Users/Application/Handlers/Queries/GetUserByIdQueryHandler.cs
index 106e1c796..ed4b0bb72 100644
--- a/src/Modules/Users/Application/Handlers/Queries/GetUserByIdQueryHandler.cs
+++ b/src/Modules/Users/Application/Handlers/Queries/GetUserByIdQueryHandler.cs
@@ -79,8 +79,8 @@ public async Task> HandleAsync(
}
logger.LogInformation(
- "User found successfully (cache hit/miss handled). CorrelationId: {CorrelationId}, UserId: {UserId}, Email: {Email}",
- correlationId, query.UserId, userDto.Email);
+ "User found successfully (cache hit/miss handled). CorrelationId: {CorrelationId}, UserId: {UserId}",
+ correlationId, query.UserId);
return Result.Success(userDto);
}
@@ -90,7 +90,7 @@ public async Task> HandleAsync(
"Failed to retrieve user by ID. CorrelationId: {CorrelationId}, UserId: {UserId}",
correlationId, query.UserId);
- return Result.Failure($"Failed to retrieve user: {ex.Message}");
+ return Result.Failure(Error.Internal("Failed to retrieve user"));
}
}
}
diff --git a/src/Modules/Users/Application/Handlers/Queries/GetUserByUsernameQueryHandler.cs b/src/Modules/Users/Application/Handlers/Queries/GetUserByUsernameQueryHandler.cs
index 07a3746c9..93f800a54 100644
--- a/src/Modules/Users/Application/Handlers/Queries/GetUserByUsernameQueryHandler.cs
+++ b/src/Modules/Users/Application/Handlers/Queries/GetUserByUsernameQueryHandler.cs
@@ -20,7 +20,7 @@ namespace MeAjudaAi.Modules.Users.Application.Handlers.Queries;
///
/// Repositório para consultas de usuários
/// Logger para auditoria e rastreamento das operações
-internal sealed class GetUserByUsernameQueryHandler(
+public sealed class GetUserByUsernameQueryHandler(
IUserRepository userRepository,
ILogger logger
) : IQueryHandler>
diff --git a/src/Modules/Users/Application/Properties/AssemblyInfo.cs b/src/Modules/Users/Application/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..9c791b4a8
--- /dev/null
+++ b/src/Modules/Users/Application/Properties/AssemblyInfo.cs
@@ -0,0 +1,4 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("MeAjudaAi.Modules.Users.Tests")]
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
\ No newline at end of file
diff --git a/src/Modules/Users/Domain/ValueObjects/Email.cs b/src/Modules/Users/Domain/ValueObjects/Email.cs
index fda82c7be..27710fc11 100644
--- a/src/Modules/Users/Domain/ValueObjects/Email.cs
+++ b/src/Modules/Users/Domain/ValueObjects/Email.cs
@@ -25,6 +25,20 @@ public Email(string value)
public static implicit operator string(Email email) => email.Value;
public static implicit operator Email(string email) => new(email);
+ ///
+ /// Validates if the email format is valid without creating an instance.
+ ///
+ /// Email string to validate
+ /// True if email format is valid, false otherwise
+ public static bool IsValid(string email)
+ {
+ if (string.IsNullOrWhiteSpace(email))
+ return false;
+ if (email.Length > ValidationConstants.UserLimits.EmailMaxLength)
+ return false;
+ return EmailRegex.IsMatch(email);
+ }
+
[GeneratedRegex(@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", RegexOptions.IgnoreCase | RegexOptions.Compiled, "en-US")]
private static partial Regex EmailGeneratedRegex();
}
diff --git a/src/Modules/Users/Infrastructure/Extensions.cs b/src/Modules/Users/Infrastructure/Extensions.cs
index 361032151..8f73c7a16 100644
--- a/src/Modules/Users/Infrastructure/Extensions.cs
+++ b/src/Modules/Users/Infrastructure/Extensions.cs
@@ -6,7 +6,6 @@
using MeAjudaAi.Modules.Users.Infrastructure.Persistence;
using MeAjudaAi.Modules.Users.Infrastructure.Persistence.Repositories;
using MeAjudaAi.Modules.Users.Infrastructure.Services;
-using MeAjudaAi.Modules.Users.Infrastructure.Services.Mock;
using MeAjudaAi.Shared.Database;
using MeAjudaAi.Shared.Events;
using Microsoft.EntityFrameworkCore;
@@ -101,16 +100,12 @@ private static IServiceCollection AddKeycloak(this IServiceCollection services,
// Se Keycloak está explicitamente desabilitado OU não há configuração válida, usa mock
var shouldUseMock = !keycloakEnabled || !hasValidKeycloakConfig;
- if (shouldUseMock)
- {
- // Registra implementação mock quando Keycloak está desabilitado ou sem configuração
- services.AddScoped();
- }
- else
+ if (!shouldUseMock)
{
// Registra serviço real quando Keycloak está habilitado e configurado
services.AddHttpClient();
}
+ // Quando shouldUseMock é true, não registra nada (será necessário configurar manualmente para testes)
return services;
}
@@ -131,18 +126,18 @@ private static IServiceCollection AddDomainServices(this IServiceCollection serv
// Se Keycloak está explicitamente desabilitado OU não há configuração válida, usa mock
var shouldUseMock = !keycloakEnabled || !hasValidKeycloakConfig;
- if (shouldUseMock)
- {
- // Registra implementações mock quando Keycloak está desabilitado ou sem configuração
- services.AddScoped();
- services.AddScoped();
- }
- else
+ if (!shouldUseMock)
{
// Registra serviços reais quando Keycloak está habilitado e configurado
services.AddScoped();
services.AddScoped();
}
+ else
+ {
+ // Registra implementações mock quando Keycloak não está disponível ou configurado
+ services.AddScoped();
+ services.AddScoped();
+ }
return services;
}
diff --git a/src/Modules/Users/Infrastructure/Properties/AssemblyInfo.cs b/src/Modules/Users/Infrastructure/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..9c791b4a8
--- /dev/null
+++ b/src/Modules/Users/Infrastructure/Properties/AssemblyInfo.cs
@@ -0,0 +1,4 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("MeAjudaAi.Modules.Users.Tests")]
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
\ No newline at end of file
diff --git a/src/Modules/Users/Infrastructure/Services/Mock/MockAuthenticationDomainService.cs b/src/Modules/Users/Infrastructure/Services/Mock/MockAuthenticationDomainService.cs
deleted file mode 100644
index 147691cfc..000000000
--- a/src/Modules/Users/Infrastructure/Services/Mock/MockAuthenticationDomainService.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using MeAjudaAi.Modules.Users.Domain.Services;
-using MeAjudaAi.Modules.Users.Domain.Services.Models;
-using MeAjudaAi.Shared.Functional;
-
-namespace MeAjudaAi.Modules.Users.Infrastructure.Services.Mock;
-
-///
-/// 🧪 MOCK DO SERVIÇO DE AUTENTICAÇÃO PARA TESTES
-///
-/// Implementação mock simples para uso quando Keycloak está desabilitado.
-/// Retorna respostas válidas e determinísticas usando MockAuthenticationHelper.
-///
-internal sealed class MockAuthenticationDomainService : IAuthenticationDomainService
-{
- public Task> AuthenticateAsync(string usernameOrEmail, string password, CancellationToken cancellationToken = default)
- {
- var result = MockAuthenticationHelper.CreateMockAuthenticationResult();
- return Task.FromResult(Result.Success(result));
- }
-
- public Task> ValidateTokenAsync(string token, CancellationToken cancellationToken = default)
- {
- var result = MockAuthenticationHelper.CreateMockTokenValidationResult();
- return Task.FromResult(Result.Success(result));
- }
-}
diff --git a/src/Modules/Users/Infrastructure/Services/Mock/MockAuthenticationHelper.cs b/src/Modules/Users/Infrastructure/Services/Mock/MockAuthenticationHelper.cs
deleted file mode 100644
index 7991ad86f..000000000
--- a/src/Modules/Users/Infrastructure/Services/Mock/MockAuthenticationHelper.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-using MeAjudaAi.Modules.Users.Domain.Services.Models;
-using MeAjudaAi.Shared.Constants;
-
-namespace MeAjudaAi.Modules.Users.Infrastructure.Services.Mock;
-
-///
-/// 🧪 HELPER PARA CRIAÇÃO DE DADOS MOCK DE AUTENTICAÇÃO DETERMINÍSTICOS
-///
-/// Centraliza a lógica de criação de dados mock para testes, garantindo
-/// valores determinísticos e evitando duplicação entre os serviços mock.
-///
-internal static class MockAuthenticationHelper
-{
- // Fixed deterministic values for consistent testing
- private static readonly Guid FixedUserId = Guid.Parse("550e8400-e29b-41d4-a716-446655440000");
- private static readonly DateTime FixedExpirationTime = new DateTime(9999, 12, 31, 23, 59, 59, DateTimeKind.Utc);
- private static int _keycloakIdCounter = 0;
-
- public static AuthenticationResult CreateMockAuthenticationResult(string[]? roles = null)
- {
- return new AuthenticationResult(
- UserId: FixedUserId,
- AccessToken: "mock-access-token",
- RefreshToken: "mock-refresh-token",
- ExpiresAt: FixedExpirationTime,
- Roles: roles ?? new[] { "user" }
- );
- }
-
- public static TokenValidationResult CreateMockTokenValidationResult(string[]? roles = null)
- {
- return new TokenValidationResult(
- UserId: FixedUserId,
- Roles: roles ?? new[] { "user" },
- Claims: new Dictionary
- {
- { AuthConstants.Claims.Subject, FixedUserId.ToString() },
- { AuthConstants.Claims.PreferredUsername, "mock-user" },
- { AuthConstants.Claims.Email, "mock@example.com" }
- }
- );
- }
-
- public static string CreateMockKeycloakId(string? userSpecificValue = null)
- {
- if (!string.IsNullOrEmpty(userSpecificValue))
- {
- return $"keycloak-{userSpecificValue}";
- }
-
- // Generate unique ID using counter for thread safety
- var uniqueId = Interlocked.Increment(ref _keycloakIdCounter);
- return $"keycloak-{FixedUserId}-{uniqueId}";
- }
-}
diff --git a/src/Modules/Users/Infrastructure/Services/Mock/MockKeycloakService.cs b/src/Modules/Users/Infrastructure/Services/Mock/MockKeycloakService.cs
deleted file mode 100644
index 2b50563b5..000000000
--- a/src/Modules/Users/Infrastructure/Services/Mock/MockKeycloakService.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-using System.Collections.Concurrent;
-using MeAjudaAi.Modules.Users.Domain.Services.Models;
-using MeAjudaAi.Modules.Users.Infrastructure.Identity.Keycloak;
-using MeAjudaAi.Shared.Functional;
-
-namespace MeAjudaAi.Modules.Users.Infrastructure.Services.Mock;
-
-///
-/// 🧪 MOCK DO SERVIÇO KEYCLOAK PARA TESTES
-///
-/// Implementação mock simples para uso quando Keycloak está desabilitado.
-/// Retorna respostas válidas e determinísticas usando MockAuthenticationHelper.
-///
-internal sealed class MockKeycloakService : IKeycloakService
-{
- // Thread-safe in-memory storage for roles per keycloak user (for testing purposes)
- private static readonly ConcurrentDictionary _userRoles = new();
-
- ///
- /// Resets the internal state of the mock service for test isolation
- ///
- public void Reset()
- {
- _userRoles.Clear();
- }
-
- public Task> CreateUserAsync(string username, string email, string firstName, string lastName, string password, IEnumerable roles, CancellationToken cancellationToken = default)
- {
- var keycloakId = MockAuthenticationHelper.CreateMockKeycloakId();
-
- // Store roles for this mock user for potential future validation
- if (roles != null)
- {
- _userRoles.TryAdd(keycloakId, roles.ToArray());
- }
-
- return Task.FromResult(Result.Success(keycloakId));
- }
-
- public Task> AuthenticateAsync(string usernameOrEmail, string password, CancellationToken cancellationToken = default)
- {
- var result = MockAuthenticationHelper.CreateMockAuthenticationResult();
- return Task.FromResult(Result.Success(result));
- }
-
- public Task> ValidateTokenAsync(string token, CancellationToken cancellationToken = default)
- {
- var result = MockAuthenticationHelper.CreateMockTokenValidationResult();
- return Task.FromResult(Result.Success(result));
- }
-
- public Task DeactivateUserAsync(string keycloakId, CancellationToken cancellationToken = default)
- {
- // Remove from our mock storage when deactivated using thread-safe method
- _userRoles.TryRemove(keycloakId, out _);
- return Task.FromResult(Result.Success());
- }
-}
diff --git a/src/Modules/Users/Infrastructure/Services/Mock/MockUserDomainService.cs b/src/Modules/Users/Infrastructure/Services/Mock/MockUserDomainService.cs
deleted file mode 100644
index d17dd6491..000000000
--- a/src/Modules/Users/Infrastructure/Services/Mock/MockUserDomainService.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using MeAjudaAi.Modules.Users.Domain.Entities;
-using MeAjudaAi.Modules.Users.Domain.Services;
-using MeAjudaAi.Modules.Users.Domain.ValueObjects;
-using MeAjudaAi.Shared.Functional;
-
-namespace MeAjudaAi.Modules.Users.Infrastructure.Services.Mock;
-
-///
-/// 🧪 MOCK DO SERVIÇO DE DOMÍNIO DE USUÁRIOS PARA TESTES
-///
-/// Implementação mock simples para uso quando Keycloak está desabilitado.
-/// Cria usuários mock válidos sem fazer chamadas reais para o Keycloak.
-///
-internal sealed class MockUserDomainService : IUserDomainService
-{
- public Task> CreateUserAsync(Username username, Email email, string firstName, string lastName, string password, IEnumerable roles, CancellationToken cancellationToken = default)
- {
- var user = new User(
- username,
- email,
- firstName,
- lastName,
- $"mock-keycloak-{Guid.NewGuid()}"
- );
-
- return Task.FromResult(Result.Success(user));
- }
-
- public Task SyncUserWithKeycloakAsync(UserId userId, CancellationToken cancellationToken = default)
- {
- // Mock sempre sincroniza com sucesso
- return Task.FromResult(Result.Success());
- }
-}
diff --git a/src/Modules/Users/Infrastructure/Services/MockAuthenticationDomainService.cs b/src/Modules/Users/Infrastructure/Services/MockAuthenticationDomainService.cs
new file mode 100644
index 000000000..870a66456
--- /dev/null
+++ b/src/Modules/Users/Infrastructure/Services/MockAuthenticationDomainService.cs
@@ -0,0 +1,97 @@
+using System.Security.Cryptography;
+using System.Text;
+using MeAjudaAi.Modules.Users.Domain.Services;
+using MeAjudaAi.Modules.Users.Domain.Services.Models;
+using MeAjudaAi.Shared.Functional;
+
+namespace MeAjudaAi.Modules.Users.Infrastructure.Services;
+
+///
+/// Mock implementation of IAuthenticationDomainService for environments where Keycloak is not available.
+/// Provides basic authentication logic for testing and development scenarios.
+///
+internal class MockAuthenticationDomainService : IAuthenticationDomainService
+{
+ ///
+ /// Authenticates users with mock credentials for testing purposes.
+ ///
+ public Task> AuthenticateAsync(
+ string usernameOrEmail,
+ string password,
+ CancellationToken cancellationToken = default)
+ {
+ // Para ambientes de teste/desenvolvimento, aceitar credenciais específicas
+ if ((usernameOrEmail == "testuser" || usernameOrEmail == "test@example.com") && password == "testpassword")
+ {
+ var deterministicUserId = GenerateDeterministicGuid(usernameOrEmail);
+ var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
+
+ var result = new AuthenticationResult(
+ UserId: deterministicUserId,
+ AccessToken: $"mock_token_{deterministicUserId}_{timestamp}",
+ RefreshToken: $"mock_refresh_{deterministicUserId}_{timestamp}",
+ ExpiresAt: DateTime.UtcNow.AddHours(1),
+ Roles: ["customer"]
+ );
+ return Task.FromResult(Result.Success(result));
+ }
+
+ return Task.FromResult(Result.Failure("Invalid credentials"));
+ }
+
+ ///
+ /// Validates mock tokens for testing purposes.
+ ///
+ public Task> ValidateTokenAsync(
+ string token,
+ CancellationToken cancellationToken = default)
+ {
+ // Para ambientes de teste, validar tokens que começam com "mock_token_"
+ if (token.StartsWith("mock_token_"))
+ {
+ // Extrair o userId do token: "mock_token_{userId}_{timestamp}"
+ var parts = token.Split('_');
+ Guid userId;
+
+ if (parts.Length >= 3 && Guid.TryParse(parts[2], out userId))
+ {
+ // Use o userId extraído do token
+ var result = new TokenValidationResult(
+ UserId: userId,
+ Roles: ["customer"],
+ Claims: new Dictionary { ["sub"] = userId.ToString() }
+ );
+ return Task.FromResult(Result.Success(result));
+ }
+ else
+ {
+ // Fallback determinístico se não conseguir extrair o userId
+ var fallbackUserId = GenerateDeterministicGuid("fallback");
+ var result = new TokenValidationResult(
+ UserId: fallbackUserId,
+ Roles: ["customer"],
+ Claims: new Dictionary { ["sub"] = fallbackUserId.ToString() }
+ );
+ return Task.FromResult(Result.Success(result));
+ }
+ }
+
+ return Task.FromResult(Result.Failure("Invalid token"));
+ }
+
+ ///
+ /// Generates a deterministic GUID based on the input string.
+ /// Same input will always produce the same GUID.
+ ///
+ private static Guid GenerateDeterministicGuid(string input)
+ {
+ // Normalize the input to lowercase for consistency
+ var normalizedInput = input.ToLowerInvariant();
+
+ // Generate MD5 hash of the normalized input
+ var hash = MD5.HashData(Encoding.UTF8.GetBytes(normalizedInput));
+
+ // Use the first 16 bytes of the hash to create a GUID
+ return new Guid(hash);
+ }
+}
\ No newline at end of file
diff --git a/src/Modules/Users/Infrastructure/Services/MockUserDomainService.cs b/src/Modules/Users/Infrastructure/Services/MockUserDomainService.cs
new file mode 100644
index 000000000..94b881f51
--- /dev/null
+++ b/src/Modules/Users/Infrastructure/Services/MockUserDomainService.cs
@@ -0,0 +1,41 @@
+using MeAjudaAi.Modules.Users.Domain.Entities;
+using MeAjudaAi.Modules.Users.Domain.Services;
+using MeAjudaAi.Modules.Users.Domain.ValueObjects;
+using MeAjudaAi.Shared.Functional;
+
+namespace MeAjudaAi.Modules.Users.Infrastructure.Services;
+
+///
+/// Mock implementation of IUserDomainService for environments where Keycloak is not available.
+/// This service creates users locally without external authentication integration.
+///
+public class MockUserDomainService : IUserDomainService
+{
+ ///
+ /// Creates a user locally without Keycloak integration.
+ /// Generates a mock Keycloak ID for consistency.
+ ///
+ public Task> CreateUserAsync(
+ Username username,
+ Email email,
+ string firstName,
+ string lastName,
+ string password,
+ IEnumerable roles,
+ CancellationToken cancellationToken = default)
+ {
+ // Para ambientes sem Keycloak, criar usuário mock com ID simulado
+ var user = new User(username, email, firstName, lastName, $"mock_keycloak_{Guid.NewGuid()}");
+ return Task.FromResult(Result.Success(user));
+ }
+
+ ///
+ /// Simulates synchronization with Keycloak.
+ /// Always returns success for mock implementation.
+ ///
+ public Task SyncUserWithKeycloakAsync(UserId userId, CancellationToken cancellationToken = default)
+ {
+ // Para ambientes sem Keycloak, simular sincronização bem-sucedida
+ return Task.FromResult(Result.Success());
+ }
+}
\ No newline at end of file
diff --git a/src/Modules/Users/Tests/Integration/Infrastructure/UserRepositoryTests.cs b/src/Modules/Users/Tests/Integration/UserRepositoryIntegrationTests.cs
similarity index 100%
rename from src/Modules/Users/Tests/Integration/Infrastructure/UserRepositoryTests.cs
rename to src/Modules/Users/Tests/Integration/UserRepositoryIntegrationTests.cs
diff --git a/src/Modules/Users/Tests/Integration/Services/UsersModuleApiIntegrationTests.cs b/src/Modules/Users/Tests/Integration/UsersModuleApiIntegrationTests.cs
similarity index 100%
rename from src/Modules/Users/Tests/Integration/Services/UsersModuleApiIntegrationTests.cs
rename to src/Modules/Users/Tests/Integration/UsersModuleApiIntegrationTests.cs
diff --git a/src/Modules/Users/Tests/Unit/Infrastructure/Services/Mock/MockAuthenticationHelperTests.cs b/src/Modules/Users/Tests/Unit/Infrastructure/Services/Mock/MockAuthenticationHelperTests.cs
deleted file mode 100644
index 228ecf5d9..000000000
--- a/src/Modules/Users/Tests/Unit/Infrastructure/Services/Mock/MockAuthenticationHelperTests.cs
+++ /dev/null
@@ -1,78 +0,0 @@
-using FluentAssertions;
-using MeAjudaAi.Modules.Users.Infrastructure.Services.Mock;
-using MeAjudaAi.Shared.Constants;
-
-namespace MeAjudaAi.Modules.Users.Tests.Unit.Infrastructure.Services.Mock;
-
-///
-/// Tests for MockAuthenticationHelper to verify deterministic and unique behavior
-///
-[Trait("Category", "Unit")]
-[Trait("Module", "Users")]
-[Trait("Component", "MockServices")]
-public class MockAuthenticationHelperTests
-{
- [Fact]
- public void CreateMockKeycloakId_WithoutParameter_ShouldReturnUniqueIds()
- {
- // Act
- var id1 = MockAuthenticationHelper.CreateMockKeycloakId();
- var id2 = MockAuthenticationHelper.CreateMockKeycloakId();
- var id3 = MockAuthenticationHelper.CreateMockKeycloakId();
-
- // Assert
- id1.Should().NotBe(id2);
- id2.Should().NotBe(id3);
- id1.Should().NotBe(id3);
-
- // All should start with "keycloak-" prefix
- id1.Should().StartWith("keycloak-");
- id2.Should().StartWith("keycloak-");
- id3.Should().StartWith("keycloak-");
- }
-
- [Fact]
- public void CreateMockKeycloakId_WithUserSpecificValue_ShouldReturnDeterministicId()
- {
- // Act
- var id1 = MockAuthenticationHelper.CreateMockKeycloakId("user123");
- var id2 = MockAuthenticationHelper.CreateMockKeycloakId("user123");
- var id3 = MockAuthenticationHelper.CreateMockKeycloakId("user456");
-
- // Assert
- id1.Should().Be(id2); // Same user should get same ID
- id1.Should().NotBe(id3); // Different users should get different IDs
-
- id1.Should().Be("keycloak-user123");
- id3.Should().Be("keycloak-user456");
- }
-
- [Fact]
- public void CreateMockAuthenticationResult_ShouldReturnValidNonExpiredToken()
- {
- // Act
- var result = MockAuthenticationHelper.CreateMockAuthenticationResult();
-
- // Assert
- result.ExpiresAt.Should().BeAfter(DateTime.UtcNow.AddYears(1000)); // Far future
- result.UserId.Should().NotBe(Guid.Empty);
- result.AccessToken.Should().NotBeNullOrEmpty();
- result.RefreshToken.Should().NotBeNullOrEmpty();
- result.Roles.Should().NotBeNull();
- }
-
- [Fact]
- public void CreateMockTokenValidationResult_ShouldReturnValidResult()
- {
- // Act
- var result = MockAuthenticationHelper.CreateMockTokenValidationResult();
-
- // Assert
- result.UserId.Should().NotBe(Guid.Empty);
- result.Roles.Should().NotBeNull();
- result.Claims.Should().NotBeNull().And.NotBeEmpty();
- result.Claims.Should().ContainKey(AuthConstants.Claims.Subject);
- result.Claims.Should().ContainKey(AuthConstants.Claims.PreferredUsername);
- result.Claims.Should().ContainKey(AuthConstants.Claims.Email);
- }
-}
diff --git a/src/Modules/Users/Tests/Unit/Infrastructure/Services/Mock/MockKeycloakServiceTests.cs b/src/Modules/Users/Tests/Unit/Mocks/Services/MockKeycloakServiceTests.cs
similarity index 62%
rename from src/Modules/Users/Tests/Unit/Infrastructure/Services/Mock/MockKeycloakServiceTests.cs
rename to src/Modules/Users/Tests/Unit/Mocks/Services/MockKeycloakServiceTests.cs
index d5a3f5367..e5633e1f6 100644
--- a/src/Modules/Users/Tests/Unit/Infrastructure/Services/Mock/MockKeycloakServiceTests.cs
+++ b/src/Modules/Users/Tests/Unit/Mocks/Services/MockKeycloakServiceTests.cs
@@ -1,7 +1,7 @@
using FluentAssertions;
-using MeAjudaAi.Modules.Users.Infrastructure.Services.Mock;
+using MeAjudaAi.Modules.Users.Tests.Infrastructure.Mocks;
-namespace MeAjudaAi.Modules.Users.Tests.Unit.Infrastructure.Services.Mock;
+namespace MeAjudaAi.Modules.Users.Tests.Unit.Mocks.Services;
///
/// Tests for MockKeycloakService to verify core functionality
@@ -16,7 +16,6 @@ public class MockKeycloakServiceTests
public MockKeycloakServiceTests()
{
_service = new MockKeycloakService();
- _service.Reset(); // Clear shared state between tests
}
[Fact]
@@ -36,21 +35,21 @@ public async Task CreateUserAsync_ShouldReturnSuccessWithKeycloakId()
// Assert
result.IsSuccess.Should().BeTrue();
result.Value.Should().NotBeNullOrEmpty();
- result.Value.Should().StartWith("keycloak-");
+ result.Value.Should().StartWith("keycloak_");
}
[Fact]
public async Task AuthenticateAsync_ShouldReturnSuccessWithValidToken()
{
// Act
- var result = await _service.AuthenticateAsync("testuser", "password123");
+ var result = await _service.AuthenticateAsync("validuser", "validpassword");
// Assert
result.IsSuccess.Should().BeTrue();
result.Value.Should().NotBeNull();
result.Value.AccessToken.Should().NotBeNullOrEmpty();
result.Value.RefreshToken.Should().NotBeNullOrEmpty();
- result.Value.ExpiresAt.Should().BeAfter(DateTime.UtcNow.AddYears(1000));
+ result.Value.ExpiresAt.Should().BeAfter(DateTime.UtcNow);
result.Value.UserId.Should().NotBe(Guid.Empty);
}
@@ -58,7 +57,7 @@ public async Task AuthenticateAsync_ShouldReturnSuccessWithValidToken()
public async Task ValidateTokenAsync_ShouldReturnSuccessWithValidResult()
{
// Act
- var result = await _service.ValidateTokenAsync("mock-token");
+ var result = await _service.ValidateTokenAsync("mock_token_123");
// Assert
result.IsSuccess.Should().BeTrue();
@@ -82,28 +81,5 @@ public async Task DeactivateUserAsync_ShouldReturnSuccess()
result.IsSuccess.Should().BeTrue();
}
- [Fact]
- public async Task ConcurrentUserCreation_ShouldBeThreadSafe()
- {
- // Arrange
- var userCount = 50;
- var tasks = new List>();
- // Act - Create users concurrently
- for (int i = 0; i < userCount; i++)
- {
- int userId = i; // Capture loop variable
- tasks.Add(Task.Run(async () =>
- {
- var result = await _service.CreateUserAsync($"user{userId}", $"user{userId}@example.com", "Test", "User", "password", new[] { "user" });
- return result.Value;
- }));
- }
-
- var keycloakIds = await Task.WhenAll(tasks);
-
- // Assert - All IDs should be unique
- keycloakIds.Should().OnlyHaveUniqueItems();
- keycloakIds.Should().AllSatisfy(id => id.Should().StartWith("keycloak-"));
- }
}
diff --git a/src/Shared/Authorization/Middleware/PermissionOptimizationMiddleware.cs b/src/Shared/Authorization/Middleware/PermissionOptimizationMiddleware.cs
index 33af02ca0..c8fa4cabb 100644
--- a/src/Shared/Authorization/Middleware/PermissionOptimizationMiddleware.cs
+++ b/src/Shared/Authorization/Middleware/PermissionOptimizationMiddleware.cs
@@ -155,7 +155,7 @@ private void ApplyReadOnlyOptimizations(HttpContext context)
return;
// Para operações de leitura em endpoints específicos, pode usar cache mais agressivo
- if (path.StartsWith("/api/users/profile", StringComparison.OrdinalIgnoreCase) ||
+ if (path.StartsWith("/api/v1/users/profile", StringComparison.OrdinalIgnoreCase) ||
path.StartsWith(ApiEndpoints.System.Health, StringComparison.OrdinalIgnoreCase))
{
context.Items["UseAggressivePermissionCache"] = true;
@@ -163,7 +163,8 @@ private void ApplyReadOnlyOptimizations(HttpContext context)
}
else if (path.StartsWith("/api/") && context.Request.Method == "GET")
{
- // Operações GET em APIs podem usar cache intermediário
+ // Catch-all para operações GET em qualquer versão da API - cache intermediário
+ // Suporta múltiplas versões da API (v1, v2, etc.) para compatibilidade
context.Items["UseAggressivePermissionCache"] = false;
context.Items["PermissionCacheDuration"] = TimeSpan.FromMinutes(10);
}
@@ -177,7 +178,7 @@ private static List GetRequiredPermissionsForPath(string path, stri
var permissions = new List();
// Users module
- if (path.StartsWith("/api/users"))
+ if (path.StartsWith("/api/v1/users"))
{
permissions.AddRange(method.ToUpperInvariant() switch
{
@@ -191,8 +192,8 @@ private static List GetRequiredPermissionsForPath(string path, stri
});
}
- // Providers module (futuro)
- else if (path.StartsWith("/api/providers"))
+ // Providers module
+ else if (path.StartsWith("/api/v1/providers"))
{
permissions.AddRange(method.ToUpperInvariant() switch
{
@@ -204,8 +205,8 @@ private static List GetRequiredPermissionsForPath(string path, stri
});
}
- // Orders module (futuro)
- else if (path.StartsWith("/api/orders"))
+ // Orders module (futuro) - Aguardando implementação do módulo completo
+ else if (path.StartsWith("/api/v1/orders"))
{
permissions.AddRange(method.ToUpperInvariant() switch
{
@@ -217,8 +218,8 @@ private static List GetRequiredPermissionsForPath(string path, stri
});
}
- // Reports module (futuro)
- else if (path.StartsWith("/api/reports"))
+ // Reports module (futuro) - Aguardando implementação do módulo completo
+ else if (path.StartsWith("/api/v1/reports"))
{
permissions.AddRange(method.ToUpperInvariant() switch
{
@@ -229,8 +230,8 @@ private static List GetRequiredPermissionsForPath(string path, stri
});
}
- // Admin endpoints
- else if (path.StartsWith("/api/admin") || path.Contains("/admin"))
+ // Admin endpoints - Verifica /api/v1/admin ou qualquer segmento "admin" no path
+ else if (path.StartsWith("/api/v1/admin") || IsAdminPath(path))
{
permissions.Add(EPermission.AdminSystem);
}
@@ -238,6 +239,19 @@ private static List GetRequiredPermissionsForPath(string path, stri
return permissions;
}
+ ///
+ /// Verifica se o path contém "admin" como um segmento distinto.
+ ///
+ private static bool IsAdminPath(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ return false;
+
+ var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
+ return segments.Any(segment =>
+ string.Equals(segment, "admin", StringComparison.OrdinalIgnoreCase));
+ }
+
///
/// Verifica se o endpoint é público e não precisa de autenticação.
///
diff --git a/src/Shared/MeAjudaAi.Shared.csproj b/src/Shared/MeAjudaAi.Shared.csproj
index ada974203..609bfee4e 100644
--- a/src/Shared/MeAjudaAi.Shared.csproj
+++ b/src/Shared/MeAjudaAi.Shared.csproj
@@ -4,6 +4,7 @@
net9.0
enable
enable
+ false
diff --git a/src/Shared/Messaging/Messages/Providers/ProviderDeletedIntegrationEvent.cs b/src/Shared/Messaging/Messages/Providers/ProviderDeletedIntegrationEvent.cs
new file mode 100644
index 000000000..2ea6c720a
--- /dev/null
+++ b/src/Shared/Messaging/Messages/Providers/ProviderDeletedIntegrationEvent.cs
@@ -0,0 +1,24 @@
+using MeAjudaAi.Shared.Events;
+
+namespace MeAjudaAi.Shared.Messaging.Messages.Providers;
+
+///
+/// Evento de integração disparado quando um prestador de serviços é excluído do sistema.
+///
+///
+/// Este evento é publicado para comunicação entre módulos quando um prestador é excluído (soft delete).
+/// Outros módulos podem usar este evento para:
+/// - Cancelar serviços associados
+/// - Enviar notificações de encerramento
+/// - Arquivar dados relacionados
+/// - Atualizar estatísticas
+///
+public sealed record ProviderDeletedIntegrationEvent(
+ string Source,
+ Guid ProviderId,
+ Guid UserId,
+ string Name,
+ string Reason,
+ DateTime DeletedAt,
+ string? DeletedBy = null
+) : IntegrationEvent(Source);
\ No newline at end of file
diff --git a/src/Shared/Messaging/Messages/Providers/ProviderProfileUpdatedIntegrationEvent.cs b/src/Shared/Messaging/Messages/Providers/ProviderProfileUpdatedIntegrationEvent.cs
new file mode 100644
index 000000000..8a4511d52
--- /dev/null
+++ b/src/Shared/Messaging/Messages/Providers/ProviderProfileUpdatedIntegrationEvent.cs
@@ -0,0 +1,26 @@
+using MeAjudaAi.Shared.Events;
+
+namespace MeAjudaAi.Shared.Messaging.Messages.Providers;
+
+///
+/// Evento de integração disparado quando o perfil de um prestador é atualizado.
+///
+///
+/// Este evento é publicado para comunicação entre módulos quando dados do prestador são alterados.
+/// Outros módulos podem usar este evento para:
+/// - Sincronizar informações em caches
+/// - Atualizar índices de busca
+/// - Notificar sistemas externos
+/// - Manter auditoria de mudanças
+///
+public sealed record ProviderProfileUpdatedIntegrationEvent(
+ string Source,
+ Guid ProviderId,
+ Guid UserId,
+ string Name,
+ IEnumerable UpdatedFields,
+ string? UpdatedBy = null,
+ string? PreviousName = null,
+ string? NewEmail = null,
+ string? NewPhoneNumber = null
+) : IntegrationEvent(Source);
\ No newline at end of file
diff --git a/src/Shared/Messaging/Messages/Providers/ProviderRegisteredIntegrationEvent.cs b/src/Shared/Messaging/Messages/Providers/ProviderRegisteredIntegrationEvent.cs
new file mode 100644
index 000000000..cc9986a31
--- /dev/null
+++ b/src/Shared/Messaging/Messages/Providers/ProviderRegisteredIntegrationEvent.cs
@@ -0,0 +1,27 @@
+using MeAjudaAi.Shared.Events;
+
+namespace MeAjudaAi.Shared.Messaging.Messages.Providers;
+
+///
+/// Evento de integração disparado quando um novo prestador de serviços é registrado no sistema.
+///
+///
+/// Este evento é publicado para comunicação entre módulos quando um prestador é criado.
+/// Outros módulos podem usar este evento para:
+/// - Criar perfis associados
+/// - Enviar notificações de boas-vindas
+/// - Sincronizar dados com sistemas externos
+/// - Atualizar estatísticas e métricas
+///
+public sealed record ProviderRegisteredIntegrationEvent(
+ string Source,
+ Guid ProviderId,
+ Guid UserId,
+ string Name,
+ string ProviderType,
+ string Email,
+ string? PhoneNumber = null,
+ string? City = null,
+ string? State = null,
+ DateTime? RegisteredAt = null
+) : IntegrationEvent(Source);
\ No newline at end of file
diff --git a/src/Shared/Messaging/Messages/Providers/ProviderVerificationStatusUpdatedIntegrationEvent.cs b/src/Shared/Messaging/Messages/Providers/ProviderVerificationStatusUpdatedIntegrationEvent.cs
new file mode 100644
index 000000000..ec79d3b33
--- /dev/null
+++ b/src/Shared/Messaging/Messages/Providers/ProviderVerificationStatusUpdatedIntegrationEvent.cs
@@ -0,0 +1,25 @@
+using MeAjudaAi.Shared.Events;
+
+namespace MeAjudaAi.Shared.Messaging.Messages.Providers;
+
+///
+/// Evento de integração disparado quando o status de verificação de um prestador é atualizado.
+///
+///
+/// Este evento é publicado para comunicação entre módulos quando um prestador tem seu status alterado.
+/// Outros módulos podem usar este evento para:
+/// - Enviar notificações de aprovação/reprovação
+/// - Atualizar permissões de acesso
+/// - Sincronizar com sistemas externos
+/// - Gerar relatórios de conformidade
+///
+public sealed record ProviderVerificationStatusUpdatedIntegrationEvent(
+ string Source,
+ Guid ProviderId,
+ Guid UserId,
+ string Name,
+ string PreviousStatus,
+ string NewStatus,
+ string? UpdatedBy = null,
+ string? Comments = null
+) : IntegrationEvent(Source);
\ No newline at end of file
diff --git a/tests/MeAjudaAi.E2E.Tests/Authorization/PermissionAuthorizationE2ETests.cs b/tests/MeAjudaAi.E2E.Tests/Authorization/PermissionAuthorizationE2ETests.cs
index 594443b34..152622b6e 100644
--- a/tests/MeAjudaAi.E2E.Tests/Authorization/PermissionAuthorizationE2ETests.cs
+++ b/tests/MeAjudaAi.E2E.Tests/Authorization/PermissionAuthorizationE2ETests.cs
@@ -36,27 +36,27 @@ public async Task BasicUserWorkflow_ShouldHaveAppropriateAccess()
// Act & Assert - Operações que o usuário básico PODE fazer
// 1. Ver seu próprio perfil
- var profileResponse = await _client.GetAsync("/api/users/profile");
+ var profileResponse = await _client.GetAsync("/api/v1/users/profile");
Assert.Equal(HttpStatusCode.OK, profileResponse.StatusCode);
// 2. Ler informações básicas de usuários
- var readResponse = await _client.GetAsync("/api/users/basic-info");
+ var readResponse = await _client.GetAsync("/api/v1/users/basic-info");
Assert.Equal(HttpStatusCode.OK, readResponse.StatusCode);
// Act & Assert - Operações que o usuário básico NÃO PODE fazer
// 3. Criar usuários (deve retornar Forbidden)
var createUserPayload = new { name = "New User", email = "new@test.com" };
- var createResponse = await _client.PostAsync("/api/users",
+ var createResponse = await _client.PostAsync("/api/v1/users",
new StringContent(JsonSerializer.Serialize(createUserPayload), Encoding.UTF8, "application/json"));
Assert.Equal(HttpStatusCode.Forbidden, createResponse.StatusCode);
// 4. Deletar usuários (deve retornar Forbidden)
- var deleteResponse = await _client.DeleteAsync("/api/users/some-user-id");
+ var deleteResponse = await _client.DeleteAsync("/api/v1/users/some-user-id");
Assert.Equal(HttpStatusCode.Forbidden, deleteResponse.StatusCode);
// 5. Acessar área administrativa (deve retornar Forbidden)
- var adminResponse = await _client.GetAsync("/api/users/admin");
+ var adminResponse = await _client.GetAsync("/api/v1/users/admin");
Assert.Equal(HttpStatusCode.Forbidden, adminResponse.StatusCode);
}
@@ -78,29 +78,29 @@ public async Task UserAdminWorkflow_ShouldHaveAdministrativeAccess()
// Act & Assert - Operações administrativas que PODE fazer
// 1. Listar todos os usuários
- var listResponse = await _client.GetAsync("/api/users");
+ var listResponse = await _client.GetAsync("/api/v1/users");
Assert.Equal(HttpStatusCode.OK, listResponse.StatusCode);
// 2. Criar usuários
var createUserPayload = new { name = "Admin Created User", email = "admin@test.com" };
- var createResponse = await _client.PostAsync("/api/users",
+ var createResponse = await _client.PostAsync("/api/v1/users",
new StringContent(JsonSerializer.Serialize(createUserPayload), Encoding.UTF8, "application/json"));
Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
// 3. Atualizar usuários
var updatePayload = new { name = "Updated Name" };
- var updateResponse = await _client.PutAsync("/api/users/some-user-id",
+ var updateResponse = await _client.PutAsync("/api/v1/users/some-user-id",
new StringContent(JsonSerializer.Serialize(updatePayload), Encoding.UTF8, "application/json"));
Assert.Equal(HttpStatusCode.OK, updateResponse.StatusCode);
// 4. Acessar área administrativa de usuários
- var adminResponse = await _client.GetAsync("/api/users/admin");
+ var adminResponse = await _client.GetAsync("/api/v1/users/admin");
Assert.Equal(HttpStatusCode.OK, adminResponse.StatusCode);
// Act & Assert - Operações que NÃO PODE fazer (sem permissão de delete)
// 5. Deletar usuários (deve retornar Forbidden - precisa de permissão específica)
- var deleteResponse = await _client.DeleteAsync("/api/users/some-user-id");
+ var deleteResponse = await _client.DeleteAsync("/api/v1/users/some-user-id");
Assert.Equal(HttpStatusCode.Forbidden, deleteResponse.StatusCode);
// 6. Operações de sistema (deve retornar Forbidden)
@@ -128,18 +128,18 @@ public async Task SystemAdminWorkflow_ShouldHaveFullAccess()
// Act & Assert - Deve ter acesso completo
// 1. Todas as operações de usuários
- var listResponse = await _client.GetAsync("/api/users");
+ var listResponse = await _client.GetAsync("/api/v1/users");
Assert.Equal(HttpStatusCode.OK, listResponse.StatusCode);
- var createResponse = await _client.PostAsync("/api/users",
+ var createResponse = await _client.PostAsync("/api/v1/users",
new StringContent(JsonSerializer.Serialize(new { name = "System User" }), Encoding.UTF8, "application/json"));
Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
- var updateResponse = await _client.PutAsync("/api/users/some-user-id",
+ var updateResponse = await _client.PutAsync("/api/v1/users/some-user-id",
new StringContent(JsonSerializer.Serialize(new { name = "Updated" }), Encoding.UTF8, "application/json"));
Assert.Equal(HttpStatusCode.OK, updateResponse.StatusCode);
- var deleteResponse = await _client.DeleteAsync("/api/users/some-user-id");
+ var deleteResponse = await _client.DeleteAsync("/api/v1/users/some-user-id");
Assert.Equal(HttpStatusCode.OK, deleteResponse.StatusCode);
// 2. Operações de sistema
@@ -147,7 +147,7 @@ public async Task SystemAdminWorkflow_ShouldHaveFullAccess()
Assert.Equal(HttpStatusCode.OK, systemResponse.StatusCode);
// 3. Área administrativa completa
- var adminResponse = await _client.GetAsync("/api/users/admin");
+ var adminResponse = await _client.GetAsync("/api/v1/users/admin");
Assert.Equal(HttpStatusCode.OK, adminResponse.StatusCode);
}
@@ -164,11 +164,11 @@ public async Task ModuleSpecificPermissions_ShouldIsolateAccess()
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", usersOnlyToken);
// Act & Assert - Acesso ao módulo Users
- var usersResponse = await _client.GetAsync("/api/users");
+ var usersResponse = await _client.GetAsync("/api/v1/users");
Assert.Equal(HttpStatusCode.OK, usersResponse.StatusCode);
// Act & Assert - Sem acesso a outros módulos (quando implementados)
- var providersResponse = await _client.GetAsync("/api/providers");
+ var providersResponse = await _client.GetAsync("/api/v1/providers");
Assert.Equal(HttpStatusCode.Forbidden, providersResponse.StatusCode);
var ordersResponse = await _client.GetAsync("/api/orders");
@@ -197,8 +197,8 @@ public async Task ConcurrentUsersSameResource_ShouldRespectIndividualPermissions
adminClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", adminUserToken);
// Act & Assert - Operação que apenas admin pode fazer
- var basicUserDeleteResponse = await basicClient.DeleteAsync("/api/users/test-user");
- var adminUserDeleteResponse = await adminClient.DeleteAsync("/api/users/test-user");
+ var basicUserDeleteResponse = await basicClient.DeleteAsync("/api/v1/users/test-user");
+ var adminUserDeleteResponse = await adminClient.DeleteAsync("/api/v1/users/test-user");
// Assert
Assert.Equal(HttpStatusCode.Forbidden, basicUserDeleteResponse.StatusCode);
@@ -222,7 +222,7 @@ public async Task PermissionCaching_ShouldWorkAcrossRequests()
for (int i = 0; i < 5; i++)
{
- var response = await _client.GetAsync("/api/users/profile");
+ var response = await _client.GetAsync("/api/v1/users/profile");
responses.Add(response);
}
@@ -241,7 +241,7 @@ public async Task TokenExpiredOrInvalid_ShouldReturnUnauthorized()
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "invalid-token");
// Act
- var response = await _client.GetAsync("/api/users/profile");
+ var response = await _client.GetAsync("/api/v1/users/profile");
// Assert
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
@@ -259,7 +259,7 @@ public async Task MissingRequiredPermission_ShouldReturnForbiddenWithDetails()
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", limitedToken);
// Act
- var createResponse = await _client.PostAsync("/api/users",
+ var createResponse = await _client.PostAsync("/api/v1/users",
new StringContent(JsonSerializer.Serialize(new { name = "Test" }), Encoding.UTF8, "application/json"));
// Assert
diff --git a/tests/MeAjudaAi.E2E.Tests/Base/TestContainerTestBase.cs b/tests/MeAjudaAi.E2E.Tests/Base/TestContainerTestBase.cs
index 6f67ed3e3..63bd57493 100644
--- a/tests/MeAjudaAi.E2E.Tests/Base/TestContainerTestBase.cs
+++ b/tests/MeAjudaAi.E2E.Tests/Base/TestContainerTestBase.cs
@@ -71,20 +71,22 @@ public virtual async ValueTask InitializeAsync()
["Keycloak:Enabled"] = "false",
["Cache:Enabled"] = "false", // Disable Redis for now
["Cache:ConnectionString"] = _redisContainer.GetConnectionString(),
- // Configuração de Rate Limiting para testes - valores muito altos para evitar bloqueios
- ["AdvancedRateLimit:Anonymous:RequestsPerMinute"] = "10000",
- ["AdvancedRateLimit:Anonymous:RequestsPerHour"] = "100000",
- ["AdvancedRateLimit:Anonymous:RequestsPerDay"] = "1000000",
- ["AdvancedRateLimit:Authenticated:RequestsPerMinute"] = "10000",
- ["AdvancedRateLimit:Authenticated:RequestsPerHour"] = "100000",
- ["AdvancedRateLimit:Authenticated:RequestsPerDay"] = "1000000",
- ["AdvancedRateLimit:General:WindowInSeconds"] = "60",
- ["AdvancedRateLimit:General:EnableIpWhitelist"] = "false",
+ // Desabilitar completamente Rate Limiting nos testes E2E
+ ["AdvancedRateLimit:General:Enabled"] = "false",
+ // Valores de fallback muito altos caso não consiga desabilitar
+ ["AdvancedRateLimit:Anonymous:RequestsPerMinute"] = "999999",
+ ["AdvancedRateLimit:Anonymous:RequestsPerHour"] = "999999",
+ ["AdvancedRateLimit:Anonymous:RequestsPerDay"] = "999999",
+ ["AdvancedRateLimit:Authenticated:RequestsPerMinute"] = "999999",
+ ["AdvancedRateLimit:Authenticated:RequestsPerHour"] = "999999",
+ ["AdvancedRateLimit:Authenticated:RequestsPerDay"] = "999999",
+ ["AdvancedRateLimit:General:WindowInSeconds"] = "3600",
+ ["AdvancedRateLimit:General:EnableIpWhitelist"] = "true",
// Configuração legada também para garantir
- ["RateLimit:DefaultRequestsPerMinute"] = "10000",
- ["RateLimit:AuthRequestsPerMinute"] = "10000",
- ["RateLimit:SearchRequestsPerMinute"] = "10000",
- ["RateLimit:WindowInSeconds"] = "60"
+ ["RateLimit:DefaultRequestsPerMinute"] = "999999",
+ ["RateLimit:AuthRequestsPerMinute"] = "999999",
+ ["RateLimit:SearchRequestsPerMinute"] = "999999",
+ ["RateLimit:WindowInSeconds"] = "3600"
});
// Adicionar ambiente de teste
@@ -115,6 +117,20 @@ public virtual async ValueTask InitializeAsync()
warnings.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning));
});
+ // Reconfigurar ProvidersDbContext com TestContainer connection string
+ var providersDescriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions));
+ if (providersDescriptor != null)
+ services.Remove(providersDescriptor);
+
+ services.AddDbContext(options =>
+ {
+ options.UseNpgsql(_postgresContainer.GetConnectionString())
+ .UseSnakeCaseNamingConvention()
+ .EnableSensitiveDataLogging(false)
+ .ConfigureWarnings(warnings =>
+ warnings.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning));
+ });
+
// Substituir IKeycloakService por MockKeycloakService para testes
var keycloakDescriptor = services.SingleOrDefault(d => d.ServiceType == typeof(IKeycloakService));
if (keycloakDescriptor != null)
@@ -148,8 +164,7 @@ public virtual async ValueTask InitializeAsync()
services.AddScoped>(provider => () =>
{
var context = provider.GetRequiredService();
- // Aplicar migrações apenas em testes
- context.Database.Migrate();
+ // Migrations are applied explicitly in ApplyMigrationsAsync, no action needed here
return context;
});
});
@@ -212,15 +227,16 @@ private async Task ApplyMigrationsAsync()
{
using var scope = _factory.Services.CreateScope();
- // Aplicar migrações no UsersDbContext
+ // Garantir que o banco está limpo primeiro
var usersContext = scope.ServiceProvider.GetRequiredService();
await usersContext.Database.EnsureDeletedAsync();
- await usersContext.Database.EnsureCreatedAsync();
- // Aplicar migrações no ProvidersDbContext
+ // Aplicar migrações no UsersDbContext (isso cria o banco e o schema users)
+ await usersContext.Database.MigrateAsync();
+
+ // Para ProvidersDbContext, só aplicar migrações (o banco já existe, só precisamos do schema providers)
var providersContext = scope.ServiceProvider.GetRequiredService();
- await providersContext.Database.EnsureDeletedAsync();
- await providersContext.Database.EnsureCreatedAsync();
+ await providersContext.Database.MigrateAsync();
}
// Helper methods usando serialização compartilhada
@@ -307,12 +323,8 @@ protected static void AuthenticateAsAnonymous()
}
protected async Task PostJsonAsync(Uri requestUri, T content)
- {
- throw new NotImplementedException();
- }
+ => await PostJsonAsync(requestUri.ToString(), content);
protected async Task PutJsonAsync(Uri requestUri, T content)
- {
- throw new NotImplementedException();
- }
+ => await PutJsonAsync(requestUri.ToString(), content);
}
diff --git a/tests/MeAjudaAi.Integration.Tests/Auth/AuthenticationTests.cs b/tests/MeAjudaAi.Integration.Tests/Auth/AuthenticationTests.cs
index f2c0915eb..6e16b8aeb 100644
--- a/tests/MeAjudaAi.Integration.Tests/Auth/AuthenticationTests.cs
+++ b/tests/MeAjudaAi.Integration.Tests/Auth/AuthenticationTests.cs
@@ -8,7 +8,7 @@ namespace MeAjudaAi.Integration.Tests.Auth;
///
/// Testes para verificar se o sistema de autenticação mock está funcionando
///
-public class AuthenticationTests : InstanceApiTestBase
+public class AuthenticationTests : ApiTestBase
{
[Fact]
public async Task GetUsers_WithoutAuthentication_ShouldReturnUnauthorized()
diff --git a/tests/MeAjudaAi.Integration.Tests/Authorization/InstancePermissionAuthorizationIntegrationTests.cs b/tests/MeAjudaAi.Integration.Tests/Authorization/InstancePermissionAuthorizationIntegrationTests.cs
index 35be64851..f254ed746 100644
--- a/tests/MeAjudaAi.Integration.Tests/Authorization/InstancePermissionAuthorizationIntegrationTests.cs
+++ b/tests/MeAjudaAi.Integration.Tests/Authorization/InstancePermissionAuthorizationIntegrationTests.cs
@@ -11,7 +11,7 @@ namespace MeAjudaAi.Integration.Tests.Authorization;
/// Elimina condições de corrida e flakiness causados por estado estático
///
[Collection("IntegrationTests")]
-public class InstancePermissionAuthorizationIntegrationTests : InstanceApiTestBase
+public class InstancePermissionAuthorizationIntegrationTests : ApiTestBase
{
[Fact]
public async Task AdminUser_ShouldHaveAccessToAllEndpoints()
diff --git a/tests/MeAjudaAi.Integration.Tests/Authorization/PermissionAuthorizationIntegrationTests.cs b/tests/MeAjudaAi.Integration.Tests/Authorization/PermissionAuthorizationIntegrationTests.cs
index 5bb526605..358358f75 100644
--- a/tests/MeAjudaAi.Integration.Tests/Authorization/PermissionAuthorizationIntegrationTests.cs
+++ b/tests/MeAjudaAi.Integration.Tests/Authorization/PermissionAuthorizationIntegrationTests.cs
@@ -23,7 +23,7 @@ namespace MeAjudaAi.Integration.Tests.Authorization;
///
/// Testes de integração para o sistema de autorização baseado em permissões.
///
-public class PermissionAuthorizationIntegrationTests : InstanceApiTestBase
+public class PermissionAuthorizationIntegrationTests : ApiTestBase
{
private readonly ITestOutputHelper _output;
diff --git a/tests/MeAjudaAi.Integration.Tests/Base/ApiTestBase.cs b/tests/MeAjudaAi.Integration.Tests/Base/ApiTestBase.cs
index f6ed68206..2880eb989 100644
--- a/tests/MeAjudaAi.Integration.Tests/Base/ApiTestBase.cs
+++ b/tests/MeAjudaAi.Integration.Tests/Base/ApiTestBase.cs
@@ -13,8 +13,9 @@
namespace MeAjudaAi.Integration.Tests.Base;
///
-/// Classe base simplificada para testes de integração
-/// Cria containers individuais para máxima compatibilidade com CI
+/// Classe base unificada para testes de integração com suporte a autenticação baseada em instância.
+/// Elimina condições de corrida e instabilidade causadas por estado estático.
+/// Cria containers individuais para máxima compatibilidade com CI.
///
public abstract class ApiTestBase : IAsyncLifetime
{
@@ -23,10 +24,11 @@ public abstract class ApiTestBase : IAsyncLifetime
protected HttpClient Client { get; private set; } = null!;
protected IServiceProvider Services => _factory!.Services;
+ protected ITestAuthenticationConfiguration AuthConfig { get; private set; } = null!;
public async ValueTask InitializeAsync()
{
- // Set environment variables for testing
+ // Define variáveis de ambiente para testes
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Testing");
Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Testing");
@@ -39,7 +41,7 @@ public async ValueTask InitializeAsync()
builder.UseEnvironment("Testing");
builder.ConfigureServices(services =>
{
- // Substitute database with test container - Remove all DbContext related services
+ // Substitui banco de dados por container de teste - Remove todos os serviços relacionados ao DbContext
var usersDbContextDescriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions));
if (usersDbContextDescriptor != null)
services.Remove(usersDbContextDescriptor);
@@ -48,7 +50,7 @@ public async ValueTask InitializeAsync()
if (providersDbContextDescriptor != null)
services.Remove(providersDbContextDescriptor);
- // Also remove the actual DbContext services if they exist
+ // Remove também os serviços DbContext se existirem
var usersDbContextService = services.SingleOrDefault(d => d.ServiceType == typeof(UsersDbContext));
if (usersDbContextService != null)
services.Remove(usersDbContextService);
@@ -57,31 +59,43 @@ public async ValueTask InitializeAsync()
if (providersDbContextService != null)
services.Remove(providersDbContextService);
- // Add test database contexts
+ // Adiciona contextos de banco de dados para testes
services.AddDbContext(options =>
{
- options.UseNpgsql(_databaseFixture.ConnectionString);
+ options.UseNpgsql(_databaseFixture.ConnectionString, npgsqlOptions =>
+ {
+ npgsqlOptions.MigrationsAssembly("MeAjudaAi.Modules.Users.Infrastructure");
+ npgsqlOptions.MigrationsHistoryTable("__EFMigrationsHistory", "users");
+ });
options.EnableSensitiveDataLogging();
+ options.ConfigureWarnings(warnings =>
+ warnings.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning));
});
services.AddDbContext(options =>
{
- options.UseNpgsql(_databaseFixture.ConnectionString);
+ options.UseNpgsql(_databaseFixture.ConnectionString, npgsqlOptions =>
+ {
+ npgsqlOptions.MigrationsAssembly("MeAjudaAi.Modules.Providers.Infrastructure");
+ npgsqlOptions.MigrationsHistoryTable("__EFMigrationsHistory", "providers");
+ });
options.EnableSensitiveDataLogging();
+ options.ConfigureWarnings(warnings =>
+ warnings.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning));
});
- // Add test authentication to override any existing authentication
+ // Adiciona autenticação de teste baseada em instância para evitar estado estático
services.RemoveRealAuthentication();
- services.AddConfigurableTestAuthentication();
+ services.AddInstanceTestAuthentication();
- // Remove ClaimsTransformation that causes hanging in tests
+ // Remove ClaimsTransformation que causa travamentos nos testes
var claimsTransformationDescriptor = services.FirstOrDefault(d =>
d.ServiceType == typeof(IClaimsTransformation));
if (claimsTransformationDescriptor != null)
services.Remove(claimsTransformationDescriptor);
});
- // Enable detailed logging for debugging
+ // Habilita logging detalhado para debug
builder.ConfigureLogging(logging =>
{
logging.ClearProviders();
@@ -95,105 +109,81 @@ public async ValueTask InitializeAsync()
Client = _factory.CreateClient();
- // Ensure database schema using EnsureCreatedAsync for testing
- // Note: UsersDbContext has pending model changes that would require new migrations
+ // Obtém a configuração de autenticação da instância do container DI
+ AuthConfig = _factory.Services.GetRequiredService();
+
+ // Aplica migrações do banco de dados para testes
+ // Nota: Ambos os módulos usam setup baseado em migrações para consistência com produção
using var scope = _factory.Services.CreateScope();
var usersContext = scope.ServiceProvider.GetRequiredService();
var providersContext = scope.ServiceProvider.GetRequiredService();
var logger = scope.ServiceProvider.GetService>();
- // Create schemas first
+ // Aplica migrações exatamente como nos testes E2E
+ await ApplyMigrationsAsync(usersContext, providersContext, logger);
+ }
+
+ private static async Task ApplyMigrationsAsync(UsersDbContext usersContext, ProvidersDbContext providersContext, ILogger? logger)
+ {
+ // Garante estado limpo do banco de dados (como nos testes E2E)
try
{
- await providersContext.Database.ExecuteSqlRawAsync("CREATE SCHEMA IF NOT EXISTS providers;");
- await usersContext.Database.ExecuteSqlRawAsync("CREATE SCHEMA IF NOT EXISTS users;");
- logger?.LogInformation("Database schemas created successfully");
+ await usersContext.Database.EnsureDeletedAsync();
+ logger?.LogInformation("🧹 Banco de dados existente limpo");
}
catch (Exception ex)
{
- logger?.LogWarning(ex, "Failed to create schemas, they may already exist");
+ logger?.LogError(ex, "❌ Falha crítica ao limpar banco existente: {Message}", ex.Message);
+ throw new InvalidOperationException("Não foi possível limpar o banco de dados antes dos testes", ex);
}
- // For UsersDbContext, use EnsureCreatedAsync (works fine for users)
+ // Aplica migrações no UsersDbContext primeiro (cria database e schema users)
try
{
- await usersContext.Database.EnsureCreatedAsync();
- logger?.LogInformation("Users database schema created successfully");
+ logger?.LogInformation("🔄 Aplicando migrações do módulo Users...");
+ await usersContext.Database.MigrateAsync();
+ logger?.LogInformation("✅ Migrações do banco Users completadas com sucesso");
}
catch (Exception ex)
{
- logger?.LogError(ex, "Failed to create Users database schema");
- throw;
+ logger?.LogError(ex, "❌ Falha ao aplicar migrações do Users: {Message}", ex.Message);
+ throw new InvalidOperationException("Não foi possível aplicar migrações do banco Users", ex);
}
- // For ProvidersDbContext, use migrations for proper table structure
+ // Aplica migrações no ProvidersDbContext (banco já existe, só precisa do schema providers)
try
{
- logger?.LogInformation("🔄 Running Providers migrations...");
+ logger?.LogInformation("🔄 Aplicando migrações do módulo Providers...");
await providersContext.Database.MigrateAsync();
- logger?.LogInformation("✅ Providers database migrations completed successfully");
+ logger?.LogInformation("✅ Migrações do banco Providers completadas com sucesso");
}
catch (Exception ex)
{
- logger?.LogWarning(ex, "⚠️ Migrations failed for Providers, trying EnsureCreatedAsync");
-
- try
- {
- await providersContext.Database.EnsureCreatedAsync();
- logger?.LogInformation("✅ Providers database schema created with EnsureCreatedAsync");
- }
- catch (Exception ensureEx)
- {
- logger?.LogWarning(ensureEx, "⚠️ EnsureCreatedAsync also failed, falling back to manual creation");
-
- try
- {
- await CreateProvidersTableManually(providersContext, logger);
- logger?.LogInformation("✅ Providers database schema created using manual table creation");
- }
- catch (Exception manualEx)
- {
- logger?.LogError(manualEx, "❌ All Providers table creation methods failed");
- throw new InvalidOperationException("Unable to initialize Providers database schema", manualEx);
- }
- }
+ logger?.LogError(ex, "❌ Falha ao aplicar migrações do Providers: {Message}", ex.Message);
+ throw new InvalidOperationException("Não foi possível aplicar migrações do banco Providers", ex);
}
- // Verify tables exist
+ // Verifica se as tabelas existem
try
{
var usersCount = await usersContext.Users.CountAsync();
- logger?.LogInformation("Users database verification successful - Count: {UsersCount}", usersCount);
+ logger?.LogInformation("Verificação do banco Users bem-sucedida - Contagem: {UsersCount}", usersCount);
}
catch (Exception ex)
{
- logger?.LogError(ex, "Users database verification failed");
- throw new InvalidOperationException("Users database is not properly initialized", ex);
+ logger?.LogError(ex, "Verificação do banco Users falhou");
+ throw new InvalidOperationException("Banco Users não foi inicializado corretamente", ex);
}
try
{
var providersCount = await providersContext.Providers.CountAsync();
- logger?.LogInformation("Providers database verification successful - Count: {ProvidersCount}", providersCount);
+ logger?.LogInformation("Verificação do banco Providers bem-sucedida - Contagem: {ProvidersCount}", providersCount);
}
catch (Exception ex)
{
- logger?.LogError(ex, "Providers database verification failed - attempting emergency table creation");
-
- // Emergency table creation as last resort
- try
- {
- await CreateProvidersTableManually(providersContext, logger);
-
- // Retry verification after manual creation
- var providersCount = await providersContext.Providers.CountAsync();
- logger?.LogInformation("Emergency table creation successful - Count: {ProvidersCount}", providersCount);
- }
- catch (Exception emergencyEx)
- {
- logger?.LogError(emergencyEx, "Emergency table creation also failed");
- throw new InvalidOperationException("Providers database could not be initialized despite all attempts", emergencyEx);
- }
+ logger?.LogError(ex, "Verificação do banco Providers falhou");
+ throw new InvalidOperationException("Banco Providers não foi inicializado corretamente", ex);
}
}
@@ -207,16 +197,16 @@ public async ValueTask DisposeAsync()
private static async Task CreateProvidersTableManually(ProvidersDbContext context, ILogger? logger)
{
- logger?.LogInformation("🔨 Starting manual Providers table creation with clean slate");
+ logger?.LogInformation("🔨 Iniciando criação manual das tabelas do Providers com estado limpo");
- // First, drop existing tables to ensure clean slate
+ // Primeiro, remove tabelas existentes para garantir estado limpo
await context.Database.ExecuteSqlRawAsync(@"
DROP TABLE IF EXISTS providers.qualification CASCADE;
DROP TABLE IF EXISTS providers.document CASCADE;
DROP TABLE IF EXISTS providers.providers CASCADE;
");
- // Create the main providers table with all necessary columns based on the EF Core model
+ // Cria a tabela principal providers com todas as colunas necessárias baseadas no modelo EF Core
var createProvidersTable = @"
CREATE TABLE IF NOT EXISTS providers.providers (
id uuid PRIMARY KEY,
@@ -244,7 +234,7 @@ zip_code varchar(20) NOT NULL,
country varchar(50) NOT NULL
);";
- // Create the documents table (owned entity)
+ // Cria a tabela de documentos (entidade owned)
var createDocumentsTable = @"
CREATE TABLE IF NOT EXISTS providers.document (
provider_id uuid NOT NULL,
@@ -255,7 +245,7 @@ document_type varchar(20) NOT NULL,
FOREIGN KEY (provider_id) REFERENCES providers.providers(id) ON DELETE CASCADE
);";
- // Create the qualifications table (owned entity)
+ // Cria a tabela de qualificações (entidade owned)
var createQualificationsTable = @"
CREATE TABLE IF NOT EXISTS providers.qualification (
provider_id uuid NOT NULL,
@@ -280,20 +270,20 @@ FOREIGN KEY (provider_id) REFERENCES providers.providers(id) ON DELETE CASCADE
try
{
await context.Database.ExecuteSqlRawAsync(createProvidersTable);
- logger?.LogInformation("✅ Created providers table manually");
+ logger?.LogInformation("✅ Tabela providers criada manualmente");
await context.Database.ExecuteSqlRawAsync(createDocumentsTable);
- logger?.LogInformation("✅ Created documents table manually");
+ logger?.LogInformation("✅ Tabela documents criada manualmente");
await context.Database.ExecuteSqlRawAsync(createQualificationsTable);
- logger?.LogInformation("✅ Created qualifications table manually");
+ logger?.LogInformation("✅ Tabela qualifications criada manualmente");
await context.Database.ExecuteSqlRawAsync(createIndices);
- logger?.LogInformation("✅ Created indices manually");
+ logger?.LogInformation("✅ Índices criados manualmente");
}
catch (Exception ex)
{
- logger?.LogError(ex, "❌ Failed to create tables manually");
+ logger?.LogError(ex, "❌ Falha ao criar tabelas manualmente");
throw;
}
}
diff --git a/tests/MeAjudaAi.Integration.Tests/Base/InstanceApiTestBase.cs b/tests/MeAjudaAi.Integration.Tests/Base/InstanceApiTestBase.cs
index c5a97a82e..543d8e013 100644
--- a/tests/MeAjudaAi.Integration.Tests/Base/InstanceApiTestBase.cs
+++ b/tests/MeAjudaAi.Integration.Tests/Base/InstanceApiTestBase.cs
@@ -67,6 +67,8 @@ public async ValueTask InitializeAsync()
npgsqlOptions.MigrationsHistoryTable("__EFMigrationsHistory", "users");
});
options.EnableSensitiveDataLogging();
+ options.ConfigureWarnings(warnings =>
+ warnings.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning));
});
services.AddDbContext(options =>
@@ -77,6 +79,8 @@ public async ValueTask InitializeAsync()
npgsqlOptions.MigrationsHistoryTable("__EFMigrationsHistory", "providers");
});
options.EnableSensitiveDataLogging();
+ options.ConfigureWarnings(warnings =>
+ warnings.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning));
});
// Add instance-based test authentication instead of static
@@ -107,68 +111,54 @@ public async ValueTask InitializeAsync()
// Get the authentication configuration instance from the DI container
AuthConfig = _factory.Services.GetRequiredService();
- // Ensure database schema using EnsureCreatedAsync for testing
- // Note: UsersDbContext has pending model changes that would require new migrations
+ // Apply database migrations for testing
+ // Note: Both modules use migration-based setup for consistency with production
using var scope = _factory.Services.CreateScope();
var usersContext = scope.ServiceProvider.GetRequiredService();
var providersContext = scope.ServiceProvider.GetRequiredService();
var logger = scope.ServiceProvider.GetService>();
- // Create schemas first
+ // Apply migrations exactly like E2E tests
+ await ApplyMigrationsAsync(usersContext, providersContext, logger);
+ }
+
+ private static async Task ApplyMigrationsAsync(UsersDbContext usersContext, ProvidersDbContext providersContext, ILogger? logger)
+ {
+ // Ensure clean database state (like E2E tests)
try
{
- await providersContext.Database.ExecuteSqlRawAsync("CREATE SCHEMA IF NOT EXISTS providers;");
- await usersContext.Database.ExecuteSqlRawAsync("CREATE SCHEMA IF NOT EXISTS users;");
- logger?.LogInformation("Database schemas created successfully");
+ await usersContext.Database.EnsureDeletedAsync();
+ logger?.LogInformation("🧹 Cleaned existing database");
}
catch (Exception ex)
{
- logger?.LogWarning(ex, "Failed to create schemas, they may already exist");
+ logger?.LogWarning(ex, "Failed to clean existing database, it may not exist");
}
- // For UsersDbContext, use EnsureCreatedAsync (works fine for users)
+ // Apply migrations on UsersDbContext first (creates database and users schema)
try
{
- await usersContext.Database.EnsureCreatedAsync();
- logger?.LogInformation("Users database schema created successfully");
+ logger?.LogInformation("🔄 Applying Users migrations...");
+ await usersContext.Database.MigrateAsync();
+ logger?.LogInformation("✅ Users database migrations completed successfully");
}
catch (Exception ex)
{
- logger?.LogError(ex, "Failed to create Users database schema");
- throw;
+ logger?.LogError(ex, "❌ Failed to apply Users migrations: {Message}", ex.Message);
+ throw new InvalidOperationException("Unable to apply Users database migrations", ex);
}
- // For ProvidersDbContext, use migrations for proper table structure
+ // Apply migrations on ProvidersDbContext (database exists, only need providers schema)
try
{
- logger?.LogInformation("🔄 Running Providers migrations...");
+ logger?.LogInformation("🔄 Applying Providers migrations...");
await providersContext.Database.MigrateAsync();
logger?.LogInformation("✅ Providers database migrations completed successfully");
}
catch (Exception ex)
{
- logger?.LogWarning(ex, "⚠️ Migrations failed for Providers, trying EnsureCreatedAsync");
-
- try
- {
- await providersContext.Database.EnsureCreatedAsync();
- logger?.LogInformation("✅ Providers database schema created with EnsureCreatedAsync");
- }
- catch (Exception ensureEx)
- {
- logger?.LogWarning(ensureEx, "⚠️ EnsureCreatedAsync also failed, falling back to manual creation");
-
- try
- {
- await CreateProvidersTableManually(providersContext, logger);
- logger?.LogInformation("✅ Providers database schema created using manual table creation");
- }
- catch (Exception manualEx)
- {
- logger?.LogError(manualEx, "❌ All Providers table creation methods failed");
- throw new InvalidOperationException("Unable to initialize Providers database schema", manualEx);
- }
- }
+ logger?.LogError(ex, "❌ Failed to apply Providers migrations: {Message}", ex.Message);
+ throw new InvalidOperationException("Unable to apply Providers database migrations", ex);
}
// Verify tables exist
@@ -190,22 +180,8 @@ public async ValueTask InitializeAsync()
}
catch (Exception ex)
{
- logger?.LogError(ex, "Providers database verification failed - attempting emergency table creation");
-
- // Emergency table creation as last resort
- try
- {
- await CreateProvidersTableManually(providersContext, logger);
-
- // Retry verification after manual creation
- var providersCount = await providersContext.Providers.CountAsync();
- logger?.LogInformation("Emergency table creation successful - Count: {ProvidersCount}", providersCount);
- }
- catch (Exception emergencyEx)
- {
- logger?.LogError(emergencyEx, "Emergency table creation also failed");
- throw new InvalidOperationException("Providers database could not be initialized despite all attempts", emergencyEx);
- }
+ logger?.LogError(ex, "Providers database verification failed");
+ throw new InvalidOperationException("Providers database is not properly initialized", ex);
}
}
diff --git a/tests/MeAjudaAi.Integration.Tests/Messaging/MessageBusSelectionTests.cs b/tests/MeAjudaAi.Integration.Tests/Messaging/MessageBusSelectionTests.cs
index 325154bda..ff6db1dfc 100644
--- a/tests/MeAjudaAi.Integration.Tests/Messaging/MessageBusSelectionTests.cs
+++ b/tests/MeAjudaAi.Integration.Tests/Messaging/MessageBusSelectionTests.cs
@@ -1,4 +1,5 @@
using FluentAssertions;
+using MeAjudaAi.Integration.Tests.Base;
using MeAjudaAi.Integration.Tests.Infrastructure;
using MeAjudaAi.Shared.Messaging;
using MeAjudaAi.Shared.Messaging.Factory;
@@ -16,7 +17,7 @@ namespace MeAjudaAi.Integration.Tests.Messaging;
/// Testes para verificar se o MessageBus correto é selecionado baseado no ambiente
///
[Collection("Integration Tests Collection")]
-public class MessageBusSelectionTests : Base.ApiTestBase
+public class MessageBusSelectionTests : ApiTestBase
{
[Fact]
public void MessageBusFactory_InTestingEnvironment_ShouldReturnMock()
diff --git a/tests/MeAjudaAi.Integration.Tests/Modules/Providers/ImplementedFeaturesTests.cs b/tests/MeAjudaAi.Integration.Tests/Modules/Providers/ImplementedFeaturesTests.cs
index 511db86e2..ba222bef0 100644
--- a/tests/MeAjudaAi.Integration.Tests/Modules/Providers/ImplementedFeaturesTests.cs
+++ b/tests/MeAjudaAi.Integration.Tests/Modules/Providers/ImplementedFeaturesTests.cs
@@ -39,7 +39,7 @@ public async Task ProvidersEndpoint_ShouldBeAccessible()
public async Task ProvidersEndpoint_WithAuthentication_ShouldReturnValidResponse()
{
// Arrange
- ConfigurableTestAuthenticationHandler.ConfigureAdmin();
+ AuthConfig.ConfigureAdmin();
// Act
var response = await Client.GetAsync("/api/v1/providers");
@@ -82,7 +82,7 @@ public async Task ProvidersEndpoint_WithAuthentication_ShouldReturnValidResponse
public async Task ProvidersEndpoint_ShouldSupportPagination()
{
// Arrange
- ConfigurableTestAuthenticationHandler.ConfigureAdmin();
+ AuthConfig.ConfigureAdmin();
// Act
var response = await Client.GetAsync("/api/v1/providers?page=1&pageSize=5");
@@ -95,7 +95,7 @@ public async Task ProvidersEndpoint_ShouldSupportPagination()
public async Task ProvidersEndpoint_ShouldSupportFilters()
{
// Arrange
- ConfigurableTestAuthenticationHandler.ConfigureAdmin();
+ AuthConfig.ConfigureAdmin();
// Act
var response = await Client.GetAsync("/api/v1/providers?name=test&type=1&verificationStatus=1");
@@ -108,7 +108,7 @@ public async Task ProvidersEndpoint_ShouldSupportFilters()
public async Task GetProviderById_Endpoint_ShouldExist()
{
// Arrange
- ConfigurableTestAuthenticationHandler.ConfigureAdmin();
+ AuthConfig.ConfigureAdmin();
var testId = Guid.NewGuid();
// Act
@@ -123,7 +123,7 @@ public async Task GetProviderById_Endpoint_ShouldExist()
public async Task CreateProvider_Endpoint_ShouldExist()
{
// Arrange
- ConfigurableTestAuthenticationHandler.ConfigureAdmin();
+ AuthConfig.ConfigureAdmin();
var providerData = new
{
userId = Guid.NewGuid(),
diff --git a/tests/MeAjudaAi.Integration.Tests/Modules/Providers/ProvidersApiTests.cs b/tests/MeAjudaAi.Integration.Tests/Modules/Providers/ProvidersApiTests.cs
index 335198b05..88ee71176 100644
--- a/tests/MeAjudaAi.Integration.Tests/Modules/Providers/ProvidersApiTests.cs
+++ b/tests/MeAjudaAi.Integration.Tests/Modules/Providers/ProvidersApiTests.cs
@@ -40,7 +40,7 @@ public async Task ProvidersEndpoint_ShouldBeAccessible()
public async Task ProvidersEndpoint_WithAuthentication_ShouldReturnValidResponse()
{
// Arrange
- ConfigurableTestAuthenticationHandler.ConfigureAdmin();
+ AuthConfig.ConfigureAdmin();
// Act
var response = await Client.GetAsync("/api/v1/providers");
@@ -87,7 +87,7 @@ public async Task ProvidersEndpoint_WithAuthentication_ShouldReturnValidResponse
public async Task ProvidersEndpoint_ShouldSupportPagination()
{
// Arrange
- ConfigurableTestAuthenticationHandler.ConfigureAdmin();
+ AuthConfig.ConfigureAdmin();
// Act
var response = await Client.GetAsync("/api/v1/providers?page=1&pageSize=5");
@@ -103,7 +103,7 @@ public async Task ProvidersEndpoint_ShouldSupportPagination()
public async Task ProvidersEndpoint_ShouldSupportFilters()
{
// Arrange
- ConfigurableTestAuthenticationHandler.ConfigureAdmin();
+ AuthConfig.ConfigureAdmin();
// Act
var response = await Client.GetAsync("/api/v1/providers?name=test&type=1&verificationStatus=1");
@@ -141,7 +141,7 @@ public async Task GetProviderById_Endpoint_ShouldExist()
public async Task CreateProvider_Endpoint_ShouldExist()
{
// Arrange
- ConfigurableTestAuthenticationHandler.ConfigureAdmin();
+ AuthConfig.ConfigureAdmin();
var providerData = new
{
userId = Guid.NewGuid(),
diff --git a/tests/MeAjudaAi.Integration.Tests/Modules/Providers/ProvidersIntegrationTests.cs b/tests/MeAjudaAi.Integration.Tests/Modules/Providers/ProvidersIntegrationTests.cs
index abd00f7b6..0dd353979 100644
--- a/tests/MeAjudaAi.Integration.Tests/Modules/Providers/ProvidersIntegrationTests.cs
+++ b/tests/MeAjudaAi.Integration.Tests/Modules/Providers/ProvidersIntegrationTests.cs
@@ -1,10 +1,10 @@
-using System.Net;
-using System.Net.Http.Json;
-using System.Text.Json;
using FluentAssertions;
using MeAjudaAi.Integration.Tests.Base;
using MeAjudaAi.Shared.Commands;
using Microsoft.Extensions.DependencyInjection;
+using System.Net;
+using System.Net.Http.Json;
+using System.Text.Json;
namespace MeAjudaAi.Integration.Tests.Modules.Providers;
@@ -17,7 +17,7 @@ namespace MeAjudaAi.Integration.Tests.Modules.Providers;
/// - Soft Delete de prestadores
/// - Gerenciamento de documentos e qualificações
///
-public class ProvidersIntegrationTests(ITestOutputHelper testOutput) : InstanceApiTestBase
+public class ProvidersIntegrationTests(ITestOutputHelper testOutput) : ApiTestBase
{
[Fact]
public async Task CreateProvider_WithValidData_ShouldReturnCreated()
diff --git a/tests/MeAjudaAi.Integration.Tests/Modules/Users/UsersApiTests.cs b/tests/MeAjudaAi.Integration.Tests/Modules/Users/UsersApiTests.cs
index 350b7969f..9bb5b831a 100644
--- a/tests/MeAjudaAi.Integration.Tests/Modules/Users/UsersApiTests.cs
+++ b/tests/MeAjudaAi.Integration.Tests/Modules/Users/UsersApiTests.cs
@@ -18,7 +18,7 @@ namespace MeAjudaAi.Integration.Tests.Modules.Users;
/// - Autorização está funcionando
/// - Dados são persistidos corretamente
///
-public class UsersApiTests : InstanceApiTestBase
+public class UsersApiTests : ApiTestBase
{
[Fact]
public async Task UsersEndpoint_ShouldBeAccessible()
diff --git a/tests/MeAjudaAi.Integration.Tests/Modules/Users/UsersIntegrationTests.cs b/tests/MeAjudaAi.Integration.Tests/Modules/Users/UsersIntegrationTests.cs
index 663af6837..5a9785680 100644
--- a/tests/MeAjudaAi.Integration.Tests/Modules/Users/UsersIntegrationTests.cs
+++ b/tests/MeAjudaAi.Integration.Tests/Modules/Users/UsersIntegrationTests.cs
@@ -17,7 +17,7 @@ namespace MeAjudaAi.Integration.Tests.Modules.Users;
/// - Soft Delete de usuários
/// - Gerenciamento via Keycloak
///
-public class UsersIntegrationTests(ITestOutputHelper testOutput) : InstanceApiTestBase
+public class UsersIntegrationTests(ITestOutputHelper testOutput) : ApiTestBase
{
[Fact]
public async Task CreateUser_WithValidData_ShouldReturnCreated()
diff --git a/tests/MeAjudaAi.Integration.Tests/RegressionTests.cs b/tests/MeAjudaAi.Integration.Tests/RegressionTests.cs
index 9fe511aea..07b7ea1af 100644
--- a/tests/MeAjudaAi.Integration.Tests/RegressionTests.cs
+++ b/tests/MeAjudaAi.Integration.Tests/RegressionTests.cs
@@ -5,7 +5,7 @@
namespace MeAjudaAi.Integration.Tests;
-public class RegressionTests : InstanceApiTestBase
+public class RegressionTests : ApiTestBase
{
private readonly ITestOutputHelper _output;
diff --git a/tests/MeAjudaAi.Shared.Tests/Infrastructure/TestInfrastructureOptions.cs b/tests/MeAjudaAi.Shared.Tests/Infrastructure/TestInfrastructureOptions.cs
index 176abc7cb..16229db8b 100644
--- a/tests/MeAjudaAi.Shared.Tests/Infrastructure/TestInfrastructureOptions.cs
+++ b/tests/MeAjudaAi.Shared.Tests/Infrastructure/TestInfrastructureOptions.cs
@@ -52,6 +52,11 @@ public class TestDatabaseOptions
/// Se deve aplicar migrations automaticamente
///
public bool AutoMigrate { get; set; } = true;
+
+ ///
+ /// Se deve usar InMemory database ao invés de PostgreSQL
+ ///
+ public bool UseInMemoryDatabase { get; set; } = false;
}
public class TestCacheOptions